Давайте поговорим об интернационализации (i18n) для Angular (не AngularJS, не Angular 2, а просто Angular 😉).

Что касается локализации JavaScript. Одним из самых популярных фреймворков является i18next. Одним из самых известных Angular-расширений для i18next является angular-i18next.
Оно было создано еще в апреле 2017 года Сергеем Романчуком.

Итак, прежде всего: «Почему i18next?»

i18next был создан в конце 2011 года. Он старше, чем большинство библиотек, которые вы будете использовать в настоящее время, включая основные технологии внешнего интерфейса (angular, react, vue и т. д.).

➡️ экологичный

Исходя из того, как долго i18next уже доступен с открытым исходным кодом, не существует реального дела i18n, которое нельзя было бы решить с помощью i18next.

➡️ зрелый

i18next можно использовать в любой среде javascript (и некоторых не-javascript — .net, elm, iOS, android, ruby, …), с любой структурой пользовательского интерфейса, с любым форматом i18n, … возможности безграничны .

➡️ расширяемый

С i18next вы получите множество функций и возможностей по сравнению с другими обычными платформами 18n.

➡️ богатый

Здесь вы можете найти больше информации о том, почему i18next особенный.

Давайте углубимся в это…

Предпосылки

Убедитесь, что у вас установлены Node.js и npm. Лучше всего, если у вас есть некоторый опыт работы с простым HTML, JavaScript и базовым Angular, прежде чем переходить к angular-i18next.

Начиная

Возьмите свой собственный проект Angular или создайте новый, т.е. с помощью Angular cli.

npx @angular/cli new my-app

Для упрощения давайте удалим «сгенерированное» содержимое angular-cli:

Мы собираемся адаптировать приложение для определения языка в соответствии с предпочтениями пользователя.
И мы создадим переключатель языков, чтобы контент переключался между разными языками.

Давайте установим некоторые зависимости i18next:

npm install i18next angular-i18next i18next-browser-languagedetector

Давайте изменим наш app.module.ts для интеграции и инициализации конфигурации i18next:

import { APP_INITIALIZER, NgModule, LOCALE_ID } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { I18NEXT_SERVICE, I18NextModule, I18NextLoadResult, ITranslationService, defaultInterpolationFormat  } from 'angular-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { AppComponent } from './app.component';
const i18nextOptions = {
  debug: true,
  fallbackLng: 'en',
  resources: {
    en: {
        translation: {
            "welcome": "Welcome to Your Angular App"
        }
    },
    de: {
        translation: {
            "welcome": "Willkommen zu Deiner Vue.js App"
        }
    }
  },
  interpolation: {
    format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
  }
};
export function appInit(i18next: ITranslationService) {
  return () => {
    let promise: Promise<I18NextLoadResult> = i18next
      .use(LocizeApi)
      .use<any>(LanguageDetector)
      .init(i18nextOptions);
    return promise;
  };
}
export function localeIdFactory(i18next: ITranslationService)  {
  return i18next.language;
}
export const I18N_PROVIDERS = [
  {
    provide: APP_INITIALIZER,
    useFactory: appInit,
    deps: [I18NEXT_SERVICE],
    multi: true
  },
  {
    provide: LOCALE_ID,
    deps: [I18NEXT_SERVICE],
    useFactory: localeIdFactory
  },
];
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    I18NextModule.forRoot()
  ],
  providers: [
    I18N_PROVIDERS
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Хорошо, теперь давайте обновим app.component.html:

<!-- Toolbar -->
<div class="toolbar" role="banner">
  <span>{{ 'welcome' | i18next }}</span>
</div>
<div class="content" role="main">
  <!-- Highlight Card -->
  <div class="card highlight-card card-small">
    <span>{{ 'welcome' | i18next }}</span>
  </div>
</div>

Теперь вы должны увидеть что-то вроде этого:

Хороший! Итак, давайте добавим дополнительный текст с интерполированным неэкранированным значением:

<!-- Toolbar -->
<div class="toolbar" role="banner">
  <span>{{ 'welcome' | i18next }}</span>
</div>
<div class="content" role="main">
  <!-- Highlight Card -->
  <div class="card highlight-card card-small">
    <span>{{ 'welcome' | i18next }}</span>
  </div>
  <br />
  <p>{{ 'descr' | i18next: { url: 'https://github.com/Romanchuk/angular-i18next' } }}</p>
</div>

Не забудьте добавить новый ключ еще и в ресурсы:

const i18nextOptions = {
  debug: true,
  fallbackLng: 'en',
  resources: {
    en: {
        translation: {
            "welcome": "Welcome to Your Angular App",
            "descr": "For a guide and recipes on how to configure / customize this project, check out {{-url}}."
        }
    },
    de: {
        translation: {
            "welcome": "Willkommen zu Deiner Vue.js App",
            "descr": "Eine Anleitung und Rezepte für das Konfigurieren / Anpassen dieses Projekts findest du in {{-url}}."
        }
    }
  },
  interpolation: {
    format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
  }
};

Это работает? - Конечно!

А благодаря языковому детектору можно еще попробовать переключить язык с параметром запроса ?lng=de:

Переключатель языков

Нам нравится предлагать возможность изменить язык с помощью своего рода переключателя языка.

Итак, давайте добавим раздел нижнего колонтитула в наш файл app.component.html:

<!-- Footer -->
<footer>
    <ng-template ngFor let-lang [ngForOf]="languages" let-i="index">
        <span *ngIf="i !== 0">&nbsp;|&nbsp;</span>
        <a *ngIf="language !== lang" href="javascript:void(0)" class="link lang-item {{lang}}" (click)="changeLanguage(lang)">{{ lang.toUpperCase() }}</a>
        <span *ngIf="language === lang" class="current lang-item {{lang}}">{{ lang.toUpperCase() }}</span>
    </ng-template>
</footer>

И нам также нужно обновить файл app.components.ts:

import { Component, Inject } from '@angular/core';
import { I18NEXT_SERVICE, ITranslationService } from 'angular-i18next';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent {
  language: string = 'en';
  languages: string[] = ['en', 'de'];
  constructor(
    @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService
  )
  {}
  ngOnInit() {
    this.i18NextService.events.initialized.subscribe((e) => {
      if (e) {
        this.updateState(this.i18NextService.language);
      }
    });
  }
  changeLanguage(lang: string){
    if (lang !== this.i18NextService.language) {
      this.i18NextService.changeLanguage(lang).then(x => {
        this.updateState(lang);
        document.location.reload();
      });
    }
  }
  private updateState(lang: string) {
    this.language = lang;
  }
}

🥳 Отлично, вы только что создали свой первый переключатель языков!

Благодаря i18next-browser-languagedetector теперь он пытается определить язык браузера и автоматически использовать этот язык, если вы предоставили для него переводы. Язык, выбранный вручную в переключателе языков, сохраняется в локальном хранилище, и при следующем посещении страницы этот язык будет использоваться в качестве предпочтительного.

Отделить переводы от кода

Наличие переводов в нашем коде работает, но не совсем подходит для работы с переводчиками.
Давайте отделим переводы от кода и поместим их в специальные файлы json.

В этом нам поможет i18next-locize-backend.

Что такое локация?

Как это выглядит?

Сначала нужно зарегистрироваться в locize и войти.
Затем создать новый проект в locize и добавить свои переводы. Вы можете добавить свои переводы либо путем импорта отдельных файлов json, либо через API, либо с помощью CLI.

npm install i18next-locize-backend

Адаптируйте файл app.modules.ts для использования i18next-locize-backend и убедитесь, что вы скопировали идентификатор проекта из своего проекта locize:

import { APP_INITIALIZER, NgModule, LOCALE_ID } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { I18NEXT_SERVICE, I18NextModule, I18NextLoadResult, ITranslationService, defaultInterpolationFormat  } from 'angular-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import LocizeApi from 'i18next-locize-backend';
import { AppComponent } from './app.component';
const i18nextOptions = {
  debug: true,
  fallbackLng: 'en',
  backend: {
    projectId: 'your-locize-project-id'
  },
  interpolation: {
    format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
  }
};
export function appInit(i18next: ITranslationService) {
  return () => {
    let promise: Promise<I18NextLoadResult> = i18next
      .use(LocizeApi)
      .use<any>(LanguageDetector)
      .init(i18nextOptions);
    return promise;
  };
}
export function localeIdFactory(i18next: ITranslationService)  {
  return i18next.language;
}
export const I18N_PROVIDERS = [
  {
    provide: APP_INITIALIZER,
    useFactory: appInit,
    deps: [I18NEXT_SERVICE],
    multi: true
  },
  {
    provide: LOCALE_ID,
    deps: [I18NEXT_SERVICE],
    useFactory: localeIdFactory
  },
];
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    I18NextModule.forRoot()
  ],
  providers: [
    I18N_PROVIDERS
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Приложение выглядит все так же, но переводы теперь полностью отделены от приложения и могут управляться и выпускаться отдельно.

сохранить недостающие переводы

Благодаря использованию функции saveMissing новые ключи добавляются для автоматического определения местоположения при разработке приложения.

Просто передайте saveMissing: true в параметрах i18next и убедитесь, что вы скопировали ключ API из своего проекта locize:

const i18nextOptions = {
  debug: true,
  saveMissing: true, // do not use the saveMissing functionality in production: https://docs.locize.com/guides-tips-and-tricks/going-production
  fallbackLng: 'en',
  backend: {
    projectId: 'my-locize-project-id',
    apiKey: 'my-api-key' // used for handleMissing functionality, do not add your api-key in a production build
  },
  interpolation: {
    format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
  }
};

Каждый раз, когда вы будете использовать новый ключ, он будет отправляться на локализацию, т.е.:

<p>{{ 'cool' | i18next: { defaultValue: 'This is very cool!' } }}</p>

приведет к локализации следующим образом:

👀 но есть еще…

Благодаря плагину locize-lastused вы сможете находить и фильтровать в locize, какие ключи используются или уже не используются.

С помощью плагина locize вы сможете использовать свое приложение в locize InContext Editor.

Наконец, с помощью рабочего процесса автоматического машинного перевода новые ключи не только добавляются для автоматической локализации при разработке приложения, но также автоматически переводятся на целевые языки с помощью машинного перевода:

npm install locize-lastused locize

используйте их в app.modules.ts:

import { APP_INITIALIZER, NgModule, LOCALE_ID } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { I18NEXT_SERVICE, I18NextModule, I18NextLoadResult, ITranslationService, defaultInterpolationFormat  } from 'angular-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import LocizeApi from 'i18next-locize-backend';
import LastUsed from 'locize-lastused';
import { locizePlugin } from 'locize';
import { AppComponent } from './app.component';
const locizeOptions = {
  projectId: 'my-locize-project-id',
  apiKey: 'my-api-key' // used for handleMissing functionality, do not add your api-key in a production buildyour
};
const i18nextOptions = {
  debug: true,
  fallbackLng: 'en',
  saveMissing: true, // do not use the saveMissing functionality in production: https://docs.locize.com/guides-tips-and-tricks/going-production
  backend: locizeOptions,
  locizeLastUsed: locizeOptions,
  interpolation: {
    format: I18NextModule.interpolationFormat(defaultInterpolationFormat)
  }
};
export function appInit(i18next: ITranslationService) {
  return () => {
    let promise: Promise<I18NextLoadResult> = i18next
      // locize-lastused
      // sets a timestamp of last access on every translation segment on locize
      // -> safely remove the ones not being touched for weeks/months
      // https://github.com/locize/locize-lastused
      // do not use the lastused functionality in production: https://docs.locize.com/guides-tips-and-tricks/going-production
      .use(LastUsed)
      // locize-editor
      // InContext Editor of locize
      .use(locizePlugin)
      // i18next-locize-backend
      // loads translations from your project, saves new keys to it (saveMissing: true)
      // https://github.com/locize/i18next-locize-backend
      .use(LocizeApi)
      .use<any>(LanguageDetector)
      .init(i18nextOptions);
    return promise;
  };
}
export function localeIdFactory(i18next: ITranslationService)  {
  return i18next.language;
}
export const I18N_PROVIDERS = [
  {
    provide: APP_INITIALIZER,
    useFactory: appInit,
    deps: [I18NEXT_SERVICE],
    multi: true
  },
  {
    provide: LOCALE_ID,
    deps: [I18NEXT_SERVICE],
    useFactory: localeIdFactory
  },
];
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    I18NextModule.forRoot()
  ],
  providers: [
    I18N_PROVIDERS
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Автоматический машинный перевод:

Фильтр последних использованных переводов:

Контекстный редактор:

Кэширование:

Объединение версий:

🧑‍💻 Полный код можно найти здесь.

🎉🥳 Поздравляем 🎊🎁

Надеюсь, вы узнали что-то новое о i18next, angular-i18next и современных рабочих процессах локализации.

Так что, если вы хотите вывести свою тему i18n на новый уровень, стоит попробовать locize.

Основатели locize также являются создателями i18next. Таким образом, используя locize, вы напрямую поддерживаете будущее i18next.

👍