Internationalization with @angular/localize

Na froncie i18n dokonał się wielki postęp! Nowy pakiet o nazwie @angular/localize został wprowadzony w Angular 9.0.

Jest on używany pod maską, aby dać nam te same funkcje, które mieliśmy wcześniej:tłumaczenia w szablonach w czasie kompilacji.

Ale pozwala nam mieć nadzieję na więcej w przyszłości, z nieudokumentowanymi funkcjami już dostępnymi, jak tłumaczenia w kodzie, lub tłumaczenia runtime zamiast tłumaczeń tylko w kompilacji 😎.

Zacznijmy od zobaczenia, co możemy zrobić z pomocą @angular/localizei CLI w v9.

i18n w szablonach

Nowy pakiet @angular/localize oferuje funkcję o nazwie $localize.

Istniejące wsparcie i18n w Angular wykorzystuje teraz $localize, co oznacza, że szablony takie jak:

<h1 i18n>Hello</h1>

będą kompilowane do wywołań $localize.

Jeśli uruchomisz ng serve z takim szablonem, napotkasz błąd runtime:

Error: It looks like your application or one of its dependencies is using i18n.Angular 9 introduced a global `$localize()` function that needs to be loaded.Please run `ng add @angular/localize` from the Angular CLI.(For non-CLI projects, add `import '@angular/localize/init';` to your `polyfills.ts` file.For server-side rendering applications add the import to your `main.server.ts` file.)

Błąd jest oczywisty: ponieważ atrybuty i18n są teraz konwertowane na wywołania $localize w wygenerowanym kodzie, musimy załadować funkcję $localize.Można to zrobić domyślnie, ale ponieważ jest to wymagane tylko wtedy, gdy używasz internacjonalizacji, nie miałoby to sensu.

Dlatego jeśli twoja aplikacja, lub jedna z jej zależności, używa atrybutów i18n w swoich szablonach, to musisz dodać import '@angular/localize/init' do swoich polyfills!

CLI oferuje schemat, aby zrobić to za ciebie.Po prostu uruchom:

ng add @angular/localize

i CLI doda pakiet do twoich zależności i niezbędny import do twoich polyfills.

Kiedy uruchomisz swoją aplikację z prostym ng serve,$localize po prostu wyświetli oryginalny komunikat.

Jak przetłumaczyć te komunikaty?

Proces jest bardzo podobny do tego, który mieliśmy poprzednio.Najpierw uruchamiasz ng xi18n, aby wyodrębnić wiadomości w pliku messages.xlf.Następnie tłumaczysz plik dla swoich języków lokalnych, na przykład messages.fr.xlfi messages.es.xlf.

Następnie należy skonfigurować CLI, w angular.json

{ "projects": { "ponyracer": { "projectType": "application", // ... "i18n": { "locales": { "fr": "src/locale/messages.fr.xlf", "es": "src/locale/messages.es.xlf", } }, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", // ... "configurations": { "production": { // ... }, "fr": { "localize": }, "es": { "localize": } } }, "serve": { // ... "configurations": { "production": { // ... }, "fr": { "browserTarget": "ponyracer:build:fr" }, "es": { "browserTarget": "ponyracer:build:es" } } } // ...}

Teraz konfiguracje es lub fr pozwalają uruchomić:

ng serve --configuration=fr

A serwowana aplikacja jest teraz w języku francuskim!

Możesz także zbudować aplikację z określonym locale:

ng build --configuration=production,es

lub ze wszystkimi locale na raz:

ng build --prod --localize

To duży postęp w porównaniu do poprzednich wersji Angulara.Kiedyś musieliśmy budować tę samą aplikację dla każdego locale, ponieważ tłumaczenie było częścią kompilacji.Teraz, gdy Angular kompiluje aplikację, generuje wywołania $localize.Następnie, gdy to się stanie, narzędzie pobiera skompilowaną aplikację i zastępuje wszystkie wywołania $localize odpowiednimi tłumaczeniami.Jest to super szybkie.Następnie masz pakiet nie zawierający wywołań do $localize i wszystkie ciągi i18n zostały przetłumaczone.

Do tej pory musiałeś zbudować swoją aplikację raz na locale, a to był pełny build. Powiedzmy, że jest to 30s kompilacji, a ty chciałeś mieć 4 wersje lokalne, więc miałeś 2 minuty.

Z nowym podejściem, kompilacja jest wykonywana raz, a następnie różne wersje i18n są generowane w ciągu kilku sekund (jest to nawet generowane równolegle, jeśli to możliwe).Więc idziesz z 2 minut do ~40 sekund! 🌈

Masz wtedy kilka pakietów, po jednym na locale, i możesz zaserwować odpowiedni dla swoich użytkowników w zależności od ich preferencji, tak jak to robiłeś.

Ta strategia nazywa się compile-time inliningas, ponieważ inline’ujesz tłumaczenia bezpośrednio, a potem nie ma już nic do zrobienia w runtime.

Teraz porozmawiajmy o czymś jeszcze nieudokumentowanym, co może się zmienić w przyszłości, ale nadal interesującym: możemy teraz również tłumaczyć wiadomości w naszym kodzie TypeScript!

Funkcja $localize, o której mówiłem, może być używana bezpośrednio. Jest to osobliwa funkcja, której można użyć do oznaczenia łańcucha szablonu w celu lokalizacji.

Ale może powinniśmy zacząć od wyjaśnienia, czym jest oznaczony łańcuch szablonu?

Szablonowe łańcuchy i funkcje znaczników

Przy użyciu łańcuchów szablonów można zdefiniować funkcję znacznika i zastosować ją do szablonowego łańcucha.Tutaj askQuestion dodaje punkt zapytania na końcu łańcucha:

const askQuestion = strings => strings + '?';const template = askQuestion`Is anyone here`;

Czym więc różni się od zwykłej funkcji?Funkcja tagu w rzeczywistości otrzymuje kilka argumentów:

  • tablica statycznych części łańcucha
  • wartości wynikające z oceny wyrażeń

Na przykład jeśli mamy szablon łańcucha zawierający wyrażenia:

const person1 = 'Cedric';const person2 = 'Agnes';const template = `Hello ${person1}! Where is ${person2}?`;

to funkcja tagu otrzyma różne statyczne i dynamiczne części.Tutaj mamy funkcję znacznika, która ma wstawiać wielkie litery w imionach bohaterów:

const uppercaseNames = (strings, ...values) => { // `strings` is an array with the static parts // `values` is an array with the evaluated expressions const names = values.map(name => name.toUpperCase()); // `names` now has // let's merge the `strings` and `names` arrays return strings.map((string, i) => `${string}${names ? names : ''}`).join('');};const result = uppercaseNames`Hello ${person1}! Where is ${person2}?`;// returns 'Hello CEDRIC! Where is AGNES?'

i18n z $localize w kodzie TypeScript

$localize wykorzystuje tę mechanikę, aby pozwolić nam napisać:

@Component({ template: '{{ title }}'})export class HomeComponent { title = $localize`You have 10 users`;}

Zauważ, że nie musisz importować funkcji.Tak długo, jak dodajesz import '@angular/localize/init' raz w swojej aplikacji, $localize zostanie dodane do obiektu globalnego.

Możesz wtedy przetłumaczyć wiadomość w taki sam sposób, jak w przypadku szablonu.Ale w tej chwili (v9.0.0), CLI nie wyodrębnia tych wiadomości za pomocą polecenia xi18n, jak to robi dla szablonów.

Jeśli obsługujesz aplikację i nie znaleziono tłumaczenia, $localize po prostu wyświetla oryginalny łańcuch i rejestruje ostrzeżenie w konsoli:

No translation found for "6480943972743237078" ("You have 10 users").

Musisz więc ręcznie dodać go do swojego messages.fr.xlfz podanym IDif chcesz spróbować:

<trans-unit> <source>You have 10 users</source> <target>Vous avez 10 utilisateurs</target></trans-unit>

Szablon mojego HomeComponent wyświetla wtedy Vous avez 10 utilisateurs!

Co się stanie, jeśli masz jakieś dynamiczne wyrażenie w swoim łańcuchu szablonu?

title = $localize`Hi ${this.name}! You have ${this.users.length} users.`;

Wyrażenia te zostaną automatycznie nazwane PH i PH_1 (PH jest dla placeholder).Następnie możesz użyć tych placeholderów gdziekolwiek chcesz w tłumaczeniach:

<trans-unit> <source>Hi <x/>! You have <x/> users.</source> <target>Bonjour <x/>&nbsp;! Vous avez <x/> utilisateurs.</target></trans-unit>

Ale najlepszą praktyką jest nadanie znaczącej nazwy placeholderowi samemu wyrażeniu, a możesz to zrobić używając składni ${expression}:placeholder:.

title = $localize`Hi ${this.name}:name:! You have ${this.users.length}:userCount: users.`;

Wtedy możesz użyć tego placeholdera gdziekolwiek chcesz w tłumaczeniach:

<trans-unit> <source>Hi <x/>! You have <x/> users.</source> <target>Bonjour <x/>&nbsp;! Vous avez <x/> utilisateurs.</target></trans-unit>

Custom IDs

Zauważ, że jeśli masz tłumaczenia z niestandardowymi identyfikatorami, są one używane przez $localize (tak jak poprzednio):

<h1 i18n="@@home.greetings">Hello</h1>

Wtedy twoje tłumaczenie wygląda jak:

<trans-unit> <source>Hello</source> <target>Bonjour</target></trans-unit>

co jest oczywiście ładniejsze w użyciu.

A co z tłumaczeniami w kodzie? $localize również rozumie składnię pozwalającą na określenie ID:

title = $localize`:@@home.users:You have 10 users`;

Składnia dla niestandardowego ID jest taka sama jak w szablonach, a ID jest otoczone dwukropkami, aby oddzielić je od treści tłumaczenia.

Tak jak w przypadku składni szablonów, możesz również określić opis i znaczenie, aby pomóc tłumaczom z odrobiną kontekstu: :meaning|description@@id:message.

Na przykład:

title = $localize`:greeting message with the number of users currently logged in@@home.users:You have 10 users`;

Miej na uwadze, że jest to niskopoziomowe, nieudokumentowane API.Zespół Angulara lub społeczność prawdopodobnie zaoferuje funkcje wyższego poziomu z lepszym doświadczeniem deweloperskim (cóż, mam taką nadzieję!).Locl od Oliviera Combe’a, autora ngx-translateis prawdopodobnie warto mieć oko 🧐.

Tłumaczenia runtime

Jak już wspominałem, jeśli używasz powyższych poleceń CLI (ng serve --configuration=fr lub ng build --localize), to aplikacja jest kompilowana i tłumaczona przed trafieniem do przeglądarki, więc nie ma żadnych wywołań $localize w runtime.

Ale $localize zostało zaprojektowane, aby zaoferować inną możliwość: tłumaczenia runtime.Co to oznacza? Cóż, bylibyśmy w stanie wysłać tylko jedną aplikację, zawierającą wywołania $localize, a przed uruchomieniem aplikacji moglibyśmy załadować tłumaczenia, które chcemy. Koniec z N buildami i N bundlami dla N lokalizacji.

Nie zagłębiając się zbytnio w szczegóły, jest to już możliwe w v9, poprzez użycie funkcji loadTranslations oferowanej przez @angular/localize.Ale to musi być zrobione przed uruchomieniem aplikacji.

Twoje tłumaczenia możesz załadować do polyfills.ts za pomocą:

import '@angular/localize/init';import { loadTranslations } from '@angular/localize';loadTranslations({ '1815172606781074132': 'Bonjour {$name}\xa0! Vous avez {$userCount} utilisateurs.'});

Jak widzisz, nie ma żadnego uwzględniania locale:po prostu ładujesz swoje tłumaczenie jako obiekt, którego kluczami są łańcuchy do przetłumaczenia, a wartościami ich tłumaczenia.

Teraz, jeśli uruchomisz proste ng serve, tytuł zostanie wyświetlony w języku francuskim! I nie ma już potrzeby używania ng xi18n, lub messages.fr.xlf lub specyficznej konfiguracji dla każdego locale w angular.json.W dłuższej perspektywie, kiedy będzie to odpowiednio wspierane i udokumentowane, powinniśmy być w stanie załadować pliki JSON w czasie wykonywania, tak jak robi to większość bibliotek i18n.Można to osiągnąć nawet w v9, jest to tylko trochę ręcznej pracy, ale jest to do zrobienia.

Co w takim razie ze zmianą locale w locie? Czy możemy załadować inny zestaw tłumaczeń, gdy aplikacja jest uruchamiana? Cóż, nie. Obecny sposób generowania wywołań $localize uniemożliwia ich zmianę po uruchomieniu: trzeba ponownie uruchomić aplikację.Ale jeśli nie masz nic przeciwko odświeżaniu przeglądarki, jest to możliwe.Przetestowałem prostą strategię, która działa:

  • użytkownik wybiera nowy język (na przykład hiszpański).
  • zapisujemy język w przeglądarce (na przykład w localStorage)
  • przeładowujemy stronę, co powoduje ponowne uruchomienie aplikacji
  • w polyfills.tszaczynamy od odczytania zapisanego języka
  • załadujemy odpowiedni zestaw tłumaczeń dla hiszpańskiego za pomocą loadTranslations.

Oczywiście, w przyszłości będzie to płynniejsze, albo w przyszłej wersji Angulara, albo poprzez bibliotekę z ekosystemu.Tak czy inaczej, jesteśmy coraz bliżej tego, aby wysyłać tylko jedną wersję naszej aplikacji, i po prostu ładować tłumaczenia w runtime \o/

Wszystkie nasze materiały (ebook, szkolenia online i treningi) są aktualne z tymi zmianami, jeśli chcesz dowiedzieć się więcej!

Leave a Reply