50. Интернационализация (i18n)
52. Интернационализация (i18n) 🌍
Заголовок раздела «52. Интернационализация (i18n) 🌍»Привет! Яша здесь. Сегодня мы разберём как сделать Angular-приложение многоязычным. i18n — это не просто перевод текста, это архитектурное решение, которое нужно принять на старте проекта. Поехали! 🚀
Что такое i18n и когда это нужно
Заголовок раздела «Что такое i18n и когда это нужно»i18n (internationalization — 18 букв между i и n) — это процесс адаптации приложения для разных языков и регионов. Включает:
- Перевод текстов
- Форматирование дат, чисел, валют
- Направление текста (RTL для арабского, иврита)
- Множественное число (pluralization)
Angular предлагает два подхода:
- @angular/localize — встроенный, compile-time, отдельная сборка на каждый язык
- ngx-translate — сторонний, runtime, один бандл для всех языков
@angular/localize: установка
Заголовок раздела «@angular/localize: установка»ng add @angular/localizeЭто добавит @angular/localize в polyfills и обновит angular.json.
// main.ts — появится импортimport '@angular/localize/init';Атрибут i18n в шаблонах
Заголовок раздела «Атрибут i18n в шаблонах»Самый простой способ — пометить элементы атрибутом i18n:
<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 (рекомендуется указывать всегда!)
$localize template tag
Заголовок раздела «$localize template tag»Для 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»ng extract-i18n
# С указанием формата и директории выводаng extract-i18n --format xlf --output-path src/locale
# Другие форматы: xlf2, xmb, json, arbng extract-i18n --format xlf2 --output-path src/locale --out-file messages.xlfЭто создаст файл messages.xlf — исходный файл для переводчиков.
XLIFF файлы
Заголовок раздела «XLIFF файлы»<!-- 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>Плюрализация (ICU expressions)
Заголовок раздела «Плюрализация (ICU expressions)»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>Конфигурация angular.json для локалей
Заголовок раздела «Конфигурация angular.json для локалей»{ "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/Форматирование с LOCALE_ID
Заголовок раздела «Форматирование с LOCALE_ID»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% -->ngx-translate: runtime-альтернатива
Заголовок раздела «ngx-translate: runtime-альтернатива»Когда нужен один бандл с переключением языка на лету:
npm install @ngx-translate/core @ngx-translate/http-loaderimport { 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 {}{ "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/localize | ngx-translate |
|---|---|---|
| Тип | Compile-time | Runtime |
| Бандл | Отдельный на язык | Один |
| Переключение | Перезагрузка | Мгновенное |
| Производительность | Лучше | Немного хуже |
| Настройка | Сложнее | Проще |
| Строгая типизация | Да (с $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');Playground 🎮
Заголовок раздела «Playground 🎮»Симуляция переключателя языка с ngx-translate-подобным поведением: