Перейти к содержимому

50. Интернационализация (i18n)

Привет! Яша здесь. Сегодня мы разберём как сделать Angular-приложение многоязычным. i18n — это не просто перевод текста, это архитектурное решение, которое нужно принять на старте проекта. Поехали! 🚀


i18n (internationalization — 18 букв между i и n) — это процесс адаптации приложения для разных языков и регионов. Включает:

  • Перевод текстов
  • Форматирование дат, чисел, валют
  • Направление текста (RTL для арабского, иврита)
  • Множественное число (pluralization)

Angular предлагает два подхода:

  1. @angular/localize — встроенный, compile-time, отдельная сборка на каждый язык
  2. ngx-translate — сторонний, runtime, один бандл для всех языков

Окно терминала
ng add @angular/localize

Это добавит @angular/localize в polyfills и обновит angular.json.

// main.ts — появится импорт
import '@angular/localize/init';

Самый простой способ — пометить элементы атрибутом i18n:

app.component.html
<h1 i18n="Заголовок главной страницы@@hero.title">
Welcome to our app!
</h1>
<p i18n="Описание@@hero.description">
The best app ever created.
</p>
<!-- Атрибуты тоже переводятся -->
<img [src]="logo" i18n-alt="Альт текст логотипа@@logo.alt" alt="Company logo">
<!-- Интерполяция поддерживается -->
<p i18n="Приветствие пользователя@@user.greeting">
Hello, {{ userName }}!
</p>

Формат атрибута i18n: meaning|description@@id

  • meaning — контекст (опционально)
  • description — описание для переводчика (опционально)
  • @@id — уникальный ID (рекомендуется указывать всегда!)

Для TypeScript кода (вне шаблонов) используем $localize:

import '@angular/localize/init';
@Component({...})
export class NotificationComponent {
// Простой перевод
title = $localize`Welcome to the app`;
// С переменными
getUserGreeting(name: string): string {
return $localize`Hello, ${name}:name:! How are you?`;
}
// С плейсхолдером и ID
getItemCount(count: number): string {
return $localize`:@@item.count:You have ${count}:count: items`;
}
// В сервисах
showError(field: string): void {
const message = $localize`:@@validation.required:Field ${field}:fieldName: is required`;
console.error(message);
}
}

Окно терминала
ng extract-i18n
# С указанием формата и директории вывода
ng extract-i18n --format xlf --output-path src/locale
# Другие форматы: xlf2, xmb, json, arb
ng extract-i18n --format xlf2 --output-path src/locale --out-file messages.xlf

Это создаст файл messages.xlf — исходный файл для переводчиков.


<!-- src/locale/messages.xlf — базовый файл (English) -->
<?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="hero.title" datatype="html">
<source>Welcome to our app!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Заголовок главной страницы</note>
</trans-unit>
<trans-unit id="hero.description" datatype="html">
<source>The best app ever created.</source>
</trans-unit>
</body>
</file>
</xliff>
<!-- src/locale/messages.ru.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" target-language="ru" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="hero.title" datatype="html">
<source>Welcome to our app!</source>
<target>Добро пожаловать в наше приложение!</target>
</trans-unit>
<trans-unit id="hero.description" datatype="html">
<source>The best app ever created.</source>
<target>Лучшее приложение в мире.</target>
</trans-unit>
</body>
</file>
</xliff>

Angular поддерживает ICU (International Components for Unicode) для множественного числа:

<!-- Plural — множественное число -->
<p i18n="@@items.count">
{itemCount, plural,
=0 {Нет товаров}
=1 {Один товар}
few {{{itemCount}} товара}
other {{{itemCount}} товаров}
}
</p>
<!-- Select — выбор по значению -->
<p i18n="@@gender.message">
{gender, select,
male {Он вошёл в систему}
female {Она вошла в систему}
other {Пользователь вошёл в систему}
}
</p>
<!-- Вложенные ICU -->
<p i18n="@@nested.icu">
{gender, select,
male {У него {itemCount, plural, =0 {нет заказов} other {есть заказы}}}
female {У неё {itemCount, plural, =0 {нет заказов} other {есть заказы}}}
}
</p>

{
"projects": {
"my-app": {
"i18n": {
"sourceLocale": "en-US",
"locales": {
"ru": {
"translation": "src/locale/messages.ru.xlf",
"baseHref": "/ru/"
},
"de": {
"translation": "src/locale/messages.de.xlf",
"baseHref": "/de/"
}
}
},
"architect": {
"build": {
"configurations": {
"ru": {
"localize": ["ru"]
},
"de": {
"localize": ["de"]
},
"production": {
"localize": true
}
}
},
"serve": {
"configurations": {
"ru": {
"browserTarget": "my-app:build:ru"
}
}
}
}
}
}
}

Окно терминала
# Разработка на конкретной локали
ng serve --configuration=ru
# Сборка для конкретной локали
ng build --configuration=production,ru
# Сборка для всех локалей сразу
ng build --configuration=production --localize
# Результат: dist/my-app/en-US/, dist/my-app/ru/, dist/my-app/de/

import { LOCALE_ID, DEFAULT_CURRENCY_CODE } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import localeRu from '@angular/common/locales/ru';
registerLocaleData(localeRu);
@NgModule({
providers: [
{ provide: LOCALE_ID, useValue: 'ru-RU' },
{ provide: DEFAULT_CURRENCY_CODE, useValue: 'RUB' },
]
})
export class AppModule {}
<!-- Автоматическое форматирование через pipes -->
<p>{{ today | date:'longDate' }}</p>
<!-- 15 января 2024 г. -->
<p>{{ price | currency }}</p>
<!-- 1 500,00 ₽ -->
<p>{{ ratio | percent:'1.2-2' }}</p>
<!-- 75,50% -->

Когда нужен один бандл с переключением языка на лету:

Окно терминала
npm install @ngx-translate/core @ngx-translate/http-loader
app.module.ts
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
@NgModule({
imports: [
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
]
})
export class AppModule {}
assets/i18n/ru.json
{
"HERO": {
"TITLE": "Добро пожаловать!",
"DESCRIPTION": "Лучшее приложение"
},
"GREETING": "Привет, {{name}}!",
"ITEMS": {
"COUNT_ONE": "{{count}} товар",
"COUNT_FEW": "{{count}} товара",
"COUNT_MANY": "{{count}} товаров"
}
}
// Использование в компоненте
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-language-switcher',
template: `
<h1>{{ 'HERO.TITLE' | translate }}</h1>
<p>{{ 'GREETING' | translate:{ name: userName } }}</p>
<select (change)="switchLang($event)">
<option value="en">English</option>
<option value="ru">Русский</option>
<option value="de">Deutsch</option>
</select>
`
})
export class AppComponent {
userName = 'Яша';
constructor(private translate: TranslateService) {
translate.setDefaultLang('en');
translate.use('en');
}
switchLang(event: Event): void {
const lang = (event.target as HTMLSelectElement).value;
this.translate.use(lang);
}
}

Критерий@angular/localizengx-translate
ТипCompile-timeRuntime
БандлОтдельный на языкОдин
ПереключениеПерезагрузкаМгновенное
ПроизводительностьЛучшеНемного хуже
НастройкаСложнееПроще
Строгая типизацияДа (с $localize)Нет из коробки

// ✅ Всегда указывайте @@id
<h1 i18n="@@page.main.title">Welcome</h1>
// ✅ Используйте description для переводчиков
<button i18n="Кнопка сохранения формы|Текст кнопки@@btn.save">Save</button>
// ✅ Выносите строки в константы
const MESSAGES = {
saved: $localize`:@@notification.saved:Changes saved successfully`,
error: $localize`:@@notification.error:Something went wrong`,
};
// ❌ Не переводите технические строки
<app-icon name="arrow-right"></app-icon> // name — не переводится
// ✅ Регистрируйте локали явно
import localeRu from '@angular/common/locales/ru';
registerLocaleData(localeRu, 'ru-RU');

Симуляция переключателя языка с ngx-translate-подобным поведением: