🌎💬 Привет, 你好, Bonjour: как реализовать мультиязычность на Typescript и React
В этой статье я расскажу вам о реализации мультиязычности на языке Typescript. Реализация поддерживает различные способы получения переводов строк как с сервера, так и из заранее подготовленных файлов в самом приложении. Данный способ не привязан к конкретному фреймворку, но в статье будет приведен пример его использования с библиотекой React.
Приложениями пользуются люди из самых разных точек мира, и далеко не всегда эти люди знают разговорный язык, на котором это самое приложение написано. Чтобы клиентам вашей системы было комфортнее ею пользоваться, вы, как программист, можете добавить поддержку мультиязычности. Количество поддерживаемых языков отталкивается от масштаба вашей системы и вашего собственного желания, но зачастую вы будете ориентироваться на конкретный рынок сбыта вашего приложения.
Также рекомендуется отвязывать эту самую реализацию от фреймворка, которым пользуется ваша команда, потому что мультиязычность – вещь достаточно универсальная и востребованная, так что при возможной миграции или переходе на проект с другим стеком технологий вы обойдетесь малой кровью.
Определим модели
Для начала нам потребуются модели самого переводимого сообщения и языка перевода. Модель языка:
Интерфейс включает в себя его название, полное и сокращенное, ссылку на изображение флага. Также надо узнать, является ли данный язык дефолтным для вашей системы. Последнее потребуется нам, чтобы установить язык в случае, если пользователь еще не выбрал его сам. Модель переводимого сообщения же выглядит так:
Интерфейс включает в себя код языка и как на этом языке читается данное сообщение.
Создадим провайдер языков
Теперь нам нужен класс, который будет отдавать нам массив языков, которые поддерживает система. Пример такого класса:
Функция invariant просто проверяет выполнение условия в первом аргументе и выбрасывает ошибку с текстом из второго при его несоблюдении.
Класс провайдера является абстрактным для возможного переопределения метода получения переводов (с сервера, из кода, со стороннего хранилища). В статье будет рассмотрено хранение поддерживаемых языков на уровне кода приложения. Конкретная реализация:
Осталось разобраться с инстанцированием текущего провайдера. Например, таким способом:
Что насчет хранения выбранного языка?
Когда пользователь перезагружает страницу, язык должен оставаться тем же самым, который был выбран им ранее. Для этого напишем класс-обертку над браузерным хранилищем:
Саму модель языка данный класс не хранит, а предоставляет нам только наименование его кода. Создавать это хранилище будем не напрямую, а через отдельную функцию:
Теперь нам понадобится класс, отвечающий за выбор начального языка, с которым будет работать пользователь:
Метод resolveUserLanguage() возвращает нам текущий язык в следующем порядке: сначала проверяет, есть ли у пользователя сохраненный код языка в хранилище, если да, то отдает язык из провайдера по этому ключу. Если сохраненного языка нет, класс смотрит настройки браузера через window.navigator.language (если вы работаете с NextJS, потребуется проверить, в какой среде выполняется код – в браузере или на сервере). Если язык браузера не входит в перечень поддерживаемых языков, то просто используется дефолтный язык приложения, фильтруя список языков по ключу isDefault.
Создается данный класс тоже через отдельную функцию, передавая в аргументах функции: конкретное хранилище и конкретный провайдер.
С переводами похожая ситуация: какие-то переводы приходят с сервера, какие-то – с заранее подготовленных файлов в самом приложении. Объединим их логику под общим интерфейсом:
Не путайте тип Translations с моделью Translation, первая просто представляет из себя словарь Record<string, string> для хранения переводов в формате ключ-значение.
В данной статье также будем рассматривать реализацию через файлы в коде приложения. Для этого под каждый поддерживаемый язык создадим по директории с названием, отражающим код языка. В каждой из них будут храниться файлы формата .ts, экспортирующие объекты с типом Translations. Пример такого файла:
Каждой уникальной строке соответствует уникальный ключ, по которому мы будем в дальнейшем переводить. Чтобы создать общий словарь для одного языка, мы объединим все подобные объекты в один через spread operator:
Каждый из деструктурированных объектов представляет собой набор тех же ключей, что и в примере выше. Важно, чтобы все ключи были уникальны, чтобы старые переводы не перезаписались новыми.
Все объекты аккумулируются в один общий словарь следующим образом:
Теперь можно написать конкретную реализацию под провайдер переводов из файлов приложения:
И функцию создания провайдера:
Функция перевода
Мы добрались до главного – функции, отвечающей за перевод. Ее код ниже:
В функцию передается язык, на который переводятся сообщения, ключ перевода и параметры (про это чуть позже). Запрашивается список сообщений по ключу языка, откуда потом берется сообщение по ключу перевода. А затем идет замена динамических параметров на значения, передаваемые в аргументе params.
Возможно, вы заметили в файле с переводами "auth" страницы строку
Перевод данной строки требует параметра :length, в который передается строковое значение минимальной длины пароля. Объект параметров будет выглядеть таким образом:
Применение в React
Для реализации на библиотеке React нам потребуется Redux, чтобы хранить текущую модель языка. Напишем slice, отвечающий за это:
И хук, предоставляющий нам текущий язык из redux-хранилища:
Затем нам нужен хук-обертка над функцией перевода, чтобы не передавать каждый раз в нее текущий язык, а получать его из хранилища. Код хука:
Сам хук получает текущий язык и возвращает нам функцию, которой не требуется язык перевода напрямую (но который можно передать его опционально третьим параметром для явного перевода на сторонний язык), принимающую ключ сообщения и параметры и работающую по той же логике, что функция t.ts.
Код небольшого компонента, использующего данный хук:
Изменение языка как в Redux state, так и в хранилище, происходит в одной и той же функции компонента (я не буду вдаваться в подробности его реализации, т. к. в контексте статьи это не важно). Код функции:
При вызове функции у нас переводится текущая страница проекта благодаря реактивности Redux, и в браузерное хранилище записывается новый код языка.
Динамические переводы
Когда речь заходит о переводах, определяемых со стороны пользователя (например, администратора), алгоритм перевода немного меняется. Как пример – в нашем проекте есть сущности в базе данных, у которых есть название на нескольких языках сразу. Хранятся они в JSON-формате следующим образом:
Как видите, это массив моделей Translation, определенных ранее. Для переводов подобного вида определим в React компонент DynamicTranslation:
Данный компонент получает массив моделей, фильтрует их в соответствии с текущим языком приложения и возвращает текст только одной из них. Если же перевода для данного языка не найдено (как в примере с массивом выше, где только два языка из трех), возвращается сообщение, что перевод для данного языка не найден.
Итоги
Данная реализация не слишком привязана к фреймворку/библиотеке, хотя в способах получения языка и присутствует немало логики, специфичной для React (хуки, Redux). При желании можно переписать данную реализацию под другой проект, затрагивая только малые ее части. Конечно, существуют определенные сложности при переводе различных сообщений на языки с другими правилами построения предложения (например, падежная система и система трех родов на русском языке). В будущем напишу отдельную статью про такие моменты и другие подводные камни при переводах.