命運多舛,在當了幾個月待業廢物後,總算找到風氣跟待遇都不錯的新工作,如果寫扣寫到懷疑人生就離職換工作吧。也為了新工作開始學習 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
1
2
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
1
2
3
4
{
"WELCOME_TO": "Welcome to",
"SELECT_LANGUAGE": "Select a language"
}
zh-TW.json
1
2
3
4
{
"WELCOME_TO": "歡迎使用",
"SELECT_LANGUAGE": "選擇語言"
}

在 template 中寫個簡單的語言切換選單

app.component.html
1
2
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
1
2
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
1
2
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
1
2
<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
1
2
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
1
2
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
1
2
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