Traducerile dinamice în Angular au fost posibile

Un ghid practic pentru implementarea traducerilor lazy-loaded

Dacă v-ați ocupat vreodată de internaționalizare (sau „i18n” pe scurt) în Angular sau sunteți pe cale să o implementați, puteți rămâne cu „ghidul oficial” care este minunat, utilizați pachete terțe care ar putea fi greu de depanat sau de ales o cale alternativă pe care o voi descrie mai jos.

Unul dintre capcanele comune atunci când utilizați i18n este dimensiunea mare a fișierelor de traducere și incapacitatea de a le împărți pentru a ascunde părți ale aplicației dvs. de privirile indiscrete. Unele soluții precum implementarea încorporată Angular sunt cu adevărat puternice și compatibile cu SEO, dar necesită multă pregătire și nu acceptă schimbarea de limbă din mers în modul de dezvoltare (ceea ce „provoca probleme” cel puțin în versiunea 9); alte soluții precum ngx-translate necesită să instalați mai multe pachete și totuși nu acceptă împărțirea unei singure limbi (actualizare: de fapt, ngx-translate acceptă acest lucru).

Deși nu există nicio „baghetă magică” pentru această caracteristică complexă care acceptă totul și se potrivește tuturor, iată o altă modalitate de implementare a traducerilor care s-ar putea să se potrivească nevoilor dvs.
Ajunge cu introducerea, am promis că va fi un lucru practic. ghid, așa că hai să sărim direct în el.

Pregătirea elementelor de bază

Primul pas este să creați un tip pentru limbi care vor fi folosite în aplicație:

export type LanguageCode = 'en' | 'de';

Una dintre caracteristicile Angular iubite este Dependency Injection, care face multe pentru noi - să o folosim pentru nevoile noastre. De asemenea, aș dori să condimentez puțin lucrurile folosind NgRx pentru acest ghid, dar dacă nu îl utilizați în proiectul dvs., nu ezitați să îl înlocuiți cu un simplu BehaviorSubject.

Ca pas opțional care va facilita dezvoltarea ulterioară cu NgRx, creați un tip pentru fabricile DI:

export type Ti18nFactory<Part> = (store: Store) => Observable<Part>;

Crearea fișierelor de traducere

Corzi generale

Să presupunem că avem câteva șiruri de caractere de bază pe care am dori să le folosim în aplicație. Câteva lucruri simple, dar comune, care nu sunt niciodată legate de un anumit modul, caracteristică sau bibliotecă, cum ar fi butoanele „OK” sau „Înapoi”.
Vom plasa aceste șiruri în modulul „nucleu” și vom începe să facem acest lucru cu o interfață simplă care ne va ajuta să nu uităm niciun șir de caractere în traduceri:

export interface I18nCore {
  errorDefault: string;
  language: string;
}

Pentru a fi clar, această interfață nu garantează că toate șirurile vor fi de fapt traduse, dar compilatorul TypeScript (și IDE-ul dvs.) va genera o eroare „TS2741” dacă uitați să includeți orice șir în fișierele „lang”.

Trecând la implementarea pentru interfață și pentru acest fragment, este extrem de important să furnizez un exemplu de cale de fișier care, în acest caz, ar fi libs/core/src/lib/i18n/lang-en.lang.ts:

export const lang: I18nCore = {
  errorDefault: 'An error has occurred',
  language: 'Language',
};

Pentru a reduce duplicarea codului și a profita la maximum de procesul de dezvoltare, vom crea, de asemenea, o fabrică de DI. Iată un exemplu de lucru care utilizează NgRx (din nou, acesta este complet opțional, puteți utiliza BehaviorSubject pentru aceasta):

export const I18N_CORE =
  new InjectionToken<Observable<I18nCore>>('I18N_CORE');

export const i18nCoreFactory: Ti18nFactory<I18nCore> =
  (store: Store): Observable<I18nCore> => 
    (store as Store<LocalePartialState>).pipe(
      select(getLocaleLanguageCode),
      distinctUntilChanged(),
      switchMap((code: LanguageCode) =>
        import(`./lang-${code}.lang`)
          .then((l: { lang: I18nCore }) => l.lang)
      ),
    );

export const i18nCoreProvider: FactoryProvider = {
  provide: I18N_CORE,
  useFactory: i18nCoreFactory,
  deps: [Store],
};

Evident, selectorul getLocaleLanguageCode va alege codul de limbă din Magazin.

Nu uitați să includeți fișierele de traducere în compilație, deoarece acestea nu sunt referite direct, astfel încât nu vor fi incluse automat. Pentru aceasta, localizați „tsconfig” relevant (cel care listează „main.ts”) și adăugați următoarele la matricea „include”:

"../../libs/core/src/lib/i18n/*.lang.ts"

Rețineți că calea fișierului de aici include un wildcard, astfel încât toate traducerile dvs. să fie incluse simultan. De asemenea, ca o chestiune de gust, îmi place să prefix fișierele similare, ceea ce explică destul de mult de ce numele exemplului ([prefix]-[langCode].lang.ts) arată atât de ciudat.

Șiruri specifice modulului

Să facem același lucru pentru orice modul, astfel încât să putem vedea cum traducerile vor fi încărcate separat în browser. Pentru a rămâne simplu, acest modul ar fi numit „tab1”.

Din nou, începeți cu interfața:

export interface I18nTab1 {
  country: string;
}

Implementați această interfață:

export const lang: I18nTab1 = {
  country: 'Country',
};

Includeți traducerile dvs. în compilație:

"../../libs/tab1/src/lib/i18n/*.lang.ts"

Și, opțional, creați o fabrică DI care ar arăta literalmente la fel ca anterioară, dar cu o altă interfață.

Furnizarea de traduceri

Prefer să reduc numărul de furnizori, astfel încât traducerile „de bază” vor fi listate numai în AppModule:

providers: [i18nCoreProvider],

Orice altă traducere ar trebui să fie furnizată numai în modulele relevante - fie în modulele de caracteristici încărcate lene sau, dacă urmați modelul SCAM, în modulele componente:

@NgModule({
  declarations: [TabComponent],
  imports: [CommonModule, ReactiveFormsModule],
  providers: [i18nTab1Provider],
})
export class TabModule {}

Rețineți, de asemenea, eleganța utilizării FactoryProviders prefabricate în loc de a adăuga obiecte aici.

Injectați jetoanele într-un component.ts:

constructor(
  @Inject(I18N_CORE)
  public readonly i18nCore$: Observable<I18nCore>,
  @Inject(I18N_TAB1)
  public readonly i18nTab1$: Observable<I18nTab1>,
) {}

Și, în sfârșit, înfășurați component.html cu ng-container și o instrucțiune simplă ngIf:

<ng-container *ngIf="{
    core: i18nCore$ | async,
    tab1: i18nTab1$ | async
  } as i18n">
    <p>{{ i18n.core?.language }}</p>
    <p>{{ i18n.tab1?.country }}: n/a</p>
</ng-container>

Verificarea rezultatului

Să rulăm asta și să vedem dacă funcționează cu adevărat și, mai important, cum vor fi încărcate aceste traduceri. Am creat o aplicație demonstrativă simplă constând din două module Angular încărcate leneș, astfel încât să puteți clona și experimenta cu ea. Dar pentru moment, iată capturile de ecran reale ale DevTools:

Beneficii

  • Cu această soluție veți fi capabil, dar nu obligați, să vă împărțiți traducerile în mai multe fișiere în orice mod de care aveți nevoie;
  • Este reactiv, ceea ce înseamnă că, fiind implementat corect, oferă utilizatorilor o experiență perfectă;
  • Nu necesită să instalați nimic care nu este livrat cu Angular din cutie;
  • Este ușor de depanat și complet personalizabil, deoarece ar fi implementat direct în proiectul dvs.;
  • Acceptă rezoluții locale complexe, cum ar fi relația cu limba browserului, preluarea setărilor regionale din contul de utilizator la autorizare și înlocuirea cu o limbă definită de utilizator - și toate acestea fără o singură reîncărcare a paginii;
  • De asemenea, acceptă completarea codului în IDE-urile moderne.

Dezavantaje

  • Deoarece aceste fișiere de traducere nu vor fi incluse în active, ele ar trebui să fie transpilate, ceea ce va crește ușor timpul de construire;
  • Este necesar să creați un utilitar personalizat sau să utilizați o soluție terță parte pentru a vă schimba traducerile cu o platformă de localizare;
  • S-ar putea să nu funcționeze foarte bine cu motoarele de căutare fără o redare adecvată pe partea serverului.

GitHub

Simțiți-vă liber să experimentați cu exemplul complet funcțional care este disponibil în „acest depozit”.
Rămâneți pozitiv și creați aplicații grozave!