diff --git a/client/src/api/client.ts b/client/src/api/client.ts index ebc2a3f0..a3744b03 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -14,10 +14,10 @@ import ru from '../i18n/translations/ru' import zh from '../i18n/translations/zh' import zhTw from '../i18n/translations/zhTw' import ar from '../i18n/translations/ar' -import ua from '../i18n/translations/ua' +import uk from '../i18n/translations/uk' const rateLimitTranslations: Record> = { - en, br, de, es, fr, it, nl, pl, cs, hu, ru, zh, 'zh-TW': zhTw, ar, ua, + en, br, de, es, fr, it, nl, pl, cs, hu, ru, zh, 'zh-TW': zhTw, ar, uk, } function translateRateLimit(): string { diff --git a/client/src/components/Layout/DemoBanner.tsx b/client/src/components/Layout/DemoBanner.tsx index a2b0ac4e..f7b08af4 100644 --- a/client/src/components/Layout/DemoBanner.tsx +++ b/client/src/components/Layout/DemoBanner.tsx @@ -246,7 +246,7 @@ const texts: Record = { selfHostLink: 'host mandiri', close: 'Mengerti', }, - ua: { + uk: { titleBefore: 'Ласкаво просимо до ', titleAfter: '', title: 'Ласкаво просимо до демо-версії TREK', diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index 9f35fd09..fdefd01b 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -15,7 +15,7 @@ import ar from './translations/ar' import br from './translations/br' import cs from './translations/cs' import pl from './translations/pl' -import ua from './translations/ua' +import uk from './translations/uk' import { SUPPORTED_LANGUAGES, SupportedLanguageCode } from './supportedLanguages' export { SUPPORTED_LANGUAGES } @@ -24,7 +24,7 @@ type TranslationStrings = Record = { - de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, id, ar, br, cs, pl, ua, + de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, id, ar, br, cs, pl, uk, } // Derived from SUPPORTED_LANGUAGES — add new languages there, not here. @@ -39,7 +39,7 @@ export function getLocaleForLanguage(language: string): string { export function getIntlLanguage(language: string): string { if (language === 'br') return 'pt-BR' - return ['de', 'es', 'fr', 'hu', 'it', 'ru', 'zh', 'zh-TW', 'nl', 'ar', 'cs', 'pl', 'id', 'ua'].includes(language) ? language : 'en' + return ['de', 'es', 'fr', 'hu', 'it', 'ru', 'zh', 'zh-TW', 'nl', 'ar', 'cs', 'pl', 'id', 'uk'].includes(language) ? language : 'en' } export function isRtlLanguage(language: string): boolean { diff --git a/client/src/i18n/supportedLanguages.ts b/client/src/i18n/supportedLanguages.ts index 5e1f420e..380dfd7b 100644 --- a/client/src/i18n/supportedLanguages.ts +++ b/client/src/i18n/supportedLanguages.ts @@ -14,7 +14,7 @@ export const SUPPORTED_LANGUAGES = [ { value: 'it', label: 'Italiano', locale: 'it-IT' }, { value: 'ar', label: 'العربية', locale: 'ar-SA' }, { value: 'id', label: 'Bahasa Indonesia', locale: 'id-ID' }, - { value: 'ua', label: 'Українська', locale: 'uk-UA' }, + { value: 'uk', label: 'Українська', locale: 'uk-UA' }, ] as const export type SupportedLanguageCode = typeof SUPPORTED_LANGUAGES[number]['value'] diff --git a/client/src/i18n/translations/ua.ts b/client/src/i18n/translations/uk.ts similarity index 99% rename from client/src/i18n/translations/ua.ts rename to client/src/i18n/translations/uk.ts index 166d05fd..0a0c4e96 100644 --- a/client/src/i18n/translations/ua.ts +++ b/client/src/i18n/translations/uk.ts @@ -1,4 +1,4 @@ -const ua: Record = { +const uk: Record = { // Common 'common.save': 'Зберегти', 'common.showMore': 'Показати більше', @@ -2373,5 +2373,5 @@ const ua: Record = { 'transport.addManual': 'Ручний транспорт', } -export default ua +export default uk diff --git a/server/src/config.ts b/server/src/config.ts index 93f63154..ea13e083 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -104,7 +104,7 @@ export const ENCRYPTION_KEY = _encryptionKey; // Supported values: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar // Must stay in sync with client/src/i18n/supportedLanguages.ts (canonical source). // Kept duplicated here because server and client are separate npm packages. -const SUPPORTED_LANG_CODES = ['de', 'en', 'es', 'fr', 'hu', 'nl', 'br', 'cs', 'pl', 'ru', 'zh', 'zh-TW', 'it', 'ar', 'ua']; +const SUPPORTED_LANG_CODES = ['de', 'en', 'es', 'fr', 'hu', 'nl', 'br', 'cs', 'pl', 'ru', 'zh', 'zh-TW', 'it', 'ar', 'uk']; const rawDefaultLang = process.env.DEFAULT_LANGUAGE || 'en'; if (!SUPPORTED_LANG_CODES.includes(rawDefaultLang)) { console.warn(`DEFAULT_LANGUAGE="${rawDefaultLang}" is not supported. Falling back to "en". Supported: ${SUPPORTED_LANG_CODES.join(', ')}`); diff --git a/server/src/services/notifications.ts b/server/src/services/notifications.ts index 98ae2a74..34c8ed85 100644 --- a/server/src/services/notifications.ts +++ b/server/src/services/notifications.ts @@ -89,7 +89,7 @@ const I18N: Record = { 'zh-TW': { footer: '您收到這封郵件是因為您在 TREK 中啟用了通知。', manage: '管理偏好設定', madeWith: 'Made with', openTrek: '開啟 TREK' }, ar: { footer: 'تلقيت هذا لأنك قمت بتفعيل الإشعارات في TREK.', manage: 'إدارة التفضيلات', madeWith: 'Made with', openTrek: 'فتح TREK' }, id: { footer: 'Anda menerima ini karena Anda telah mengaktifkan notifikasi di TREK.', manage: 'Kelola preferensi di Pengaturan', madeWith: 'Dibuat dengan', openTrek: 'Buka TREK' }, - ua: { footer: 'Ви отримали це, тому що у вас увімкнені сповіщення в TREK.', manage: 'Керувати налаштуваннями', madeWith: 'Зроблено з', openTrek: 'Відкрити TREK' }, + uk: { footer: 'Ви отримали це, тому що у вас увімкнені сповіщення в TREK.', manage: 'Керувати налаштуваннями', madeWith: 'Зроблено з', openTrek: 'Відкрити TREK' }, }; // Translated notification texts per event type @@ -276,7 +276,7 @@ const EVENT_TEXTS: Record> = { packing_tagged: p => ({ title: `Pengepakan: ${p.category}`, body: `${p.actor} menugaskan Anda ke kategori "${p.category}" di "${p.trip}".` }), version_available: p => ({ title: 'Versi TREK baru tersedia', body: `TREK ${p.version} sekarang tersedia. Kunjungi panel admin untuk memperbarui.` }), }, - ua: { + uk: { trip_invite: p => ({ title: `Запрошення до "${p.trip}"`, body: `${p.actor} запросив ${p.invitee || 'учасника'} до поїздки "${p.trip}".` }), booking_change: p => ({ title: `Нове бронювання: ${p.booking}`, body: `${p.actor} додав бронювання "${p.booking}" (${p.type}) до "${p.trip}".` }), trip_reminder: p => ({ title: `Нагадування про поїздку: ${p.trip}`, body: `Ваша поїздка "${p.trip}" скоро почнеться!` }), @@ -363,7 +363,7 @@ const PASSWORD_RESET_I18N: Record = { br: { subject: 'Redefinir sua senha', greeting: 'Olá', body: 'Recebemos um pedido para redefinir a senha da sua conta TREK. Clique no botão abaixo para definir uma nova senha.', ctaIntro: 'Redefinir senha', expiry: 'Este link expira em 60 minutos.', ignore: 'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.' }, cs: { subject: 'Obnovení hesla', greeting: 'Ahoj', body: 'Obdrželi jsme žádost o obnovení hesla k tvému účtu TREK. Klikni na tlačítko níže a nastav nové heslo.', ctaIntro: 'Obnovit heslo', expiry: 'Odkaz vyprší za 60 minut.', ignore: 'Pokud jsi o obnovení nežádal/a, tento e-mail ignoruj — heslo zůstane beze změny.' }, pl: { subject: 'Zresetuj hasło', greeting: 'Cześć', body: 'Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta TREK. Kliknij przycisk poniżej, aby ustawić nowe hasło.', ctaIntro: 'Zresetuj hasło', expiry: 'Link wygaśnie za 60 minut.', ignore: 'Jeśli to nie Ty, zignoruj tę wiadomość — Twoje hasło pozostanie bez zmian.' }, - ua: { subject: 'Скидання пароля', greeting: 'Привіт', body: 'Ми отримали запит на скидання пароля для вашого облікового запису TREK. Натисніть кнопку нижче, щоб встановити новий пароль.', ctaIntro: 'Скинути пароль', expiry: 'Це посилання дійсне протягом 60 хвилин.', ignore: 'Якщо ви не запитували скидання — просто проігноруйте цей лист, пароль залишиться без змін.' }, + uk: { subject: 'Скидання пароля', greeting: 'Привіт', body: 'Ми отримали запит на скидання пароля для вашого облікового запису TREK. Натисніть кнопку нижче, щоб встановити новий пароль.', ctaIntro: 'Скинути пароль', expiry: 'Це посилання дійсне протягом 60 хвилин.', ignore: 'Якщо ви не запитували скидання — просто проігноруйте цей лист, пароль залишиться без змін.' }, }; function buildPasswordResetHtml(subject: string, strings: PasswordResetStrings, recipient: string, resetUrl: string, lang: string): string { diff --git a/wiki/Contributing.md b/wiki/Contributing.md index b8e6d09d..e3a16abf 100644 --- a/wiki/Contributing.md +++ b/wiki/Contributing.md @@ -60,4 +60,4 @@ See the [[Development Environment|Development-environment]] page for the full se | Database | SQLite with WAL mode | | Auth | JWT (HS256), bcrypt, TOTP MFA, OIDC | | Maps | Leaflet + react-leaflet, OSRM, Nominatim, CartoDB tiles | -| i18n | 16 languages (EN, DE, ES, FR, NL, IT, PT-BR, CS, PL, HU, RU, ZH, ZH-TW, AR, ID, UA) | +| i18n | 16 languages (EN, DE, ES, FR, NL, IT, PT-BR, CS, PL, HU, RU, ZH, ZH-TW, AR, ID, UK) | diff --git a/wiki/Environment-Variables.md b/wiki/Environment-Variables.md index 33b9f842..f9ed1d05 100644 --- a/wiki/Environment-Variables.md +++ b/wiki/Environment-Variables.md @@ -40,7 +40,7 @@ Setting `ENCRYPTION_KEY` explicitly is recommended so you can back it up indepen Verified in `server/src/config.ts` (line 107): -`de`, `en`, `es`, `fr`, `hu`, `nl`, `br`, `cs`, `pl`, `ru`, `zh`, `zh-TW`, `it`, `ar`, `ua` +`de`, `en`, `es`, `fr`, `hu`, `nl`, `br`, `cs`, `pl`, `ru`, `zh`, `zh-TW`, `it`, `ar`, `uk` > **Note:** `id` (Indonesian / Bahasa Indonesia) appears in `client/src/i18n/supportedLanguages.ts` but is not in the server's supported-codes list in `config.ts`. Setting `DEFAULT_LANGUAGE=id` will fall back to `en` with a warning in the server log.