Angularの多言語対応(i18n)
以前AngularにBootstrapを導入した際、「@angular/localize」という多言語化用のパッケージがあることを知りました。
そこで、以前より検討してた、Angularのメッセージ部を日本語と英語に表示を切り替えを、Angular:「国際化(i18n)」などのページを参考にしながら、対応してみることにしました。
i18n,locale
他のプログラムでも言語設定に触れる時、「i18n」やlocale(ロケール)といった言葉を目にします。
参照先のサイトによればi18nはinternationalizationの略語だそうです。18はiとnの間に18文字あることからきているそうです。
locale(ロケール)は、日本語や英語といった自然言語の違いだけでなく、他には通貨名や日付フォーマット、等を含んだ設定全般の事を指します。たとえば「en-US」や「ja-JP」といった表記がそれにあたります。ふたつのパートのうち前半で言語、後半で他の要素を表します。たとえば、英語を使う国々の場合先頭は接頭はenになりますが、イギリスはen-GB、アメリカはen-US、オーストラリアはen-AUという風に分かれます。
ja_JP.UTF-8のようにコードセットが付与された表記の場合もあります。
@angular/localize
Angularでは多言語化する場合は@angular/localizeパッケージをインストールします。
PS> ng add @angular/localize...
この後の流れは、おおむね次のようになります。
- テンプレート(html)の、多言語化したい場所をマークする
テンプレートに記述している静的な文字列の変換の方法を説明します。
- コンポーネント(ts)の、多言語化したい文字列をマークする
テンプレート側とは若干書式が違います。ただ、公式ページによるとテンプレート側のマークは、内部的には一度コンポーネント側の記述に変換されて処理されるようです。
- 変換(翻訳)用ファイルを作成する
Angular CLIから「ng extract-i18n」コマンドを実行すると、マークした場所をピックアップした変換(翻訳)用のファイルが生成されます。
- 変換(翻訳)用ファイルに変換箇所の記述をする
- buildやserveにおけるangular.json設定を変更する
テンプレートのマーク
変換対象の語句は「i18n」のカスタム属性でマークします。カスタム属性であってAngularのディレクティブではないため、これはツールやコンパイラによってそれと認識されます。またコンパイラは翻訳後、この属性を削除します。
app.component.html
<h1 i18n>The words here will be translated.</h1>
Angularのデフォルトのロケールは「en-US」なので、変換したい言葉の初期値は英語で設定しておきます。
この属性の値として、説明文となるテキストコメントを入れることができます。
app.component.html
<h1 i18n="description">The words here will be translated.</h1>
単に説明としてテキストだけを入れることも可能ですが、"(意味)|(説明)"というふうに|で分離して意味を付け加える事もできます。
app.component.html
<h1 i18n="meaning|description">The words here will be translated.</h1>
変換元のテキストに加えて、この説明文(または意味と説明文)を元に、変換用の識別子となりますので、同じ意味の言葉を繰り返して使う場合は、変換元のテキストだけでなくこの中身も同じにしておきます。
逆に、元の言語では同じ単語でも変換後の単語が違う場合は、値を別にしておきます。
また、識別子を固定にしたい(自動生成させたくない)場合は@@の後に任意に設定した識別子をつけることができます。説明文や意味も合わせて設定しておきたい場合は@@idはその最後に付与します。
識別子を固定にした時は、変換元のテキストや説明が違う場合でも同じものとみなされます。間違えて違う単語に同じ識別子を設定してしまった場合は先に記述されている識別子のルールに従います。
app.component.html
<h3 i18n="@@title">The words here will be translated.</h3> <h3 i18n="subtitle|this is h3 subtitle@@subtitle">Angular localize sample.</h3>
変換したい部分にタグがない場合はspan等でも代替できますが、コンパイル後そこに意味を持たないタグが残ります。それを防ぎたい場合はng-containerで囲むことで回避できます。
app.component.html
<ng-container i18n>part of</ng-container>
さらに属性値に対して変換をしたい場合は、「i18n-」の接頭を適用させたい属性名につけた属性を付与します。他の書式は前述と同じです。たとえばalt属性につけたい場合は、「i18n-alt」属性をタグの中に加えます。
コンポーネントのマーク
コンポーネント(ts)でマークをする場合は対象となる文字列をバッククオート(`)で囲った上にその先頭に$localizeという文字を加えます。
この$localizeはの実態は単なる関数ですが、タグ付きテンプレートの機能を使うことで、`between string and ${param} param`、といったようにプレースホルダ(変数)を含んだ文字列を渡すことができます。
app.component.ts
$localize`Next` $localize`残り${count}項目の入力が必要です`
コンポーネント側で意味や説明IDを加えたい場合は、先頭に:(コロン)変換元の文字列の前に付け加えます。また換元の文字列との区切りにも:(コロン)を付けます。
意味|説明@@idの書式はそのままです。
app.component.ts
$localize`:意味|説明@@id:変換元文字列`; $localize`:意味|:変換元文字列`; $localize`:説明:変換元文字列`; $localize`:@@id:変換元文字列`;
変換元文字列が:で始まる場合で、意味や説明、IDを付与しない場合は、先頭の:を\でエスケープします。
変換用ファイルの生成
変換用ファイルは「ng extract-i18n」コマンドで生成されます。
デフォルトでは、プロジェクトのルートディレクトリにmessages.xlfというファイルが生成されます。
作成されたら、後で管理しやすいようにディレクトリを作成しコピー・リネームしてその中に入れて編集します。ここでは、src¥localeデイレクトリを作成して、messages.ja.xlfとして保存しました。
ファイル生成の段階で、--out-file オプションを使うことで出力先や名前を指定することもできます。また、ファイルフォーマットも--format=オプションで指定できます。指定可能な値は「xmb,xlf,xlif,xliff,xlf2,xliff2,json,arb,legacy-migrate」です。
ng extract-i18n --out-file src/locale/messages.ja.xlf
この.xlfの拡張子がついたファイルに変換の対象となるテキストがピックアップされていますので、そこへ変換後のテキストを追加していきます。XLIFF(XML Localization Interchange File Format)形式を意味しますが、正式なフォーマットがわからなくても編集は可能です。中身は次のようになっています。
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="ngb.alert.close" datatype="html">
<source>Close</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">79</context>
</context-group>
<note priority="1" from="description">description value</note>
<note priority="1" from="meaning">meain valueg</note>
</trans-unit>
...
この後はtrans-unit要素の繰り返しになります。このtrans-unitでかこまれたブロックが変換の定義のひとつのまとまりになります。この要素のid属性に、テンプレートで@@idの値として設定した値が入ります(@の文字は除去されます)。idを指定していない場合は、自動の場合は自動生成値が入ります。この値で変換前のテキストと変換後のテキストが結び付けられます。
trans-unit要素は次の子要素を持ちます。
- source
変換元のテキストです。この値を直接変更しても何もおきません。
- target
初期時は存在しないと思います。このタグで囲んだ形で変換後のテキストを作成します。
- content-group purpose="location"
「context content-type-"sourcefile"」に変換元のテキストが出現するファイルパス、「context content-type="linenumber"」にそれが出現する行番号が入ります。
- note...
オプション値です。テンプレート側で「説明」が設定されていれば属性値「from="description"」が入った要素のタグに「説明」が、「意味」が設定されていれば「from="meaning"」の属性の入った要素に「意味」が入ります。
コピーしてファイル名を変えたxlfファイルでは、sourceタグでかこまれた部分を下段にコピーしtargetに変えた上で、中身を変換後のものに変えて保存します。
messages.ja.xlf
...
<source>Return</source>
<target>戻る</target>
...
angular.jsonファイルの変更
変換用のファイルの記述が終わったら、angular.jsonファイルに反映させて利用可能にします。
まずはprojects/プロジェクト名オブジェクトの直下にi18nオブジェクトを作成します。この時、先ほど作成した変換後のファイルの場所を指定します。
angular.json
...
"projects":{
"プロジェクト名":{
"projectType": "application",
...,
"i18n":{
"sourceLocale": "en-US",
"locales": {
"ja": "src/locale/messages.ja.xlf"
}
},
...
この状態でja用のビルドができるようになりました。次のようにコマンドを実行すると、デフォルトロケールのen-USとjaそれぞれのフォルダにわかれて2種類のアプリがビルドされます。
各フォルダにあるindex.htmはbaseHrefの値にロケール名を付与した場所のjsやcssを参照するように記述されます。例えば「/href」をbaseHrefにしていた場合は「/herf/en-US」や「/href/ja」という具合です。
そのため、通常のbuildと同じように「./」と相対パスをにすると「./en-US」や「./ja」はフォルダとして存在しませんので必要なファイルが見つからずページが表示されません。そこで、多言語化した場合のbaseHrefは一つ上のフォルダを指定する「../」と設定します。
> ng build --configuration="production" --localize --base-href=../
先のコマンドではangular.jsonのi18nに設定されたロケールすべてでビルドするものになります。これだとserveで実行できないので、configurationsを設定して日本語のアプリだけをビルドすることも可能にします。
基本的な設定は"production"のもののコピーでいいとおもいます。そこに「"localize": ["ja"]」と、もし起動時のスクリプトの切り分けをしたいなら「"main": "src/main-for-ja.ts"」として設定します。
angular.json
...
"build": {
...
"configurations": {
"production": {
...
},
"development": {
...
},
"ja":{
"localize": ["ja"],
"main": "src/main.ts",//localeによりmainを切り分けているなら
...あとはproductionのオブジェクトのコピー
}
...
設定を終え、次のようにするとロケールがjaだけのビルドをします。
> ng build --configuration="ja"
serve側の設定です、これはターゲットをjaにするだけです。serveではbuildと違ってすべての言語を一度に実行することはできません。
angular.json
...
"serve": {
...
"configurations": {
"production": {
"browserTarget": "プロジェクト名:build:production"
},
"development": {
"browserTarget": "プロジェクト名:build:development"
},
"ja":{
"browserTarget": "プロジェクト名:build:ja"
}
...
上記の設定で実際に実行してみると次のようなエラーが出ますが、serve用にビルドしたものを本番環境で使わなければ問題ないと思われます。
> ng serve --configuration="ja" --open ... This is a simple server for use in testing or debugging Angular applications locally. It hasn't been reviewed for security issues. DON'T USE IT FOR PRODUCTION!
パイプ
先ほどまでの話とは別で、DatePipe、CurrencyPipe、DecimalPipe、 および PercentPipeパイプは、多言語化の機能を持っています。
これにパイプされた値はデフォルトで、アプリケーションに設定されたLOCALE_IDに従って、そのロケールに沿った形でフォーマットします。
先ほども記述した通りAngularのデフォルトのロケールはen-USです。パイプで他のロケールを利用するには、ロケールをインポートする必要があります。
インポートしたロケールを、registerLocaleDataを使ってセットします。
また、デフォルトのLOCALE_IDを変更するには、app.module.tsのprovidersに設定します。
app.module.ts
...
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';//フランス語のロケールインポート
import localeJa from '@angular/common/locales/ja';//日本語語のロケールインポート
registerLocaleData(localeFr, 'fr-FR');//フランス語登録
registerLocaleData(localeJa, 'ja');//日本語 拡張部がない設定の仕方も可能です
providers: [
...
{ provide: LOCALE_ID, useValue: 'ja'}, //登録時にja-JPとした場合はここもja-JPとします。
...
違いがわかりやすかったのが、dateパイプなので、それを利用して例示します。
app.component.html
... <p>{{ 0 | date }}</p> ...
結果は次の通りでした。正式にはJavaScriptのDateオブジェクトを渡すべきですが、テストなので0(1970年の1月1日のタイムスタンプ)を渡しています。
- ja
1970/01/01
- fr-FR
1 janv. 1970
- LOCALE_ID設定なし(en-US)
Jan 1, 1970
関連設定
もし、Angularのバックエンドからメッセージを受け取っている場合はそちらは別に設定が必要です。筆者がバックエンドとしてよく使うPHPでの多言語化については別ページで紹介していますので、合わせてお読みいただければ幸いです。