Wenn Sie jemals an der Entwicklung eines großen Angular-Projekts mit Lokalisierungsunterstützung teilgenommen haben, ist dieser Artikel genau das Richtige für Sie. Wenn nicht, fragen Sie sich möglicherweise, wie wir das Problem des Herunterladens großer Dateien mit Übersetzungen zu Beginn der Anwendung gelöst haben: in unserem Fall ~ 2300 Zeilen und ~ 200 KB für jede Sprache.
Ein bisschen Kontext
Hallo! Ich bin Frontend-Entwickler bei ISPsystem im 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. , , . , .
, , .
? ?