Faules Laden von Übersetzungen mit Angular

Bild



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, .



GitHub

Stackblitz





VMmanager 6. , , . , .



, , .



? ?




All Articles