# Angular i18n 筆記
## 產生 i18n 來源檔案
### step 1. 標記需要翻譯的元件
在需要翻譯的元件加上 `i18n` 這個屬性:
```htmlmixed=
<h1 i18n>Hello World!</h1>
```
<br />
為了幫助翻譯語意流暢,還可為上面的 `i18n` 加上描述文字:
```htmlmixed=
<h1 i18n="user greeting | the greetin for this example">Hello World!</h1>
```
屬性值格式:`i18n="<meaning> | <description>"`
此屬性值是給翻譯人員理解上下文用,並不會顯示在網頁上。
<br />
Angualr i18n tool 在產生翻譯源檔時,為每個翻譯單元產生 unique id。在我們變更模板中要翻譯的文字內容時,i18n tool 會為這個 unit 產生新的 id,但是手動複製的其他語言版本,並不會自動同步這個新的 id,所以需要手動同步
```htmlmixed=
<trans-unit id="1e9a15da9ecb3574be8b466c285ed4aca1d89e4b" datatype="html">
```
我們也可以主動在 `i18n` 屬性值加上自訂的 id,這個 id 就不會因為修改翻譯文字而被變更:
```htmlmixed=
<h2 i18n="@@sub-title">Tour of Heroes</h2>
```
:::info
i18n 屬性值的格式:
`i18n="<meaning> | <description>@@<id>"`
:::
---
如果需要翻譯的文字在 DOM Element Attribute 中時,例如:
```htmlmixed=
// 希望翻譯文字在元件的 title 屬性中
<img title="Angular Logo" />
```
<br />
上例可使用 `i18n-title` 屬性來標註:
```htmlmixed=
<img i18n-title title="Angular Logo" />
```
:::info
i18n-x 屬性也可使用同樣的屬性值格式:
`i18n-x="<meaning> | <description>@@<id>"`
:::
<br />
如果需要翻譯的文字會因變數而改變時,可以使用 `plural` 和 `select`
**`plural`**
在某些語系中,文字會因數量不同而使用不同的文字表示,例如英文中單複數主詞搭配的動詞型態會有不同。
以下例子標示出,當更新時間不同,以適當文字顯示:
- 當更新時間接近 0 分鐘時,顯示 just now
- 1 分鐘時,顯示 one minute ago
- 1 分鐘以上,則顯示 xxx minutes ago
```htmlmixed=
<span i18n>
Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}
</span>
```
- 第一個參數 minutes 代表此處文字連動的變數名稱
- 第二個參數表示此翻譯單元使用 `plural` 類型
- 第三個參數代表 `plural` 的複數類型(變數值),{} 內為其所映射的文字內容
- 在對應的文字內容中,可以使用 angualr interpolation 及 HTML (`other` 後面的 `{{minutes}}` 即為 angualr interpolation)
> 上述複數類型包含以下幾種:
> - `=0`、`=1`...
> - zero
> - one
> - two
> - few
> - many
> - other
<br />
**`select`**
若變數值綁定的是固定種類的字串內容,則可使用 `select` 標示:
```htmlmixed=
<span i18n>
The author is {gender, select, male {male} female {female} other {other}}
</span>
```
- 第一個參數為連動變數名稱
- 第二個參數表示此翻譯單元使用 `select` 類型
- 第三個參數為變數值與他們映射顯示的文字內容
<br />
### step 2. 產生本土化來源檔案
> 常用指令:
ng xi18n --output-path locale
<br />
在 Angular CLI 中,使用 `ng xi18n` 指令產生本土化來源檔案,預設會在 `src/` 資料夾中,產出 `messages.xlf` 檔案。
可使用參數:
- `--i18n-format=< xlf | xlf2 | xmb >` 變更翻譯文件檔案格式
> Angular 支援以下翻譯文件格式
> - XLIFF 1.2(預設)
> - XLIFF 2
> - XML Message Bundle (XMB)
>
- `--output-path <path>` 變更產生文件的檔案位置
> 預設將檔案產生在 `src/`
>
- `--out-file <file name>` 變更文件名稱
> 預設值為 `messages.xlf`
- `--i18n-locale fr` 變更文件中 `source-language` 參數值
> 這個參數資訊在 Angular 中未被使用,但可能在其他翻譯工具中會使用到。
>
<br />
### step 3. 翻譯各語系的來源文本
> 前置準備,以翻譯至法文文本為例:
> 1. 將 `ng xi18n` 產生的 message.xlf 複製一份
> 2. 將副本放置在 /src/locale 資料夾中。
> 3. 將副本改名為 message.fr.xlf 作為翻譯結果文本
> (註:繁體中文依標準是 zh-Hant)
>
xlf 檔案可以使用翻譯專用的 XLIFF 編輯器來處理,也可以直接用文字編輯器來替換翻譯內容,下例顯示一個翻譯單元(trans-unit):
```htmlmixed=
<trans-unit id="introductionHeader" datatype="html">
<source>Hello i18n!</source>
<note priority="1" from="description">An introduction header for this sample</note>
<note priority="1" from="meaning">User welcome</note>
</trans-unit>
```
包含在 `<source>` 內是需要翻譯的文字內容,將 `<source>Hello i18n!</source>` 複製一份貼在下方,並將 tag name 由 source 改為 target,再將需要翻譯的文字修改為翻譯後的內容,即完成這個翻譯單元囉。上例完成翻譯後應如下:
```htmlmixed=
<trans-unit id="introductionHeader" datatype="html">
<source>Hello i18n!</source>
<target>i18n 您好!</target>
<note priority="1" from="description">An introduction header for this sample</note>
<note priority="1" from="meaning">User welcome</note>
</trans-unit>
```
---
同一個模板元件中,如果同時包含 `plural` 或 `select` 和一般文字內容時,`plural` / `select` 和一般文字內容會被拆分為兩個翻譯單元,例如:
```
<span i18n>The author is {gender, select, male {male} female {female} other {other}}</span>
```
在翻譯原檔中,會顯示為:
```xml=
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.ts</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="eff74b75ab7364b6fa888f1cbfae901aaaf02295" datatype="html">
<source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.ts</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
```
在第一個翻譯單元中,處理字串 `The author is `,第二個翻譯單元中,處理 {} 包含的部分即可。
<br />
## 編譯
### 1) 使用 JIT 編譯
#### 官網的範例
需要修改 `main.ts` ,將翻譯文件導入成字串常數值。
##### src/main.ts
```htmlmixed=
import { enableProdMode, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
// use the require method provided by webpack
declare const require;
// we use the webpack raw-loader to return the content as a string
const translations = require(`raw-loader!./locale/messages.fr.xlf`);
platformBrowserDynamic().bootstrapModule(AppModule, {
providers: [
{provide: TRANSLATIONS, useValue: translations},
{provide: TRANSLATIONS_FORMAT, useValue: 'xlf'}
]
});
```
- TRANSLATIONS 是包含翻譯文件內容的字串
- TRANSLATIONS_FORMAT 翻譯文件檔案格式(xlf | xlf2 | xmb)
##### src/app/app.module.ts
在 AppModule 的 providers 陣列中加上 LOCALE_ID 設定,內建的 pipe 會依據 LOCALE_ID 做在地化顯示。
> LOCALE_ID 是影響 pipe 等工具,但與 xi18n 產生的翻譯文件顯示無關...
>
```htmlmixed=
import { LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from '../src/app/app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
providers: [ { provide: LOCALE_ID, useValue: 'fr' } ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
```
LOCALE_ID 是地區語言標誌,常用如下:
| 地區/語言 | ID |
|:-------------|:-------|
| 繁體中文 | zh-Hant |
| 繁體中文(香港)| zh-Hant-HK |
| 簡體中文 |zh-Hans |
| 美式英文 | en-US |
<br />
### (實作)在 JIT 編譯時 依據瀏覽器語系 在啟動前動態變更 i18n 源檔
LocaleService 由瀏覽器取得顯示語系,再依語系回傳適當的 providers 陣列給 bootstrapModule
##### locale.service.ts
```htmlmixed=
import { TRANSLATIONS, TRANSLATIONS_FORMAT, Injectable } from '@angular/core';
// use the require method provided by webpack
declare const require;
@Injectable({
providedIn: 'root'
})
export class LocaleService {
localeID = window.navigator.language;
constructor() { }
getProviders() {
// 瀏覽器抓到繁體中文的locale id 會是 zh-TW
if ( this.localeID === 'zh-TW' ) {
// we use the webpack raw-loader to return the content as a string
const translations = require(`raw-loader!../locale/messages.zh-Hant.xlf`);
return [
{provide: TRANSLATIONS, useValue: translations},
{provide: TRANSLATIONS_FORMAT, useValue: 'xlf'}
];
} else {
return [];
}
}
}
```
##### main.ts
因為 bootstrapModule 沒有 constructor,所以使用 injector 的方式注入 LocaleService
```htmlmixed=
import { enableProdMode, ReflectiveInjector } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { LocaleService } from './app/locale.service';
if (environment.production) {
enableProdMode();
}
const injector = Injector.create([{provide: LocaleService, deps: []}]);
const localeService = injector.get(LocaleService);
platformBrowserDynamic().bootstrapModule(AppModule, {
providers: [...localeService.getProviders()]
})
.catch(err => console.error(err));
```
這樣即可在 JIT 編譯的情況下,依照瀏覽器系統語言顯示不同語言
<br />
### 2) 使用 AOT 編譯
:::info
AOT 編譯器會將每種語言版本分別編譯成一包,並需要依靠伺服器端檢測或是用網址參數的方式判斷要顯示哪個語言。
:::
使用 AOT 編譯時,需要為 AOT compiler 設定翻譯文件的配置,可以在 angular.json 中作設定,也可以在 Angular CLI 中下參數。需要指示的設定如下:
- i18nFile:翻譯文件路徑
- i18nFormat:翻譯文件檔案格式
- i18nLocale:區域 ID
還可加上(非必填的設定):
- i18nMissingTranslation:會在翻譯文件有缺漏項目時警告,可以設定的值有 Error、Warning、Ignore
- outputPath:可為每個語言版本建置在不同資料夾中
- baseHref:設定此語言版本的 URL
outputPath 和 baseHref 搭配設定,可以直接使用該參數值做語言顯示判斷,例:
```
https://<yourpath>/zh-Hant
```
#### 在 angular.json 中可以這麼設定:
```json=
"build": {
...
"configurations": {
...
"zh-Hant": {
"aot": true,
"i18nFile": "src/locale/messages.zh-Hant.xlf",
"i18nFormat": "xlf",
"i18nLocale": "zh-Hant",
"i18nMissingTranslation": "error",
"outputPath": "dist/zh-Hant/",
"baseHref": "zh-Hant"
...
}
}
},
"serve": {
...
"configurations": {
...
"zh-Hant": {
"browserTarget": "myapp:build:zh-Hant"
}
}
}
```
做了以上設定之後,在 Angular CLI 可以做以下指令:
`ng serve --configuration=zh-Hant` ( `--configuration` 可簡寫為 `-c` )
在 localhost:4200 可以看到翻譯後的結果
`ng build --configuration=zh-Hant`
會在指定位置建置翻譯後的版本
如果要做 production 的優化,需新增包含 production 設定和 i18n 設定的設定區塊,範例如下:
```json=
"prod-zh-Hant": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"outputPath": "dist/i18n-aot-zh-Hant/",
"i18nFile": "src/locale/messages.zh-Hant.xlf",
"i18nFormat": "xlf",
"i18nLocale": "zh-Hant",
"i18nMissingTranslation": "error"
}
```
做以上設定後,可下如下指令產生為 production 環境優化的 build
`ng build --prod -c=prod-zh-Hant`
<br />
#### 在 CLI 直接下 i18n 參數
也可以在 Angular CLI 下 `ng serve` 和 `ng build` 時,直接加入 i18n 的參數,例如:
```
ng build --prod --i18n-file src/locale/messages.zh-Hant.xlf --i18n-format xlf --i18n-locale zh-Hant
```