diff --git a/crowdin.yml b/crowdin.yml index 9df169a69e..b4d1af4907 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -6,7 +6,7 @@ "files": [ { "source": "/**/**/i18n/en.json", - "translation": "/%original_path%/%two_letters_code%.%file_extension%", + "translation": "/%original_path%/%locale%.%file_extension%", "export_only_approved": "true", "update_option": "update_without_changes" } diff --git a/lib/content-services/src/lib/i18n/ar.json b/lib/content-services/src/lib/i18n/ar-SA.json similarity index 100% rename from lib/content-services/src/lib/i18n/ar.json rename to lib/content-services/src/lib/i18n/ar-SA.json diff --git a/lib/content-services/src/lib/i18n/cs.json b/lib/content-services/src/lib/i18n/cs-CZ.json similarity index 100% rename from lib/content-services/src/lib/i18n/cs.json rename to lib/content-services/src/lib/i18n/cs-CZ.json diff --git a/lib/content-services/src/lib/i18n/da.json b/lib/content-services/src/lib/i18n/da-DK.json similarity index 100% rename from lib/content-services/src/lib/i18n/da.json rename to lib/content-services/src/lib/i18n/da-DK.json diff --git a/lib/content-services/src/lib/i18n/de.json b/lib/content-services/src/lib/i18n/de-DE.json similarity index 100% rename from lib/content-services/src/lib/i18n/de.json rename to lib/content-services/src/lib/i18n/de-DE.json diff --git a/lib/content-services/src/lib/i18n/es.json b/lib/content-services/src/lib/i18n/es-ES.json similarity index 100% rename from lib/content-services/src/lib/i18n/es.json rename to lib/content-services/src/lib/i18n/es-ES.json diff --git a/lib/content-services/src/lib/i18n/fi.json b/lib/content-services/src/lib/i18n/fi-FI.json similarity index 100% rename from lib/content-services/src/lib/i18n/fi.json rename to lib/content-services/src/lib/i18n/fi-FI.json diff --git a/lib/content-services/src/lib/i18n/fr.json b/lib/content-services/src/lib/i18n/fr-FR.json similarity index 100% rename from lib/content-services/src/lib/i18n/fr.json rename to lib/content-services/src/lib/i18n/fr-FR.json diff --git a/lib/content-services/src/lib/i18n/it.json b/lib/content-services/src/lib/i18n/it-IT.json similarity index 100% rename from lib/content-services/src/lib/i18n/it.json rename to lib/content-services/src/lib/i18n/it-IT.json diff --git a/lib/content-services/src/lib/i18n/ja.json b/lib/content-services/src/lib/i18n/ja-JP.json similarity index 100% rename from lib/content-services/src/lib/i18n/ja.json rename to lib/content-services/src/lib/i18n/ja-JP.json diff --git a/lib/content-services/src/lib/i18n/nb.json b/lib/content-services/src/lib/i18n/nb-NO.json similarity index 100% rename from lib/content-services/src/lib/i18n/nb.json rename to lib/content-services/src/lib/i18n/nb-NO.json diff --git a/lib/content-services/src/lib/i18n/nl.json b/lib/content-services/src/lib/i18n/nl-NL.json similarity index 100% rename from lib/content-services/src/lib/i18n/nl.json rename to lib/content-services/src/lib/i18n/nl-NL.json diff --git a/lib/content-services/src/lib/i18n/pl.json b/lib/content-services/src/lib/i18n/pl-PL.json similarity index 100% rename from lib/content-services/src/lib/i18n/pl.json rename to lib/content-services/src/lib/i18n/pl-PL.json diff --git a/lib/content-services/src/lib/i18n/pt.json b/lib/content-services/src/lib/i18n/pt-PT.json similarity index 100% rename from lib/content-services/src/lib/i18n/pt.json rename to lib/content-services/src/lib/i18n/pt-PT.json diff --git a/lib/content-services/src/lib/i18n/ru.json b/lib/content-services/src/lib/i18n/ru-RU.json similarity index 100% rename from lib/content-services/src/lib/i18n/ru.json rename to lib/content-services/src/lib/i18n/ru-RU.json diff --git a/lib/content-services/src/lib/i18n/sv.json b/lib/content-services/src/lib/i18n/sv-SE.json similarity index 100% rename from lib/content-services/src/lib/i18n/sv.json rename to lib/content-services/src/lib/i18n/sv-SE.json diff --git a/lib/core/src/lib/common/models/default-languages.model.ts b/lib/core/src/lib/common/models/default-languages.model.ts index 9563cb885f..ccb43e1321 100644 --- a/lib/core/src/lib/common/models/default-languages.model.ts +++ b/lib/core/src/lib/common/models/default-languages.model.ts @@ -18,21 +18,21 @@ import { LanguageItem } from '../services/language-item.interface'; export const DEFAULT_LANGUAGE_LIST: LanguageItem[] = [ - { key: 'de', label: 'Deutsch' }, + { key: 'de-DE', label: 'Deutsch' }, { key: 'en', label: 'English' }, - { key: 'es', label: 'Español' }, - { key: 'fr', label: 'Français' }, - { key: 'it', label: 'Italiano' }, - { key: 'ja', label: '日本語' }, - { key: 'nb', label: 'Bokmål' }, - { key: 'nl', label: 'Nederlands' }, + { key: 'es-ES', label: 'Español' }, + { key: 'fr-FR', label: 'Français' }, + { key: 'it-IT', label: 'Italiano' }, + { key: 'ja-JP', label: '日本語' }, + { key: 'nb-NO', label: 'Bokmål' }, + { key: 'nl-NL', label: 'Nederlands' }, { key: 'pt-BR', label: 'Português (Brasil)' }, - { key: 'ru', label: 'Русский' }, + { key: 'ru-RU', label: 'Русский' }, { key: 'zh-CN', label: '中文简体' }, - { key: 'cs', label: 'Čeština' }, - { key: 'da', label: 'Dansk' }, - { key: 'fi', label: 'Suomi' }, - { key: 'pl', label: 'Polski' }, - { key: 'sv', label: 'Svenska' }, - { key: 'ar', label: 'العربية', direction: 'rtl' } + { key: 'cs-CZ', label: 'Čeština' }, + { key: 'da-DK', label: 'Dansk' }, + { key: 'fi-FI', label: 'Suomi' }, + { key: 'pl-PL', label: 'Polski' }, + { key: 'sv-SE', label: 'Svenska' }, + { key: 'ar-SA', label: 'العربية', direction: 'rtl' } ]; diff --git a/lib/core/src/lib/common/services/user-preferences.service.ts b/lib/core/src/lib/common/services/user-preferences.service.ts index 5577feb04d..2d1bfaf439 100644 --- a/lib/core/src/lib/common/services/user-preferences.service.ts +++ b/lib/core/src/lib/common/services/user-preferences.service.ts @@ -352,7 +352,12 @@ export class UserPreferencesService { if (Array.isArray(customLanguages)) { language = customLanguages.find((customLanguage) => key.includes(customLanguage.key)); } - language ??= DEFAULT_LANGUAGE_LIST.find((defaultLang) => defaultLang.key === key) ?? defaultLanguage; + // Try exact match first, then try base language (e.g., 'ar' matches 'ar-SA') + language ??= DEFAULT_LANGUAGE_LIST.find((defaultLang) => defaultLang.key === key); + language ??= DEFAULT_LANGUAGE_LIST.find( + (defaultLang) => defaultLang.key.startsWith(key + '-') || key.startsWith(defaultLang.key.split('-')[0]) + ); + language ??= defaultLanguage; return language; } } diff --git a/lib/core/src/lib/common/utils/date-fns-utils.spec.ts b/lib/core/src/lib/common/utils/date-fns-utils.spec.ts index 8ee1b95184..d9442e22d4 100644 --- a/lib/core/src/lib/common/utils/date-fns-utils.spec.ts +++ b/lib/core/src/lib/common/utils/date-fns-utils.spec.ts @@ -16,29 +16,45 @@ */ import { addMinutes, isValid } from 'date-fns'; -import { ar, cs, da, de, enUS, es, fi, fr, it as italian, ja, nb, nl, pl, ptBR, ru, sv, zhCN } from 'date-fns/locale'; +import { ar, arSA, cs, da, de, enGB, enUS, es, fi, fr, it as italian, ja, nb, nl, pl, pt, ptBR, ru, sv, zhCN } from 'date-fns/locale'; import { DateFnsUtils } from './date-fns-utils'; describe('DateFnsUtils', () => { describe('getLocaleFromString', () => { const localeTestCases = [ + { localeCode: 'ar-SA', expectedLocale: arSA, description: 'Arabic (Saudi Arabia)' }, { localeCode: 'ar', expectedLocale: ar, description: 'Arabic' }, - { localeCode: 'cs', expectedLocale: cs, description: 'Czech' }, - { localeCode: 'da', expectedLocale: da, description: 'Danish' }, - { localeCode: 'de', expectedLocale: de, description: 'German' }, + { localeCode: 'cs-CZ', expectedLocale: cs, description: 'Czech' }, + { localeCode: 'cs', expectedLocale: cs, description: 'Czech (short)' }, + { localeCode: 'da-DK', expectedLocale: da, description: 'Danish' }, + { localeCode: 'da', expectedLocale: da, description: 'Danish (short)' }, + { localeCode: 'de-DE', expectedLocale: de, description: 'German' }, + { localeCode: 'de', expectedLocale: de, description: 'German (short)' }, + { localeCode: 'en-GB', expectedLocale: enGB, description: 'English (UK)' }, { localeCode: 'en', expectedLocale: enUS, description: 'English (US)' }, - { localeCode: 'es', expectedLocale: es, description: 'Spanish' }, - { localeCode: 'fi', expectedLocale: fi, description: 'Finnish' }, - { localeCode: 'fr', expectedLocale: fr, description: 'French' }, - { localeCode: 'it', expectedLocale: italian, description: 'Italian' }, - { localeCode: 'ja', expectedLocale: ja, description: 'Japanese' }, - { localeCode: 'nb', expectedLocale: nb, description: 'Norwegian Bokmål' }, - { localeCode: 'nl', expectedLocale: nl, description: 'Dutch' }, - { localeCode: 'pl', expectedLocale: pl, description: 'Polish' }, - { localeCode: 'pt', expectedLocale: ptBR, description: 'Portuguese' }, + { localeCode: 'es-ES', expectedLocale: es, description: 'Spanish' }, + { localeCode: 'es', expectedLocale: es, description: 'Spanish (short)' }, + { localeCode: 'fi-FI', expectedLocale: fi, description: 'Finnish' }, + { localeCode: 'fi', expectedLocale: fi, description: 'Finnish (short)' }, + { localeCode: 'fr-FR', expectedLocale: fr, description: 'French' }, + { localeCode: 'fr', expectedLocale: fr, description: 'French (short)' }, + { localeCode: 'it-IT', expectedLocale: italian, description: 'Italian' }, + { localeCode: 'it', expectedLocale: italian, description: 'Italian (short)' }, + { localeCode: 'ja-JP', expectedLocale: ja, description: 'Japanese' }, + { localeCode: 'ja', expectedLocale: ja, description: 'Japanese (short)' }, + { localeCode: 'nb-NO', expectedLocale: nb, description: 'Norwegian Bokmål' }, + { localeCode: 'nb', expectedLocale: nb, description: 'Norwegian Bokmål (short)' }, + { localeCode: 'nl-NL', expectedLocale: nl, description: 'Dutch' }, + { localeCode: 'nl', expectedLocale: nl, description: 'Dutch (short)' }, + { localeCode: 'pl-PL', expectedLocale: pl, description: 'Polish' }, + { localeCode: 'pl', expectedLocale: pl, description: 'Polish (short)' }, + { localeCode: 'pt-PT', expectedLocale: pt, description: 'Portuguese (Portugal)' }, { localeCode: 'pt-BR', expectedLocale: ptBR, description: 'Portuguese (Brazilian)' }, - { localeCode: 'ru', expectedLocale: ru, description: 'Russian' }, - { localeCode: 'sv', expectedLocale: sv, description: 'Swedish' }, + { localeCode: 'pt', expectedLocale: ptBR, description: 'Portuguese (short)' }, + { localeCode: 'ru-RU', expectedLocale: ru, description: 'Russian' }, + { localeCode: 'ru', expectedLocale: ru, description: 'Russian (short)' }, + { localeCode: 'sv-SE', expectedLocale: sv, description: 'Swedish' }, + { localeCode: 'sv', expectedLocale: sv, description: 'Swedish (short)' }, { localeCode: 'zh-CN', expectedLocale: zhCN, description: 'Chinese (Simplified)' } ]; diff --git a/lib/core/src/lib/common/utils/date-fns-utils.ts b/lib/core/src/lib/common/utils/date-fns-utils.ts index 3e9e3f4ff1..10f36321c3 100644 --- a/lib/core/src/lib/common/utils/date-fns-utils.ts +++ b/lib/core/src/lib/common/utils/date-fns-utils.ts @@ -16,7 +16,7 @@ */ import { format, parse, parseISO, isValid, isBefore, isAfter } from 'date-fns'; -import { ar, cs, da, de, enUS, es, fi, fr, it, ja, nb, nl, pl, ptBR, ru, sv, zhCN } from 'date-fns/locale'; +import { ar, arSA, cs, da, de, enGB, enUS, es, fi, fr, it, ja, nb, nl, pl, pt, ptBR, ru, sv, zhCN } from 'date-fns/locale'; const panDate = (num: number = 1): string => { let text = num.toString(); @@ -27,66 +27,46 @@ const panDate = (num: number = 1): string => { }; export class DateFnsUtils { + private static readonly localeMap: Record = { + 'ar-SA': arSA, + ar: ar, + 'cs-CZ': cs, + cs: cs, + 'da-DK': da, + da: da, + 'de-DE': de, + de: de, + 'en-GB': enGB, + 'en-AU': enGB, + en: enUS, + 'es-ES': es, + es: es, + 'fi-FI': fi, + fi: fi, + 'fr-FR': fr, + fr: fr, + 'it-IT': it, + it: it, + 'ja-JP': ja, + ja: ja, + 'nb-NO': nb, + nb: nb, + 'nl-NL': nl, + nl: nl, + 'pl-PL': pl, + pl: pl, + 'pt-PT': pt, + 'pt-BR': ptBR, + pt: ptBR, + 'ru-RU': ru, + ru: ru, + 'sv-SE': sv, + sv: sv, + 'zh-CN': zhCN + }; + static getLocaleFromString(locale: string): Locale { - let dateFnsLocale: Locale; - switch (locale) { - case 'ar': - dateFnsLocale = ar; - break; - case 'cs': - dateFnsLocale = cs; - break; - case 'da': - dateFnsLocale = da; - break; - case 'de': - dateFnsLocale = de; - break; - case 'en': - dateFnsLocale = enUS; - break; - case 'es': - dateFnsLocale = es; - break; - case 'fi': - dateFnsLocale = fi; - break; - case 'fr': - dateFnsLocale = fr; - break; - case 'it': - dateFnsLocale = it; - break; - case 'ja': - dateFnsLocale = ja; - break; - case 'nb': - dateFnsLocale = nb; - break; - case 'nl': - dateFnsLocale = nl; - break; - case 'pl': - dateFnsLocale = pl; - break; - case 'pt': - case 'pt-BR': - dateFnsLocale = ptBR; - break; - case 'ru': - dateFnsLocale = ru; - break; - case 'sv': - dateFnsLocale = sv; - break; - case 'zh-CN': - dateFnsLocale = zhCN; - break; - default: - dateFnsLocale = enUS; - break; - } - return dateFnsLocale; + return this.localeMap[locale] ?? enUS; } private static readonly momentToDateFnsMap = { diff --git a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts index 7c18301696..1b63470cdb 100644 --- a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts +++ b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts @@ -74,7 +74,7 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit super(); // Use effect to react to locale signal changes (must be in injection context) effect(() => { - this.userLocale = this.userPreferencesService.localeSignal() || 'en'; + this.userLocale = this.userPreferencesService.localeSignal() || 'en-GB'; this.setConfig(); }); } diff --git a/lib/core/src/lib/i18n/ar.json b/lib/core/src/lib/i18n/ar-SA.json similarity index 100% rename from lib/core/src/lib/i18n/ar.json rename to lib/core/src/lib/i18n/ar-SA.json diff --git a/lib/core/src/lib/i18n/cs.json b/lib/core/src/lib/i18n/cs-CZ.json similarity index 100% rename from lib/core/src/lib/i18n/cs.json rename to lib/core/src/lib/i18n/cs-CZ.json diff --git a/lib/core/src/lib/i18n/da.json b/lib/core/src/lib/i18n/da-DK.json similarity index 100% rename from lib/core/src/lib/i18n/da.json rename to lib/core/src/lib/i18n/da-DK.json diff --git a/lib/core/src/lib/i18n/de.json b/lib/core/src/lib/i18n/de-DE.json similarity index 100% rename from lib/core/src/lib/i18n/de.json rename to lib/core/src/lib/i18n/de-DE.json diff --git a/lib/core/src/lib/i18n/es.json b/lib/core/src/lib/i18n/es-ES.json similarity index 100% rename from lib/core/src/lib/i18n/es.json rename to lib/core/src/lib/i18n/es-ES.json diff --git a/lib/core/src/lib/i18n/fi.json b/lib/core/src/lib/i18n/fi-FI.json similarity index 100% rename from lib/core/src/lib/i18n/fi.json rename to lib/core/src/lib/i18n/fi-FI.json diff --git a/lib/core/src/lib/i18n/fr.json b/lib/core/src/lib/i18n/fr-FR.json similarity index 100% rename from lib/core/src/lib/i18n/fr.json rename to lib/core/src/lib/i18n/fr-FR.json diff --git a/lib/core/src/lib/i18n/it.json b/lib/core/src/lib/i18n/it-IT.json similarity index 100% rename from lib/core/src/lib/i18n/it.json rename to lib/core/src/lib/i18n/it-IT.json diff --git a/lib/core/src/lib/i18n/ja.json b/lib/core/src/lib/i18n/ja-JP.json similarity index 100% rename from lib/core/src/lib/i18n/ja.json rename to lib/core/src/lib/i18n/ja-JP.json diff --git a/lib/core/src/lib/i18n/nb.json b/lib/core/src/lib/i18n/nb-NO.json similarity index 100% rename from lib/core/src/lib/i18n/nb.json rename to lib/core/src/lib/i18n/nb-NO.json diff --git a/lib/core/src/lib/i18n/nl.json b/lib/core/src/lib/i18n/nl-NL.json similarity index 100% rename from lib/core/src/lib/i18n/nl.json rename to lib/core/src/lib/i18n/nl-NL.json diff --git a/lib/core/src/lib/i18n/pl.json b/lib/core/src/lib/i18n/pl-PL.json similarity index 100% rename from lib/core/src/lib/i18n/pl.json rename to lib/core/src/lib/i18n/pl-PL.json diff --git a/lib/core/src/lib/i18n/pt.json b/lib/core/src/lib/i18n/pt-PT.json similarity index 100% rename from lib/core/src/lib/i18n/pt.json rename to lib/core/src/lib/i18n/pt-PT.json diff --git a/lib/core/src/lib/i18n/ru.json b/lib/core/src/lib/i18n/ru-RU.json similarity index 100% rename from lib/core/src/lib/i18n/ru.json rename to lib/core/src/lib/i18n/ru-RU.json diff --git a/lib/core/src/lib/i18n/sv.json b/lib/core/src/lib/i18n/sv-SE.json similarity index 100% rename from lib/core/src/lib/i18n/sv.json rename to lib/core/src/lib/i18n/sv-SE.json diff --git a/lib/core/src/lib/mock/language.service.mock.ts b/lib/core/src/lib/mock/language.service.mock.ts index c6e7376bb5..1da65df02c 100644 --- a/lib/core/src/lib/mock/language.service.mock.ts +++ b/lib/core/src/lib/mock/language.service.mock.ts @@ -23,23 +23,23 @@ import { LanguageItem } from '../common/services/language-item.interface'; @Injectable() export class LanguageServiceMock implements LanguageServiceInterface { private readonly languages = new BehaviorSubject([ - { key: 'de', label: 'Deutsch' }, + { key: 'de-DE', label: 'Deutsch' }, { key: 'en', label: 'English' }, - { key: 'es', label: 'Español' }, - { key: 'fr', label: 'Français' }, - { key: 'it', label: 'Italiano' }, - { key: 'ja', label: '日本語' }, - { key: 'nb', label: 'Bokmål' }, - { key: 'nl', label: 'Nederlands' }, + { key: 'es-ES', label: 'Español' }, + { key: 'fr-FR', label: 'Français' }, + { key: 'it-IT', label: 'Italiano' }, + { key: 'ja-JP', label: '日本語' }, + { key: 'nb-NO', label: 'Bokmål' }, + { key: 'nl-NL', label: 'Nederlands' }, { key: 'pt-BR', label: 'Português (Brasil)' }, - { key: 'ru', label: 'Русский' }, + { key: 'ru-RU', label: 'Русский' }, { key: 'zh-CN', label: '中文简体' }, - { key: 'cs', label: 'Čeština' }, - { key: 'da', label: 'Dansk' }, - { key: 'fi', label: 'Suomi' }, - { key: 'pl', label: 'Polski' }, - { key: 'sv', label: 'Svenska' }, - { key: 'ar', label: 'العربية', direction: 'rtl' } + { key: 'cs-CZ', label: 'Čeština' }, + { key: 'da-DK', label: 'Dansk' }, + { key: 'fi-FI', label: 'Suomi' }, + { key: 'pl-PL', label: 'Polski' }, + { key: 'sv-SE', label: 'Svenska' }, + { key: 'ar-SA', label: 'العربية', direction: 'rtl' } ]); languages$ = this.languages.asObservable(); diff --git a/lib/core/src/lib/stories/core-story.providers.ts b/lib/core/src/lib/stories/core-story.providers.ts index f510432ce4..b6e961e75a 100644 --- a/lib/core/src/lib/stories/core-story.providers.ts +++ b/lib/core/src/lib/stories/core-story.providers.ts @@ -42,7 +42,7 @@ export function provideStoryCore(): (Provider | EnvironmentProviders)[] { provideCoreAuthTesting(), provideAppInitializer(() => { const appConfig = inject(AppConfigService); - appConfig.config = { ...appConfig.config, locale: 'en' }; + appConfig.config = { ...appConfig.config, locale: 'en-GB' }; }), provideRouter([], withHashLocation()) ]; diff --git a/lib/core/src/lib/translation/translate-loader.service.ts b/lib/core/src/lib/translation/translate-loader.service.ts index d48656a8eb..143a02b7b0 100644 --- a/lib/core/src/lib/translation/translate-loader.service.ts +++ b/lib/core/src/lib/translation/translate-loader.service.ts @@ -21,7 +21,7 @@ import { TranslateLoader } from '@ngx-translate/core'; import { Observable, forkJoin, throwError, of } from 'rxjs'; import { ComponentTranslationModel } from '../models/component.model'; import { ObjectUtils } from '../common/utils/object-utils'; -import { map, catchError, retry } from 'rxjs/operators'; +import { map, catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -40,8 +40,33 @@ export class TranslateLoaderService implements TranslateLoader { private queue: string[][] = []; private defaultLang: string = 'en'; - setDefaultLang(value: string) { - this.defaultLang = value || 'en'; + private readonly localeAliases: Record = { + 'en-US': 'en', + 'en-GB': 'en', + 'en-AU': 'en', + de: 'de-DE', + es: 'es-ES', + fr: 'fr-FR', + it: 'it-IT', + ja: 'ja-JP', + nb: 'nb-NO', + nl: 'nl-NL', + ru: 'ru-RU', + cs: 'cs-CZ', + da: 'da-DK', + fi: 'fi-FI', + pl: 'pl-PL', + sv: 'sv-SE', + ar: 'ar-SA', + pt: 'pt-BR' + }; + + setDefaultLang(lang: string): void { + this.defaultLang = lang; + } + + getDefaultLang(): string { + return this.defaultLang; } registerProvider(name: string, path: string) { @@ -57,23 +82,49 @@ export class TranslateLoaderService implements TranslateLoader { return this.providers.some((x) => x.name === name); } - fetchLanguageFile(lang: string, component: ComponentTranslationModel, fallbackUrl?: string): Observable { - const translationUrl = fallbackUrl || `${component.path}/${this.prefix}/${lang}${this.suffix}?v=${Date.now()}`; + private resolveLocale(lang: string): string { + if (this.localeAliases[lang]) { + return this.localeAliases[lang]; + } + return lang; + } + + private getLocaleFallbacks(lang: string): string[] { + const fallbacks: string[] = [lang]; + const resolved = this.resolveLocale(lang); + if (resolved !== lang) { + fallbacks.push(resolved); + } + const baseLang = lang.split('-')[0]; + if (baseLang !== lang && !fallbacks.includes(baseLang)) { + const resolvedBase = this.resolveLocale(baseLang); + if (!fallbacks.includes(resolvedBase)) { + fallbacks.push(resolvedBase); + } + } + return fallbacks; + } + + fetchLanguageFile(lang: string, component: ComponentTranslationModel): Observable { + const fallbacks = this.getLocaleFallbacks(lang); + return this.tryFetchWithFallbacks(lang, fallbacks, component); + } + + private tryFetchWithFallbacks(originalLang: string, fallbacks: string[], component: ComponentTranslationModel): Observable { + if (fallbacks.length === 0) { + return throwError(() => new Error(`Failed to load translations for ${originalLang}`)); + } + + const [currentLocale, ...remainingFallbacks] = fallbacks; + const translationUrl = `${component.path}/${this.prefix}/${currentLocale}${this.suffix}?v=${Date.now()}`; return this.http.get(translationUrl).pipe( map((res: any) => { - component.json[lang] = res; + component.json[originalLang] = res; }), - retry(3), catchError(() => { - if (!fallbackUrl && lang.includes('-')) { - const [langId] = lang.split('-'); - - if (langId && langId !== this.defaultLang) { - const url = `${component.path}/${this.prefix}/${langId}${this.suffix}?v=${Date.now()}`; - - return this.fetchLanguageFile(lang, component, url); - } + if (remainingFallbacks.length > 0) { + return this.tryFetchWithFallbacks(originalLang, remainingFallbacks, component); } return throwError(() => new Error(`Failed to load ${translationUrl}`)); }) diff --git a/lib/core/src/lib/translation/translate-loader.spec.ts b/lib/core/src/lib/translation/translate-loader.spec.ts index d1da33693c..683998aa0e 100644 --- a/lib/core/src/lib/translation/translate-loader.spec.ts +++ b/lib/core/src/lib/translation/translate-loader.spec.ts @@ -17,6 +17,7 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; import { TranslateLoaderService } from './translate-loader.service'; describe('TranslateLoader', () => { @@ -25,7 +26,7 @@ describe('TranslateLoader', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideHttpClientTesting(), TranslateLoaderService] + providers: [provideHttpClient(), provideHttpClientTesting(), TranslateLoaderService] }); customLoader = TestBed.inject(TranslateLoaderService); httpMock = TestBed.inject(HttpTestingController); @@ -60,4 +61,107 @@ describe('TranslateLoader', () => { subscription.unsubscribe(); httpMock.verify(); })); + + describe('locale fallback', () => { + it('should fallback from en-US to en when en-US file does not exist', fakeAsync(() => { + let nextInvoked = false; + + const subscription = customLoader.getTranslation('en-US').subscribe({ + next: () => (nextInvoked = true), + error: () => fail('Should not call error handler') + }); + + const enUsRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/en-US.json')); + enUsRequest.flush(null, { status: 404, statusText: 'Not Found' }); + tick(); + + const enRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/en.json')); + enRequest.flush({ 'TEST.KEY': 'Test value' }); + tick(); + + expect(nextInvoked).toBeTrue(); + subscription.unsubscribe(); + httpMock.verify(); + })); + + it('should fallback from en-GB to en when en-GB file does not exist', fakeAsync(() => { + let nextInvoked = false; + + const subscription = customLoader.getTranslation('en-GB').subscribe({ + next: () => (nextInvoked = true), + error: () => fail('Should not call error handler') + }); + + const enGbRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/en-GB.json')); + enGbRequest.flush(null, { status: 404, statusText: 'Not Found' }); + tick(); + + const enRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/en.json')); + enRequest.flush({ 'TEST.KEY': 'Test value' }); + tick(); + + expect(nextInvoked).toBeTrue(); + subscription.unsubscribe(); + httpMock.verify(); + })); + + it('should fallback from short locale (de) to full locale (de-DE)', fakeAsync(() => { + let nextInvoked = false; + + const subscription = customLoader.getTranslation('de').subscribe({ + next: () => (nextInvoked = true), + error: () => fail('Should not call error handler') + }); + + const deRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/de.json')); + deRequest.flush(null, { status: 404, statusText: 'Not Found' }); + tick(); + + const deDeRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/de-DE.json')); + deDeRequest.flush({ 'TEST.KEY': 'Testwert' }); + tick(); + + expect(nextInvoked).toBeTrue(); + subscription.unsubscribe(); + httpMock.verify(); + })); + + it('should use the file directly when it exists without fallback', fakeAsync(() => { + let nextInvoked = false; + + const subscription = customLoader.getTranslation('fr-FR').subscribe({ + next: () => (nextInvoked = true), + error: () => fail('Should not call error handler') + }); + + const frFrRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/fr-FR.json')); + frFrRequest.flush({ 'TEST.KEY': 'Valeur de test' }); + tick(); + + expect(nextInvoked).toBeTrue(); + subscription.unsubscribe(); + httpMock.verify(); + })); + + it('should register translations under the original requested locale', fakeAsync(() => { + let translationResult: any; + + const subscription = customLoader.getTranslation('en-US').subscribe({ + next: (result) => (translationResult = result), + error: () => fail('Should not call error handler') + }); + + const enUsRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/en-US.json')); + enUsRequest.flush(null, { status: 404, statusText: 'Not Found' }); + tick(); + + const enRequest = httpMock.expectOne((request) => request.url.includes('assets/adf-core/i18n/en.json')); + enRequest.flush({ 'TEST.KEY': 'Test value' }); + tick(); + + expect(translationResult).toEqual({ 'TEST.KEY': 'Test value' }); + subscription.unsubscribe(); + httpMock.verify(); + })); + }); }); diff --git a/lib/core/src/lib/translation/translation.service.spec.ts b/lib/core/src/lib/translation/translation.service.spec.ts index a580237ec7..7abc353bfd 100644 --- a/lib/core/src/lib/translation/translation.service.spec.ts +++ b/lib/core/src/lib/translation/translation.service.spec.ts @@ -29,10 +29,6 @@ describe('TranslationService', () => { // No implementation needed for this test }; - setDefaultLang = (_lang: string): void => { - // No implementation needed for this test - }; - getTranslation = (lang: string) => { const translations = { en: { diff --git a/lib/core/src/lib/translation/translation.service.ts b/lib/core/src/lib/translation/translation.service.ts index 94a57d46b0..3fe8c59cb2 100644 --- a/lib/core/src/lib/translation/translation.service.ts +++ b/lib/core/src/lib/translation/translation.service.ts @@ -62,8 +62,10 @@ export class TranslationService { this.customLoader = this.translate.currentLoader as TranslateLoaderService; this.defaultLang = 'en'; + if (typeof this.customLoader?.setDefaultLang === 'function') { + this.customLoader.setDefaultLang(this.defaultLang); + } this.translate.setFallbackLang(this.defaultLang); - this.customLoader.setDefaultLang(this.defaultLang); if (this.providers && this.providers.length > 0) { for (const provider of this.providers) { diff --git a/lib/insights/karma.conf.js b/lib/insights/karma.conf.js index a102bb44cc..9fea99cedb 100644 --- a/lib/insights/karma.conf.js +++ b/lib/insights/karma.conf.js @@ -17,7 +17,7 @@ module.exports = function (config) { served: true, watched: false }, - { pattern: 'lib/insights/src/lib/i18n/**/en.json', included: false, served: true, watched: false }, + { pattern: 'lib/insights/src/lib/i18n/**/en-GB.json', included: false, served: true, watched: false }, { pattern: 'lib/config/app.config.json', included: false, served: true, watched: false } ], @@ -25,7 +25,7 @@ module.exports = function (config) { proxies: { '/base/assets/': '/base/lib/insights/src/lib/assets/', - '/assets/adf-insights/i18n/en.json': '/base/lib/insights/src/lib/i18n/en.json', + '/assets/adf-insights/i18n/en-GB.json': '/base/lib/insights/src/lib/i18n/en-GB.json', '/app.config.json': '/base/lib/config/app.config.json' }, diff --git a/lib/insights/src/lib/i18n/ar.json b/lib/insights/src/lib/i18n/ar-SA.json similarity index 100% rename from lib/insights/src/lib/i18n/ar.json rename to lib/insights/src/lib/i18n/ar-SA.json diff --git a/lib/insights/src/lib/i18n/cs.json b/lib/insights/src/lib/i18n/cs-CZ.json similarity index 100% rename from lib/insights/src/lib/i18n/cs.json rename to lib/insights/src/lib/i18n/cs-CZ.json diff --git a/lib/insights/src/lib/i18n/da.json b/lib/insights/src/lib/i18n/da-DK.json similarity index 100% rename from lib/insights/src/lib/i18n/da.json rename to lib/insights/src/lib/i18n/da-DK.json diff --git a/lib/insights/src/lib/i18n/de.json b/lib/insights/src/lib/i18n/de-DE.json similarity index 100% rename from lib/insights/src/lib/i18n/de.json rename to lib/insights/src/lib/i18n/de-DE.json diff --git a/lib/insights/src/lib/i18n/es.json b/lib/insights/src/lib/i18n/es-ES.json similarity index 100% rename from lib/insights/src/lib/i18n/es.json rename to lib/insights/src/lib/i18n/es-ES.json diff --git a/lib/insights/src/lib/i18n/fi.json b/lib/insights/src/lib/i18n/fi-FI.json similarity index 100% rename from lib/insights/src/lib/i18n/fi.json rename to lib/insights/src/lib/i18n/fi-FI.json diff --git a/lib/insights/src/lib/i18n/fr.json b/lib/insights/src/lib/i18n/fr-FR.json similarity index 100% rename from lib/insights/src/lib/i18n/fr.json rename to lib/insights/src/lib/i18n/fr-FR.json diff --git a/lib/insights/src/lib/i18n/it.json b/lib/insights/src/lib/i18n/it-IT.json similarity index 100% rename from lib/insights/src/lib/i18n/it.json rename to lib/insights/src/lib/i18n/it-IT.json diff --git a/lib/insights/src/lib/i18n/ja.json b/lib/insights/src/lib/i18n/ja-JP.json similarity index 100% rename from lib/insights/src/lib/i18n/ja.json rename to lib/insights/src/lib/i18n/ja-JP.json diff --git a/lib/insights/src/lib/i18n/nb.json b/lib/insights/src/lib/i18n/nb-NO.json similarity index 100% rename from lib/insights/src/lib/i18n/nb.json rename to lib/insights/src/lib/i18n/nb-NO.json diff --git a/lib/insights/src/lib/i18n/nl.json b/lib/insights/src/lib/i18n/nl-NL.json similarity index 100% rename from lib/insights/src/lib/i18n/nl.json rename to lib/insights/src/lib/i18n/nl-NL.json diff --git a/lib/insights/src/lib/i18n/pl.json b/lib/insights/src/lib/i18n/pl-PL.json similarity index 100% rename from lib/insights/src/lib/i18n/pl.json rename to lib/insights/src/lib/i18n/pl-PL.json diff --git a/lib/insights/src/lib/i18n/pt.json b/lib/insights/src/lib/i18n/pt-PT.json similarity index 100% rename from lib/insights/src/lib/i18n/pt.json rename to lib/insights/src/lib/i18n/pt-PT.json diff --git a/lib/insights/src/lib/i18n/ru.json b/lib/insights/src/lib/i18n/ru-RU.json similarity index 100% rename from lib/insights/src/lib/i18n/ru.json rename to lib/insights/src/lib/i18n/ru-RU.json diff --git a/lib/insights/src/lib/i18n/sv.json b/lib/insights/src/lib/i18n/sv-SE.json similarity index 100% rename from lib/insights/src/lib/i18n/sv.json rename to lib/insights/src/lib/i18n/sv-SE.json diff --git a/lib/process-services-cloud/karma.conf.js b/lib/process-services-cloud/karma.conf.js index b0d7c302ba..31a01343c9 100644 --- a/lib/process-services-cloud/karma.conf.js +++ b/lib/process-services-cloud/karma.conf.js @@ -30,8 +30,8 @@ module.exports = function (config) { '/base/assets/': '/base/lib/process-services/assets/', '/assets/adf-core/i18n/en.json': '/base/lib/core/src/lib/i18n/en.json', '/assets/adf-core/i18n/en-GB.json': '/base/lib/core/src/lib/i18n/en.json', - '/assets/adf-process-services-cloud/i18n/en.json': '/base/lib/process-services-cloud/lib/i18n/en.json', - '/assets/adf-process-services-cloud/i18n/en-GB.json': '/base/lib/process-services-cloud/lib/i18n/en.json', + '/assets/adf-process-services-cloud/i18n/en.json': '/base/lib/process-services-cloud/src/lib/i18n/en.json', + '/assets/adf-process-services-cloud/i18n/en-GB.json': '/base/lib/process-services-cloud/src/lib/i18n/en.json', '/app.config.json': '/base/lib/config/app.config.json' }, plugins: [ diff --git a/lib/process-services-cloud/src/lib/i18n/ar.json b/lib/process-services-cloud/src/lib/i18n/ar-SA.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/ar.json rename to lib/process-services-cloud/src/lib/i18n/ar-SA.json diff --git a/lib/process-services-cloud/src/lib/i18n/cs.json b/lib/process-services-cloud/src/lib/i18n/cs-CZ.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/cs.json rename to lib/process-services-cloud/src/lib/i18n/cs-CZ.json diff --git a/lib/process-services-cloud/src/lib/i18n/da.json b/lib/process-services-cloud/src/lib/i18n/da-DK.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/da.json rename to lib/process-services-cloud/src/lib/i18n/da-DK.json diff --git a/lib/process-services-cloud/src/lib/i18n/de.json b/lib/process-services-cloud/src/lib/i18n/de-DE.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/de.json rename to lib/process-services-cloud/src/lib/i18n/de-DE.json diff --git a/lib/process-services-cloud/src/lib/i18n/es.json b/lib/process-services-cloud/src/lib/i18n/es-ES.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/es.json rename to lib/process-services-cloud/src/lib/i18n/es-ES.json diff --git a/lib/process-services-cloud/src/lib/i18n/fi.json b/lib/process-services-cloud/src/lib/i18n/fi-FI.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/fi.json rename to lib/process-services-cloud/src/lib/i18n/fi-FI.json diff --git a/lib/process-services-cloud/src/lib/i18n/fr.json b/lib/process-services-cloud/src/lib/i18n/fr-FR.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/fr.json rename to lib/process-services-cloud/src/lib/i18n/fr-FR.json diff --git a/lib/process-services-cloud/src/lib/i18n/it.json b/lib/process-services-cloud/src/lib/i18n/it-IT.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/it.json rename to lib/process-services-cloud/src/lib/i18n/it-IT.json diff --git a/lib/process-services-cloud/src/lib/i18n/ja.json b/lib/process-services-cloud/src/lib/i18n/ja-JP.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/ja.json rename to lib/process-services-cloud/src/lib/i18n/ja-JP.json diff --git a/lib/process-services-cloud/src/lib/i18n/nb.json b/lib/process-services-cloud/src/lib/i18n/nb-NO.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/nb.json rename to lib/process-services-cloud/src/lib/i18n/nb-NO.json diff --git a/lib/process-services-cloud/src/lib/i18n/nl.json b/lib/process-services-cloud/src/lib/i18n/nl-NL.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/nl.json rename to lib/process-services-cloud/src/lib/i18n/nl-NL.json diff --git a/lib/process-services-cloud/src/lib/i18n/pl.json b/lib/process-services-cloud/src/lib/i18n/pl-PL.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/pl.json rename to lib/process-services-cloud/src/lib/i18n/pl-PL.json diff --git a/lib/process-services-cloud/src/lib/i18n/pt.json b/lib/process-services-cloud/src/lib/i18n/pt-PT.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/pt.json rename to lib/process-services-cloud/src/lib/i18n/pt-PT.json diff --git a/lib/process-services-cloud/src/lib/i18n/ru.json b/lib/process-services-cloud/src/lib/i18n/ru-RU.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/ru.json rename to lib/process-services-cloud/src/lib/i18n/ru-RU.json diff --git a/lib/process-services-cloud/src/lib/i18n/sv.json b/lib/process-services-cloud/src/lib/i18n/sv-SE.json similarity index 100% rename from lib/process-services-cloud/src/lib/i18n/sv.json rename to lib/process-services-cloud/src/lib/i18n/sv-SE.json diff --git a/lib/process-services/src/lib/i18n/ar.json b/lib/process-services/src/lib/i18n/ar-SA.json similarity index 100% rename from lib/process-services/src/lib/i18n/ar.json rename to lib/process-services/src/lib/i18n/ar-SA.json diff --git a/lib/process-services/src/lib/i18n/cs.json b/lib/process-services/src/lib/i18n/cs-CZ.json similarity index 100% rename from lib/process-services/src/lib/i18n/cs.json rename to lib/process-services/src/lib/i18n/cs-CZ.json diff --git a/lib/process-services/src/lib/i18n/da.json b/lib/process-services/src/lib/i18n/da-DK.json similarity index 100% rename from lib/process-services/src/lib/i18n/da.json rename to lib/process-services/src/lib/i18n/da-DK.json diff --git a/lib/process-services/src/lib/i18n/de.json b/lib/process-services/src/lib/i18n/de-DE.json similarity index 100% rename from lib/process-services/src/lib/i18n/de.json rename to lib/process-services/src/lib/i18n/de-DE.json diff --git a/lib/process-services/src/lib/i18n/es.json b/lib/process-services/src/lib/i18n/es-ES.json similarity index 100% rename from lib/process-services/src/lib/i18n/es.json rename to lib/process-services/src/lib/i18n/es-ES.json diff --git a/lib/process-services/src/lib/i18n/fi.json b/lib/process-services/src/lib/i18n/fi-FI.json similarity index 100% rename from lib/process-services/src/lib/i18n/fi.json rename to lib/process-services/src/lib/i18n/fi-FI.json diff --git a/lib/process-services/src/lib/i18n/fr.json b/lib/process-services/src/lib/i18n/fr-FR.json similarity index 100% rename from lib/process-services/src/lib/i18n/fr.json rename to lib/process-services/src/lib/i18n/fr-FR.json diff --git a/lib/process-services/src/lib/i18n/it.json b/lib/process-services/src/lib/i18n/it-IT.json similarity index 100% rename from lib/process-services/src/lib/i18n/it.json rename to lib/process-services/src/lib/i18n/it-IT.json diff --git a/lib/process-services/src/lib/i18n/ja.json b/lib/process-services/src/lib/i18n/ja-JP.json similarity index 100% rename from lib/process-services/src/lib/i18n/ja.json rename to lib/process-services/src/lib/i18n/ja-JP.json diff --git a/lib/process-services/src/lib/i18n/nb.json b/lib/process-services/src/lib/i18n/nb-NO.json similarity index 100% rename from lib/process-services/src/lib/i18n/nb.json rename to lib/process-services/src/lib/i18n/nb-NO.json diff --git a/lib/process-services/src/lib/i18n/nl.json b/lib/process-services/src/lib/i18n/nl-NL.json similarity index 100% rename from lib/process-services/src/lib/i18n/nl.json rename to lib/process-services/src/lib/i18n/nl-NL.json diff --git a/lib/process-services/src/lib/i18n/pl.json b/lib/process-services/src/lib/i18n/pl-PL.json similarity index 100% rename from lib/process-services/src/lib/i18n/pl.json rename to lib/process-services/src/lib/i18n/pl-PL.json diff --git a/lib/process-services/src/lib/i18n/pt.json b/lib/process-services/src/lib/i18n/pt-PT.json similarity index 100% rename from lib/process-services/src/lib/i18n/pt.json rename to lib/process-services/src/lib/i18n/pt-PT.json diff --git a/lib/process-services/src/lib/i18n/ru.json b/lib/process-services/src/lib/i18n/ru-RU.json similarity index 100% rename from lib/process-services/src/lib/i18n/ru.json rename to lib/process-services/src/lib/i18n/ru-RU.json diff --git a/lib/process-services/src/lib/i18n/sv.json b/lib/process-services/src/lib/i18n/sv-SE.json similarity index 100% rename from lib/process-services/src/lib/i18n/sv.json rename to lib/process-services/src/lib/i18n/sv-SE.json