[AAE-46242] - Changed the name convention for translation files to ISO standard naming (#11924)

* [AAE-46242] - Changed the name convention for translation files to ISO standard naming

* [AAE-46242] - revert en-GB to en.json

* [AAE-46242] - fixed crowding config

* [AAE-46242] - added fallback fix'

* [AAE-46242] - Added defaultLang missing method

* [AAE-46242] - Fixing unit test

* [AAE-46242] - fixed core unit tests
This commit is contained in:
Vito Albano
2026-05-29 19:11:17 +01:00
committed by GitHub
parent e40fd2250e
commit a2b026ed7e
89 changed files with 286 additions and 132 deletions
+1 -1
View File
@@ -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"
}
@@ -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' }
];
@@ -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;
}
}
@@ -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)' }
];
+40 -60
View File
@@ -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<string, Locale> = {
'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 = {
@@ -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();
});
}
+14 -14
View File
@@ -23,23 +23,23 @@ import { LanguageItem } from '../common/services/language-item.interface';
@Injectable()
export class LanguageServiceMock implements LanguageServiceInterface {
private readonly languages = new BehaviorSubject<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' }
]);
languages$ = this.languages.asObservable();
@@ -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())
];
@@ -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<string, string> = {
'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<void> {
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<void> {
const fallbacks = this.getLocaleFallbacks(lang);
return this.tryFetchWithFallbacks(lang, fallbacks, component);
}
private tryFetchWithFallbacks(originalLang: string, fallbacks: string[], component: ComponentTranslationModel): Observable<void> {
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}`));
})
@@ -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();
}));
});
});
@@ -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: {
@@ -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) {
+2 -2
View File
@@ -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'
},
+2 -2
View File
@@ -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: [