Node.js - это асинхронная среда выполнения JavaScript, управляемая событиями, которая предназначена для создания масштабируемых сетевых приложений. По сути, это позволяет Javascript работать в бэкэнде как серверный код.

Экосистема Node обширна и полагается на проекты сообщества. Хотя в Интернете есть множество руководств по изучению Node.js и его библиотек, тема I18n почти осталась позади.

Поэтому, когда вам нужны масштабируемые решения I18n, которые легко использовать и внедрять, стоит заранее сделать некоторые разумные исключения из архитектуры программного обеспечения.

Эта статья о попытке восполнить этот большой пробел; показав способы интеграции I18n и разумной адаптации к различным культурным правилам и привычкам в ваших Node.js-приложениях.

В этом руководстве я буду использовать последнюю версию среды выполнения Node.js LTS v8.94, а код этого руководства размещен на Github. Для удобства мы собираемся использовать флаг - экспериментальные-модули, чтобы использовать импорт es6 в коде. Такого же результата можно добиться, используя babel с preset-es2015.

Давайте начнем.

Настройка проекта

Библиотека времени выполнения Node.js предоставляет только базовые низкоуровневые примитивы для написания серверных приложений. Итак, чтобы начать работу с i18n, мы должны начать с нуля. Это отчасти хорошо, потому что позволяет нам полностью контролировать проектные решения, которые могут повлиять на объем нашего проекта.

Запустить новое приложение Node.js

Создайте исходную структуру папок.

$ mkdir node-i18n-example && cd node-i18n-example
$ npm init --yes

Создать службу локали

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

1. Создайте служебную папку:

$ mkdir -p app/services && cd app/services

2. Добавьте содержимое файла с помощью вашего любимого редактора:

$ touch localeService.mjs

Файл: app / services / localeService.mjs

/**
 * LocaleService
 */
export class LocaleService {
  /**
   *
   * @param i18nProvider The i18n provider
   */
  constructor(i18nProvider) {
    this.i18nProvider = i18nProvider;
  }
  /**
   *
   * @returns {string} The current locale code
   */
  getCurrentLocale() {
    return this.i18nProvider.getLocale();
  }
  /**
   *
   * @returns string[] The list of available locale codes
   */
  getLocales() {
    return this.i18nProvider.getLocales();
  }
  /**
   *
   * @param locale The locale to set. Must be from the list of available locales.
   */
  setLocale(locale) {
    if (this.getLocales().indexOf(locale) !== -1) {
      this.i18nProvider.setLocale(locale)
    }
  }
  /**
   *
   * @param string String to translate
   * @param args Extra parameters
   * @returns {string} Translated string
   */
  translate(string, args = undefined) {
    return this.i18nProvider.translate(string, args)
  }
  /**
   *
   * @param phrase Object to translate
   * @param count The plural number
   * @returns {string} Translated string
   */
  translatePlurals(phrase, count) {
    return this.i18nProvider.translateN(phrase, count)
  }
}

LocaleService - это простой класс, который принимает объект i18nProvider, который поможет немного упростить работу с языковыми стандартами. Таким образом, у нас может быть немного больше гибкости, поскольку мы можем решить сменить поставщика, не нарушая особых требований API.

Теперь не хватает фактического провайдера i18n. Давайте добавим еще один.

Добавление провайдера I18n

Хотя есть несколько разумных вариантов, когда дело доходит до I18n, самой популярной библиотекой на данный момент является i18n-node.

Установка довольно проста:

1. Установите пакет

$ npm install i18n --save

2. Создайте объект конфигурации i18n, потому что перед использованием библиотеки нам необходимо ее настроить:

$ touch app/i18n.config.mjs

Файл: app / i18n.config.mjs

import i18n from 'i18n';
import path from 'path';
i18n.configure({
  locales: ['en', 'el'],
  defaultLocale: 'en',
  queryParameter: 'lang',
  directory: path.join('./', 'locales'),
  api: {
    '__': 'translate',  
    '__n': 'translateN' 
  },
});
export default i18n;

Здесь мы добавили поддержку двух языков, одним из которых по умолчанию является en. Мы также определили каталог языкового стандарта, который будет использоваться библиотекой для автоматического создания исходных файлов и строк перевода. Свойство API - это просто отображение вызова __ на вызов translate в нашей localeService. Если вам не нужна такая организация, вы можете изменить вызов с this.i18nProvider.translate (строка, аргументы) на this.i18nProvider .__ (строка, аргументы).

Совет. Посмотрите весь список параметров конфигурации для этой библиотеки здесь.

3. Протестируйте службу Locale, создав экземпляр объекта i18n и localeService:

$ touch index.mjs

Файл: index.mjs

import { LocaleService } from './app/services/localeService.mjs';
import i18n from './app/i18n.config.mjs';
const localeService = new LocaleService(i18n);
console.log(localeService.getLocales()); // ['en', 'el']
console.log(localeService.getCurrentLocale()); // 'en'
console.log(localeService.translate('Hello')); //  'Hello'
console.log(localeService.translatePlurals('You have %s message', 3)); // 'You have 3 messages'

Затем в командной строке выполните следующее:

$ node --experimental-modules index.mjs

Это автоматически создаст каталог locales в корневой папке, содержащий соответствующие строки перевода для текущего языка:

$ tree locales 
locales
├── el.json
└── en.json

Файл: locales / en.json

{
   "Hello": "Hello",
   "You have %s message": {
      "one": "You have %s message",
      "other": "You have %s messages"
   }
}

Добавьте следующую строку в index.mjs, чтобы протестировать создание переводимых строк для другого языка:

localeService.setLocale('el');

Файл: locales / el.json

{
   "Hello": "Για Σας",
   "You have %s message": {
      "one": "Έχεις %s μύνημα",
      "other": "Έχεις %s μύνηματα"
   }
}

Снова запустите приложение и убедитесь, что перевод выполняется:

$ node --experimental-modules index.mjs
Για Σας
Έχεις 3 μύνηματα

Подключите все к контейнеру DI

В настоящее время, чтобы использовать наш класс localeService, мы должны вручную создать его экземпляр и передать объект конфигурации i18n. Что еще хуже, нам нужно сохранить только одну ссылку на этот объект, поскольку нам нужно сохранить текущее состояние локали в одном месте.

Было бы неплохо, если бы у нас был способ настраивать и получать по запросу наш экземпляр localeService всякий раз, когда его запрашивает наше приложение? Было бы это даже лучше, чем разрешить любые параметры, которые также необходимо предоставить во время создания?

Оказывается, есть способ сделать это с помощью контейнера инверсии зависимостей. Это просто объект, который предоставляет API верхнего уровня, который позволяет нам регистрировать наши ценные объекты или услуги и запрашивать их в другое время и в другом месте. Этот контейнер инверсии зависимостей является одной из форм инверсии управления (IoC) и помогает с возможностью повторного использования, тестируемости и лучшего контроля.

Для Node.js есть хорошая библиотека под названием awilix, которая предлагает эти функции. Давайте посмотрим, как мы можем интегрировать эту библиотеку для улучшения структуры приложения.

1. Установите awilix

$ npm install awilix --save

2. Создайте файл с именем container.mjs, который будет отслеживать все необходимые нам регистрации служб.

$ touch app/container.mjs

Файл: app / container.mjs

import awilix from 'awilix';
import i18n from './i18n.config';
import { LocaleService } from './services/localeService.mjs';
const container = awilix.createContainer();
container
  .register({
    localeService: awilix.asClass(LocaleService, { lifetime: awilix.Lifetime.SINGLETON })
  })
  .register({
    i18nProvider: awilix.asValue(i18n)
  });
export default container;

Как видите, у нас больше гибкости в том, как мы хотим создавать экземпляры наших объектов. В этом примере мы хотим, чтобы класс LocaleService был одноэлементным объектом, а i18n config был просто значением, потому что мы только что его настроили. В документации awilix есть больше возможностей для управления временем жизни.

Давайте соединим нашу LocaleService и нашу конфигурацию i18n вместе в конструкторе, чтобы каждый раз, когда мы разрешаем, все настраивается для нас:

1. Измените конструктор класса LocaleService, чтобы он принял объект i18nProvider:

Файл: app / services / localeService.mjs.

/**
 * LocaleService
 */
export class LocaleService {
  /**
   *
   * @param i18nProvider The i18n provider
   */
  constructor(opts) {
    this.i18nProvider = opts.i18nProvider;
  }
...

2. Протестируйте разрешение нашей службы, заменив вызовы в файле index.mjs на вызовы, использующие контейнер.

Файл: index.mjs

import container from './app/container';
const localeService = container.resolve('localeService');
localeService.getLocales(); // ['en', 'el']
localeService.getCurrentLocale(); // 'en'
localeService.setLocale('el');
console.log(localeService.translate('Hello'));
console.log(localeService.translatePlurals('You have %s message', 3));
$ node --experimental-modules index.mjs
Για Σας
Έχεις 3 μύνηματα

Совет. Вы можете назвать свою разрешенную службу по своему усмотрению. В этом примере мы сохранили то же имя для удобства чтения.

Пример с Express.js и Mustache.js

Давайте посмотрим, как мы можем использовать то, что у нас есть, в примере приложения с использованием Express.js. Express.js - это небольшая, но мощная веб-платформа для Node.js, которая позволяет создавать надежный набор функций для веб-приложений и мобильных приложений.

Однако перед установкой Express.js нам нужно добавить еще несколько абстракций в наше приложение, чтобы учесть это изменение.

Нам нужен класс App, который будет принимать объект класса Server. Класс App будет знать только, как использовать объект сервера для запуска сервера приложений, а объект Server будет знать, как запустить наше приложение Express.js.

Добавление Express.js

1. Создайте файл application.mjs и добавьте конструктор, принимающий объект сервера:

$ touch app/application.mjs

Файл: app / application.mjs

export class Application {
  constructor({ server }) {
    this.server = server;
  }
  async start() {
    await this.server.start();
  }
}

Мы хотим, чтобы сервер запустил асинхронный режим, так как нам нужно выполнить дополнительные задачи, когда это будет разрешено.

2. Добавьте наш класс Application в контейнер разрешения:

Файл: app / container.mjs

import { Application } from './application.mjs';
container
  .register({
    app: awilix.asClass(Application, { lifetime: awilix.Lifetime.SINGLETON })
  })

3. Установите Express.js.

$ npm install express --save

4 Создайте файл с именем server.mjs, содержащий нашу логику инициализации:

$ touch app/server.mjs

Файл: app / server.mjs

import express from 'express';
export class Server {
  constructor() {
    this.express = express();
    this.express.disable('x-powered-by');
  }
  start() {
    return new Promise((resolve) => {
      const http = this.express.listen(8000, () => {
        const { port } = http.address();
        console.info(`[p ${process.pid}] Listening at port ${port}`);
        resolve();
      });
    });
  }
}

Наш сервер в настоящее время ничего не делает, кроме открытия порта на 8000 и регистрации информации.

Теперь подключим его к нашему приложению:

5. Добавьте класс Server в контейнер разрешения:

Файл: app / container.mjs

import { Server } from './server.mjs'
container
  .register({
    server: awilix.asClass(Server, { lifetime: awilix.Lifetime.SINGLETON }),
  })

6. Добавьте этот код, чтобы разрешить приложение и запустить сервер:

Файл: index.mjs

import container from './app/container';
const app = container.resolve('app');
app
  .start()
  .catch((error) => {
    console.warn(error);
    process.exit();
  })

7. Запустите сервер, чтобы проверить его работу:

node --experimental-modules index.mjs
[p 99922] Listening at port 8000

Добавление Mustache.js

Express.js по умолчанию не имеет механизма отрисовки шаблонов. Однако он очень настраиваемый и открыт для расширений. В этом примере мы собираемся использовать очень популярный шаблонизатор для рендеринга наших переводов под названием Mustache.js.

1. Установите Moustache и его помощник.

$ npm install consolidate mustache --save

2. Настройте и добавьте движок рендеринга в наш Server.js.

import express from 'express';
import consolidate from 'consolidate';
export class Server {
  constructor() {
    ...
    // setup mustache to parse .html files
    this.express.set('view engine', 'html');
    this.express.engine('html', consolidate.mustache);
  }
  ...
}

Теперь мы готовы использовать наш движок для рендеринга HTML-страниц с переводимыми строками. Для этого у нас есть возможность использовать функцию промежуточного программного обеспечения, поставляемую из библиотеки i18n. Это внедрит свой API в объект req, как это предусмотрено фреймворком, чтобы мы могли использовать его, не импортируя ничего другого.

1. Вставьте i18nProvider на наш сервер и добавьте промежуточное ПО в поток Express.js.

export class Server {
  constructor({i18nProvider}) {
    ...
    this.express.use(i18nProvider.init);
    this.express.get('/:name?', (req, res) => {
      const name = req.params.name;
      res.render('index', {
        'currentLocale': res.locale,
        'name': name || 'Theo',
        'hello': req.__('Hello'),
        'message': req.__('How are you?')
      });
    });
  }
  start() {
    return new Promise((resolve) => {
      const http = this.express.listen(8000, () => {
        const { port } = http.address();
        console.info(`[p ${process.pid}] Listening at port ${port}`);
        resolve();
      });
    });
  }
}

2. Создайте наше представление index.html, которое будет отображаться, когда мы перейдем к начальному пути:

$ touch views/index.html

Файл: views / index.html

<!DOCTYPE html>
<html lang="{{currentLocale}}">
<head>
    <title>Node i18n</title>
<body>
{{hello}} {{name}}<br>
{{message}}
</body>
</html>

3. Запустите сервер и перейдите к localhost: 8000 / или localhost: 8000 /: param, чтобы увидеть следующий результат:

Переключение языкового стандарта

С практической точки зрения, пользователь приложений в идеале хочет изменить языковой стандарт из пользовательского интерфейса. Таким образом, приложение должно определить текущий языковой стандарт на основе некоторых параметров клиента. Есть несколько способов определить это предпочтение. Мы можем использовать cookie, в котором хранится текущий языковой стандарт, параметр query, который запрашивает конкретный языковой стандарт, или accept-language, который указывает, какие языки, какие языки понимают клиент, и какой вариант языкового стандарта является предпочтительным.

В этом руководстве мы будем использовать параметр запроса, поскольку его относительно легко понять и реализовать.

Нам нужно определить параметр, который мы будем использовать, чтобы определить текущую настройку локали. Назовем его lang. Добавить его в наше приложение очень просто.

1. Добавьте параметр lang к нашему объекту i18nProvider:

Файл: app / i18n.config.mjs

import i18n from 'i18n';
import path from 'path';
i18n.configure({
  locales: ['en', 'el'],
  defaultLocale: 'en',
  queryParameter: 'lang',
  directory: path.join('./', 'locales')
});
export default i18n;

2. Добавьте соответствующие переводы для целевого языка:

Файл: locales / el.json

{
   "Hello": "Για Σας",
   "How are you?": "Πώς είστε?",
   "You have %s message": {
      "one": "'Έχεις %s μύνημα",
      "other": "'Έχεις %s μύνηματα"
   }
}

3. Запустите сервер и перейдите к localhost: 8000 /? Lang = el.

Добавление помощников по шаблону

Если вам не нравится добавлять ключи и значения перевода в код приложения, вы можете включить помощники шаблонов, которые анализируют ключи и автоматически применяют значение перевода непосредственно из файлов шаблонов. Посмотрим, как это сделать с помощью Moustache.

1. Добавьте следующую функцию промежуточного программного обеспечения в наш файл Server.mjs:

Файл: app / Server.mjs

//https://github.com/janl/mustache.js#functions
this.express.use((req, res, next) => {
  // mustache helper
  res.locals.i18n = () => (text, render) => req.__(text, render);
  next();
});

Здесь res.locals означает все функции, доступные в нашем движке mustache.js. На самом деле мы добавляем еще один вспомогательный шаблон для тега i18n, который будет просто вызывать метод req .__, который прикреплен к i18nProviderand указав необходимые параметры.

2. Добавьте дополнительные теги в наш файл index.html и проверьте, выполняется ли перевод:

Файл: views / index.html

<!DOCTYPE html>
<html lang="{{currentLocale}}">
<head>
    <title>Node i18n</title>
<body>
{{hello}} {{name}}<br>
{{message}}
<div>
    {{#i18n}}This is a translation helper{{/i18n}}
</div>
</body>
</html>

Файл: locales / el.json

{
   "Hello": "Για Σας",
   "How are you?": "Πώς είστε?",
   "You have %s message": {
      "one": "'Έχεις %s μύνημα",
      "other": "'Έχεις %s μύνηματα"
   },
   "This is a translation helper": "Αυτός είναι βοηθός μετάφρασης"
}

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

Файл: views / index.html

<!DOCTYPE html>
<html lang="{{currentLocale}}">
<head>
    <title>Node i18n</title>
<body>
{{hello}} {{name}}<br>
{{message}}
<div>
    {{#i18n}}This is a translation helper,{{/i18n}}<br>
    {{#i18np}}You have %s message,{{messageCount}}{{/i18np}}
</div>
</body>
</html>

Файл: app / Server.mjs

this.express.use((req, res, next) => {
  // mustache helper
  res.locals.i18np = () => (text, render) => {
    const parts = text.split(',');
    if (parts.length > 1) {
      const renderedCount = render(parts[1]);
      return req.__n(parts[0], renderedCount, render)
    }
  };
  next();
});

В этом случае мы используем метод render, предоставленный из mustache, чтобы найти правильный счетчик для замены из нашего хранилища данных. Только не забудьте добавить этот ключ к нашим переменным ответа:

Файл: app / server.mjs

this.express.get('/:name?', (req, res) => {
  const name = req.params.name;
  res.render('index', {
    'currentLocale': res.locale,
    'name': name || 'Theo',
    'hello': req.__('Hello'),
    'messageCount': 5,
    'message': req.__('How are you?')
  });
});

Снова запустите приложение, чтобы увидеть результат:

Использование нашего собственного промежуточного программного обеспечения локали для изменения языка

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

/**
 *
 * @returns {Function} A middleware function to use with Web Frameworks.
 */
getMiddleWare() {
  return (req, res, next) => {
    const queryParameter = 'lang';
    if (req.url) {
      const urlObj = url.parse(req.url, true);
      if (urlObj.query[queryParameter]) {
        const language = urlObj.query[queryParameter].toLowerCase();
        this.setLocale(language);
      }
    }
    next();
  }
}

Таким образом, мы можем повторно использовать наш объект localeService без перекрестных ссылок на другие библиотеки.

Я оставляю читателю в качестве упражнения замену вызовов для перевода строк с использованием нашего объекта localeService вместо объекта req в нашем файле Server.mjs.

Фраза

Phrase поддерживает множество разных языков и фреймворков, включая Node.js и Javascript. Это позволяет легко импортировать и экспортировать данные переводов и искать недостающие переводы, что действительно удобно. Кроме того, вы можете сотрудничать с переводчиками, ведь гораздо лучше профессионально сделать локализацию вашего сайта. Если вы хотите узнать больше о Phrase, обратитесь к Руководству по началу работы. Вы также можете получить 14-дневную пробную версию. И так, чего же ты ждешь?

Вывод

В этой статье предпринята отважная попытка подробно описать шаги, необходимые для добавления i18n в ваше приложение Node.js. Надеюсь, вам понравилась статья и она помогла вам понять, что требуется для локализации приложений Node.js.

Это ни в коем случае не исчерпывающее руководство, поскольку каждое приложение имеет разные потребности и требования к области применения. Пожалуйста, оставайтесь на связи, чтобы увидеть более подробные статьи по этой теме.

Изначально опубликовано в The Phrase Blog.