If you have ever participated in the development of a large Angular project with localization support, then this article is for you. If not, then you might be wondering how we solved the problem of downloading large files with translations at application startup: in our case, ~ 2300 lines and ~ 200 KB for each language.
A bit of context
Hello! I am a Frontend developer at ISPsystem in the VMmanager team .
, frontend-. angular 9- . ngx-translate. json-. POEditor.
?
-, json- .
, , 2 .
, , ( , , ), .
-, json- .
, . namespace . , TITLE
, HOME
(HOME.....TITLE
), TITLE
, HOME
.
?
: , .
angular. angular-, .
() , . , , , , ? .
, , ยซยป ( ).
:
<projectRoot>/i18n/
ru.json
en.json
HOME/
ru.json
en.json
HOME.COMMON/
ru.json
en.json
ADMIN/
ru.json
en.json
json โ , (, ). HOME
โ . ADMIN
โ .
HOME.COMMON
โ , .
json- , namespace:
-
{...}
; -
ADMIN
{ "ADMIN": {...} }
; -
HOME.COMMON
{ "HOME": { "COMMON": {...} } }
; - ..
, .
. , .
ngx-translate , , :
- โ , ;
- โ , .
: TranslateLoader
, abstract getTranslation(lang: string): Observable<any>
. TranslateLoader
( ngx-translate), .
, - , , :
export class MyTranslationLoader extends TranslateLoader implements OnDestroy {
/** ( , ) */
private static TRANSLATES_LOADED: { [lang: string]: { [scope: string]: boolean } } = {};
/** ( ) */
private sortedScopes = typeof this.scopes === 'string' ? [this.scopes] : this.scopes.slice().sort((a, b) => a.length - b.length);
private getURL(lang: string scope: string): string {
// ,
// i18n
return `i18n/${scope ? scope + '/' : ''}${lang}.json`;
}
/** , */
private loadScope(lang: string, scope: string): Observable<object> {
return this.httpClient.get(this.getURL(lang, scope)).pipe(
tap(() => {
if (!MyTranslationLoader.TRANSLATES_LOADED[lang]) {
MyTranslationLoader.TRANSLATES_LOADED[lang] = {};
}
MyTranslationLoader.TRANSLATES_LOADED[lang][scope] = true;
})
);
}
/**
*
* .. , ,
* ,
* , scope ,
* HOME.COMMON HOME,
*/
private merge(scope: string, source: object, target: object): object {
// root
if (!scope) {
return { ...target };
}
const parts = scope.split('.');
const scopeKey = parts.pop();
const result = { ...source };
// ,
const sourceObj = parts.reduce(
(acc, key) => (acc[key] = typeof acc[key] === 'object' ? { ...acc[key] } : {}),
result
);
//
sourceObj[scopeKey] = parts.reduce((res, key) => res[key] || {}, target)?.[scopeKey] || {};
return result;
}
constructor(private httpClient: HttpClient, private scopes: string | string[]) {
super();
}
ngOnDestroy(): void {
// , hot reaload
MyTranslationLoader.TRANSLATES_LOADED = {};
}
getTranslation(lang: string): Observable<object> {
// scope
const loadScopes = this.sortedScopes.filter(s => !MyTranslationLoader.TRANSLATES_LOADED?.[lang]?.[s]);
if (!loadScopes.length) {
return of({});
}
//
return zip(...loadScopes.map(s => this.loadScope(lang, s))).pipe(
map(translates => translates.reduce((acc, t, i) => this.merge(loadScopes[i], acc, t), {}))
);
}
}
, scope url , json, .
, .
: MissingTranslationHandler
, , handle
. MissingTranslationHandler
, ngx-translate.
ngx-translate :
export declare abstract class MissingTranslationHandler {
/**
* A function that handles missing translations.
*
* @param params context for resolving a missing translation
* @returns a value or an observable
* If it returns a value, then this value is used.
* If it return an observable, the value returned by this observable will be used (except if the method was "instant").
* If it doesn't return then the key will be used as a value
*/
abstract handle(params: MissingTranslationHandlerParams): any;
}
: Observable
.
export class MyMissingTranslationHandler extends MissingTranslationHandler {
// Observable , .. , ,
// translate pipe handle
private translatesLoading: { [lang: string]: Observable<object> } = {};
handle(params: MissingTranslationHandlerParams) {
const service = params.translateService;
const lang = service.currentLang || service.defaultLang;
if (!this.translatesLoading[lang]) {
// loader ( , )
this.translatesLoading[lang] = service.currentLoader.getTranslation(lang).pipe(
// ngx-translate
// true ,
tap(t => service.setTranslation(lang, t, true)),
map(() => service.translations[lang]),
shareReplay(1),
take(1)
);
}
return this.translatesLoading[lang].pipe(
//
map(t => service.parser.interpolate(service.parser.getValue(t, params.key), params.interpolateParams)),
// , โ
catchError(() => of(params.key))
);
}
}
(HOME.TITLE
), ngx-translate (['HOME', 'TITLE']
). , catchError
of(typeof params.key === 'string' ? params.key : params.key.join('.'))
.
, TranslateModule
:
export function loaderFactory(scopes: string | string[]): (http: HttpClient) => TranslateLoader {
return (http: HttpClient) => new MyTranslationLoader(http, scopes);
}
// ...
// app.module.ts
TranslateModule.forRoot({
useDefaultLang: false,
loader: {
provide: TranslateLoader,
useFactory: loaderFactory(''),
deps: [HttpClient],
},
})
// home.module.ts
TranslateModule.forChild({
useDefaultLang: false,
extend: true,
loader: {
provide: TranslateLoader,
useFactory: loaderFactory(['HOME', 'HOME.COMMON']),
deps: [HttpClient],
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler,
},
})
// admin.module.ts
TranslateModule.forChild({
useDefaultLang: false,
extend: true,
loader: {
provide: TranslateLoader,
useFactory: loaderFactory(['ADMIN', 'HOME.COMMON']),
deps: [HttpClient],
},
missingTranslationHandler: {/*...*/},
})
useDefaultLang: false
missingTranslationHandler
.
extend: true
( ngx-translate@12.0.0) , .
, , :
export function translateConfig(scopes: string | string[]): TranslateModuleConfig {
return {
useDefaultLang: false,
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory(scopes),
deps: [HttpClient],
},
};
}
@NgModule()
export class MyTranslateModule {
static forRoot(scopes: string | string[] = [], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {
return TranslateModule.forRoot({
...translateConfig([''].concat(scopes)),
...config,
});
}
static forChild(scopes: string | string[], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {
return TranslateModule.forChild({
...translateConfig(scopes),
extend: true,
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler,
},
...config,
});
}
}
, ( translate ) TranslateModule
.
( ngx-translate@12.1.2) , , , translate
[object Object]
. .
POEditor
, POEditor, . API:
, . , , .
python3 .
, MyTranslateLoader
. , , .
:
split
โ , , ( โ i18n);join
โ : json stdout, ;download
โ POEditor, , , ;upload
โ POEditor , ;hash
โ md5 . , , .
argparse
, --help
.
, , .
, , . stackblitz, .
VMmanager 6. , , . , .
, , .
? ?