Мультиязычность на Typescript и React
Об авторе
Юрий Думов работает архитектором программного обеспечения в SAP, имеет более 10 лет опыта в разработке веб и мобильных приложений.
Прежде всего, давайте введем некоторые сокращения. Internationalization — достаточно длинное слово, поэтому предлагаю его заменить в нашем контексте на “intl”.
Интернационализация в общем плане может быть разделена на следующие подпункты:
- Определение пользовательской локали
- Перевод UI-элементов, заголовков, подсказок и прочего
- Поддержка местных специфических сервисов, таких как даты, валюты и числа
На заметку: в этой статье я заострю ваше внимание лишь на front-end части. Мы разработаем несложное универсальное React-приложение с полной поддержкой интернационализации.
Для начала предлагаю воспользоваться моим репозиторием. Здесь у нас есть веб-сервер Express для серверного рендеринга, вебпак для построения клиентского JS, Babel для конвертации современного JavaScript в ES5. Также мы будем использовать npm-run, nodemon для запуска веб-сервера под средой разработчика и webpak-dev-server для ассетов.
Нашей точкой входа в серверное приложение будет служить server.js. Здесь мы загружаем Babel и babel-polyfill для создания прочего серверного кода в современном стандарте JavaScript.
В качестве бизнес-логики сервера мы используем src/server.jsx. Тут мы устанавливаем Express-сервер для прослушки порта 3001. Для рендеринга мы используем очень простой компонент components/App.
jsx, который также является частью точки входа в универсальное приложение.
Точкой входа в клиентский JS нам служит src/client.jsx. Тут мы фиксируем корневой компонент — component/App.jsx — для placeholder'а react-view в HTML-разметке, предоставляемой Express-сервером.
Таким образом, сейчас мы склонируем репозиторий, запустим npm-install и выполним nodemon и webpack-dev-server одновременно в двух консолях.
В первой консоли:
И во второй:
Наш веб-сервер должен быть доступен по localhost:3001. Откройте предпочитаемый вами браузер и убедитесь в этом сами!
Итак, мы готовы начать.
- Определение пользовательской локали
Существует два возможных решения этого вопроса. По какой-то причине большинство популярных веб-сайтов, включая Skype и NBA, используют гео-IP для определения местоположения пользователя, таким образом определяя его родной язык. Подобный подход не только является дорогим в плане реализации, но и еще не совсем точным.
Сейчас очень много людей путешествует, по этой причине местоположение пользователя не является надежным ориентиром при выборе подходящего языка приложения. Вместо этого мы предпочтем второй способ решить данную проблему при помощи обработки на стороне сервера заголовка Accept-Language.
Этот заголовок отправляется любым более-менее современным браузером.
Этот заголовок предоставляет информацию о возможных вариантах языка, принятого в качестве ответа. Каждый язык обладает своим «приоритетом», показывая, как часто пользователь может его использовать.
По умолчанию уровень «приоритетности» равен 1. К примеру, «Accept-Language: da, en-gb;q=0.8, en;q=0.7» означает «я предпочитаю датский, но могу также принять британский или другие виды английского».
(Также стоит упомянуть, что сей подход так же несовершенен. К примеру, пользователь может посетить ваш веб-сайт из интернет-кафе или публичного ПК. Лучше всего разработать виджет, при помощи которого пользователь на интуитивном уровне сможет поменять язык сайта.)
Реализация определения локали пользователя
Вот пример кода веб-сервера Node.js. Мы используем пакет accept-language, что извлекает локали из http-заголовков и находит наиболее предпочтительные, исходя из поддерживаемых вашим веб-сайтом. Если таковые не были найдены, тогда сайт будет использовать свою дефолтную локаль.
Приступим к установке пакетов:
После чего в src/server.jsx у нас будет следующее:
Здесь мы импортируем accept-language и устанавливаем поддержку английских и русских локалей. Также мы реализовываем функцию detectLocale, что извлекает значение локали из куки.
Если ни одна не была обнаружена, начинается обработка Accept-Language. Наконец, мы выбираем дефолтную локаль. После обработки запроса мы добавим заголовок Set-Cookie для обнаруженной локали в ответ.
Это значение будет использовано для всех последующих запросов.
- Перевод UI-элементов, заголовков, подсказок и прочего
Здесь я собираюсь использовать React Intl-пакет. Это наиболее популярная и, так сказать, проверенная боем реализация интернационализации для React-приложений.
Впрочем все библиотеки так или иначе строятся по одному принципу: они обеспечивают нас компонентами высшего порядка, что внедряют уже готовые функции интернационализации для обработки сообщений, дат, номеров, валют посредством специальных фич React.
Во-первых, мы должны установить провайдер интернационализации. Для этого мы немного изменим src/server.jsx и src/client.jsx.
Так, теперь IntlProvider-дочерний компонент получит доступ к функциям интернационализации. Давайте добавим переведенный текст в наше приложение и клавишу для изменения локали.
У нас есть два способа, как это можно сделать: через FormattedMessage или через formatMessage – функцию.
Разница в том, что компонент будет обернут в span-тэг, что хорошо для текстовых данных, но не хорошо для значений HTML-атрибутов, таких как alt и title. Давайте опробуем и их!
Вот src/components/App.jsx:
Отметьте, что атрибут id должен быть всегда уникальным для всего приложения в целом, так что было бы не лишним установить для себя некоторые правила именования сообщений. Я предпочитаю следовать формату «имяКомпонента.некийУникальныйИдентификатор». В качестве некой дефолтной локали будет использовано сообщение defaultMessage. Атрибут description предоставит некую информацию для переводчика.
Перезапустите nodemon и обновите страницу в браузере. Вы должны увидеть «Hello World». Но если вы открываете статью при помощи инструментов разработчика, вы увидите, что текст теперь внутри тэга span. В этом случае это не ошибка, но иногда мы предпочитаем просто получить текст, без никаких дополнительных тэгов. Для этого нам нужен прямой доступ к объекту интернационализации React Intl.
Давайте вернемся назад к src/components/App.jsx:
Нам нужно написать гораздо больше кода, чем раньше. Во-первых, мы используем injectIntl, который «упаковывает» наш компонент приложения и внедряет intl-объект. Чтобы получить переведенное сообщение, нам нужно вызвать formatMessage и передать в качестве параметра message. Этот message должен иметь свой уникальный идентификатор и атрибуты defineMesasges из React Intl.
Лучшее, что есть в React Intl, так это его экосистема. Давайте добавим babel-plugin-react-intl к нашему приложению и построим словарь трансляции. Мы передадим этот словарь переводчикам, которым не нужно никаких задатков программирования для выполнения своей работы.
Перезапустите nodemon, и вы увидите, что папка build/messages была успешно создана в корневой директории проекта, вместе с некоторыми другими папками и файлами внутри. Теперь нам необходимо собрать эти файлы в один JSON. Для этого вы можете использовать мой скрипт. Сохраните его как scripts/translate.js.
- Теперь нам нужно добавить новый скрипт в package.json:
- Что же, попробуем!
В конце вы должны увидеть файл en.json в build/lang.
И все работает! Теперь пришло время для кое-чего поинтересней. На стороне сервера мы можем загрузить все переводы в память и, соответственно, выдавать их в зависимости от характера запроса. Однако на клиентской стороне этот подход неприемлем. Потому вместо этого мы будем один раз принимать json-файл со всеми переводами, а клиент автоматически определит, какой из текстов ему нужен.
Скопируем результирующий файл в public/assets.
На заметку: если вы пользуетесь Windows, симлинки для вас недоступны. Таким образом, вам нужно будет вручную копировать команды каждый раз, как только вы будете перестраивать ваши переводы.
- public/assets/ru.json применим следующее:
- Теперь нам нужно повязать серверный и клиентский коды.
- Для сервера наш src/server.jsx должен выглядеть так:
- Здесь мы делаем следующее:
- Кэшируем сообщения и специфичный для данной локали JS для валют, DateTime, Number во время запуска приложения.
- Расширяем метод renderHTML так, что мы можем вставить специфичный для данной локали JS прямо в разметку.
- Предоставляем переведенные сообщения IntlProvider (все те сообщения теперь доступны в качестве дочерних компонентов).
Что же касается стороны сервера, во-первых, нам нужно установить библиотеку для выполнения AJAX-запросов. Я предпочитаю использовать изоморфное обновление, так как, скорее всего, нам предстоит запрашивать данные из сторонних API, и изоморфное обновление очень хорошо с этим справляется.
- Вот src/client.jsx:
Также мы должны затронуть src/server.jsx, чтобы Express предоставлял json с переводом на сторону клиента. Заметьте, что на продакшине вы, скорее всего, будете использовать что-то вроде nginx.
После инициализации JavaScript, client.jsx возьмет локаль из куки и запросит JSON с переводом. Во всем остальном наше приложение будет работать, как и раньше.
Пришло время проверить, как все будет работать в браузере. Откройте вкладку «Network» в панели разработчика и удостоверьтесь, что JSON с переводом был успешно получен нашим клиентом.
- Подведя итог, давайте добавим небольшой виджет для изменения локали в src/components/LocaleButton.jsx:
- И так же в src/components/App.jsx:
- Заметьте, как только пользователь меняет свою локаль, мы перезагружаем страницу, чтобы убедиться, что новый JSON-файл с переводом был успешно извлечен.
Теперь же время протестировать! Окей, мы изучили, как определять локаль пользователя и как отображать переведенные сообщения. Перед тем, как двигаться в направлении заключительной части, давайте обсудим пару также немаловажных нюансов.
Плюрализация и шаблоны
В английском большинство слов могут принимать одну или две возможные формы: «одно яблоко», «много яблок». В других языках все намного сложнее. К примеру, русский имеет 4 различные формы. Надеемся, React сумеет справиться и с этим. Он также поддерживает шаблоны, так что вы можете предоставить переменные, которые могут быть подставлены в шаблон во время рендеринга. Вот как это работает.
В src/components/App.jsx у нас есть:
Тут мы определяем шаблон с переменной count. Мы напечатаем или «одно яблоко», если значение переменной равно 1, 21 и так далее, или «два яблока» в противном случае. Нам нужно передать все переменные в formatMessage.
- Давайте перестроим наш файл переводов и добавим русский перевод для теста.
- Вот наш public/assets/ru.json файл:
- Теперь все случаи предусмотрены!
- Поддержка местных специфических сервисов, таких как даты, валюты и числа
Ваша информация будет представляться по-разному в зависимости от локали. К примеру, русская локаль нам покажет $500.00 и 12/10/2016.
- Intl предоставляет React-компоненты для такого типа данных, которые автоматически обновляются каждые 10 секунд, если вы за это время не перезаписывали значения.
- это в src/components/App.jsx:
Обновите браузер и проверьте страницу. Вам необходимо будет подождать 10 секунд, чтобы увидеть, как обновится FormattedRelative.
Круто, не правда ли? Что же, теперь мы можем столкнуться еще с одной проблемой – универсального рендеринга.
В среднем между формированием разметки и инициализацией JS проходит порядка 2-х секунд. Это значит, что все DateTime`сы, сгенерированные на странице, могут иметь разные значения на стороне клиента и сервера. Что, как следствие, разрушает универсальный рендеринг.
- Вот src/server.jsx:
Перезапустите nodemon и проблема почти исчезнет! Она может, правда, остаться в случае, если вы используете Date.now() вместо показаний, записанных в базе. Чтобы сделать пример более «жизненным», заменим в app.jsx Date.now() на последний таймштамп, что-то вроде 1480187019228.
(Впрочем, вы можете столкнутся с другой проблемой – когда сервер не в состоянии отрендерить DateTime в нормальном формате. Это потому, что 4 версия Node.js по умолчанию не поддерживала Intl.)
Звучит слишком хорошо, чтобы быть правдой, не так ли? Мы как истинные фронт-енд разработчики всегда были на стороже того, что касается браузеров и платформ. React Intl использует нативный браузерный API для обработки форматов DateTime и Number.
И, несмотря на тот факт, что подобная функциональность была представлена в 2012 году, до сих пор есть современные браузеры, которые ее все еще не поддерживают. Даже Safari поддерживает ее только частично.
Вот таблица с детальной информацией:
Это значит, что если вы желаете покрыть большинство браузеров, которые не поддерживают Intl API на нативном уровне, polyfill для вас просто незаменим. Хвала Всевышнему, существует Intl.js.
С одной стороны, кажется, вот оно – идеальное решение, но, как показывает мой опыт, всегда существуют свои недостатки. Во-первых, вам нужно добавить Js-bundle, что несколько ресурсоемко. Также для уменьшения размера вам нужно будет передавать ваш Intl.
Js только для браузеров, у которых нет своей нативной поддержки. Конечно, все эти техники уже давным-давно известны, и существует великое множество статей, посвященных им. Но все равно, Intl.
js – не абсолютное решение, так как все же представления DateTime и Number на стороне сервера и на стороне клиента могут несколько отличаться, что, разумеется, влечет за собой ошибки при серверном рендеринге.
Наконец, я пришел к своему собственному решению, которое хоть и имеет свои недостатки, но мои запросы в большинстве случаев оно устраивает. Я реализовал очень поверхностный polyfill, что имеет лишь небольшую часть из требуемой функциональности.
В то время, как в большинстве случаев подобное решение непригодное, оно лишь добавляет лишние 2 КБ к размеру JS-файла, так что даже нет необходимости реализовывать динамическую загрузку кода для устаревших браузеров, что значительно упрощает решение в целом.
И в заключение
Возможно, сейчас вас не покидает чувство, как будто здесь написано слишком сложное решение. Также, возможно, сейчас вы подумываете о том, чтобы реализовать все самим. Я бы не советовал этого делать. В конце концов, вы все равно придете к выводам, представленным в данной статье. Или, что хуже, зайдете в тупик одного решения и не сможете увидеть остальные.
Вы можете подумать, что можно легко решить проблему с Intl API, используя вместо него Moment.js (я специально не рассматривал другие библиотеки, так как они либо неподдерживаемые, либо неиспользуемые). К счастью, я уже опробовал это, так что я могу сохранить вам много времени. Moment.
js монолитен и очень тяжел, так что если для кого-то он и подойдет, то остальная масса пользователей будет неудовлетворенной результатом. Разработка собственного polyfill не звучит интригующе, так как вам наверняка придется выделить определенное время для борьбы с возникающими при этом багами.
Подведя итог, могу лишь сказать, что не существует идеального решения касательно проблемы на данный момент: просто выберите то, что вам подойдет лучше всего.
Надеюсь, эта статья дала вам все, что нужно знать для создания интернационализируемого React front-end приложения. Теперь вы выучили, как определять локаль пользователя, сохранять ее в куки, писать виджет для изменения локали и многое другое! Также вы ознакомились с некоторыми возможными проблема и ловушками, в которые вы можете попасть в процессе написания приложения.
- Удачи в разработке!
- Автор перевода: Евгений Лукашук
- Источник
Внедрить многоязычную поддержку в приложении TypeScript / React с помощью lingui.js
В последнее время одна из задач нашей инженерной группы была сосредоточена на извлечении функций из нашего основного монолита Rails в различные более мелкие микросервисы. Одна из этих задач заключалась в том, чтобы позаботиться о пользовательском интерфейсе, который используется для создания вакансии. Этот новый микросервис одностраничного приложения был разработан с использованием React и TypeScript. Поскольку нам нужно было, чтобы он поддерживал несколько языков (по крайней мере, на данный момент японский и английский), я хотел бы поделиться здесь процессом перевода этого пользовательского интерфейса с помощью библиотеки lingui-js (2.8.3). .
Выбор правильного фреймворка
Прежде чем вдаваться в подробности, я хотел бы остановиться на некоторых мотивах выбора lingui-js вместо react-intl, i18next-react, polyglot и т. Д.
Проще говоря, react-intl выглядел слишком большим для наших нужд. i18next-react подходил, но после его прототипирования мы обнаружили, что это было довольно сложно. использовать многочисленные плагины i18next-response ' с React / TypeScript.
Например, i18next имеет много подключаемых модулей, и кулон реагирования является одним из них, но не все подключаемые модули кажутся перекрестно совместимыми; особенно те, которые могут извлекать / проверять переводы. С другой стороны, polyglot не обеспечивает достаточной функциональности (боковая загрузка, извлечение, поддерживает только один формат для файлов перевода).
В конце концов, мы остались очень довольны lingui-js из-за того, что он работал из коробки и имел некоторую хорошую документацию.
Установка
Поскольку мы используем приложение React Create, доступное руководство по установке было простым и не требующим пояснений.
yarn add -D @lingui/cli @lingui/macro @babel/core babel-core@bridge
yarn add @lingui/react
Добавьте файл .linguirc в корневой каталог с помощью:
{
«localeDir»: «src/assets/locales/»,
«srcPathDirs»: [«src/»],
«format»: «po»,
«sourceLocale»: «ja»
}
Добавьте скрипты в package.json (обратите внимание, что мы решили добавить к нашим командам префикс i18n:):
{
«scripts»: {
…
«i18n:add-locale»: «lingui add-locale»,
«i18n:extract»: «lingui extract»,
«i18n:compile»: «lingui compile»
…
}
}
Добавьте наши исходные локали:
yarn i18n:add-locale ja en
Инициализация
Прежде чем вдаваться в подробности, я хотел бы упомянуть некоторые требования нашего приложения, которые заставят нас сделать определенный выбор в дальнейшем:
- нет необходимости менять язык на лету
- странице необходимо дождаться загрузки данных сеанса, прежде чем что-либо отобразить
Из-за наших особых потребностей и архитектуры мы решили вставить компонент сразу под тот, который загружает данные сеанса (затем он предоставляет locale через наш useLocale() хук) и чуть выше того, который отображает фактическое содержимое.
В нашем приложении уже было следующее поведение: показывать загрузчик до тех пор, пока не появится что-то для отображения. Таким образом, нам нужно было загрузить правильные файлы перевода и отобразить пустой div, пока это не было сделано, чтобы загрузчик отобразил.
import React from «react»;
import { I18nProvider, I18n } from «@lingui/react»;
import { useLocale } from «../hooks/useLocale»;
export const I18nContextProvider: React.FC = props => { const [catalogs, setCatalogs] = React.useState({}); const locale = useLocale();
React.useEffect(() => { const loadCatalog = async (locale: string) => { const catalog = await import( /* webpackMode: «lazy», webpackChunkName: «i18n-[index]» */ `@lingui/loader!../../assets/locales/${locale}/messages.po`
);
setCatalogs({
…catalogs,
[locale]: catalog,
});
};
loadCatalog(locale);
}, [locale]);
if (!catalogs[locale]) return ;
return (
{props.children}
);
};
Затем мы просто вставили нашего провайдера между нашим SessionContextProvider и нашим AppRouter:
…
…
Добыча
После того, как базовая инфраструктура была настроена, нам нужно было извлечь все существующие тексты (в нашем случае на японском языке), и именно здесь может пригодиться команда yarn i18n:extract. Чтобы извлечь большую часть наших текстов, мы следовали этому руководству:
- Импортируйте определение Trans: import { Trans } from «@lingui/macro»;
- Окружите текст, который мы хотим извлечь, элементами
- Дайте конкретный идентификатор этому элементу
- Запустите команду yarn i18n:extract
- Удалите текстовую часть внутри узла
- (Необязательно) Если в оставшемся ничего не осталось, замените его на
- (Необязательно) Если оставшиеся
Однако иногда ключ не может быть определен без предварительного расчета, поэтому вместо «example» нам нужно будет вызвать функцию.
Это отлично работает во время выполнения, но инструмент извлечения не может распознать ключ / значение для извлечения больше, поэтому для смягчения этой проблемы мы можем использовать помощник i18nMark, который позволит экстрактору найти подходящие идентификаторы в коде:
Крючки
Поскольку использование компонента «‹I18n› render prop» было немного многословным, мы решили создать небольшую ловушку, которую мы можем повторно использовать в нашем коде:
import React, { useContext } from «react»;
import { I18n } from «@lingui/core»;
const I18nFuncContext = React.createContext(null);
export const I18nFuncContextProvider = I18nFuncContext.Provider;
export function useI18n(): I18n {
const i18n = useContext(I18nFuncContext);
if (!i18n) {
throw new Error(«No context found»);
}
return i18n;
}Затем мы можем просто инициализировать его в нашем настраиваемом провайдере:
return (
{({ i18n }) => {props.children}}
);
И используйте это в нашем коде:
import { useI18n } from «./useI18n»;
…
const i18n = useI18n();
……
Обратите внимание, что хуки должны быть изначально доступны в версии 3 lingui-js.
Смена названия
Изменить заголовок страницы может быть немного сложно. Поэтому после небольшого исследования мы создали специальный компонент для этой цели и вставили его в качестве первого дочернего элемента нашего :
…
…
Компонент заголовка использует простой эффект для изменения заголовка документа при загрузке компонента:
import React, { useEffect } from «react»;
import { useI18n } from «../hooks/useI18n»;
export const I18nTitle: React.FC = () => {
const i18n = useI18n();
useEffect(() => {
document.title = i18n._(«common/documentTitle»);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};Заключительные слова
Мы вполне довольны результатом использования lingui-js для перевода нашего приложения. Некоторые мелкие детали можно улучшить, например:
- формат po время от времени вызывает у нас некоторые конфликты, когда мы параллельно работаем над одним и тем же компонентом с git
- кастомный крючок
- отсутствие опции автоматического стирания исходного текста при извлечении
В целом, я могу сказать, что мы, вероятно, потратили больше времени на оценку и выбор правильной библиотеки, чем на использование этой для перевода нашего собственного приложения. ???? ????
Вот беглый взгляд на результат нашего yarn i18n:extract и окончательный результат нашего пользовательского интерфейса:
┌─────────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├─────────────┼─────────────┼─────────┤
│ en │ 187 │ 1 │
│ ja (source) │ 187 │ — │
└─────────────┴─────────────┴─────────┘(Отсутствует URL-адрес, который на данный момент существует только на японском языке)
Спасибо за чтение! ????
Также благодарим Юки Иванага (Creasty) за помощь в наведении порядка с помощью нашего пользовательского крючка.
Multilingual React App with i18next Typescript
Before we get started, it’s high recommend to clone the Github repo and we can follow along with the article contents.
The first step after we downloaded the repo is to checkout the the starter folder and run npm install and npm run dev to start our React app locally.
Understand the concept
You should see the following UI after you start the server
You may wonder the wordings here look a little bit weird as it is fully uppercase and the words are joined with underscore. You may checkout the folders under /src/language/en.ts and /src/language/cn.ts and you may aware the words showing in our homepage are actually the keys of these translations.
What are we going to do later is we will utilize some methods from the react-i18next and translate these keys into it’s own translation value.
Initialize translation module
We can now navigate to the /src/language.ts file start configuring our translation module.
import i18n from «i18next»;
import { initReactI18next } from «react-i18next»;
import { cn, en } from «./languages»; i18n.use(initReactI18next).init({ resources: { cn: { translation: cn }, en: { translation: en }, }, lng: «en», fallbackLng: «en»,
}); export default i18n;
Explaination
- We first pass the i18n instance to react-i18next.
- Initialize the instance with necessary options.
- We could pass more keys into the resources field if we have more language to be translated.
- cn: { translation: cn }. This line of code means we can switch our application language by passing in cn or en into the changeLanguage method. We also need to specified the objects when the language is selected. For example, we are passing in the languages file we have earlier into it’s respective resources key.
- The lng is refering to the default language use when the application is loaded
- fallbackLng is stands for fall back language which will be used when the given language is not available.
Implement language switcher
We can now navigate to the /src/App.tsx file to implement the language switcher logic.
import i18n from «./language»;
import { Routes, Route } from «react-router-dom»;
import «./App.css»;
import About from «./About»;
import Home from «./Home»; function App() { function changeLanguage(language: «en» | «cn») { i18n.changeLanguage(language); } return ( changeLanguage(«en»)}>English changeLanguage(«cn»)}>Chinese );
} export default App;
Outcome
Explaination
We can utilize the changeLanguage method to change the language and it’s now working great where it will stick back to the selected language even if we navigate to other page.
However, it’s not working when we are having hard refresh or we enter the url by ourselve. One of the reason is because, when we are doing hard refreshing and changing the web url directly, no state will be preserved and React will actually rerender our UI.
This cause as issue if we have selected cn at the beginning but our default language is en
Translate TypeScript projects with i18next
Last month I had to add internationalization support for a Node.js project which
had already been running in production. The project is not a typical web app but
a worker process responsible for generating push notifications for a mobile
application.
I was looking for a simple internationalization library that could
fit in without much effort and wasn't dependant on Express or other frameworks.
After a quick research I found about i18next which promised to deliver all the
features I was looking for.
I was pleasantly surprised how fast I was able to
integrate it and how well it worked for my use case.
What is i18next
i18next is a framework agnostic tool for translating
JavaScript projects. It has simple API, it's easy to setup and provides thorough
documentation. Most JavaScript frameworks, both front-end and back-end, have
official bindings for i18next: Angular, React, Vue, Polymer, Express, Koa,
Next.
js and more! This is perfect for full-stack developers who can reuse the
same API across projects using different frameworks. During my career as
software engineer I have used angular-gettext, ngx-translate, transloco,
react-intl, i18n-node, Polymer's app-localize-behavior and in-house
solutions — all coming with their own APIs, workflow and documentation.
Imagine
how much time and effort I could have saved juts by using the same tool at all
places.
What is i18next-scanner
A feature which I look after the most when deciding what internationalization
library to use is the ability to automatically generate the translation files
given to translators. This process consists of the following steps:
- Scan the source code for translation strings.
- Extract the found strings into resource files — JSON, PO or XLIF format for
each language. - Merge the newly found strings with the existing strings.
- Delete all unused translation strings. (optional)
- Sort the keys alphabetically. (optional)
Having to do this manually for large code bases is painful, time consuming and
error prone. That's why most internationalization frameworks provide their own
tools to help developers in this task.
i18next-scanner is the scan and extract tool for i18next. There are three
ways to use it:
- Command line interface
- Gulp or Grunt plugin
- Node.js API (for custom build scripts)
Integrating i18next in JavaScript projects
Let's see i18next in action by creating a simple JavaScript project.
mkdir i18next-sample && cd i18next-sample && npm init -y && touch index.mjs
I'm using Node.js 14.5.0 and the new ECMAScript modules feature instead of
CommonJS modules (import/export instead of require()/module.exports).
That's why I'm naming my file with the .mjs extension.
Now you must install i18next as a dependency for your project:
npm i i18next
and i18next-scanner as development dependency:
npm i -D i18next-scanner
Open the index.mjs file add the following contents:
import i18n from «i18next»
import { readFileSync } from «fs»
// load the translation files
const en = JSON.parse(readFileSync(«./i18n/translations/en.json», «utf-8»))
const fr = JSON.parse(readFileSync(«./i18n/translations/fr.json», «utf-8»))
i18n.init({
lng: «en»,
resources: {
en: {
translation: en,
},
fr: {
translation: fr,
},
},
})
console.log(i18n.t(«greetPerson», { name: «John» }))
Now create a configuration file for the i18next-scanner tool called
i18next-scanner.config.js:
i18next-scanner.config.js
module.exports = {
/**
* Input folder (source code)
**/
input: [«**/*.{mjs,js}»],
/**
* Output folder (translations)
**/
output: «./»,
options: {
removeUnusedKeys: true,
/**
* Whether to sort translation keys in alphabetical order
**/
sort: true,
func: {
/**
* List of function names which mark translation strings
**/
list: [«i18next.t», «i18n.t», «t», «__»],
extensions: [«.mjs», «.js»],
},
/**
* List of supported languages
**/
lngs: [«en», «fr»],
defaultLng: «en»,
/**
* Default value returned for missing translations
**/
defaultValue: «»,
resource: {
/**
* Where translation files should be loaded from
**/
loadPath: «i18n/translations/{{lng}}.json»,
/**
* Where translation files should be saved to
**/
savePath: «i18n/translations/{{lng}}.json»,
jsonIndent: 2,
lineEnding: »
«,
},
keySeparator: «.»,
pluralSeparator: «_»,
contextSeparator: «_»,
contextDefaultValues: [],
/**
* Values surrounded by {{ }} are treated as params
* e.g. «Hello {{ name }}» — «name» must be provided at runtime
**/
interpolation: {
prefix: «{{«,
suffix: «}}»,
},
},
}
I've taken the default options from the documentation and edited them to my
likings. You can find the original defaults
here.
Now let's run the scanner for the first time:
npx i18next-scanner
The scanning algorithm passed through our source code and found all usages of
the i18n.t function and marked its first arguments as translation keys. It
then generated the files i18n/translations/en.json and
i18n/translations/fr.json which contain a single translation key with empty
value:
{
«greetPerson»: «»
}
Let's add our first message which accepts a parameter called name.
{
«greetPerson»: «Hello, {{name}}!»
}
and run our application:
node index.mjs
# Output
Hello, John!
Now let's add another invocation of the i18n.t function but with a new
translation key:
console.log(i18n.t(«farewell»))
Running i18next-scanner again will result in the new string being added to all
translation files. If we remove the last line in our source code and run
i18next-scanner once again, the farewell key will be removed since it's no
longer referenced in our source code.
Using i18next-scanner for TypeScript projects
Fast-forward to week ago, I had to bootstrap a new React Native application.
Nowadays I'm always writing new projects in TypeScript as it catches most coding
mistakes at build time and makes refactoring a breeze even in a large codebase.
It's great for writing React components since it prevents incorrect usage of
props and adds autocomplete capabilities in IDEs. Typechecking with PropTypes
feels so clunky and inferior in comparison.
Nowadays creating a new React Native
project with TypeScript support is as easy as running a single command:
npx react-native init MyTypeScriptApp —template react-native-template-typescript
The next step for me was to add internationalization support. In my experience,
the earlier in the project you set up internationalization, the better.
I
decided to use i18next to take advantage of my previous knowledge working with
this library. Its i18next-react package provides React components and hooks
for translating strings and changing the language at runtime.
This is how you
can install the library in a React project:
npm i i18next i18next-react
The library is intuitive to use in React and its hooks API is elegant. In
comparison with react-intl, which I have used in other React apps, it does not
require to pass the i18n object as a prop to each component which is going to
use it. Instead calling the useTranslation hook inside any component will give
you a reference to the i18next object instance. Neat!
So far so good, but when I ran the i18n-scanner command, to my dismay the
translation strings were not extracted. I made sure that I ran the command with
the correct glob pattern to match TypeScript files but that wasn't the cause.
After having a look at the source code of i18n-scanner on GitHub I realized
that the parser supports only JavaScript. The scanner does not understand how to
work with .ts or .tsx files and it ignores them! The solution to this
problem ended up being a simple one.
You have to compile the project and run the
scanner on the compiled JavaScript files. Let's create a command in our
package.json which will take care of this hurdle.
{
…,
«scripts»: {
«i18n:scan»: «mkdir -p ./tmp && rm -rf ./tmp && npx tsc —jsx preserve —target ES6 —module es6 —noEmit false —outDir ./tmp && npx i18next-scanner»
},
…,
}
The steps involved in this command are:
-
Before running the compiler and the scanner, delete the previous compilation
artifacts.- mkdir -p ./tmp ensures rm -rf ./tmp does not error out with «folder
does not exist»
- mkdir -p ./tmp ensures rm -rf ./tmp does not error out with «folder
-
Compile the source code into JavaScript and store the result in the tmp
folder inside the project directory.- —jsx preserve ensures JSX is not compiled down to raw
React.createElement() calls and instead leaves this task to Babel
plugins. In our case the i18next-scanner needs to parse .jsx files for
components which contain translation strings. - —target ES6 —module es6 makes sure import and export keywords are
preserved and all components are in their own files (mirroring the
TypeScript source files structure). - —noEmit false tells the compiler to actually write the compiled files on
disk, otherwise the tmp folder won't be created. By default, React Native
projects have noEmit: true in their tsconfig.json file! - —outDir ./tmp writes the compilation artifacts in the tmp folder.
- —jsx preserve ensures JSX is not compiled down to raw
-
Run the i18next-scanner on the compiled files.
This is what my i18next-scanner configuration file contains:
i18next-scanner.config.js
module.exports = {
input: [«./tmp/**/*.{js,jsx}»],
output: «./»,
options: {
removeUnusedKeys: true,
sort: true,
func: {
list: [«i18next.t», «i18n.t», «t», «__»],
extensions: [«.js», «.jsx»],
},
trans: {
component: «Trans»,
i18nKey: «i18nKey»,
defaultsKey: «defaults»,
extensions: [«.js», «.jsx»],
fallbackKey: false,
},
lngs: [«en», «bg», «fr», «de»],
defaultLng: «en»,
defaultValue: «»,
resource: {
loadPath: «./src/i18n/translations/{{lng}}.json»,
savePath: «./src/i18n/translations/{{lng}}.json»,
jsonIndent: 2,
lineEnding: »
«,
},
keySeparator: «.»,
pluralSeparator: «_»,
contextSeparator: «_»,
contextDefaultValues: [],
interpolation: {
prefix: «{{«,
suffix: «}}»,
},
},
}
The translations will be extracted to src/i18n/translations/{{language}}.json:
├── src
│ ├── i18n
│ │ └── translations
│ │ ├── bg.json
│ │ ├── de.json
│ │ ├── en.json
│ │ └── fr.json
Now we can run:
npm run i18n:scan
and this will generate the translation files for all our supported languages.
Don't forget to add the tmp folder in your .gitignore file since build
artifacts should not be tracked in version control systems.
And we're done! Have fun translating your TypeScript project with i18next!
How to Use React Context to Build a Multilingual Website Pt.1
React Context is one of the most interesting features in React 16.3.0. There had been a lot of buzz about this hot feature. In this easy tutorial, we will take a look at how to use React, React Router and React Context to build a simple multilingual website. I hope you will enjoy this tutorial. And now, without further ado, let’s begin.
How to Use React Context to Build a Multilingual Website Part 2.
What are we building
Our primary goal fr this tutorial is simple. We will use React, React Router and React Context to build a simple website that will allow visitors browse its content in four different languages.
These languages will be Czech, English, French and German. All texts for these languages will be stored in JSON format in external files.
The best thing about our website is that there will be no need to reload the page when visitor changes the language.
For example, when visitor decide to switch from English to French, the change will be instant and visitor will immediately see content in French. The same is true for changing the page itself.
Everything will be seamless. When visitor decide to change the page, content will re-render without reloading the page itself.
This will be possible thanks to the React Router handling the routing and React state handling the language switching.
So, this is the goal for this tutorial, creating a simple “one-page” website with four language variants. That’s for the briefing. Now, let’s get our hands dirt … with zeros and ones.
Project setup
The first thing we need to do is creating the initial setup for this project. This means especially two things. First, we need to decide what dependencies do we want to use in this project and install them.
This quick tutorial on React Context and build a simple multilingual website will be very simple. However, there are still some 3rd party packages we will need. Second, we need to put together necessary npm scripts.
Let’s tackle both of these tasks, one by one. First, the dependencies. We will need to add and install following packages: react, react-dom, react-router-dom and react-scripts.
That’s not so much, right? Second, the npm scripts. As some of you may have already guessed, we will make this part easier by using scripts and configs provided by Create React App project.
In other words, we will nod need to setup Webpack or anything else.
For the scripts, we will create four scripts: start, build, test and eject. These are the default scripts for developing React app with Create React App. Well, today and in this tutorial we will need only the first one, the start script. Now, we can put all this information together and create the final version of package.json.