国際化(i18n)と ローカライゼーション
Angularにはフレームワーク組み込みの国際化(i18n)機能があり、大規模なマルチリージョンアプリケーション開発において標準的な手法でローカライゼーションを実装できます。
1. Angular 組み込み i18n
Angular 標準の i18n は @angular/localize パッケージと Angular CLI で構成されます。テンプレート中の翻訳対象に i18n 属性を付け、CLI で翻訳ソースを抽出し、翻訳者が訳文を埋めたファイルをビルド時に取り込む、という流れです。
ng add @angular/localize
i18n 属性によるマークアップ
<h1 i18n="@@home.title">ようこそ</h1>
<!-- 意味と説明を付けると、翻訳者に文脈が伝わる -->
<button i18n="ボタン|注文確定の操作@@order.submit">注文する</button>
<!-- 属性値の翻訳は i18n-属性名 -->
<img src="logo.png" alt="会社のロゴ" i18n-alt="@@home.logoAlt" />
<!-- 余分な要素を作りたくない場合は ng-container -->
<ng-container i18n="@@home.lead">Angular で作られたアプリです。</ng-container>
i18n="意味|説明@@カスタムID" の書式で、@@ 以降が翻訳 ID です。ID を明示しないと内容から自動生成されるため、原文を少し直しただけで ID が変わり、翻訳が失われます。必ずカスタム ID を付けてください。
コード内の文字列には $localize タグ付きテンプレートリテラルを使います。
const message = $localize`:@@cart.empty:カートは空です`;
翻訳ファイルの抽出
ng extract-i18n --format xlf2 --output-path src/locale
messages.xlf が生成され、翻訳者はこれを複製して messages.en.xlf のように訳文を記入します。形式は次の中から選べます。
| 形式 | --format | 特徴 |
|---|---|---|
| XLIFF 1.2 | xlf | 既定。翻訳ツール(CAT ツール)の対応が最も広い |
| XLIFF 2.0 | xlf2 | 新しい規格。新規なら推奨 |
| XMB / XTB | xmb | Google 系の形式。抽出は xmb、訳文は xtb |
| ARB | arb | Flutter と共通の JSON ベース形式 |
| JSON | json | 人間が読み書きしやすい単純な形式 |
ビルド時のロケール切り替え
angular.json にロケールを定義します。
{
"projects": {
"my-app": {
"i18n": {
"sourceLocale": "ja",
"locales": {
"en": { "translation": "src/locale/messages.en.xlf" },
"zh-Hans": { "translation": "src/locale/messages.zh-Hans.xlf" }
}
},
"architect": {
"build": {
"options": { "localize": true }
}
}
}
}
}
ng build --localize # 全ロケール分をビルド
ng build --localize=en # 特定ロケールのみ
ng serve --configuration=en # 開発時は 1 ロケールずつ
dist/my-app/browser/ja/, en/, zh-Hans/ のようにロケール別の成果物が生成され、Web サーバーや CDN で URL パスまたはサブドメインごとに振り分けます。
Next.js の App Router には i18n の標準機構がなく(Pages Router にはロケールルーティングのみあった)、next-intl や react-i18next、react-intl などサードパーティの実行時ライブラリに頼るのが一般的です。Angular はビルド時に翻訳を静的に埋め込む標準機構を持つ点が大きな違いです。
2. ビルド時 i18n vs ランタイム i18n
| 観点 | ビルド時 i18n(Angular 標準) | ランタイム i18n(Transloco 等) |
|---|---|---|
| 翻訳の適用 | ビルド時にテンプレートへ静的に埋め込み | 実行時に JSON を読み込み置換 |
| ビルド数 | ロケール数分 | 1 つ |
| 言語切り替え | 別 URL へ遷移(ページ再読み込み) | 再読み込みなしで即時 |
| 実行時コスト | 翻訳ロジックなし、バンドルも最小 | ライブラリ分と翻訳 JSON の取得が必要 |
| ビルド時間 | ロケールが増えるほど長い | 一定 |
| 翻訳の更新 | 再ビルド・再デプロイが必要 | JSON の差し替えのみで反映可能 |
| 型安全・検証 | 抽出時に欠落を検知しやすい | キーの検証は別途ツールが必要 |
選択の目安は次のとおりです。
- SEO が重要で、ロケールごとに URL が分かれるサイト(SSR / SSG を含む)はビルド時 i18n が向いています。
- ログイン後の業務アプリで、ユーザー設定により言語を即時に切り替えたい場合はランタイム i18n が向いています。
- ロケール数が多い(10 以上)とビルド時間と成果物が膨らむため、ランタイムを検討します。
- 両者は排他ではありません。ただし同じ文言を二重に管理しないよう、役割分担を明確にしてください。
3. ランタイム翻訳ライブラリ
@ngx-translate/core
長年使われてきたデファクトのライブラリで、既存プロジェクトでは今も広く見かけます。TranslateService と translate パイプが中心です。
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideTranslateService, provideTranslateHttpLoader } from '@ngx-translate/core';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideTranslateService({
lang: 'ja',
fallbackLang: 'ja',
loader: provideTranslateHttpLoader({ prefix: '/assets/i18n/', suffix: '.json' }),
}),
],
};
@Component({
selector: 'app-header',
imports: [TranslatePipe],
template: `
<h1>{{ 'header.title' | translate }}</h1>
<p>{{ 'header.welcome' | translate: { name: userName() } }}</p>
<button (click)="change('en')">English</button>
`,
})
export class HeaderComponent {
private translate = inject(TranslateService);
userName = signal('Taro');
change(lang: string): void {
this.translate.use(lang);
}
}
なお、設定 API は v16 以降に変更されており(provideTranslateService や lang / fallbackLang など)、メジャーバージョンごとに差があります。導入時は使用バージョンのドキュメントを確認してください。
@jsverse/transloco
ngx-translate の次の選択肢として人気のモダンなライブラリです。遅延ロードされるスコープ、Signal 対応、キー抽出・欠落検出の CLI/プラグインが揃っています。
import { provideTransloco } from '@jsverse/transloco';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideTransloco({
config: {
availableLangs: ['ja', 'en'],
defaultLang: 'ja',
reRenderOnLangChange: true,
prodMode: !isDevMode(),
},
loader: TranslocoHttpLoader, // assets/i18n/{lang}.json を取得するローダー
}),
],
};
@Component({
selector: 'app-profile',
imports: [TranslocoPipe],
template: `
<h2>{{ 'profile.title' | transloco }}</h2>
<p>{{ t('profile.greeting', { name: name() }) }}</p>
`,
})
export class ProfileComponent {
private transloco = inject(TranslocoService);
name = signal('Taro');
// Signal として現在の翻訳を扱う
t = (key: string, params?: Record<string, unknown>) => this.transloco.translate(key, params);
switchTo(lang: string): void {
this.transloco.setActiveLang(lang);
}
}
遅延ロードによるロケールファイルの分割
アプリ全体を 1 つの巨大な JSON にすると初期ロードが重くなります。Transloco では機能(ルート)単位でスコープを切り、必要になった時点で取得できます。
export const ADMIN_ROUTES: Routes = [
{
path: '',
component: AdminComponent,
providers: [provideTranslocoScope('admin')], // assets/i18n/admin/ja.json など
},
];
assets/i18n/
ja.json # 共通
en.json
admin/
ja.json # 管理画面用
en.json
ngx-translate でも provideTranslateService の loader を機能ごとに追加するなど、同様の分割は可能です(拡張ライブラリが必要になる場合があります)。
新規開発なら Transloco、既存の ngx-translate 資産がある場合は無理に移行せず継続、が現実的な指針です。ngx-translate のメンテナンス状況は変化するため、採用時に最新の状況を確認してください。
4. 日付・数値・通貨のローカライゼーション
組み込みパイプは現在のロケール(LOCALE_ID)に従って整形します。
| パイプ | 用途 | 例 |
|---|---|---|
DatePipe | 日付・時刻 | date: 'longDate' |
CurrencyPipe | 通貨 | currency: 'JPY' |
DecimalPipe | 桁区切りの数値 | number: '1.0-2' |
PercentPipe | パーセント | percent: '1.1-1' |
<p>{{ today | date: 'fullDate' }}</p>
<p>{{ price | currency: 'JPY' : 'symbol' : '1.0-0' }}</p>
<p>{{ 1234567.891 | number: '1.0-2' }}</p>
LOCALE_ID の設定とロケールデータの登録
LOCALE_ID の既定は en-US で、ビルド時 i18n を使うとビルドしたロケールが自動で設定されます。ランタイムで切り替える場合は、ロケールデータを登録し LOCALE_ID を提供します。
import { registerLocaleData } from '@angular/common';
import localeJa from '@angular/common/locales/ja';
import localeEn from '@angular/common/locales/en';
registerLocaleData(localeJa);
registerLocaleData(localeEn);
providers: [{ provide: LOCALE_ID, useValue: 'ja-JP' }]
LOCALE_ID はアプリ起動時に一度評価されるため、パイプの表示ロケールを実行時に切り替えたい場合は、LOCALE_ID を動的に返すファクトリを用意するか、パイプの第 3 引数・第 4 引数(date: 'short' : undefined : 'en')でロケールを明示します。
Intl.DateTimeFormat / Intl.NumberFormat といったブラウザ標準 API は、ロケールデータの登録が不要で、registerLocaleData による追加バンドルも発生しません。パイプで要件を満たせない場合の選択肢です。
5. 複数形・性別の処理(ICU式)
Angular 標準の i18n は ICU メッセージフォーマットに対応しており、複数形(plural)と分岐(select)をテンプレート内で記述できます。構文は {変数, plural, ...} の形です。
<span i18n="@@items.count">
{count, plural, =0 {商品はありません} one {商品が 1 件あります} other {商品が {{ count }} 件あります}}
</span>
ここでは count が分岐対象の式、=0 が値の完全一致、one / other が言語ごとの複数形カテゴリです。日本語は複数形の区別がないため other のみで足りますが、英語など他言語に翻訳する際に one / other へ展開されます(ロシア語のように few / many を持つ言語もあります)。
select 式は、文字列値による分岐に使います。性別だけでなく、状態やカテゴリにも応用できます。
<span i18n="@@post.author">
{gender, select, male {彼がコメントしました} female {彼女がコメントしました} other {コメントがありました}}
</span>
複数形と select は入れ子にできます。
<span i18n="@@invite.message">
{gender, select,
male {{count, plural, =0 {彼は招待していません} other {彼は {{ count }} 人を招待しました}}}
other {{count, plural, =0 {招待者はいません} other {{{ count }} 人が招待されました}}}
}
</span>
ランタイムライブラリで ICU を使いたい場合は、Transloco では @jsverse/transloco-messageformat プラグイン、ngx-translate では ngx-translate-messageformat-compiler を使います。React の react-intl / next-intl が ICU を標準でサポートしているのと比べると、ランタイム系 Angular ライブラリは追加パッケージが必要になる点に注意してください。
6. RTL(右から左)レイアウトの対応
アラビア語・ヘブライ語などの RTL 言語に対応するには、文書方向の切り替えと、方向に依存しないスタイルの 2 点が基本です。
dir 属性の動的切替
@Injectable({ providedIn: 'root' })
export class DirectionService {
private doc = inject(DOCUMENT);
private rtlLangs = new Set(['ar', 'he', 'fa', 'ur']);
apply(lang: string): void {
const dir = this.rtlLangs.has(lang) ? 'rtl' : 'ltr';
this.doc.documentElement.setAttribute('dir', dir);
this.doc.documentElement.setAttribute('lang', lang);
}
}
言語切り替えの処理(TranslocoService.langChanges$ など)から apply() を呼びます。
CSS logical properties の活用
margin-left のような物理方向のプロパティを避け、論理プロパティを使うと、dir の切り替えだけでレイアウトが追従します。
.card {
margin-inline-start: 16px; /* margin-left の代わり */
padding-inline: 12px 8px;
border-inline-end: 1px solid #ccc;
text-align: start; /* left の代わり */
}
Angular CDK の Bidi
@angular/cdk/bidi の Directionality は、現在の文書方向を取得し、変化を購読できます。CDK や Angular Material のコンポーネントは内部でこれを参照します。
import { Directionality } from '@angular/cdk/bidi';
@Component({ /* ... */ })
export class CarouselComponent {
private dir = inject(Directionality);
next(): void {
const step = this.dir.value === 'rtl' ? -1 : 1;
this.scrollBy(step);
}
}
矢印アイコンの反転や、スライダー・カルーセルの進行方向など、CSS で解決できないロジックは Directionality で分岐します。
7. 大規模プロジェクトでの i18n 戦略
翻訳ワークフローの設計
- 原文(ソースロケール)は開発者が管理し、訳文は翻訳者が管理する、と責任を分けます。
- 手作業でのファイル受け渡しではなく、Crowdin、Lokalise、Phrase などの翻訳管理サービスと同期すると、更新漏れを防げます。
- 翻訳者に文脈が伝わるよう、
i18n属性の意味・説明を必ず書きます。
翻訳キー(ID)の命名規約
<機能>.<画面や部品>.<要素>
例: order.confirm.submitButton / auth.login.errorInvalid
- 英小文字とドット区切りで、機能単位の階層を持たせます。
- 文言の内容ではなく役割で名前を付けます(文言を変えても ID を変えずに済みます)。
- 同じ訳文でも文脈が異なる場合は別 ID にします(例えば「保存」ボタンと「保存」のメニュー項目)。
- 共通語彙(OK・キャンセルなど)は
common.*に集約します。
翻訳の欠落チェック
- ビルド時 i18n では、
angular.jsonのi18nMissingTranslationオプションで挙動を制御できます。
"build": {
"options": {
"i18nMissingTranslation": "error"
}
}
| 値 | 挙動 |
|---|---|
warning | 既定。警告を出して原文を使う |
error | ビルドを失敗させる |
ignore | 何も出さない |
- ランタイムライブラリでは、Transloco の
transloco-keys-manager(キーの抽出・欠落・未使用検出)や、missingHandlerの設定で、未翻訳キーを検知します。 - 本番では、
errorに設定するか、欠落キーを監視に送るようにします。
CI/CD への統合
- プルリクエストごとに
ng extract-i18nを実行し、差分が出たら(翻訳ソースが最新でなければ)失敗させる。 i18nMissingTranslation: errorで全ロケールをビルドし、欠落を検出する。- 翻訳ファイルの形式(XLIFF の構文など)を lint する。
- ロケールごとの成果物を別パスでデプロイし、CDN のキャッシュキーにロケールを含める。
- run: npm ci
- run: npx ng extract-i18n --format xlf2 --output-path src/locale
- run: git diff --exit-code src/locale/messages.xlf
- run: npx ng build --localize
ロケールが増えるとビルド時間が伸びるため、ロケール単位でジョブを並列化する構成も検討してください。