﻿# 如何將 Angular 專案加入 Internationalization (i18n) 國際化設定

我們最近有個 Angular 專案需要用到多國語系架構，所以重新看了一遍 [Angular Internationalization](https://angular.io/guide/i18n-overview) 官方文件，並且實際操作了一遍，這篇文章我就稍微記錄一下這個過程與注意事項。

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/a7870100148c374f46f3-8bb87baa-5064-4e36-8fd3-d292c8a1049d.webp)

### 簡介多國語系架構

國際化 (Internationalization) 又稱為 i18n，是為了讓您的專案適用於世界各地的不同地區而進行的設計和準備過程。

本地化 (Localization) 是為不同地區建立您的專案版本的過程。本地化過程包括以下動作：

-   提取文本以便翻譯成不同語言
-   為特定地區格式化資料

地區設定 (`Locale`) 則用來識別一個地區，在這個地區人們講某種**特定語言**或**語言變體**。可能的地區包括**國家**和**地理區域**。地區設定確定以下細節的格式化和解析。

-   包括**日期和時間**、**數字**和**貨幣**在內的**測量單位**
-   翻譯名稱，包括**時區**、**語言**和**國家**

> Angular 的 `DatePipe`, `CurrencyPipe`, `DecimalPipe`, `PercentPipe` 管道 (Pipes) 都有內建根據 Locale 進行本地化格式的轉換。

想要快速瞭解本地化和國際化，請到 [Angular 國際化](https://angular.tw/guide/i18n-overview) 觀看有中文字幕的影片！

### 對專案設定本地化套件

1.  建立全新專案
    
    ```bash
    ng new ng-i18n-demo --ssr=false --style=css
    cd ng-i18n-demo
    ```
    
2.  新增 `@angular/localize` 套件
    
    ```bash
    ng add @angular/localize
    ```
    
    > 這個命令的變更記錄可以[從這裡](https://github.com/doggy8088/ng-i18n-demo/commit/0f2c7044c2e9ad51191b0659ed486ca1368ea9c3)查看完整變更記錄
    
    預設 Angular 的本地化套件，會在 `ng build` 的時候將所有的本地化資源都打包進去，以提升執行效能。但如果你想在 Component 的 TypeScript 檔案中使用 [$localize](https://angular.dev/api/localize/init) 來將標記範本文字字串進行本地化，那就要用以下命令初始化：
    
    ```bash
    ng add @angular/localize --use-at-runtime
    ```
    
    > 到這個步驟為止，這兩種 `ng add` 方法並沒有太大差異，唯一差別僅在於 `@angular/localize` 套件是註冊在 `devDependencies` 或 `dependencies` 的差別而已。
    
3.  定義來源 Locale 與目標 Locale 設定
    
    我們在開發網站時，主要以什麼 Locale 為主？Angular 預設為 `en-US`，但如果你想要以「繁體中文」為主的話，要設定為 `zh-Hant` 才是正確的，要注意不要設定成 `zh-TW` 喔，因為 `@angular/common` 套件內並沒有包含 `zh-TW` 或 `zh-Hant-TW` 地區設定！
    
    > 至於不包含 `zh-TW` 或 `zh-Hant-TW` 地區設定的原因，是因為 Angular 依循 [Unicode CLDR Project](https://cldr.unicode.org/) 的國際化定義，目前 CLDR 就真的[沒有列出](https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-core/availableLocales.json)台灣的地區設定，所以才不支援的。
    
    目標 Locale 可以設定多國語言，其中需定義翻譯檔的路徑，還有網站的 `baseHref` 路徑！
    
    修改你的 `angular.json` 設定檔，在主要的 Angular 專案設定下，加入 `i18n` 設定，範例如下：
    
    ```json
    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "ng-i18n-demo": {
          "projectType": "application",
          "schematics": {},
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
    
          "i18n": {
            "sourceLocale": "zh-Hant",
            "locales": {
              "en-US": {
                "translation": "src/locale/messages.en-US.xlf",
                "baseHref": "/en-US/"
              }
            }
          },
    ```
    
    > 上述設定是定義 `ng-i18n-demo` 專案的 `sourceLocale` 為 `zh-Hant`，並且定義 `en-US` 為目標 Locale，翻譯檔的路徑為 `src/locale/messages.en-US.xlf` (等等我們會建立)，網站的 `baseHref` 路徑為 `/en-US/`，也代表網站未來部署的網站路徑會從 `/en-US/` 開始瀏覽。
    
4.  設定 `ng build` 的時候要一次建置多國語言網站
    
    Angular 內建的 i18n 功能，事實上是在建置時會替每一個 Locale 分別建置應用程式，如果你在上個步驟定義 `zh-Hant` 為來源 Locale 與定義一個 `en-US` 為目標網站的話，最後在用 `ng build` 的時候，就會建置出兩個網站出來，分別是讓你部署到網站的 `/zh-Hant/` 與 `/en-US/` 目錄下。
    
    你可以用 `ng build --localize` 一次建置多個網站，也可以在 `angular.json` 的 `architect.build.options` 設定區段中加入 `"localize": true` 選項設定，這樣你直接執行 `ng build` 就可以一口氣建置多個網站了！
    
    下圖是多國語系網站在部署時的目錄結構，基本上一個 Locale 就會有一組網站應用程式被輸出：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/github.com/3a21b1e82271a3240dbd-e9def43c-177c-4ad9-9ffb-6ed7c5bebb3a.png)
    
    > 你可以在 `angular.json` 的 `architect.build.options` 設定區段中加入 `"i18nMissingTranslation": "error"` 選項設定，這樣在 `ng build` 的時候，若發現有沒有翻譯到的單元，就會直接報錯！
    
5.  建立開發時期所需的組態設定檔
    
    我們在開發多國語系網站時，會需要測試不同 Locale 的效果，但你用 `ng serve` 測試網站的時候，一次只能看一個 Locale 的網站，所以我們要對 `angular.json` 進行設定，讓我們可以用以下命令，啟動單一個 Locale 的版本。
    
    ```bash
    ng serve --configuration=en-US
    ```
    
    首先，我們可以在 `production` 組態加入以下兩項設定：
    
    ```json
    "zh-Hant": {
      "localize": ["zh-Hant"],
      "baseHref": "/zh-Hant/"
    },
    "en-US": {
      "localize": ["en-US"],
      "baseHref": "/en-US/"
    }
    ```
    
    而在 `serve` 組態也要加入以下兩項設定：
    
    ```json
    "zh-Hant": {
      "buildTarget": "ng-i18n-demo:build:development,zh-Hant"
    },
    "en-US": {
      "buildTarget": "ng-i18n-demo:build:development,en-US"
    }
    ```
    
    > 注意: Angular 16 以前要將 `buildTarget` 修改為 `browserTarget` 才能正常運作。
    

步驟 3 到步驟 5 的 `angular.json` 設定，可以[從這裡](https://github.com/doggy8088/ng-i18n-demo/commit/34a7938ec82009648c998921b13b045926d11e08)查看完整的變更記錄。

### 開始對專案進行本地化翻譯

1.  在 Template 套用 `i18n` 屬性，用於「標記」要翻譯的文字
    
    最簡單的用法，有以下三種。
    
    第一種：直接在任意標籤上套用 `i18n` 屬性
    
    ```html
    <p i18n>恭喜！你的應用程式正在運行中！🎉</p>
    ```
    
    第二種：在 `<ng-container>` 標籤上套用 `i18n` 屬性，最終輸出的文字不會包含額外的 DOM 元素
    
    ```html
    <ng-container i18n>哈囉</ng-container>
    ```
    
    第三種：在任意標籤上套用 `i18n` 屬性並給予一個翻譯的註記(Note)，方便日後翻譯人員翻譯成其他地區語言：
    
    ```html
    <ng-container i18n="放置在首頁的招呼語">哈囉</ng-container>
    ```
    
    第四種：在任意標籤上套 `i18n-*` 屬性，標記特定 HTML Attribute 的內容需要翻譯
    
    ```html
    <div class="left-side" i18n-title title="這是 Angular 標誌">
    ```
    
    第五種：在 `*.ts` 檔案中使用 `$localize` 來標記字串需要翻譯
    
    ```typescript
    title = $localize`i18n範例`;
    ```
    
2.  標記完成後，程式或範本中的內容預設就是 `sourceLocale` 的預設內容，但是翻譯的內容就要靠 Angular CLI 提供的命令來自動產生，也就是產生所需的翻譯檔。
    
    首先，我們要先擷取所有標記需要翻譯的內容並且自動產生翻譯檔，命令如下：
    
    ```bash
    # 產生預設 Locale 的翻譯檔
    ng extract-i18n --output-path "src/locale"
    # 產生 en-US Locale 的翻譯檔
    ng extract-i18n --output-path "src/locale" --out-file "messages.en-US.xlf"
    ```
    
    這個命令會把需要翻譯的 `xlf` 檔案輸出到 `src/locale/messages.en-US.xlf` 路徑下。如果有多國語系要輸出翻譯檔，就執行多次即可，例如：
    
    ```bash
    ng extract-i18n --output-path "src/locale"
    ng extract-i18n --output-path "src/locale" --out-file "messages.en-US.xlf"
    ng extract-i18n --output-path "src/locale" --out-file "messages.ja-JP.xlf"
    ng extract-i18n --output-path "src/locale" --out-file "messages.zh-Hans.xlf"
    ```
    
3.  開始翻譯
    
    我們先來看看 `src/locale/messages.en-US.xlf` 檔案的內容，這是一份 XML 檔案，明確定義了要翻譯的內容，其中 `<trans-unit>` 就代表一個翻譯單元，裡面的 `<source>` 標籤定義了要被翻譯的內容：
    
    ```xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
      <file source-language="zh-Hant" datatype="plaintext" original="ng2.template">
        <body>
          <trans-unit id="822056772914261675" datatype="html">
            <source>這是 Angular 標誌</source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">182,183</context>
            </context-group>
          </trans-unit>
          <trans-unit id="8022582046775660363" datatype="html">
            <source>哈囉</source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">228</context>
            </context-group>
            <note priority="1" from="description">放置在首頁的招呼語</note>
          </trans-unit>
          <trans-unit id="1136077837762423862" datatype="html">
            <source>恭喜！你的應用程式正在運行中！🎉</source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">229,230</context>
            </context-group>
          </trans-unit>
          <trans-unit id="2065990894244085449" datatype="html">
            <source>i18n範例</source>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.ts</context>
              <context context-type="linenumber">13</context>
            </context-group>
          </trans-unit>
        </body>
      </file>
    </xliff>
    ```
    
    我們在每一個 `<trans-unit>` 翻譯單元中，要在 `<source>` 後面自行加入一個 `<target>` 標籤，並加入翻譯後的內容！
    
    > 分享一個 GitHub Copilot 的咒語給大家：`請將每個 trans-unit 加入 <target> 標籤並翻譯為英文`
    
    以下是翻譯完成的結果：
    
    ```xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
      <file source-language="zh-Hant" datatype="plaintext" original="ng2.template">
        <body>
          <trans-unit id="822056772914261675" datatype="html">
            <source>這是 Angular 標誌</source>
            <target>This is the Angular logo</target>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">182,183</context>
            </context-group>
          </trans-unit>
          <trans-unit id="8022582046775660363" datatype="html">
            <source>哈囉</source>
            <target>Hello</target>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">228</context>
            </context-group>
            <note priority="1" from="description">放置在首頁的招呼語</note>
          </trans-unit>
          <trans-unit id="1136077837762423862" datatype="html">
            <source>恭喜！你的應用程式正在運行中！🎉</source>
            <target>Congratulations! Your application is running! 🎉</target>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.html</context>
              <context context-type="linenumber">229,230</context>
            </context-group>
          </trans-unit>
          <trans-unit id="2065990894244085449" datatype="html">
            <source>i18n範例</source>
            <target>i18n example</target>
            <context-group purpose="location">
              <context context-type="sourcefile">src/app/app.component.ts</context>
              <context context-type="linenumber">13</context>
            </context-group>
          </trans-unit>
        </body>
      </file>
    </xliff>
    ```
    
4.  翻譯完成後，可以用以下命令來預覽結果
    
    ```bash
    ng serve --configuration=en-US
    ```
    

### 進階翻譯技巧

我們以這段為例：

```html
<ng-container i18n="放置在首頁的招呼語">哈囉</ng-container>
```

我們抓 `<trans-unit>` 出來看看，裡面有個 `id` 代表這個翻譯的唯一編號，又稱 `Custom ID`：

```xml
<trans-unit id="8022582046775660363" datatype="html">
  <source>哈囉</source>
  <target>Hello</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">228</context>
  </context-group>
  <note priority="1" from="description">放置在首頁的招呼語</note>
</trans-unit>
```

這個 `Custom ID` 是自動產生的，但你只要變更了 `<source>` 原始內容，這個 Custom ID 就會改變，因為不同的文字，可能代表著不同的含意，因此 `Custom ID` 變了，意味著多國語系的翻譯也要一起重新翻譯。

不過，即便是一段相同的文字，在不同的語境底下，可能是不同的含意，如此一來翻譯就有可能會有問題。這個時候，我們其實可以在「翻譯標記」上面加上一些 Metadata，讓相同的一段文字也可以有不同的 `Custom ID`，也就代表會有多份翻譯的意思。

事實上，這個 `Custom ID` 是依據 `meaning` (含意) 與 `text` (文字) 自動計算出來的，我們可以用以下格式定義翻譯標記的 Metadata：

```typescript
{meaning}|{description}@@{id}
```

你不一定要明確指定 `{id}` (通常不需要)，但只要能指定額外的 `meaning` 的部分，不同的 `meaning` 就會產生不同的 `Custom ID`。

你可以在 `i18n` 屬性(Attribute)的內容用 `|` 分隔 `{meaning}` 與 `{description}`，例如：

```html
<ng-container i18n="招呼語|放置在首頁的招呼語">哈囉</ng-container>
```

> 這段「招呼語」就是 `meaning`，而「放置在首頁的招呼語」就是 `description`，還有「哈囉」就是 `text` 本文部分。

如果是在 `*.ts` 中，你可以用 `:` 分隔 Metadata 與要翻譯的內容，其表示法為：

```typescript
title = $localize`:網站標題|這是本網站的主要標題:i18n範例`;
```

> 請注意不要忽略字串中的第一個字元為 `:` 喔！

如此一來，你在產生新的 `xlf` 翻譯檔的時候，就會看到 `meaning` 的註記：

```bash
ng extract-i18n --output-path src/locale --out-file messages.en-US.xlf
```

```xml
<trans-unit id="7033928152690784306" datatype="html">
  <source>哈囉</source>
  <target>Hello</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">228</context>
  </context-group>
  <note priority="1" from="description">放置在首頁的招呼語</note>
  <note priority="1" from="meaning">招呼語</note>
```

我用以下範例說明：

```html
<ng-container i18n="招呼語|放置在首頁的招呼語">哈囉</ng-container>
<ng-container i18n="內文招呼語|放置在內文的招呼語">哈囉</ng-container>
```

執行完 `ng extract-i18n` 之後，會產生兩段翻譯單元：

```xml
<trans-unit id="7033928152690784306" datatype="html">
  <source>哈囉</source>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">228</context>
  </context-group>
  <note priority="1" from="description">放置在首頁的招呼語</note>
  <note priority="1" from="meaning">招呼語</note>
</trans-unit>
<trans-unit id="2211465157521451795" datatype="html">
  <source>哈囉</source>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">230,231</context>
  </context-group>
  <note priority="1" from="description">放置在內文的招呼語</note>
  <note priority="1" from="meaning">內文招呼語</note>
</trans-unit>
```

### 注意事項

1.  你在開發時期更新 `src/locale/messages.*.xlf` 翻譯檔的時候，並不會即時反應在瀏覽器上，每次有更新都必須重新執行 `ng serve` 才會生效。
    
    理論上你沒必要每翻譯一個 `<trans-unit>` 就看一次網頁的翻譯效果，所以這個問題影響不大。
    
2.  每次執行 `ng extract-i18n` 命令時，都會覆寫之前翻譯好的檔案，這點就不是非常方便，要小心檔案被覆蓋
    
    這個算是個缺點，如果你關注這個 DX 議題，可以追蹤這個 Issue: [#23428](https://github.com/angular/angular-cli/issues/23428)
    
    基本上，你可多利用 Git 版控，可以在版控時將蓋掉的內容還原回來。
    
    除此之外，你也可以利用 [ng-extract-i18n-merge](https://github.com/daniel-sc/ng-extract-i18n-merge) 套件，這個工具會自動取代 Angular CLI 內建的 `extract-i18n` builder，並且在執行 `ng extract-i18n` 命令時，會自動將翻譯檔案合併，而不是覆蓋。
    
    ```bash
    ng add ng-extract-i18n-merge
    ```
    
    執行完上述命令後，你的 `angular.json` 會自動更新，之後就只要這樣執行即可：
    
    ```bash
    ng extract-i18n
    ```
    
    > 目前這套還[有點問題](https://github.com/daniel-sc/ng-extract-i18n-merge/issues/95)，等待作者解決。
    
3.  多國語翻譯這件事沒有想像中簡單，中文比較沒有單數與複數的概念，但其他語言就有這些特性，因此在做多國語系網站時要考量的地方還有很多，建議把 [Work with translation files](https://angular.io/guide/i18n-common-translation-files) 文件看完，裡面有很多細節需要注意。
    

### 相關連結

-   [Angular - Angular Internationalization](https://angular.io/guide/i18n-overview)
-   [Angular - Work with translation files](https://angular.io/guide/i18n-common-translation-files)
-   [Angular - Example Angular Internationalization application](https://angular.io/guide/i18n-example)
-   [Unable to find `zh-Hant-TW` or `zh-TW` locale in `@angular/common` · Issue #54114 · angular/angular](https://github.com/angular/angular/issues/54114)
-   範例 1: [https://angular.io/generated/live-examples/i18n/stackblitz.html](https://angular.io/generated/live-examples/i18n/stackblitz.html)
-   範例 2: [https://github.com/doggy8088/ng-i18n-demo/tree/master](https://github.com/doggy8088/ng-i18n-demo/tree/master)
