Как настроить локализацию?

August 3, 2015 · View on GitHub

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

Читаем Википедию.

Локализация — процесс адаптации веб-приложения к культуре какой-либо страны.

Интернационализация — технологические приёмы разработки, упрощающие адаптацию продукта к языковым и культурным особенностям регионов, отличных от того, в котором разрабатывался продукт.

Важное отличие одного от другого можно сформулировать совсем коротко. Интернационализация — это адаптация продукта для использования в разных странах, в то время как локализация — это добавление специальных функций для использования в какой-то определённой стране.

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

Как настроить интернационализацию?

В английском языке для слова internationalization принято сокращение i18n. При этом число 18 означает количество пропущенных между i и n букв. Для Локализации Localization применяют сокращение L10n. Заглавная буква L используется чтобы не путать с i в L10n, число 10 — количество букв между L и n.

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

Освоить их проще простого, если вы вместе с нами освоили задания из последнего занятия про yblock.

Теперь у вас есть страница /pages/test-page/test-page.page.js, на которой к этому моменту задекларирован один единственный блок form. Добавим в блок новую опцию titleText, в которую передадим произвольный текст. Его-то мы и будем переводить на разные языки.

У вас должна получиться такая структура:

module.exports = function (pages) {
    pages.declare('test-page', function (params) {
        var options = params.options;
        return {
            block: 'page',
            title: 'test page',
            styles: [
                {url: options.assetsPath + '.css'}
            ],
            scripts: [
                {url: options.assetsPath + '.' + params.lang + '.js'}
            ],
            body: [
                {
                    block: 'form',
                    titleText: 'Лучший кофе на дороге'  // <---------- Новая опция
                }
            ]
        };
    });
}

Допишем шаблон для формы. Будем принимать опцию titleText и создавать элемент блока head, который в DOM-дереве будет представлен заголовком h1 с переданным текстом.

Было:

module.exports = function (bt) {

    bt.match('form', function (ctx) {
        ctx.enableAutoInit();

        ctx.setTag('span');

        ctx.setContent('Содержимое блока');
    });

};

Стало:

module.exports = function (bt) {

    bt.match('form', function (ctx) {
        ctx.enableAutoInit();

        ctx.setTag('span');

        var title = ctx.getParam('titleText');
        ctx.setContent(
            {
                elem: 'head',
                text: title
            }
        );
    });

    bt.match('form__head', function (ctx) {
        ctx.setTag('h1');

        var headText = ctx.getParam('text');
        ctx.setContent(headText);
    });

};

Я нарочно дал совершенно разные имена опциям шаблона и локальным переменным, чтобы вам было легче проследить, как текст "Лучший кофе на дороге" передаётся от опции ctx.getParam('titleText') до установки контента в ctx.setContent(headText);. Проследите-проследите :) Освежите свои знания про генерацию HTML из BT-шаблонов.

Обновим страницу localhost:8080/test в браузере и увидим огромный заголовок про кофе. Я вижу, а вы?

Очевидно, что в опцию titleText передавать готовую строку на русском языке нельзя. Надо передавать какой-то ключ, по которому нужно где-то получать русский текст для интерфейса на русском языке, английский текст для интерфейса на английском языке, украинский для украинского, турецкий для турецкого и т.д.

Слово где-то я выделил курсивом, чтобы сразу о нём поговорить. Невозможно локализировать сайт, не имея некоего хранилища переведённых текстов. В BEViS такое хранилище мы организовали в виде файлов. Создадим его вместе с вами.

Создайте папку blocks/i18n/_keyset.

Это будет специальное место, где будут храниться переводы всех блоков. Почему внутри папки blocks/i18n мы создали подпапку _keyset — об этом не задумывайтесь. Это всё обсудим позже. Сейчас переходим к самому интересному.

Мы готовим переводы для блока Form, поэтому создадим набор ключей для этого блока. Набор ключей будем называть кисетом (если на ваш взгляд слово "кейсет" звучит лучше, называйте так). Давайте создадим кисет для формы.

Создаём папку blocks/i18n/_keyset/i18n_keyset_form.i18n. Опять же, пока не задумываемся, почему такое странное название у директории. А внутри неё создаём единственный (пока!) языковой-файл ru.js и пишем в него единственный (пока!) ключ с переводом:

blocks/i18n/_keyset/i18n_keyset_form.i18n/ru.js:

module.exports = {
    "form": {
        "title-text": "Лучший кофе на дороге"
    }
};

Остановимся на содержимом подробнее.

Языковой файл ru.js — это обычный Node.js-модуль, который экспортирует объект с единственным полем, который мы назвали form. Мы назвали его так, чтобы имя кисета перекликалось с именем блока Form, который мы видим на web-странице. Когда имена перекликаются, так легче.

В этом файле сейчас мы описали только один ключ title-text и его значением сделали тот самый текст про кофе. Для наглядности добавим ещё один ключ. Сейчас он нам не нужен, а позже пригодится.

blocks/i18n/_keyset/i18n_keyset_form.i18n/ru.js:

module.exports = {
    "form": {
        "title-text": "Лучший кофе на дороге",
        "hint-content": "Иллюзионист иллюстрирует иллюзорно"
    }
};

Хранилище готово. Конечно, оно готово только для русского языка. Сразу приготовим тексты для английского языка. Создадим в той же папке файл с именем en.js.

blocks/i18n/_keyset/i18n_keyset_form.i18n/en.js:

module.exports = {
    "form": {
        "title-text": "The best coffee on the road",
        "hint-content": "Illusionist illustrates illusorily"
    }
};

А теперь давайте сделаем так, чтобы приложение ходило за текстом про кофе именно в это хранилище.

Локализация внутри декларации страницы

Первое, что мы делаем всегда после создания нового блока, указываем зависимость — сообщаем, что такой-то блок будет использоваться на такой-то странице.

Редактируем pages/test-page/test-page.deps.yaml, добавляемость зависимость к блоку i18n и указываем, что на этой странице используется кисет form:

- page
- block: block
  elem: auto-init
- input
- super-input
- form
- block: i18n
  keyset: form  # <----- Вот он

Всё. Теперь в декларации страницы убираем русский текст и вместо него просим ключ из хранилища:

module.exports = function (pages) {
    pages.declare('test-page', function (params) {
        var options = params.options;
        return {
            block: 'page',
            title: 'test page',
            styles: [
                {url: options.assetsPath + '.css'}
            ],
            scripts: [
                {url: options.assetsPath + '.' + params.lang + '.js'}
            ],
            body: [
                {
                    block: 'form',
                    titleText: pages.i18n('form', 'title-text') // <------ Здесь зовём i18n-ключ
                }
            ]
        };
    });
};

Обновляем страницу в браузере, видим текст про кофе. Как понять, а вдруг это кеш, и у нас ничего не получилось? Можно изменить текст в кисете и проверить. Редактируем?

blocks/i18n/_keyset/i18n_keyset_form.i18n/ru.js:

module.exports = {
    "form": {
        "title-text": "Лучший кофе на дороге — отхлебнёшь, протянешь ноги! (с)", // <----- Изменили значение
        "hint-content": "Иллюзионист иллюстрирует иллюзорно"
    }
};

Обновляем в браузере. Есть новый текст! Оглушительный успех ;)

Мы позвали перевод, находясь внутри декларации страницы, с помощью метода pages.i18n, который умеет принимать два параметра — имя кисета и имя ключа:

pages.i18n('form', 'title-text')

Этот метод ходит в хранилище переводов, находит там нужный кисет form, а в нём нужный ключ title-text и возвращает значение ключа Лучший кофе на ...

Похожий метод (похожий, но не этот же самый) доступен нам не только в декларации страницы, но и в bt-шаблонах любого блока.

Локализация в bt-шаблонах

Откроем blocks/form/form.bt.js и добавим новый элемент hint:

module.exports = function (bt) {

    bt.match('form', function (ctx) {
        ctx.enableAutoInit();

        ctx.setTag('span');

        var title = ctx.getParam('titleText');
        ctx.setContent([
            {
                elem: 'head',
                text: title
            },
            {
                elem: 'hint',                                     // <----- Создали новый элемент
                textHint: bt.lib.i18n('form', 'hint-content')     // <----- Позвали ключ для него
            }
        ]);
    });

    // Шаблон для элемента hint
    bt.match('form__hint', function (ctx) { 
        ctx.setContent(ctx.getParam('textHint'));
    });

    bt.match('form__head', function (ctx) {
        ctx.setTag('h1');

        var headText = ctx.getParam('text');
        ctx.setContent(headText);
    });

};

В шаблоне формы мы сгенерили новый элемент hint и в его опцию textHint передали значение ключа hint-content. Когда обновим браузер, увидим под заголовком текст про иллюзиониста.

Успех!

Мы рассмотрели способы, как звать ключи, находясь в декларации страницы или в bt-шаблонах блока.

Декларация страницы работает на сервере, создаёт статический HTML, который летит пользователю в браузер. BT-шаблоны умеют работать как на сервере, так и в браузере.

А как пользоваться i18n в клиентском javascript?

Локализация в клиентских js-модулях

modules.define(
    'form',
    [
        'inherit',
        'block',
        'input',
        'super-input',
        'y-i18n'        // <---------- Позвали клиентский модуль y-i18n
    ],
    function (
        provide,
        inherit,
        YBlock,
        Input,
        SuperInput,
        i18n            // <---------- Приняли его в качестве аргумента i18n
    ) {
        var form = inherit(YBlock, {
            __constructor: function () {
                this.__base.apply(this, arguments);

                var formDomNode = this.getDomNode();

                // Создаём инпут
                this._greetingInput = new Input({
                    value: 'Привет, Бивис',
                    name: 'loginField',
                    placeholder: 'Инпут на сайте',

                    parentNode: formDomNode
                });
                this._greetingInput.on('input-submitted', this._onInputSubmitted, this);

                // Создаём инпут для пароля
                this._passwordInput = new SuperInput({
                    name: 'passwordField',
                    type: 'password',
                    placeholder: i18n('form', 'hint-content'), // <---------- Позвали любой ключ

                    parentNode: formDomNode
                });
                this._passwordInput.on('input-submitted', this._onInputSubmitted, this);
            },

            /**
             * Реагирует на нажатие клавиши Enter в `Input`
             * @param {YEventEmitter} e
             */
            _onInputSubmitted: function (e) {
                console.log('Форма поймала событие на Input = ', e);
            }
        }, {
            getBlockName: function () {
                return 'form';
            }
        });

        provide(form);
});

Напомню, что классы Input, SuperInput и класс Form мы создавали с вами вместе, когда читали статью про клиентское программирование с помощью YBlock. Код этих компонентов можно увидеть в отдельной ветке input-and-form.

В коде выше мы смотрим на компонент Form. Для того, чтобы использовать локализацию в клиентсокм модуле, я минимально изменил класс Form, пометив все изменения комментариями. В прейсхолдер инпута для пароля я позвал строку про иллюзиониста. Обновите страницу в браузере. Успех?

Обратите внимание на важный момент.

В проекте есть две сущности с почти одинаковыми названиями.

  1. В движке проекта уже есть специальный модуль с именем y-i18n, который вы можете звать в своих клиентских модулях, как в примере выше. Этот модуль создавать не нужно, он просто есть по умолчанию, находится где-то в файлах BEViS-движка. Он умеет умеет ходить в ваше хранилище переводов и забирать оттуда строки на разных языках.
  2. И есть ваше хранилище переводов, которое находится по адресу blocks/i18n. Хранилище представлено неким блоком с именем i18n, и его дочерним элементом _keyset. Мы специально реализовали хранилище в виде так называемого блока, чтобы им удобно было пользоваться. Напомню, чтобы им пользоваться, нужно в зависимостях страницы указать имя этого блока и имя нужного кисета, как мы делали это выше в файле pages/test/test-page.deps.yaml:
- page
- block: block
  elem: auto-init
- input
- super-input
- form
- block: i18n    # <----- Позвали на страницу хранилище переводов 
  keyset: form   # <----- Указали, каким кисетом будем пользоваться.

Можно указывать столько кисетов, сколько хочется, если в yaml-файле перечислить их в виде массива:

- block: i18n     
  keyset: [form, any-other] # <----- Позвали кисеты form и any-other

Зависимости можно указывать не только в страничных файлах, а в блоках. Например, мы знаем что модуль Form использует внутри себя переводы. Тогда кисеты можно подтягивать не в страничном pages/test/test-page.deps.yaml, а в блочном blocks/form/form.deps.yaml. Этого файла ещё нет. Создадим его и напишем в нём:

- block: i18n     
  keyset: [form, any-other]

А в страничном pages/test/test-page.deps.yaml удалим эту зависимость и обновим страницу в браузере. Ничего не сломалось, иллюзионист что-то там иллюстрирует в поле для пароля. Ну, отлично. Получается, что любой блок в своём файле зависимостей может указать, от каких других блоков именно он зависит. Если зависимости указывать таким образом, гарантирован порядок — на страницу придут только те модули, которые нужно конкретным блокам на текущей странице.

Обратили внимание, что мы указали зависимость к кисету any-other, а его нет на файловой системе? Ну и ничего не сломалось при обновлении страницы. А что случится, если мы в клиентском модуле попробуем получить ключ из этого кисета? Пробуем? Давайте отредактируем в модуле Form вызов конструктора инпута для пароля:

// Создаём инпут для пароля
this._passwordInput = new SuperInput({
    name: 'passwordField',
    type: 'password',
    placeholder: i18n('any-other', 'my-key'), // <---- Позвали несуществующий ключ 'my-key' из 
                                              //       несуществующего кисета 'any-other'

    parentNode: formDomNode
});

После рефреша страницы в консоли видим ошибку:

Uncaught Error: form init error: Keyset "any-other" was not found.

Отлично, приложение помогает понять, что мы что-то забыли. Давайте создадим этот кисет и будем заканчивать.

Кстати, я не сказал, в приложении есть скрипт для создания кисетов из терминала:

make keyset
# Введите имя keyset

Этот скрипт создаёт в хранилище нужную папку для кисета и два файла с переводами - ru.js и en.js. В каждом создаёт один фейковый ключ, который назван незатейливо my-key. В английском файле значение ключа установлено в my value, а в русском в моё значение. Я же говорю, кисет незатейливый :)

После рефреша в поле пароля видим плейсхолдер "моё значение".

Всё, теперь вы знаете всё, чтобы сделать сайт интернационализированным. Осталось самое главное :)

Как переключать интерфейс на другой язык?

Конечно, мы наделали кучу файлов с переводами, умеем из них получать строки, умеем это делать не только в страничном btjson, но и в bt-шаблонах и даже в клиентских javascript-модулях. А как переключить приложение на другой язык? Ведь это же самое главное :)

Откроем pages/test/test-page.page.js:

 module.exports = function (pages) {
     pages.declare('test-page', function (params) {
         var options = params.options;
         return {
             block: 'page',
             title: 'test page',
             styles: [
                 {url: options.assetsPath + '.css'}
             ],
             scripts: [
                 {url: options.assetsPath + '.' + params.lang + '.js'}  // <----- Здесь!  
             ],
             body: [
                 {
                     block: 'form',
                     titleText: pages.i18n('form', 'title-text')
                 }
             ]
         };
     });
 };

Видите строку {url: options.assetsPath + '.' + params.lang + '.js'}?

Именно переменная params.lang отвечает за то, какой язык будет использоваться в клиентских js-модулях. Я не спроста выделил курсивом последние слова. Убедимся на опыте. Сделайте быструю замену прямо сейчас. Замените params.lang на 'en'

Было:

{url: options.assetsPath + '.' + params.lang + '.js'}  

Стало:

{url: options.assetsPath + '.en.js'}  

И обновите страницу в браузере. Инпуты не отренедрились? А в консоли вообще ошибка!

GET 404 http://localhost:8080/build/test/_test.en.js 

Файл не загрузился, нет его! Хм, так и должно быть. Ведь файла с расширением *.en.js не существует. Убедитесь сами, загляните в папку для собраннх файлов build/test. Там есть только файлы с расширением *.ru.js. Дело в том, что BEViS ничего не знает о том, что (оказывается!) он должен собирать ещё и файлы с расширением *.en.js. Так давайте скажем ему.

Это происходит в файле .enb/make.js. Это очень важный файл, в котором мы описываем конфигурацию сборки наших
файлов. Там много разного написано, нас же сейчас интересует одна единственная строка:

config.setLanguages(['ru']);

Именно в ней мы указываем, с какими языками собирать файлы. Допишите туда en, чтобы получилось так:

config.setLanguages(['ru', 'en']);

Обновим страницу. Так, инпуты появились. Но тексты на русском языке!

Погодите-погодите, один текст всё-таки на английском - тот, который внутри инпута, который мы устанавливали из клиентского модуля form.js. Убедились?

Всё верно, так и должно быть — страничный build/test/_test.en.js отвечает за локализацию в клиентских модулях и клиентских bt-шаблонах.

Локализация клиентских bt-шаблонов и клиентских js-модулей

Запомним. Вся локализация на клиенте зависит от того, какой файл прилетел в браузер. Прилетит test.ru.js — будет сайт на русском. Прилетит test.en.js — будет сайт на английском. Но! Локализуются только те переменные, которые использовались в клиентском javascript — в клиентских js-модулях и в тех bt-шаблонах, которые работают в браузере (не на сервере!). Не очень понятно, да?

Смотрите. В теле нашей страницы в конце <body> загружается файл build/test/_test.en.js. Это собранный файл, состоящий из множества клиентских блочных модулей, таких как blocks/form/form.js и blocks/input/input.js (и ещё множество других файлов). Когда страничный файл загружается в память браузера, он инициализирует все модули, в том числе и модуль form.js. А в его конструкторе в том числе написано такое:

// Создаём инпут для пароля
this._passwordInput = new SuperInput({
    name: 'passwordField',
    type: 'password',
    placeholder: i18n('any-other', 'my-key'),

    parentNode: formDomNode
});

Что происходит после new SuperInput({/*здесь btjson блока*/}), вы знаете. На лету в памяти браузера (то есть, "на клиенте") BEViS выполняет bt-шаблоны, которые из btjson-а строят HTML — те самые шаблоны, которые описаны в файле input.bt.js. Вы должны уже помнить, что в собранный build/test/_test.en.js попадают не только все клиентские модули, а ещё и все bt-шаблоны, которые умеют строить HTML не только на сервере, но ещё и налету на клиенте (то есть, "в памяти браузера").

Ещё раз.

Вся локализация на клиенте зависит от того, какой файл прилетел в браузер. Прилетит test.ru.js — будет сайт на русском. Прилетит test.en.js — будет сайт на английском. Но! Локализуются только те переменные, которые использовались в клиентском javascript — в клиентских js-модулях и в тех bt-шаблонах, которые работают в браузере (не на сервере!).

А как быть с сервером?

Локализация серверных bt-шаблонов

Во-первых, если мы делаем локализацию сайта, странно хардкодить язык в декларацию страницы. Поэтому, давайте откатим последнее изменение в pages/test/test-page.page.js, чтобы в строке снова появилась переменная params.lang

{url: options.assetsPath + '.' + params.lang + '.js'}  

Если сейчас обновим браузер и посмотрим в сгенеренный HTML, увидим, что благодаря этой строке запрашивается файл build/test/_test.ru.js. Следовательно в переменной params.lang хранится значение ru. А где же оно устанавливается?

Откройте файл server/page.js. Кстати, вам известно, как устроено Node.js-приложение? Если известно, вас этот файл не испугает . А если вы ещё не знакомы с устройством Node.js-приложений, не пугайтесь — вам пока не надо понимать, что происходит в этом файле. Я и сам в нём не всё понимаю ;)

А если серьёзно, этот файл — часть Node.js-приложения, отвечающая за генерацию статического HTML. И непосредственно генерация происходит в методе handle:

/**
* Build page
*
* @returns {Promise} promise
*/
handle: function () {
   return vow.all([
           this._getPages(),
           this._getTemplate(),
           this._getI18n()
       ])
       .spread(function (pages, bt, buildI18n) {
           var i18n = buildI18n();
           pages.setI18n(i18n);
           bt.lib.i18n = i18n;

           return pages.exec(this._pageName, {
               query: this._query,
               options: this._getPageOptions(),
               lang: this._lang
           }).then(function (btJson) {
               return this._applyTemplate(btJson, bt);
           }.bind(this));
       }.bind(this));
}

Вдумываться в то, что здесь написано, не надо. Вообще! Если вы ещё не знакомы с Promises, вам совершенно непонятно, что тут происходит. Да вообще не важно! Нам бы с языком разобраться и понять, где формируется объект params и его переменная params.lang. А смотрите, именно в этом методе они и формируются. Вот где:

           return pages.exec(this._pageName, {    //   ----
               query: this._query,                //       |
               options: this._getPageOptions(),   //        >  Вот этот объект!
               lang: this._lang                   //       |
           })                                     //   ----

А потом этот самый объект приходит в файл pages/test/test-page.page.js в качества параметра params:

 module.exports = function (pages) {   
     pages.declare('test-page', function (params) {   
         var options = params.options;  //  ^------------- Вот этот же объект!
         return {
             block: 'page',
             title: 'test page',
             styles: [
                 {url: options.assetsPath + '.css'}
             ],
             scripts: [
                 {url: options.assetsPath + '.' + params.lang + '.js'}  // <----- Здесь!  
             ],
             body: [
                 {
                     block: 'form',
                     titleText: pages.i18n('form', 'title-text')
                 }
             ]
         };
     });
 };

А в нём есть поле lang, которое получает значение из внутренней переменной объекта Page (см.строку lang:this._lang). А что хранится в this._lang? Смотрим выше, в конструктор Page и видим интересное:

/**
 * Page
 *
 * @param {String} id Relative path to page (pages/index, for example)
 * @param {String} pageName Page name.
 * @param {Request} req Node request Object
 * @param {Response} res Node response Object
 */
__constructor: function (id, pageName, req, res) {
    this._id = id;
    this._pageName = pageName;
    this._req = req;
    this._res = res;
    this._lang = req.query.lang || 'ru'; // <-------- Вот оно!
    this._query = req.query;
}

Теперь понятно. Если не определена переменная req.query.lang, тогда установи this._lang в ru!

Объект req — это объект Node.js-сервера, в котором собрана вся информация о HTTP-запросе в сервер. Там есть о запросе всё — имя страницы, которую вы запросили в браузере, все параметры запроса (я имею в виду GET, POST), все куки, HTTP-заголовки, — вообще всё!

Конкретно параметры запроса хранятся в поле req.query. Следовательно, если мы запросим страницу test с GET-параметром lang=en, тогда наш сайт станет англоязычным, что ли? Ну-ка, попробуем:

http://localhost:8080/test?lang=en

Да, оглушительный успех! Я вижу страницу с английскими текстами.

Ну, кроме первого инпута, в котором по-русски написано "Привет, Бивис". Но это не ошибка, мы же сами захардкодили эти тексты в blocks/form/form.js.

Вот и ответ на вопрос. Чтобы серверные bt-шаблоны получили нужный язык, нужно его установить в параметрах генерации статического HTML, то есть здесь:

/**
* Build page
*
* @returns {Promise} promise
*/
handle: function () {
   return vow.all([
           this._getPages(),
           this._getTemplate(),
           this._getI18n()
       ])
       .spread(function (pages, bt, buildI18n) {
           var i18n = buildI18n();
           pages.setI18n(i18n);
           bt.lib.i18n = i18n;

           return pages.exec(this._pageName, {
               query: this._query,
               options: this._getPageOptions(),
               lang: this._lang  // <----------------------- Здесь!
           }).then(function (btJson) {
               return this._applyTemplate(btJson, bt);
           }.bind(this));
       }.bind(this));
}

Если в BEViS-приложении в запросе передавать GET-параметр lang, тогда можно легко менять язык интерфейса. Правда, есть ошибка, которую мы исправлять в bevis-stub не будем. Сделайте запрос к несуществующему языку, например к турецкому:

http://localhost:8080/test?lang=tr

Приложение упадёт с "Internal error", а в терминале вывалится сообщение типа такого:

00:14:31 - error: Error: There is no tech for target build/test/test.lang.tr.js.

Нет такого языка, вот вам и ошибка.

Но исправлять это мы не будем, потому что от проекта к проекту способы получать язык пользователя разнятся. Кто-то будет читать язык из GET-параметра (и ещё не факт, что он будет называться lang), кто-то будет читать его из кук, в каком-то проекте язык интерфейса будет приходить из базы данных. Поэтому это остаётся на ваше усмотрение. Ну и да, придётся чуть больше узнать про то, как устроено Node.js-приложение, почитать мануалы.

Главное — (и вы это, бесспорно, давно поняли) чтобы серверные bt-шаблоны сгенерили HTML с нужным языком, следует в server/page .js в методе handle установить параметр lang.

Всё, теперь всё. Теперь вы можете перевести свой сайт на разные языки. А когда это сделаете, предлагаю вам продолжить практикум.

Я хочу вам рассказать о том, как использовать MVC-паттерн в BEViS. Хочется порядка в коде проекта,
хочется привычных абстракций в виде контроллеров, моделей и вью. Это делается быстро и несложно. Посмотрим вместе?

Сделайте вкусного чая и приходите, я жду вас здесь.


PS. Переводы проекта хранятся в папке blocks/i18n/_keyset/*. В Яндексе мы не создаём руками эти кисеты. У нас есть специальный внутренний веб-сервис, в котором можно завести кисеты, создать там через веб-интерфейс нужные ключи на русском языке и "призвать" переводчиков, чтобы они перевели ключи на нужные языки. А потом мы с помощью некоторого скрипта обращаемся по HTTP к этому сервису, как к бекенду, и получаем переводы из него в виде готовых файлов ru.js, en.js, tr.js. И этот же скрипт сам раскладывает файлы переводов внутри папки blocks/i18n/_keyset/*.

Возможно, в своей команде вы тоже сделаете какой-то такой сервис, с ним удобно. А если по каким-то причинам не сделаете, не беда, хранилище переводов можно поддерживать и руками.