Se vocĂȘ jĂĄ participou do desenvolvimento de um grande projeto Angular com suporte de localização, este artigo Ă© para vocĂȘ. Se nĂŁo, vocĂȘ deve estar se perguntando como resolvemos o problema de baixar arquivos grandes com traduçÔes quando o aplicativo Ă© iniciado: em nosso caso, aproximadamente 2300 linhas e aproximadamente 200 KB para cada idioma.
Um pouco de contexto
OlĂĄ! Eu sou um desenvolvedor Frontend no ISPsystem na equipe 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. , , . , .
, , .
? ?