Możliwe dynamiczne tłumaczenia w Angular

Praktyczny przewodnik po wdrażaniu leniwie ładowanych tłumaczeń

Jeśli kiedykolwiek miałeś do czynienia z internacjonalizacją (w skrócie „i18n”) w Angularze lub masz zamiar ją wdrożyć, możesz trzymać się „oficjalnego przewodnika”, który jest świetny, używać pakietów stron trzecich, które mogą być trudne do debugowania lub wyboru alternatywną ścieżkę, którą opiszę poniżej.

Jedną z typowych pułapek podczas korzystania z i18n jest duży rozmiar plików tłumaczeń i brak możliwości ich podzielenia w celu ukrycia części aplikacji przed wzrokiem ciekawskich. Niektóre rozwiązania, takie jak wbudowana implementacja Angulara, są naprawdę potężne i kompatybilne z SEO, ale wymagają wielu przygotowań i nie obsługują przełączania języków w locie w trybie programistycznym (co „powodowało problemy” przynajmniej w wersji 9); inne rozwiązania, takie jak ngx-translate wymagają zainstalowania kilku pakietów i nadal nie obsługują podziału na jeden język (aktualizacja: w rzeczywistości ngx-translate to obsługuje).

Chociaż nie ma „magicznej różdżki” dla tej złożonej funkcji, która obsługuje wszystko i pasuje do każdego, oto inny sposób wdrożenia tłumaczeń, który może odpowiadać Twoim potrzebom.
Dość wprowadzenia, obiecałem, że będzie to praktyczne przewodnika, więc przejdźmy od razu do niego.

Przygotowanie podstaw

Pierwszym krokiem jest utworzenie typu dla języków, które będą używane w aplikacji:

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

Jedną z ulubionych funkcji Angulara jest wstrzykiwanie zależności, które robi dla nas wiele — wykorzystajmy ją do naszych potrzeb. Chciałbym też trochę urozmaicić, używając NgRx w tym przewodniku, ale jeśli nie używasz go w swoim projekcie, możesz zastąpić go prostym obiektem BehaviourSubject.

Jako opcjonalny krok, który ułatwi dalszy rozwój z NgRx, utwórz typ dla fabryk DI:

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

Tworzenie plików tłumaczeniowych

Ciągi ogólne

Załóżmy, że mamy kilka podstawowych ciągów znaków, których chcielibyśmy używać w aplikacji. Kilka prostych, ale typowych rzeczy, które nigdy nie są powiązane z konkretnym modułem, funkcją lub biblioteką, jak przyciski „OK” lub „Wstecz”.
Umieścimy te ciągi w module „podstawowym” i zaczniemy to robić za pomocą prostego interfejsu to pomoże nam nie zapomnieć żadnego pojedynczego ciągu w naszych tłumaczeniach:

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

Żeby było jasne, ten interfejs nie gwarantuje, że wszystkie ciągi znaków zostaną faktycznie przetłumaczone, ale kompilator TypeScriptu (i twoje IDE) zgłosi błąd „TS2741”, jeśli zapomnisz dołączyć jakikolwiek ciąg do plików „lang”.

Przechodząc do implementacji interfejsu i tego fragmentu, niezwykle ważne jest podanie przykładowej ścieżki pliku, która w tym przypadku będzie wynosić libs/core/src/lib/i18n/lang-en.lang.ts:

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

Aby ograniczyć powielanie kodu i maksymalnie wykorzystać proces rozwoju, utworzymy również fabrykę DI. Oto działający przykład wykorzystania NgRx (ponownie jest to całkowicie opcjonalne, możesz w tym celu użyć BehaviourSubject):

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],
};

Oczywiście selektor getLocaleLanguageCode wybierze kod języka ze Sklepu.

Nie zapomnij dołączyć plików tłumaczeń do swojej kompilacji, ponieważ nie są one bezpośrednio przywoływane, a zatem nie zostaną uwzględnione automatycznie. W tym celu znajdź odpowiedni plik „tsconfig” (ten, który zawiera listę „main.ts”) i dodaj następujące elementy do tablicy „include”:

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

Pamiętaj, że ścieżka pliku zawiera symbol wieloznaczny, dzięki czemu wszystkie tłumaczenia zostaną uwzględnione jednocześnie. Ponadto, ze względu na gust, lubię poprzedzać podobne pliki, co w dużym stopniu wyjaśnia, dlaczego przykładowa nazwa ([prefix]-[langCode].lang.ts) wygląda tak dziwnie.

Ciągi specyficzne dla modułu

Zróbmy to samo dla dowolnego modułu, abyśmy mogli zobaczyć, jak tłumaczenia będą ładowane osobno w przeglądarce. Dla uproszczenia moduł ten będzie nosił nazwę „tab1”.

Ponownie zacznij od interfejsu:

export interface I18nTab1 {
  country: string;
}

Zaimplementuj ten interfejs:

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

Dołącz swoje tłumaczenia do kompilacji:

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

I opcjonalnie utwórz fabrykę DI, która wyglądałaby dosłownie tak samo jak poprzednia, ale z innym interfejsem.

Dostarczanie tłumaczeń

Wolę zmniejszyć liczbę dostawców, więc „podstawowe” tłumaczenia będą wymienione tylko w AppModule:

providers: [i18nCoreProvider],

Wszelkie inne tłumaczenia należy udostępniać wyłącznie w odpowiednich modułach — albo w modułach funkcji ładowanych z opóźnieniem, albo, jeśli stosujesz wzorzec SCAM, w modułach składowych:

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

Zwróć także uwagę na elegancję korzystania z gotowych dostawców FactoryProviders zamiast dodawania tutaj obiektów.

Wstrzyknij tokeny w component.ts:

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

I na koniec zawiń component.html kontenerem ng i prostą instrukcją 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>

Sprawdzanie wyniku

Uruchommy to i zobaczmy, czy to rzeczywiście działa, a co ważniejsze, jak dokładnie te tłumaczenia zostaną załadowane. Stworzyłem prostą aplikację demonstracyjną składającą się z dwóch leniwie ładowanych modułów Angulara, więc możesz ją klonować i eksperymentować z nią. Ale na razie oto rzeczywiste zrzuty ekranu DevTools:

Korzyści

  • Dzięki temu rozwiązaniu możesz, ale nie musisz, podzielić swoje tłumaczenia na kilka plików w dowolny sposób;
  • Jest reaktywny, co oznacza, że ​​poprawnie wdrożony zapewnia użytkownikom bezproblemową obsługę;
  • Nie wymaga instalowania czegokolwiek, co nie jest dostarczane z Angularem od razu po wyjęciu z pudełka;
  • Można go łatwo debugować i w pełni dostosowywać, ponieważ można go zaimplementować bezpośrednio w projekcie;
  • Obsługuje złożone rozwiązania regionalne, takie jak odniesienie do języka przeglądarki, pobieranie ustawień regionalnych z konta użytkownika po autoryzacji i zastępowanie językiem zdefiniowanym przez użytkownika – a wszystko to bez ponownego ładowania jednej strony;
  • Obsługuje także uzupełnianie kodu w nowoczesnych środowiskach IDE.

Wady

  • Ponieważ te pliki tłumaczeń nie zostaną uwzględnione w zasobach, należy je w rzeczywistości przetransponować, co nieznacznie wydłuży czas kompilacji;
  • Wymaga utworzenia niestandardowego narzędzia lub skorzystania z rozwiązania innej firmy w celu wymiany tłumaczeń z platformą lokalizacyjną;
  • Może nie działać zbyt dobrze z wyszukiwarkami bez odpowiedniego renderowania po stronie serwera.

GitHub

Zachęcamy do eksperymentowania z w pełni działającym przykładem, który jest dostępny w „tym repozytorium”.
Bądź dobrej myśli i twórz wspaniałe aplikacje!