命運多舛,在當了幾個月待業廢物後,總算找到風氣跟待遇都不錯的新工作,如果寫扣寫到懷疑人生就離職換工作吧。也為了新工作開始學習 Angular,解開使用三大前端框架的成就,這次來記錄 Angular i18n 套件 ngx-translate 上所碰到的問題與解決方法。
TL;DR
主要是實務上需要使用到
| 1
 | setTranslation(lang: string, translations: Object, shouldMerge: boolean = false)
 | 
這個 method 時,所歸納的要點:
- setTranslation()第三個參數- shouldMerge結果不符預期,使用 Lodash 的- _.merge()自幹解決
- setTranslation()一定要在- use()與- setDefaultLang()之前完成,否則會吃到舊的翻譯檔,也就是不會熱更新
- 由於 setTranslation()沒有 return Observerble,因此用setTimeout(0)來實現第二點的非同步。
心路歷程
基本設置
用 Angular-cli 新建一個 project,照著 ngx-translate 文件 npm install --save 安裝 @ngx-translate/core 與 @ngx-translate/http-loader ,在 app,module.ts 裡接好 loader 
app.module.ts| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | import { HttpClientModule, HttpClient} from '@angular/common/http';import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
 import { TranslateHttpLoader } from '@ngx-translate/http-loader';
 
 export function HttpLoaderFactory(http: HttpClient) {
 return new TranslateHttpLoader(http);
 }
 
 imports: [
 
 HttpClientModule,
 TranslateModule.forRoot({
 loader: {
 provide: TranslateLoader,
 useFactory: HttpLoaderFactory,
 deps: [HttpClient]
 }
 })
 ],
 
 | 
 
寫兩份翻譯檔,放在 src/assets/i18n/ 資料夾底下
en.json| 12
 3
 4
 
 | {"WELCOME_TO": "Welcome to",
 "SELECT_LANGUAGE": "Select a language"
 }
 
 | 
 
zh-TW.json| 12
 3
 4
 
 | {"WELCOME_TO": "歡迎使用",
 "SELECT_LANGUAGE": "選擇語言"
 }
 
 | 
 
在 template 中寫個簡單的語言切換選單
app.component.html| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | <div style="text-align:center"><h1>
 {{ 'WELCOME_TO' | translate }} {{ title }}!
 </h1>
 <label for="lang">
 {{ 'SELECT_LANGUAGE' | translate }}:
 <select #langSelect name="lang" (change)="translate.use(langSelect.value)">
 <option *ngFor="let lang of translate.getLangs()" [value]="lang"
 [selected]="lang === translate.currentLang">{{ lang }}</option>
 </select>
 </label>
 </div>
 
 | 
 
在 constructor 中使用 TranslateService
app.component.ts| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
 
 title = 'ngx-translate';
 constructor(
 public translate: TranslateService
 ) {
 translate.addLangs(['en', 'zh-TW']);
 translate.setDefaultLang('en');
 }
 
 | 
 
ng serve 跑起來,就能看到我們簡單的 i18n 網頁
 
 
語系判別 & 儲存設定
我們也能將選取的語言值儲存到 localStorage,下次開啟網頁時就能沿用,並使用 getBrowserCultureLang() 做預設 fallback,改完的 constructor 長這樣:
app.component.ts| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | translate.addLangs(['en', 'zh-TW']);translate.onLangChange.subscribe((event: LangChangeEvent) => {
 const {lang} = event;
 localStorage.setItem('useLang', lang);
 });
 translate.setDefaultLang('en');
 const useLang = localStorage.getItem('useLang');
 if (useLang) {
 translate.use(useLang);
 } else {
 const browserLang = translate.getBrowserCultureLang();
 translate.use( translate.langs.includes(browserLang) ? browserLang : 'en');
 }
 
 | 
 
Merge 額外的翻譯
真正的需求來了,如果想將選單裡的「en」、「zh-TW」也翻成「English」與「正體中文」:
app.component.html| 12
 
 | <option *ngFor="let lang of translate.getLangs()" [value]="lang"[selected]="lang === translate.currentLang">{{ lang  | translate }}</option>
 
 | 
 
不想把翻譯重複寫在 json 裡,另外 merge key-value 進去可以嗎?看文件似乎使用 setTranslate() 並設定第三個參數為 true 就能達到我們要的結果
app.component.ts| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | translate.onLangChange.subscribe((event: LangChangeEvent) => {
 const {lang} = event;
 localStorage.setItem('useLang', lang);
 });
 for (const lang of translate.getLangs() ) {
 translate.setTranslation(lang, {
 'en': 'English',
 'zh-TW': '正體中文'
 }, true);
 }
 
 | 
 
實際上卻不是這樣…
 
原有的翻譯不見了,只剩下選單的翻譯,這行為看起來是 replace 而非 merge,文件居然陰我!? 查了 Github 的 issue 發現一堆人發了類似問題,也都沒有回應根本被放生WTF…
 
山不轉路轉,只要直接給它 merge 好的翻譯就可以了吧?因此把 Lodash 拉進來改寫…
app.component.ts| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | import { merge } from 'lodash';
 for (const lang of translate.getLangs() ) {
 const json = require(`../assets/i18n/${lang}.json`);
 const mergedLocale = merge(json, {
 'en': 'English',
 'zh-TW': '正體中文'
 });
 translate.setTranslation(lang, mergedLocale);
 }
 
 | 
 
先後順序
 merge 後的翻譯 log 出來是正確的,結果只有中文頁 OK 但英文頁沒翻譯到選單?撞牆了一段時間才發現順序的重要性: **必須先執行完 setTranslation(),再執行 setDefaultLang() 與 use() ** 。由於 setTranslation() 並沒有回傳值或是 Observerble,只好利用 JS 本身的 Queue:將 setDefaultLang() 與 use() 包在 setTimeout(0) 裡面,就能保證非同步順序:
app.component.ts| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | translate.addLangs(['en', 'zh-TW']);translate.onLangChange.subscribe((event: LangChangeEvent) => {
 const {lang} = event;
 localStorage.setItem('useLang', lang);
 });
 for (const lang of translate.getLangs() ) {
 const json = require(`../assets/i18n/${lang}.json`);
 const mergedLocale = merge(json, {
 'en': 'English',
 'zh-TW': '正體中文'
 });
 translate.setTranslation(lang, mergedLocale);
 }
 setTimeout(() => {
 translate.setDefaultLang('en');
 const useLang = localStorage.getItem('useLang');
 if (useLang) {
 translate.use(useLang);
 } else {
 const browserLang = translate.getBrowserCultureLang();
 translate.use( translate.langs.includes(browserLang) ? browserLang : 'en');
 }
 }, 0);
 
 | 
 
總算,整頁與選單都能正確翻譯了
 
小結
這麼長一串有點像幫第三方擦屁股,不確定 setTranslation() 裡面是不是有讓 TranslateService subscribe 觸發翻譯更新,文件也沒提到多少細節,還是呼籲大家慎選第三方 library 。
Reference