前言

前一篇已經說過為甚麼多語言的網站很重要,也示範了如何達到多語言的效果。但是,我們對於語言翻譯的需求還有很多,例如我如果想要讓一串句子每次搭配不同的變數怎麼辦,像是「你好,Tiger Liu」或是「你好,Tiger」這樣的可變性。或是如果有些語詞在某些語言中翻譯不出來、不想翻譯或是不小心忘記增加字典時,例如「掰掰囉!」在英文翻成「Bye Bye!」可是在日文的字典檔忘記加上這個字,要讓網站還是能夠顯示「Bye Bye!」怎麼做?

目標

  • 讓我們的翻譯可以有占位符號 ( placeholder),如此以來可以讓句子的可變性變大,如前言所述一般。
  • 當在語言包找不到符合的字句的時候,自動用備用的語言當替補。

我們將繼續用上一篇的程式來修改
完成的 Plunker

開工

語言包

先來看看我們的語言包最後長怎樣

// lang-en.ts

export const LANG_EN_NAME = 'en';

export const LANG_EN_TRANS = {
'hello world': 'hello world',
'hello greet': 'Hello, %0 %1!', // two placeholder
'well done': 'Well done %0', // one placeholder
'good bye': 'bye bye', // 只有在英文中定義
}

// lang-zh.ts

export const LANG_ZH_NAME = 'zh';

export const LANG_ZH_TRANS = {
'hello world': '你好,世界',
'hello greet': '你好, %0 %1!',
'well done': '幹得好, %0',
};

更新 translate 服務

// app/translate/translate.service.ts
...

public instant(key: string, words?: string | string[]) { // 增加可能的變數
const translation: string = this.translate(key);

if (!words) return translation;
return this.replace(translation, words); // 呼叫 replace 

}


接著來定義replace

// app/translate/translate.service.ts
...

private PLACEHOLDER = '%'; // 我們的佔位符

public replace(word: string = '', words: string | string[] = '') {
let translation: string = word;

const values: string[] = [].concat(words);// 導入輸入的參數字串
values.forEach((e, i) => {
    //會用 e 替換掉 %i: %0, %1, ...
    translation = translation.replace(this.PLACEHOLDER.concat(<any>i), e);
});

return translation;

}


更新 Pipe

// app/translate/translate.pipe.ts
...

transform(value: string, args: string | string[]): any { // args 可以是單一字串或是字串列
if (!value) return;
return this._translate.instant(value, args); // 加入 args
}

更新模板

<!--app/app.component.html-->

<!– 多值通道 –>
<p>
Translate <strong class="text-muted">Hello, %0 %1!</strong>:
<br>
<strong>{{ 'hello greet' | translate:['Jane', 'Doe'] }}</strong>
</p>

<!– 單一值通道 –>
<p>
Translate <strong class="text-muted">Well done %0</strong>:
<br>
<strong>{{ 'well done' | translate:'John' }}</strong>
</p>

建立事件

// app/translate/translate.service.ts
...

import { Injectable, Inject, EventEmitter } from '@angular/core'; // 引入 event emitter

// 增加事件
public onLangChanged: EventEmitter<string> = new EventEmitter<string>();
..

// 選取用甚麼語言
public use(lang: string): void {

this.onLangChanged.emit(lang); // 發布變化
}


主程式

先移除 selectLang() 中的 refreshText(),我們要重新架構。

// app/app.component.ts
...

ngOnInit() {
// 載入一些東西

this.subscribeToLangChanged(); // subscribe to language changes

// &#x8A2D;&#x5B9A;&#x76EE;&#x524D;&#x8A9E;&#x8A00;
this.selectLang(&apos;zh&apos;);

}

selectLang(lang: string) {
// 設為預設
this._translate.use(lang);
// this.refreshText(); // 刪掉這行
}

subscribeToLangChanged() {
// 刷新文字
return this._translate.onLangChanged.subscribe(x => this.refreshText());
}


這樣設定之後,只要語言改變,就會自動更新

預設 & 備用語言

備用語言的概念是,假設預設是英文,當日文沒有字串時,就回去用預設的英文字串。

// app/translate/translate.service.ts
...

// 增加 3 個 properties
private _defaultLang: string;
private _currentLang: string;
private _fallback: boolean;

public get currentLang() {
return this._currentLang || this._defaultLang; // 當沒有當前選取語言時用預設語言
}

public setDefaultLang(lang: string) {
this._defaultLang = lang; // 設定預設
}

public enableFallback(enable: boolean) {
this._fallback = enable; // 是否啟用備用
}

private translate(key: string): string { // 翻譯傳入的字串
let translation = key;

// &#x767C;&#x73FE;&#x76EE;&#x524D;&#x9078;&#x7684;&#x8A9E;&#x8A00;&#x6709;&#x9019;&#x500B;&#x5B57;&#x4E32;
if (this._translations[this.currentLang] &amp;&amp; this._translations[this.currentLang][key]) {
    return this._translations[this.currentLang][key];
}

// &#x4E0D;&#x7528;&#x5099;&#x7528;
if (!this._fallback) { 
    return translation;
}

// &#x767C;&#x73FE;&#x9810;&#x8A2D;&#x8A9E;&#x8A00;&#x6709;&#x9019;&#x5B57;&#x4E32;
if (this._translations[this._defaultLang] &amp;&amp; this._translations[this._defaultLang][key]) {
    return this._translations[this._defaultLang][key];
}

// &#x90FD;&#x6C92;&#x6709;
return translation;

}
..

當然我們也可以

// app/app.component.ts
...

ngOnInit() {
// 可以做一些事情..

this.subscribeToLangChanged();

// &#x8A2D;&#x5B9A;&#x9810;&#x8A2D;&#x8A9E;&#x8A00;
this._translate.setDefaultLang(&apos;en&apos;); // set English as default
this._translate.enableFallback(true); // enable fallback

// &#x8A2D;&#x5B9A;&#x76EE;&#x524D;&#x8981;&#x7528;&#x7684;&#x8A9E;&#x8A00;
this.selectLang(&apos;zh&apos;);

}


現在可以來測試語言包沒有字串時,是否會回去找預設的語言字串

<p>
    Translate <strong class="text-muted">Good bye (fallback)</strong>: 
    <br>
    <strong>{{ 'good bye' | translate }}</strong>
</p>

上面這一段,不管換西班牙語或是中文,都會回去找英文的字串來用