ローカリゼーションをサポートする大規模なAngularプロジェクトの開発に参加したことがある場合は、この記事が役に立ちます。そうでない場合は、アプリケーションの起動時に翻訳付きの大きなファイルをダウンロードする問題をどのように解決したのか疑問に思われるかもしれません。この場合、各言語で約2300行と約200KBです。
ちょっとした文脈
こんにちは!私はVMmanagerチームのISPsystemのフロントエンド開発者です。
, 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. , , . , .
, , .
? ?