[!] この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
i18n とは
- "internationalization"(国際化)の略で、アプリケーションを様々な言語や文化に合わせて利用できるように改良することを i18n といいます。
- 単にテキストを翻訳するだけでなく、日付、通貨単位や重さなどの単位、縦書きか横書きかなど様々なことを考慮に入れないといけません。
- 国際化は、Angular アプリケーションを実装する際、ぜひとも取り入れたい要件の一つではありますが、国際化を実装するにあたり、単純な方法はなく、様々な要因からベストな実装方法を模索していくことになります。
- 国際化を実現するには主に 2 つの方法があります。今回は Angular でそれぞれの方法の中で人気のライブラリについて、いくつかピックアップしてご紹介していきます。
この記事の対象者
- Angular の基礎を理解している方
- 国際化対応をする際、選択肢が多くてお困りの方
本記事で使用している開発環境
- Angular:16.0.2
- OS: Windows 11 Pro
i18n を実現する2つの方法
コンパイル時変換
- コンパイル時に変換する必要があるすべての文字列を置換する方法
- メリット
- すべての変換はコンパイル時に事前に置換されるため、アプリケーションの実行時にパフォーマンスの低下を気にする必要がない
- デメリット
- 変換したい言語の数だけ成果物をビルドする必要がある。動的に言語選択したいときは、選択した言語の成果物に切り替える必要があるため、実行時変換に比べて Web サーバの構成や、ルーティングが複雑になりやすい
実行時変換
- アプリケーションの実行時に変換を行う方法
- 予め用意した翻訳ファイルをアプリケーション実行時に読み込むことで動的に変換を行う
- メリット
- アプリケーションの構成が国際化対応する前と比べて複雑になりづらい
- デメリット
- アプリケーション実行時に翻訳ファイルをロードすることになるので、http 通信が発生してしまう
- 規模が大きいアプリケーションほど、動的変換によるパフォーマンス低下の影響を受ける
コンパイル時変換の代表的なライブラリ
@angular/localize
- Angular 組み込みの公式ライブラリ
- メリット
- 現時点で Angular i18n ライブラリでは最も人気があり、公式ドキュメントや情報が充実している
- Angular の一部であるため、更新頻度がとても高く(少なくとも月一回以上)サポートが手厚い
- pipe を使用した日付のフォーマットや通貨マーク等の動的変換ができる
- コンパイル時変換方式であるため、パフォーマンスの低下を気にする必要がなく、大規模開発向け
- デメリット
- 他のライブラリに比べ学習難度が高い
- 変換する場所が増えるたびにファイル生成をし直し、既存のファイルとのマージ作業が発生するためメンテナンス性には難点がある
導入方法
サンプルとして、簡単な購入フォームを作成して 日本語 ⇔ 英語 の国際化対応をしてみたいと思います。なお、スタイリングの為に Angular Material を使用していますが、そちらの解説は今回は割愛します。 下記は今回使用するサンプルコードです。
app.component.ts
import { Component, Inject, LOCALE_ID } from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent {
vegetables = [
{ name: "トマト", num: 10, shippingTime: "2023/05/24", price: 430 },
{ name: "ブロッコリー", num: 20, shippingTime: "2023/05/23", price: 355 },
{ name: "じゃがいも", num: 3, shippingTime: "2023/05/25", price: 150 },
{ name: "人参", num: 5, shippingTime: "2023/05/22", price: 160 },
{ name: "ピーマン", num: 10, shippingTime: "2023/05/23", price: 470 },
{ name: "キャベツ", num: 20, shippingTime: "2023/05/24", price: 170 },
];
arrayNumberLength(number: number): any[] {
return [...Array(number)];
}
}
app.component.html
<mat-toolbar color="primary">
<span>i18n sample app</span>
<span class="example-spacer"></span>
</mat-toolbar>
<div class="content">
<ng-container *ngFor="let vagetable of vegetables">
<mat-card class="card">
<mat-card-header class="card-header">
<mat-card-title>{{vagetable.name}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<span>出荷数:{{vagetable.num}} kg</span>
</mat-card-content>
<mat-card-content>
<span>出荷日:{{vagetable.shippingTime | date:'yyyy年MM月dd日'}}</span>
</mat-card-content>
<mat-card-content>
<span>値段(kg):{{vagetable.price | currency:'JPY'}} 円</span>
</mat-card-content>
<mat-card-content>
<mat-form-field>
<mat-label>購入数(kg)</mat-label>
<mat-select>
<mat-option
*ngFor="let num of arrayNumberLength(vagetable.num); index as i"
[value]="i"
>
{{i + 1}}kg
</mat-option>
</mat-select>
</mat-form-field>
</mat-card-content>
<mat-card-content class="card-content">
<button mat-flat-button color="accent" type="submit">buy</button>
</mat-card-content>
</mat-card>
</ng-container>
</div>
app.component.css
.example-spacer {
flex: 1 1 auto;
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.card {
width: 400px;
margin: 3rem auto;
}
.card-header {
display: flex;
justify-content: center;
padding: 1rem;
}
.card-content {
display: flex;
flex-direction: column;
}
まず、プロジェクトに@angular/localize パッケージを追加します。
ng add @angular/localize
@anuglar/localize では変換した言語ファイルで ng serve
や ng build
をするためにはangular.json
を変更する必要があります。
ここら辺は公式ドキュメントが分かりやすいですが、設定が必要な部分を抜粋して紹介します。
angular.json
"projects": {
"angular-i18n-sample": {
// 追加
"i18n": {
"sourceLocale": "en-US",
"locales": {
"ja": {
"translation": "src/locale/messages.ja.json"
},
"en": {
"translation": "src/locale/messages.en.json"
}
}
},
}
}
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
// 追加
"localize": true,
},
}
"build": {
"configurations": {
// 追加
"ja": {
"localize": ["ja"]
},
"en": {
"localize": ["en"]
}
},
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
// 追加
"ja": {
"browserTarget": "angular-i18n-sample:build:development,ja"
},
"en": {
"browserTarget": "angular-i18n-sample:build:development,en"
}
},
},
ここまでで初期設定は終了です。
次に実際に変換作業に移っていきます。
i18n 属性を使用して、コンポーネントテンプレート内の全てのテキストをマークしていきます。
<!--これを-->
<span>i18n sample app</span>
<!--これに-->
<span i18n>i18n sample app</span>
ソースコード内のテキストは $localize とバッククオート文字( ` ) で囲むことで変換できます。
// これを
name: "トマト";
// これに
name: $localize`トマト`;
日付のフォーマットも locale によって変更できます。
Angular には pipe というフォーマット機能がありますが、これは locale に対応しています。
今回は pipe を使用して、yyyy年MM月dd日
と固定表示させていますので、カスタムフォーマットをやめて、locale ごとのデフォルトフォーマットにしてみます。
<!--これを-->
<span>出荷日:{{vagetable.shippingTime | date:'yyyy年MM月dd日'}}</span>
<!--これに-->
<span>出荷日:{{vagetable.shippingTime | date"}}</span>
一度変換ファイルを出力してみます。このとき、コマンドオプションを追加することで、ソース言語ファイルの場所、フォーマット、ファイル名を指定することができます。
--format
(出力ファイルのフォーマットを設定する)--out-file
(出力ファイルのファイル名を設定する)--output-path
(出力先ディレクトリのパスを設定する)
今回は下記で実行してみます。
ng extract-i18n --format=json --out-file=messages.en.json --output-path=./src/locale/
src 配下に locale フォルダが作成され、messages.en.json
が作成されました。
作成したmessages.en.json
を確認してみます。
messages.en.json
{
"locale": "en-US",
"translations": {
"4620635300831144134": "i18n sample app",
"9214206249205702546": "出荷数:{$INTERPOLATION} kg",
"1295556927564936626": "出荷日:{$INTERPOLATION}",
"7917787923726762220": "値段(kg):{$INTERPOLATION} 円",
"8717191150242400960": "購入",
"6117368252728179045": "トマト",
"1323480278677303850": "ブロッコリー",
"4074549688188433745": "じゃがいも",
"8335428839579026746": "人参",
"8762090999450365602": "ピーマン",
"4288587544101204934": "キャベツ"
}
}
i18n の属性を付けたテキストが json 形式で出力されています。
テキストの key として ID が付けられていますが、このままでは数字の羅列なので分かりづらいです。
カスタム ID を設定してみます。カスタム ID はテンプレートテキストの場合は i18n=@@"カスタムID"
で設定することができます
<!--これを-->
<span i18n>i18n sample app</span>
<!--これに-->
<span i18n="@@i18n sample app">i18n sample app</span>
ソースコード内のテキストの場合は
// これを
name: $localize`トマト`;
// これに
name: $localize`:@@トマト:トマト`;
こうすることでカスタム ID が設定できます。
もう一度変換ファイルを出力してみると
messages.ja.json
{
"locale": "en-US",
"translations": {
"i18n sample app": "i18n sample app",
"収穫数": "収穫数:{$INTERPOLATION} kg",
"収穫日": "収穫日:{$INTERPOLATION}",
"値段(kg)": "値段(kg):{$INTERPOLATION} 円",
"購入数(kg)": "購入数(kg)",
"購入": "購入",
"トマト": "トマト",
"ブロッコリー": "ブロッコリー",
"じゃがいも": "じゃがいも",
"人参": "人参",
"ピーマン": "ピーマン",
"キャベツ": "キャベツ"
}
}
自分で付与したカスタム ID で出力できました。
後はこちらの変換ファイルを変換したい言語に書き換えます。
{
"locale": "en-US",
"translations": {
"i18n sample app": "i18n sample app",
"収穫数": "Harvest number:{$INTERPOLATION} kg",
"収穫日": "Harvest day:{$INTERPOLATION}",
"値段(kg)": "price(kg):{$INTERPOLATION} yen",
"購入数(kg)": "Order(kg)",
"購入": "Buy",
"トマト": "Tomato",
"ブロッコリー": "Broccoli",
"じゃがいも": "Potatoes",
"人参": "Carrot",
"ピーマン": "Green pepper",
"キャベツ": "Cabbage"
}
}
書き換えが完了したら実際に変換した画面を表示してみましょう。
言語を英語にするためには下記コマンドで実行します。
ng serve --configuration=en
無事英語化した画面が出力されました。
実際にビルドもしてみます。 ビルドする前に忘れずに日本語の変換ファイルも作成しておきます。
ng build --localize
dist にen
,ja
というビルド成果物が配置されました。
次に web サーバと接続して動的に言語選択できるようにしてみます。
web サーバはここでは golang を使ってみたいと思います。
作成するディレクトリ構成
angular-i18n-sample
├─angular-i18n-sample.go
└─app
├─en
└─ja
golang を公式サイトからインストールしてきたら、angular-i18n-sample.go
という go ファイルを作成します。
golang の実装の説明は省きますがサンプルを置いておきます。
angular-i18n-sample.go
package main
import (
"fmt"
"html/template"
"log"
"net/http"
)
func jaHandler(w http.ResponseWriter, r *http.Request) {
html, err := template.ParseFiles("app/ja/index.html")
if err != nil {
log.Fatal(err)
}
if err := html.Execute(w, nil); err != nil {
log.Fatal(err)
}
}
func enHandler(w http.ResponseWriter, r *http.Request) {
html, err := template.ParseFiles("app/en/index.html")
if err != nil {
log.Fatal(err)
}
if err := html.Execute(w, nil); err != nil {
log.Fatal(err)
}
}
func main() {
http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("app"))))
http.HandleFunc("/ja", jaHandler)
http.HandleFunc("/en", enHandler)
fmt.Println("Golang Server Start...")
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
angular-i18n-sample.go
と同じ階層に app フォルダを作成し、先ほどビルドしたen
,ja
を配置します。
ここまで来たら web サーバを立ち上げてみます。
go run angular-i18n-sample.go
http://localhost:8080/ja
http://localhost:8080/en
購入フォームが各言語で表示できました。
言語選択のボタンを実装してみます。
app.component.html
・・・
<mat-form-field class="locale">
<mat-select (selectionChange)="onChange()" [(ngModel)]="formLocale">
<mat-option *ngFor="let locale of locales" [value]="locale.code">
{{ locale.name }}
</mat-option>
</mat-select>
</mat-form-field>
app.component.ts
・・・
locales = [
{ code: 'en', name: 'English' },
{ code: 'ja', name: '日本語' },
];
constructor(
@Inject(LOCALE_ID) public formLocale: string
) {}
onChange() {
window.location.href = `/${this.formLocale}`;
}
再度ビルドして web サーバに配置します。
動的に変更できるボタンの実装ができました。
実行時変換の代表的なライブラリ
@ngx-translate
- 標準の方法とともに人気度の高いライブラリ
- メリット
- こちらも人気のライブラリなので情報が充実している
- 実装方法自体はシンプルで、学習難度も低い
- 実行時変換手法なので動的な変換も容易に行える
- デメリット
- 現在開発段階がメンテナンスモードなため、更新が最低限しか行われていない *2023年6月現在
- 最新の更新が 2023/5/9
- 前回の更新が 2021/11/8
- 現在開発段階がメンテナンスモードなため、更新が最低限しか行われていない *2023年6月現在
導入方法
先ほどのサンプルを初期状態に戻し、次は@ngx-translate
を用いて国際化対応してみたいと思います。
まずはパッケージのインストールから始めます。下記のコマンドを実行してください。
npm install @ngx-translate/core
npm install @ngx-translate/http-loader
次にsrc/app/app.module.ts
を下記のように書き換えます。
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatButtonModule } from "@angular/material/button";
import { MatInputModule } from "@angular/material/input";
import { MatCardModule } from "@angular/material/card";
import { HttpClient, HttpClientModule } from "@angular/common/http";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from "@ngx-translate/core";
// 追加
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
}
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
MatCheckboxModule,
MatButtonModule,
MatInputModule,
MatCardModule,
// 追加
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient],
},
defaultLanguage: "ja",
}),
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {
// 追加
constructor(translate: TranslateService) {
translate.use("en");
}
}
ここまでで@ngx-translate の初期設定は終わりです。簡単ですね。 では実際に変換作業に移っていきます。
まずは@angular/localize と同じようにテンプレート内のテキストに変換用の属性をつけていきます。
@ngx-translate では translate
という属性をつけていきます。
注意点として、translate 属性はbutton
などの一部タグでは使用できません。 *2023年6月現在
https://github.com/ngx-translate/core/issues/849 ⧉
回避策として、<span>
を追加する方法を取ります。
<!--これを-->
<span>i18n sample app</span>
<!--これに-->
<span translate>i18n sample app</span>
<button mat-flat-button color="accent" type="submit">購入</button>
<!--span追加-->
<button mat-flat-button color="accent" type="submit">
<span translate>購入</span>
</button>
動的に言語選択させるには TranslateService
を使用します。
app.component.html
<mat-form-field class="locale">
<mat-label translate>言語選択</mat-label>
<mat-select (selectionChange)="onChange($event.value)">
<mat-option *ngFor="let locale of locales" [value]="locale.code">
{{ locale.name }}
</mat-option>
</mat-select>
</mat-form-field>
app.component.ts
import { TranslateService } from '@ngx-translate/core';
export class AppComponent {
locales = [
{ code: 'en', name: 'English' },
{ code: 'ja', name: '日本語' },
];
constructor(
// 追加
private translate: TranslateService
) { }
// 書き換え
onChange(lang: string) {
this.translate.use(lang);
}
}
変換ファイルも準備します。
src/assets/i18n/
に変換ファイルを json 形式で用意します。
今回は en.json
、ja.json
を作成します。
en.json
{
"i18n sample app": "i18n sample app",
"トマト": "Tomato",
"ブロッコリー": "Broccoli",
"じゃがいも": "Potatoes",
"人参": "Carrot",
"ピーマン": "Green pepper",
"キャベツ": "Cabbage",
"収穫数": "Harvest number",
"収穫日": "Harvest day",
"値段(kg)": "price(kg)",
"購入数(kg)": "Order(kg)",
"購入": "Buy",
"言語選択": "Select Language"
}
ja.json
{
"i18n sample app": "i18n sample app",
"トマト": "トマト",
"ブロッコリー": "ブロッコリー",
"じゃがいも": "じゃがいも",
"人参": "人参",
"ピーマン": "ピーマン",
"キャベツ": "キャベツ",
"収穫数": "収穫数",
"収穫日": "収穫日",
"値段(kg)": "値段(kg)",
"購入数(kg)": "購入数(kg)",
"購入": "購入",
"言語選択": "言語選択"
}
これで変換作業は終了です。ng serve
を実行してみると、変換されたログインフォームが確認できます。
しかしこのままだと収穫日の日付が動的に変換できていません。
これは@ngx-translate
だと pipe を用いた動的変換ができないことに起因しています。
回避策としてはカスタムパイプを実装するか、pipe を使うことをやめて表記を統一するかになると思います。
@ngneat/transloco
- 2019 年頃に開発された比較的新しいライブラリ
- メリット
- 導入方法はシンプル
- コミュニティの開発が活発(少なくともひと月に 1 回以上)
- 選択した言語に対す言語 API や変換したソースに対する変換 API など、機能面が充実している
- デメリット
- 情報が少ない
導入方法
まずはパッケージのインストールから。 下記コマンドを実行してください。
ng add @ngneat/transloco
インストールが始まったら何点か設定に関する質問が表示されます。適宜回答しながら進めてください。 今回は変換ファイルとして、英語、日本語の変換ファイルを作成したいので、
which languages do you need? -> en, ja
と回答しました。
回答が終わると自動で新しいモジュール(transloco-root.module.ts)と変換ファイル(en.json, ja.json)が作成され、app.module.ts でも、TranslocoRootModule をインポートするよう変更されます。
これで@ngneat/transloco の初期設定は終わりです。
次に実際に変換してみます。
テンプレートテキストの変換方法は *transloco
という構造ディレクティブを使用することが推奨されています。
<ng-container *transloco="let t">
<p>{{ t('key')}}</p>
</ng-container>
というように変換したいテキストの部分を key にして変換ファイルで記述すれば変換結果が出力できます。
app.component.html
<ng-container *transloco="let t">
<mat-toolbar color="primary">
<span>{{ t ("i18n sample app")}}</span>
<span class="example-spacer"></span>
<mat-form-field class="locale">
<mat-label>{{ t ("言語選択")}}</mat-label>
<mat-select (selectionChange)="onChange($event.value)">
<mat-option *ngFor="let locale of locales" [value]="locale.code">
{{ locale.name }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-toolbar>
<div class="content">
<ng-container *ngFor="let vagetable of vegetables">
<mat-card class="card">
<mat-card-header class="card-header">
<mat-card-title>{{vagetable.name | transloco}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<span>{{ t ("収穫数")}}:{{vagetable.num}} kg</span>
</mat-card-content>
<mat-card-content>
<span>{{ t ("収穫日")}}:{{vagetable.harvestTime | date }}</span>
</mat-card-content>
<mat-card-content>
<span
>{{ t ("値段(kg)")}}:{{vagetable.price | currency:'JPY' }}{{ t
("円")}}</span
>
</mat-card-content>
<mat-card-content>
<mat-form-field>
<mat-label>{{ t ("購入数(kg)")}}</mat-label>
<mat-select>
<mat-option
*ngFor="let num of arrayNumberLength(vagetable.num); index as i"
[value]="i"
>
{{i + 1}}kg
</mat-option>
</mat-select>
</mat-form-field>
</mat-card-content>
<mat-card-content class="card-content">
<button mat-flat-button color="accent" type="submit">
{{ t ("購入")}}
</button>
</mat-card-content>
</mat-card>
</ng-container>
</div>
</ng-container>
en.json
{
"i18n sample app": "i18n sample app",
"トマト": "Tomato",
"ブロッコリー": "Broccoli",
"じゃがいも": "Potatoes",
"人参": "Carrot",
"ピーマン": "Green pepper",
"キャベツ": "Cabbage",
"収穫数": "Harvest number",
"収穫日": "Harvest day",
"値段(kg)": "price(kg)",
"円": "yen",
"購入数(kg)": "Order(kg)",
"購入": "Buy",
"言語選択": "Select Language"
}
動的に言語選択させるには TranslocoService
を使用します。
import { TranslocoService } from '@ngneat/transloco';
export class AppComponent {
constructor(
private translate: TranslocoService
) { }
onChange(lang: string) {
this.translate.setActiveLang(lang);
}
}
ng serve
で画面を確認してみると変換できていますね。
ここまでは @ngx-translate ととてもよく似ています。
ngneat-transloco がすごいのはここからです。
transloco にはLocale L10N
という優秀なプラグインが存在しています。
因みにL10N
とは Localization の略で、特定の地域に合わせた言語や文化を使用することを言います。
Locale L10N をインストールして実際に使ってみます。
npm i @ngneat/transloco-locale
transloco-root.module.ts
import { TranslocoLocaleModule } from '@ngneat/transloco-locale';
@NgModule({
imports: [
TranslocoLocaleModule.forRoot()
],
...
})
export class TranslocoRootModule {}
Locale L10N をインポートしたらローカリゼーションパイプである日付パイプを使ってみます
app.component.html
<!--これを-->
<span>{{ t ("収穫日")}}:{{vagetable.harvestTime: date }}</span>
<!--これに-->
<span
>{{ t ("収穫日")}}:{{vagetable.harvestTime | translocoDate:{ dateStyle:
'medium' } }}</span
>
日付を locale ごとのフォーマットで表示できるようになりました。
Locale L10N プラグインにはそのほかにも便利なパイプやオプションが多数存在しています。
この点が transloco の明確な強みだなと感じますね。
それぞれの特徴
-
1 週間当たりの DL 数 *2023年6月現在
- @angular/localize 80 万 DL
- @ngx-translate/core 75 万 DL
- @ngneat/transloco 10 万 DL
ひと昔前は @angular/localize は変換ファイルが分かりづらいことや、メンテナンス性に難点があったことから、@ngx-translate が人気のライブラリでした。
しかし、現在は @angular/localize の機能も充実していること。@ngx-translate がメンテナンスモードとなっていて、活発な開発が行われていないことからそれぞれの利用者はほぼほぼ同数である状況です。
@ngneat/transloco は上記 2 つに比べると人気のないライブラリであることが分かります。ただ、決して機能的に劣っているわけではなく、単純に新しいライブラリであるため情報が少ないことが避けられている要因かなと思います。
所感
- 今回は i18n を実現する 2 つの方法と、Angular で人気のライブラリについて紹介しました。
- 個人的にはサポートが手厚いことや、ドキュメントが豊富なことから@angular/localize を推しています。
- @ngx-translate は翻訳機能に特化しているといった印象で、ローカライズ面では他には劣るのかなと思います。また、今後新たな機能の追加も現時点ではあまり望めないので、今からこのライブラリを使用して i18n を進めるのはあまりお勧めしません。
- 大規模開発に使用するなら@angular/localize。小規模開発に使用するなら@ngneat/transloco がいい選択肢かなと自分は考えます。
- ここまで読んでいただきありがとうございます。この記事が読者の i18n 対応の手助けになれば幸いです。
参考サイト
https://angular.jp/guide/i18n-overview ⧉