Личный сайт Мустафина Ильгиза

React-i18next в Rails: кеширование через файлопровод

Как использовать файлопровод (Asset Pipeline) в Ruby on Rails для эффективного кеширования файлов перевода.

Здесь мы рассмотрим простейший способ интернационализации react-rails приложения, потом мы обсудим проблемы с кешированием этого подхода, и, наконец, мы подключим файлопровод (Asset Pipeline) решения этих проблем.

Использование react-i18next

В этой секции мы добавим react-i18next в приложение и сделаем файлы переводов доступными из папки public.

Сначала добавим необходимые зависимости

yarn add i18next i18next-http-backend react-i18next

Инициализируем i18next в необходимых точках входа (например, app/javascript/packs/application.js):

import i18n from 'i18next';
import I18nextHttpBackend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';

i18n
  .use(I18nextHttpBackend)
  .use(initReactI18next)
  .init();

Плагин i18next-http-backend отвечает за скачивание необходимых файлов перевода для выбранного языка, а react-i18next даёт доступ к i18next из самого React приложения.

Сейчас мы можем реализовать интернационализированную версию страницы «Здравствуй, мир!» с возможностью переключения языка:

import React, { Suspense } from 'react';
import { useTranslation } from 'react-i18next';

const TranslatedHelloWorld = () => {
  const { t, i18n } = useTranslation();
  
  return (
    <>
      <h1>{t('helloWorld')}</h1>

      <button onClick={() => i18n.changeLanguage('ru')}>
        Русский
      </button>

      <button onClick={() => i18n.changeLanguage('tt')}>
        Татарча
      </button>
    </>
  );
};

const HelloWorld = () => (
  <Suspense loading='...'>
    <TranslatedHelloWorld />
  </Suspense>
);

export default HelloWorld;

Если вы посмотрите на получившуюся страничку, то вы увидите строку «helloWorld» потому, что мы ещё не предоставили сами переводы для ключа helloWorld, а i18next в таких случаях по умолчанию отображает сам ключ вместо перевода.

По умолчанию i18next-http-backend ожидает, что файлы переводов доступны по адресу /locales/{{lng}}/{{ns}}.json (см. опцию loadPath), где {{lng}} — это код языка, а {{ns}} — это пространство имён. Стандартное пространство имён называется translation (см. опцию defaultNS).

Так, в нашем примере сервер должен обрабатывать два пути: /locales/ru/translation.json для русской версии и /locales/tt/translation.json для татарской. Мы можем создать JSON файлы переводов в папке public и они будут доступны по этим путям.

Файлы переводов для русского языка будут находится в файле public/locales/ru/translation.json:

{
  "helloWorld": "Здравствуй, мир!"
}

Файлы переводов для татарского языка будут находиться в файле public/locales/tt/translation.json:

{
  "helloWorld": "Сәлам, дөнья!"
}

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

Проблемы, связанные с кешированием

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

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

Как вариант, можно полностью отключить кеширование этих файлов через настройки на сервере, или через опцию requestOptions в i18next-http-backend, или даже через дописывание текущего времени к URL-у для загрузки файлов как в этом ответе на StackOverflow (адаптировано для i18next-http-backend):

{
  loadPath: '/locales/{{lng}}/{{ns}}.json?cb=' + new Date().getTime(),
}

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

Лучшим подходом будет правильная настройка кеширования для этих файлов. Это можно сделать несколькими способами, здесь мы рассмотрим способ, который обычно используется в Rails, а именно — файлопровод.

Использование файлопровода для кеширования файлов перевода i18next

В этой секции мы обсудим использование файлопровода Ruby on Rails для оборачивания имён файлов (filename revving).

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

Сейчас мы рассмотрим как пустить файлы переводов по файлопроводу, а затем настроим i18next-http-backend чтобы он брал файлы из файлопровода.

Перемещение файлов переводов в файлопровод

Чтобы файлы переводов оказались в файлопроводе, нам нужно всего лишь переместить их из директории public в директорию app/assets. В нашем случае у нас получится два файла:

  • app/assets/locales/ru/translation.json
  • app/assets/locales/tt/translation.json

Проверьте, что файлопровод увидел файлы переводов, используя консоль (bundle exec rails c):

> ActionController::Base.helpers.asset_path('ru/translation.json')
"/assets/ru/translation-1516916289b1be2609ec39a8f887f301260d6a7db6e5b39aa7da3b0f0ff2dd14.json" 

Если вместо этого вы получаете ошибку Sprockets::Rails::Helper::AssetNotPrecompiled:

> ActionController::Base.helpers.asset_path('ru/translation.json')
Traceback (most recent call last):
        1: from (irb):1
Sprockets::Rails::Helper::AssetNotPrecompiled (ru/translation.json)

То, возможно, вы используете Sprockets 4. В таком случае вам нужно обновить файл манифеста ассетов.

Обновление файлов манифеста для Sprockets 4

В зависимости от версии гема sprockets вам может быть нужно или не нужно обновлять файл манифеста ассетов. Вы можете проверить версию командой:

bundle info sprockets

Если у вас в проекте Sprockets версии 4, то вам нужно включить директорию с локалям в файле app/assets/config/manifest.js:

//= link_tree ../locales

Это требование было добавлено в Sprockets 4 (перевод и акцентирование — от меня):

Если вы используете sprockets старее, чем 4.0, то Rails будет компилировать application.css, application.js, и любые файлы в ваших директориях ассетов, которые не распознаны как JS или CSS, но у которых есть расширение в имени файла.

Если вы используете Sprockets 4, то Rails будет использовать другую логику для определения входных точек компиляции: будет использоваться только файл ./app/assets/config/manifest.js для определения начальных файлов.

Получение путей файлов после файлопровода из JavaScript

После перемещения файлов переводов в файлопровод, их больше нельзя получить просто по их именам (/locales/ru/translation.json). Теперь в их именах должны присутствовать хеши (/assets/ru/translations-151...d14.json).

Эти новые имена можно получить в Ruby из помощника asset_path, но их нельзя получить напрямую в JavaScript. Вместо этого, мы можем использовать Erb шаблоны чтобы подставилять значения, вычисленные в Ruby, в JavaScript код.

Добавьте поддержку Erb в webpacker по оффициальным инструкциям.

Опция loadPath библиотеки i18next-http-backend может принимать и функцию (languages, namespaces) => loadPath. Хоть аргументы languages и namespaces должны быть массивами, они будут содержать по одному элементу если опция allowMultiLoading выставлена в значение false (по умолчанию это так).

Напишем нашу функцию loadPath в файле app/javascript/loadPath.js.erb:

const loadPath = (languages, namespaces) => {
  if (languages[0] === 'ru') {
    return '<%= ActionController::Base.helpers.asset_path("ru/translation.json") %>';
  }
  
  if (languages[0] === 'tt') {
    return '<%= ActionController::Base.helpers.asset_path("tt/translation.json") %>';
  }
  
  return undefined;
};

export default loadPath;

И передадим её в i18next в файле app/javascript/packs/application.js:

import loadPath from 'loadPath.js.erb';

i18n
  .use(I18nextHttpBackend)
  .use(initReactI18next)
  .init({
    backend: {
      loadPath,
    },
  });

Сейчас вы можете обновить страницу и увидеть, что кнопки снова работают.

Вот и всё. Желаю вам счастливой интернационализации!