Angular的翻译延迟加载

图片



如果您曾经参与过带有本地化支持的大型Angular项目的开发,那么本文适合您。如果不是,那么您可能想知道我们如何解决在应用程序启动时下载带有翻译的大文件的问题:在我们的情况下,每种语言大约2300行,大约200 KB。



一点背景



你好!我是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, .



GitHub

Stackblitz





VMmanager 6. , , . , .



, , .



? ?




All Articles