Si alguna vez ha participado en el desarrollo de un gran proyecto Angular con soporte de localización, este artículo es para usted. Si no es así, es posible que se pregunte cómo resolvimos el problema de descargar archivos grandes con traducciones al inicio de la aplicación: en nuestro caso ~ 2300 líneas y ~ 200 KB para cada idioma.
Un poco de contexto
¡Hola! Soy un desarrollador de frontend en ISPsystem en el equipo VMmanager .
, 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. , , . , .
, , .
? ?