Angular i18n

【 Angular 】Angular i18n 教學

最近因為工作需要使用到 Angular i18n 的功能,所以就決定在看 document 的過程中整理一下 Angular i18n 的一些用法和觀念。

1. 建立 Angular 專案

首先在全域安裝 Angular cli:

npm install -g @angular/cli

接著使用 Angular cli 快速建立新的專案:

ng new i18n-tutorial

2. 安裝相關套件

使用 Angular cli 來安裝相關的套件:localize 為 Angular i18n 會使用到的套件,material 則是 Angular 的 UI framework。

本篇文章就不多贅述 Angular material 的用法,想了解詳細用法的話可以參考官方文件:Angular Material UI component library

ng add @angular/localize
ng add @angular/material

3. 新增 component 以及 import module

3.1 Import module

接著 import 會用到的 module,這時候我會習慣將 material 會用到的 module 獨立出來,然後在 shared-module 裡 import material-module ,會讓檔案結構看起來比較乾淨一點,等專案變大時也會比較好維護:

// src/app/shared/material.module.ts
import { NgModule } from '@angular/core';

import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';


@NgModule({
  imports: [
    MatIconModule,
    MatSelectModule,
    MatFormFieldModule,
    MatInputModule,
    MatToolbarModule,
    MatMenuModule,
    MatButtonModule
  ],
  exports: [
    MatIconModule,
    MatSelectModule,
    MatFormFieldModule,
    MatInputModule,
    MatToolbarModule,
    MatMenuModule,
    MatButtonModule
  ],
  providers: []
})

export class MaterialModule { }
// src/app/shared/shared.module.ts
import { NgModule } from '@angular/core';

import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';

import { MaterialModule } from './material.module';
@NgModule({
  imports: [
    FormsModule,
    ReactiveFormsModule,
    MaterialModule
  ],
  exports: [
    FormsModule,
    ReactiveFormsModule,
    MaterialModule
  ],
  providers: []
})
export class SharedModule { }

3.2 新增 component

再來使用 Angular cli 來快速建立 component:

ng g c nav-bar
ng g c i18n-form

使用 Angular cli 來產生 component 最大的優點是會自動幫你在 module 的 decalrations 裡新增該 component,因此我們接著只要在 app.module.ts 裡 import 剛剛寫的 shared-module.ts 就好:

// src/app/app.module.ts
import { NgModule } from '@angular/core';

import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';

import { MaterialModule } from './material.module';
@NgModule({
  imports: [
    FormsModule,
    ReactiveFormsModule,
    MaterialModule
  ],
  exports: [
    FormsModule,
    ReactiveFormsModule,
    MaterialModule
  ],
  providers: []
})
export class SharedModule { }

目前為止,專案的架構如下:

 ├── src
 │   ├── app
 │   │   ├── app-routing.module.ts
 │   │   ├── app.component.css
 │   │   ├── app.component.html
 │   │   ├── app.component.spec.ts
 │   │   ├── app.component.ts
 │   │   ├── app.module.ts
 │   │   ├── i18n-form
 │   │   │   ├── i18n-form.component.css
 │   │   │   ├── i18n-form.component.html
 │   │   │   ├── i18n-form.component.spec.ts
 │   │   │   └── i18n-form.component.ts
 │   │   ├── nav-bar
 │   │   │   ├── nav-bar.component.css
 │   │   │   ├── nav-bar.component.html
 │   │   │   ├── nav-bar.component.spec.ts
 │   │   │   └── nav-bar.component.ts
 │   │   └── shared
 │   │       ├── material.module.ts
 │   │       └── shared.module.ts

4. 切版以及指定要使用 Angular i18n 的 html 標籤

在這個小專案我們會有 3 個 component:
1. nav-bar: 包含一個 select 用來選擇語言
2. i18n-form: 模擬使用到多國語言的 form
3. app: 用來放以上 2 個 component

4.1 nav-bar.component

nav-bar component 主要的功能是使用 select 標籤來切換各種語言,因此在切換標籤的時候會需要用到瀏覽器提供的 api ,來利用 url 中的字串判斷目前的語言,以及切換語言後要把 url 中的語言替換成目標語言,再 redirect 到替換後的 url。

<!-- src/app/nav-bar/nav-bar.component.html -->
<mat-toolbar color="primary" class="nav-bar mat-elevation-z2">
  <ng-container>
      <mat-select
        (selectionChange)="onSelectionChange($event.value)"
        [value]="displayLanguage"
        class="selectContainer"
      >
        <mat-select-trigger>
          <div class="select-trigger-container">
            <mat-icon>language</mat-icon>
              <div>{{ i18nLang }}</div>
          </div>
        </mat-select-trigger>
          <mat-option *ngFor="let language of languageList" [value]="language.code">{{ language.name }}</mat-option>
      </mat-select>
  </ng-container>
</mat-toolbar>
/* src/app/nav-bar/nav-bar.component.css */
.nav-bar {
  position: fixed;
  top: 0;
  z-index: 99;
}

.selectContainer {
  width: 50px;
  height: 100%;
}
// src/app/nav-bar/nav-bar.component.ts
import { Component, OnInit } from '@angular/core';

interface LanguageMapping {
  [key: string]: string
}

@Component({
  selector: 'app-nav-bar',
  templateUrl: './nav-bar.component.html',
  styleUrls: ['./nav-bar.component.css']
})

export class NavBarComponent implements OnInit {
  displayLanguage: string = 'en';
  languageList = [
    { code: 'en', name: 'English' },
    { code: 'tw', name: 'Traditional-Chinese' },
    { code: 'jp', name: 'Japan' }
  ];
  get i18nLang() {
    const mapping: LanguageMapping = {
      en: 'US',
      tw: 'ZH',
      jp: 'JP'
    }
    return mapping[this.displayLanguage];
  }

  constructor() { }
  
  ngOnInit() {
    this.displayLanguage = this.getCurrentLanguage();
  }

  onSelectionChange($event: string) {
    this.redirectTo($event);
  }

  private redirectTo(redirectLang: string) {
    const redirectPathName = window.location.pathname.replace(`/${this.displayLanguage}/`, `/${redirectLang}/`);
    window.location.pathname = redirectPathName;
  }

  private getCurrentLanguage = () => {
    const lang = ['en', 'tw', 'jp'];
    const currentLang = lang.find(l => new RegExp(`/${l}/`).test(window.location.pathname));
    if (!currentLang) {
      return 'en';
    }
    return currentLang;
  };

}

4.2 i18n-form.component

i18n-form component 就是個一般的 component,然而要使用 angular i18n 的功能則必須在 html 的標籤加上 i18n attribute,加上 i18n attribute 的 html element 會被等等下的指令(後面會提到)給轉換為語言檔。

<!-- src/app/i18n-form/i18n-form.component.html -->
<form [formGroup]="i18nForm" (ngSubmit)="onSubmit()">
  <mat-form-field appearance="outline">
    <mat-label i18n>Email</mat-label>
    <input
      matInput
      type="email"
      placeholder="Email"
      formControlName="email"
    />
  </mat-form-field>
  <mat-form-field appearance="outline">
    <mat-label i18n>Password</mat-label>
    <input
      matInput
      type="password"
      placeholder="密碼"
      formControlName="password"
    />
  </mat-form-field>
  <div class="buttonContainer">
    <button
      mat-raised-button
      type="reset"
      (click)="onClear()"
      i18n
    >
      Reset
    </button>
    <button
      mat-raised-button
      color="primary"
      type="submit"
      [disabled]="!i18nForm.valid"
      i18n
    >
      Sign Up
    </button>
  </div>
</form>
/* src/app/i18n-form/i18n-form.component.css */
form {
  display: flex;
  flex-direction: column;
  padding: 2em;
  margin: 5rem auto;
  width: 20%;
}

.buttonContainer {
  display: flex;
  justify-content: space-between;
}
// src/app/i18n-form/i18n-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators, AbstractControl, ValidatorFn } from '@angular/forms';

@Component({
  selector: 'app-i18n-form',
  templateUrl: './i18n-form.component.html',
  styleUrls: ['./i18n-form.component.css']
})
export class I18nFormComponent implements OnInit {
  i18nForm = this.fb.group(
    {
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(4)]],
    }
  );
  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
  }

  onClear(): void {
    this.i18nForm.reset();
  }

  onSubmit(): void {
    console.log(this.i18nForm.getRawValue());
  }
}

4.3 app.component

<!-- src/app/app.component.html -->
<app-nav-bar></app-nav-bar>
<app-i18n-form></app-i18n-form>
// src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'i18n-tutorial';
}

5. 產生語言檔

5.1 新增翻譯原始檔

使用 Angular cli 來產生語言檔:

# --out-path 用來指定編譯完的檔案要放在哪裡
ng extract-i18n --output-path src/locale

下完這指令後 Angular 會自動幫你把有套用到 i18n attribute 的 html element 給抓出來,並產生翻譯的原始檔如下:

<!-- src/locale/messages.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="4768749765465246664" datatype="html">
        <source>Email</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">3</context>
        </context-group>
      </trans-unit>
      <trans-unit id="1431416938026210429" datatype="html">
        <source>Password</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5478121364779850827" datatype="html">
        <source> Reset </source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">27,28</context>
        </context-group>
      </trans-unit>
      <trans-unit id="744750008248382275" datatype="html">
        <source> Sign Up </source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">36,37</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

5.2 新增英文檔

接著複製上面那份檔案,並修正副檔名,因為原始語言是英文,所以不用做任何更動,修正副檔名即可:

<!-- src/locale/messages.en.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="4768749765465246664" datatype="html">
        <source>Email</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">3</context>
        </context-group>
      </trans-unit>
      <trans-unit id="1431416938026210429" datatype="html">
        <source>Password</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5478121364779850827" datatype="html">
        <source> Reset </source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">27,28</context>
        </context-group>
      </trans-unit>
      <trans-unit id="744750008248382275" datatype="html">
        <source> Sign Up </source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">36,37</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

5.3 新增中文翻譯檔

複製翻譯原始檔,並將副檔名改為 zh.hant.xlf ,再將要翻譯後的文字用 target 標籤包起來:

<!-- src/locale/messages.zh.hant.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="4768749765465246664" datatype="html">
        <source>Email</source>
        <target>電子郵件</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">3</context>
        </context-group>
      </trans-unit>
      <trans-unit id="1431416938026210429" datatype="html">
        <source>Password</source>
        <target>密碼</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5478121364779850827" datatype="html">
        <source> Reset </source>
        <target> 重設 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">27,28</context>
        </context-group>
      </trans-unit>
      <trans-unit id="744750008248382275" datatype="html">
        <source> Sign Up </source>
        <target> 註冊 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">36,37</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

5.4 新增日文翻譯檔

同中文翻譯檔做法,只是 target 標籤裡變成翻譯過後的日文:

<!-- src/locale/messages.jp.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="4768749765465246664" datatype="html">
        <source>Email</source>
        <target>Eメール</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">3</context>
        </context-group>
      </trans-unit>
      <trans-unit id="1431416938026210429" datatype="html">
        <source>Password</source>
        <target>パスワード</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5478121364779850827" datatype="html">
        <source> Reset </source>
        <target> リセット </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">27,28</context>
        </context-group>
      </trans-unit>
      <trans-unit id="744750008248382275" datatype="html">
        <source> Sign Up </source>
        <target> 登録する </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">36,37</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

5.5 翻譯檔架構

目前的翻譯檔架構如下:

src/locale
 ├── messages.en.xlf
 ├── messages.jp.xlf
 ├── messages.xlf
 └── messages.zh.hant.xlf

6. 修改 angular.json

接著要修改 angular.json 讓 angular 知道以 development 或是在 build 專案時該用什麼語言檔

6.1 修改專案的 i18n 設定

在這個步驟可以指定要使用的翻譯檔以及該語言要使用什麼 url:

// angular.json
{
  ...
  "projects": {
    "i18n-tutorial": {
      "i18n": {
        "locales": {
          "en": {
            "translation": "src/locale/messages.en.xlf",
            "baseHref": "/en/"
          },
          "tw": {
            "translation": "src/locale/messages.zh.Hant.xlf",
            "baseHref": "/tw/"
          },
          "jp": {
            "translation": "src/locale/messages.jp.xlf",
            "baseHref": "/jp/"
          }
        }
      },
      ...
    },
  },
}

6.2 修改 build 的設定

// angular.json
"build": {
  ...
  "configurations": {
    "en": {
      "localize": ["en"]
    },
    "tw": {
      "localize": ["tw"]
    },
    "jp": {
      "localize": ["jp"]
    }
  }
},

6.3 修改 serve 的設定

// angular.json
"serve": {
  ...
  "configurations": {
    "production": {
      "browserTarget": "i18n-tutorial:build:production"
    },
    "en": {
      "browserTarget": "i18n-tutorial:build:en"
    },
    "tw": {
      "browserTarget": "i18n-tutorial:build:tw"
    },
    "jp": {
      "browserTarget": "i18n-tutorial:build:jp"
    }
  }
}

7. 用 development mode 驗證 i18n 功能

7.1 使用預設的 development

ng serve

這時跑起來的畫面就是我們最原始的 html 檔,也就是英文的版本:

Angular i18n

7.2 使用 configuration=tw

ng serve --configuration=tw

跑起來後會發現 Angular 把翻譯檔套用到 html 裡面了,現在有使用 i18n 的標籤都會被 Angular 翻譯:

Angular i18n

8. build app

最後把 app build 起來,來驗證 Angular i18n 的功能吧!

8.1 build –localize

ng build --localize

8.2 安裝 http-server

我們需要安裝 http-server 來跑 build 後的檔案

npm install -g http-server

8.3 用 http-server 來跑 build 後的檔案

需要先進到 build 後的資料夾裡,接著直接執行 http-server ,就大功告成了:

cd dist/i18n-tutorial
http-server

可以看到使用 select 標籤切換語言時,url 會跟著變化, form 裡面的語言也會跟著翻譯,成功完成了一個 Angular i18n 的簡單 app!

Angular i18n

9. Angular i18n 進階用法

9.1 使用 i18n id

我們可以觀察一下剛剛加了 i18n attribute 的 html element 在被 Angular 編譯為翻譯檔時會有 id:

Angular i18n
Angular i18n

這個時候的 id 是由 Angular 產生的一串數字,然而很多時候當我們的專案成長到一定的程度時,會有許多地方會用到相同的 i18n 功能,例如:很多個 component 可能都有 email 的欄位,要是我每次在 email 加一個 i18n 的 attribute,那麼在每個用到 email i18n 的地方都會產生獨立的 id。

i18n 提供了指定 id 的功能,只要加上 @@ ,就可以告訴 Angular :我的這個 html element 要用的是 id = … 的 i18n 。

舉例來說,我在剛剛的 i18n-form.component 下增加了一個 input 和 button element ,模擬其他 component 用到 email i18n 的狀況,這時候只要加上 i18n=”@@Email” ,Angular 就知道這三個 element 用的都是 id = Email 的 i18n:

<!-- src/app/i18n-form/i18n-form.component.html -->
<form [formGroup]="i18nForm" (ngSubmit)="onSubmit()">
  <mat-form-field appearance="outline">
    <mat-label i18n="@@Email">Email</mat-label>
    <input
      matInput
      type="email"
      placeholder="Email"
      formControlName="email"
    />
  </mat-form-field>
  <mat-form-field appearance="outline">
    <mat-label i18n>Password</mat-label>
    <input
      matInput
      type="password"
      placeholder="密碼"
      formControlName="password"
    />
  </mat-form-field>
  <div class="buttonContainer">
    <button
      mat-raised-button
      type="reset"
      (click)="onClear()"
      i18n
    >
      Reset
    </button>
    <button
      mat-raised-button
      color="primary"
      type="submit"
      [disabled]="!i18nForm.valid"
      i18n
    >
      Sign Up
    </button>
  </div>
  <mat-form-field appearance="outline" style="margin-top: 2em;">
    <mat-label i18n="@@Email">Email</mat-label>
    <input
      matInput
      type="email"
      placeholder="Email"
    />
  </mat-form-field>
  <div>
    <button
      mat-raised-button
      color="primary"
      i18n="@@Email"
    >
      Email
    </button>
  </div>
</form>

編譯過後的翻譯原始檔如下:

<!-- src/locale/messages.xlf -->
...
      <trans-unit id="Email" datatype="html">
        <source>Email</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">4</context>
        </context-group>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">41</context>
        </context-group>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">54,55</context>
        </context-group>
      </trans-unit>
...

如此一來我在修正其他翻譯檔時就只要改一個地方就好。

9.2 在 TypeScript 中使用 i18n

很多時候我們的資料並不是寫死在 html 檔裡,而是寫在 TypeScript 檔裡再讓 html 去讀取 TypeScript 裡的 data,我們也可以直接在 TypeScript 中使用 Angular i18n。

舉例來說:我在剛剛的 i18n-form.component 中增加了一個 select 欄位,裡面的資料是由 TypeScript 產生:

<!-- src/app/i18n-form/i18n-form.component.html -->
<form [formGroup]="i18nForm" (ngSubmit)="onSubmit()">
  <mat-form-field appearance="outline">
    <mat-label i18n="@@Email">Email</mat-label>
    <input
      matInput
      type="email"
      placeholder="Email"
      formControlName="email"
    />
  </mat-form-field>
  <mat-form-field appearance="outline">
    <mat-label i18n>Password</mat-label>
    <input
      matInput
      type="password"
      placeholder="密碼"
      formControlName="password"
    />
  </mat-form-field>
  <mat-form-field appearance="outline">
  <mat-select formControlName="address">
    <mat-option
      *ngFor="let city of cityList"
      [value]="city.id"
    >{{ city.name }}</mat-option>
  </mat-select>
</mat-form-field>
  <div class="buttonContainer">
    <button
      mat-raised-button
      type="reset"
      (click)="onClear()"
      i18n
    >
      Reset
    </button>
    <button
      mat-raised-button
      color="primary"
      type="submit"
      [disabled]="!i18nForm.valid"
      i18n
    >
      Sign Up
    </button>
  </div>
  <mat-form-field appearance="outline" style="margin-top: 2em;">
    <mat-label i18n="@@Email">Email</mat-label>
    <input
      matInput
      type="email"
      placeholder="Email"
    />
  </mat-form-field>
  <div>
    <button
      mat-raised-button
      color="primary"
      i18n="@@Email"
    >
      Email
    </button>
  </div>
</form>

這時候只要在 TypeScript 裡加上 $localize`:@@[i18n id]:[預設HTML顯示的字樣]`就有如同 i18n 的效果:

// src/app/i18n-form/i18n-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators, AbstractControl, ValidatorFn } from '@angular/forms';

const cityList = [
  {
    id: 1,
    name: $localize`:@@TaipeiCity:Taipei City`
  },
  {
    id: 2,
    name: $localize`:@@NewTaipeiCity:New Taipei City`
  },
  {
    id: 3,
    name: $localize`:@@KeelungCity:Keelung City`
  }
]

@Component({
  selector: 'app-i18n-form',
  templateUrl: './i18n-form.component.html',
  styleUrls: ['./i18n-form.component.css']
})
export class I18nFormComponent implements OnInit {
  cityList = cityList;
  i18nForm = this.fb.group(
    {
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(4)]],
      address: [1, [Validators.required]]
    }
  );
  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
  }

  onClear(): void {
    this.i18nForm.reset();
  }

  onSubmit(): void {
    console.log(this.i18nForm.getRawValue());
  }
}

編譯過後的翻譯原始檔:

...
      <trans-unit id="TaipeiCity" datatype="html">
        <source>Taipei City</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.ts</context>
          <context context-type="linenumber">8</context>
        </context-group>
      </trans-unit>
      <trans-unit id="NewTaipeiCity" datatype="html">
        <source>New Taipei City</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.ts</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="KeelungCity" datatype="html">
        <source>Keelung City</source>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.ts</context>
          <context context-type="linenumber">16</context>
        </context-group>
      </trans-unit>
...

最後面直接多了剛剛的 select list。

接著只要跟剛剛一樣為每個翻譯檔加上 target 標籤,標籤內容為翻譯過後的語言, i18n 就能發揮作用囉!

<!-- src/locale/messages.zh.hant.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
  <file source-language="en-US" datatype="plaintext" original="ng2.template">
    <body>
      <trans-unit id="Email" datatype="html">
        <source>Email</source>
        <target>電子郵件</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">4</context>
        </context-group>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">49</context>
        </context-group>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">62,63</context>
        </context-group>
      </trans-unit>
      <trans-unit id="1431416938026210429" datatype="html">
        <source>Password</source>
        <target>密碼</target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="5478121364779850827" datatype="html">
        <source> Reset </source>
        <target> 重設 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">27,28</context>
        </context-group>
      </trans-unit>
      <trans-unit id="744750008248382275" datatype="html">
        <source> Sign Up </source>
        <target> 註冊 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.html</context>
          <context context-type="linenumber">36,37</context>
        </context-group>
      </trans-unit>
      <trans-unit id="TaipeiCity" datatype="html">
        <source>Taipei City</source>
        <target> 台北市 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.ts</context>
          <context context-type="linenumber">8</context>
        </context-group>
      </trans-unit>
      <trans-unit id="NewTaipeiCity" datatype="html">
        <source>New Taipei City</source>
        <target> 新北市 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.ts</context>
          <context context-type="linenumber">12</context>
        </context-group>
      </trans-unit>
      <trans-unit id="KeelungCity" datatype="html">
        <source>Keelung City</source>
        <target> 基隆市 </target>
        <context-group purpose="location">
          <context context-type="sourcefile">src/app/i18n-form/i18n-form.component.ts</context>
          <context context-type="linenumber">16</context>
        </context-group>
      </trans-unit>
    </body>
  </file>
</xliff>

用 development 驗證 i18n 是否作用:

ng serve --configuration=tw

畫面如下:

Angular i18n

10. 參考資料

Localizing your app – Angular
Angular 9 Tutorial on Internationalization (i18n)
Angular Material UI component library

Leave a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Scroll to Top