
Содержание статьи
Эта публикация будет использована react-intl
чтобы помочь вам уйти из create-react-app
до настройки фреймворка для завершенной переведенной веб-приложения!
При написании этой публикации я зафиксировал код, так что вы сможете просмотреть мою историю комитов, чтобы легко увидеть, как мой код развивался от начала до конца.

Что такое интернационализация?
Учитывая, что вы решили нажать ссылку на эту публикацию, скорее всего, у вас есть определенное представление о том, что такое интернационализация (i18n). Взято прямо с веб-сайта W3:
«Интернационализация – это проектирование и разработка продукта, программы или содержимого документа, который разрешает легкая локализация для целевых аудиторий, отличающихся культурой, регионом или языком».
Как разработчик, вы хотите, чтобы ваше содержимое легко читалось и использовалось разными людьми по всему миру. Я думаю, что все согласны с этим. Но я знаю, что вы думаете:
«Разработать веб-приложение для людей моей собственной культуры/региона/языка уже достаточно сложно! У меня нет ни времени, ни сил для i18n!
Я вижу, вы уже знаете жаргон. Надеемся, эта публикация поможет вам понять, что настройка i18n для вашего проекта не так сложна или трудоемка, как кажется.
Что делает и что не делает react-intl
Если вы новичок в i18n, у вас могут возникнуть некоторые мысли о том, что вы думаете о такой библиотеке, как react-intl
должны и не должны уметь делать.
Это делает:
- Помочь вам собрать все ваше разрозненное содержимое, чтобы его можно было легко перевести позже
- Помочь вам разобраться с переводом текста в дополнение к датам, числам и т.д.
- Обеспечьте простой способ импорта переводов в вашу программу
Это не:
- Переведите ваше содержимое для вас
- Расскажу, как узнать, какую локаль хочет пользователь
- Исправьте эту несвязанную ошибку, с которой вы имели дело последние пару часов (неприятно, правда?)
Ладно, давайте приступим к делу!
Настройка примера проекта
$ npx create-react-app i18n-example
Я собираюсь добавить маршрутизатор React, чтобы показать, как это сделать react-intl
работает с несколькими страницами.
$ cd i18n-example && npm install react-router-dom
Моим примером программы будет три компонента React: одна главная страница, одна подстраница и один компонент, импортируемый на подстраницу. Смотрите структуру файла и страницы ниже:
/src
/components
Weather.js
/pages
Home.js
Day.js

Состояние проекта к этому моменту можно посмотреть здесь.
Настраивая react-intl
Теперь начинается самое интересное. Будем устанавливать react-intl
и к работе!
$ npm install react-intl
Главная цель позади react-intl
заключается в обеспечении поддержки i18n, минимизируя влияние на обычный поток кодирования. Конечно, у вас есть содержимое во многих местах в вашей веб-приложении. У вас есть текст, числа и даты в абзацах, таблицах и заголовках.
Что бы вы сделали, если бы вам пришлось создать библиотеку i18n? Ну, у вас есть эти фрагменты содержимого по всему веб-приложению. И вы хотите, чтобы все это легко переводилось. Если бы вы собирались передать содержимое переводчику, вы бы не давали ему свой код и не говорили «желаю успеха, к работе».
Вы хотели бы найти способ поместить все свое содержимое в один файл, а затем предоставить им этот один файл. Они переведут его на другой язык, скажем, с английского на испанский, и предоставят вам один файл со всем испанским содержимым.
ОК здорово. Итак, вы это сделали, но теперь вам нужно взять испанское содержимое в этом одном файле и распространить его обратно в исходное место. Как бы вы это сделали программно? Возможно, вы назначили идентификаторы каждому фрагменту содержимого, чтобы не потерять исходное расположение каждого фрагмента содержимого.
И почти все!
Первый шаг – завернуть вашу заявку в <IntlProvid
er> компонент:
<IntlProvider>
<App />
</IntlProvider>
Теперь вам нужно определить содержимое для react-intl
которые в конце концов будут переведены. На домашней странице моей программы у меня есть такой абзац:
<p>It is a beautiful day outside.</p>
Мне нужно рассказать react-intl
что я хочу перевести это содержимое и предоставить ему идентификатор, чтобы оно могло отслеживать это содержимое и его оригинальное расположение:
<FormattedMessage
id="Home.dayMessage"
defaultMessage="It's a beautiful day outside."
/>
По умолчанию текст будет выведен в a <sp
an> , поэтому нам нужно будет завернуть это в оригинал <p>
если мы хотим, чтобы он остался абзацем.
<p>
<FormattedMessage
id="Home.dayMessage"
defaultMessage="It's a beautiful day outside."
/>
</p>
Теперь я сделаю это для всего содержимого моего веб-приложения.
Состояние проекта сегодня можно посмотреть здесь.
Добавление babel-plugin-react-intl
Теперь, когда мы все настроили, вам может быть интересно, как можно легко объединить все это содержимое в один файл. Однако для целей настройки может быть полезно иметь отдельные файлы JSON для каждого компонента React. Догадайтесь, для этого есть плагин babel!
$ npm install babel-plugin-react-intl
Этот плагин создаст копию вашего src
каталог, но вместо файлов ваших компонентов React будет иметь файлы json с содержимым сообщения и идентификатором. По одному для каждого файла компонента в вашем src
каталог. Он сделает это, когда вы бежите npm run build
.
Теперь нам нужно выйти из create-react-app, чтобы мы могли добавить наш новый плагин в нашу конфигурацию babel. Убедитесь, что вы зафиксировали любые изменения, а затем выполните:
$ npm run eject
Теперь нам нужно будет добавить a .babelrc
файл в корне нашего проекта с таким содержимым:
{
"presets":["react-app"],
"plugins": [
["react-intl", {
"messagesDir": "./public/messages/"
}]
]
}
Теперь, когда babel может использовать наш удивительный новый плагин, который мы только что добавили, мы можем переходить к следующему шагу: создание файлов JSON.
$ npm run build
После запуска вы должны заметить, что у вас есть a public/messages/src
каталог, который выглядит как клон вашего оригинала src
каталог, за исключением того, что все файлы компонентов действительно являются файлами JSON.
/messages
/src
/components
Weather.json
/pages
Home.json
Day.json
Теперь давайте посмотрим содержимое одного из них, Home.json:
[
{
"id": "Home.header",
"defaultMessage": "Hello, world!"
},
{
"id": "Home.dayMessage",
"defaultMessage": "It's a beautiful day outside."
},
{
"id": "Home.dayLink",
"defaultMessage": "Click here to find out why!"
}
]
Состояние проекта сегодня можно посмотреть здесь.
Объединение файлов JSON
Он сделал то, что мы думали. Организация нашего содержимого в такой структуре может быть полезна, но в конце концов мы хотим, чтобы оно содержалось в одном файле, и нам нужно, чтобы оно содержало любые переводы, которые мы сделаем.
Теперь нам нужно создать сценарий, который сделает это для нас. К счастью, люди в react-intl
дал нам хорошую отправную точку с этим сценарием.
import * as fs from "fs";
import { sync as globSync } from "glob";
import { sync as mkdirpSync } from "mkdirp";
import last from "lodash/last";
const MESSAGES_PATTERN = "./public/messages/**/*.json";
const LANG_DIR = "./public/locales/";
const LANG_PATTERN = "./public/locales/*.json";
// Try to delete current json files from public/locales
try {
fs.unlinkSync("./public/locales/data.json");
} catch (error) {
console.log(error);
}
// Merge translated json files (es.json, fr.json, etc) into one object
// so that they can be merged with the eggregated "en" object below
const mergedTranslations = globSync(LANG_PATTERN)
.map(filename => {
const locale = last(filename.split("/")).split(".json")[0];
return { [locale]: JSON.parse(fs.readFileSync(filename, "utf8")) };
})
.reduce((acc, localeObj) => {
return { ...acc, ...localeObj };
}, {});
// Aggregates the default messages that were extracted from the example app's
// React components via the React Intl Babel plugin. An error will be thrown if
// there are messages in different components that use the same `id`. The result
// is a flat collection of `id: message` pairs for the app's default locale.
const defaultMessages = globSync(MESSAGES_PATTERN)
.map(filename => fs.readFileSync(filename, "utf8"))
.map(file => JSON.parse(file))
.reduce((collection, descriptors) => {
descriptors.forEach(({ id, defaultMessage }) => {
if (collection.hasOwnProperty(id)) {
throw new Error(`Duplicate message id: ${id}`);
}
collection[id] = defaultMessage;
});
return collection;
}, {});
// Create a new directory that we want to write the aggregate messages to
mkdirpSync(LANG_DIR);
// Merge aggregated default messages with the translated json files and
// write the messages to this directory
fs.writeFileSync(
`${LANG_DIR}data.json`,
JSON.stringify({ en: defaultMessages, ...mergedTranslations }, null, 2)
);
Нам нужно будет немного изменить его, потому что как оно есть, этот сценарий будет генерировать фальшивый перевод. Мы не хотим этого, потому что это не практично.
Мы лучше этого! Мы хотим, чтобы он читал настоящий перевод!
Ниже представлен сценарий, который мы будем использовать для этого:
Нам нужно будет сохранить этот файл в нашем scripts
каталог, а затем отредактируйте package.json
чтобы он фактически запускал сценарий.
Прежде чем мы сделаем это, нам нужно будет сделать несколько вещей, чтобы наш код ESNext можно было понять. Сначала нам нужно будет добавить babel-cli
чтобы убедиться, что сценарий будет транслирован.
$ npm install --save-dev babel-cli
Дальше нам нужно добавить env
установлено на наш .babelrc
чтобы это выглядело так:
{
"presets":["react-app", "env"],
"plugins": [
["react-intl", {
"messagesDir": "./public/messages/"
}]
]
}
Наконец, нам нужно отредактировать наш package.json
чтобы он запускал наш сценарий:
{...
"scripts": {
"build:langs": "NODE_ENV='production' babel-node
scripts/mergeMessages.js",
"build": "npm run build:langs && node scripts/build.js",
...
},
...
}
Обратите внимание, что раньше мы запускали сценарий mergeMessages npm run build
. Это потому, что мы хотим создать наш финал data.json
файл в /public
до того, как наш сценарий сборки скопирует его /build
.
Хорошо, теперь, когда мы бежим npm run build
мы должны увидеть build/locales/data.json
который объединяет все наши JSON файлы в один.
Состояние проекта сегодня можно посмотреть здесь.
Пора начинать перевод
Теперь, когда мы создали сценарий, объединяющий наши типовые сообщения и наши переводы в один файл, давайте сделаем несколько переводов! Для этого примера мы переведем на испанский. Наш сценарий, который мы только что создали, будет читать все *.json
файлы с /public/locales
поэтому нам нужно будет назвать наш новый файл перевода /public/locales/es.json
и добавьте содержимое ниже:
{
"Weather.message": "¡Porque es soleado!",
"Day.homeLink": "Regresar a inicio",
"Home.header": "¡Hola Mundo!",
"Home.dayMessage": "Es un hermoso día afuera.",
"Home.dayLink": "¡Haz clic aquí para averiguar por qué!"
}
Теперь, когда мы бежим npm run build
наш сценарий mergeMessages создаст a data.json
файл в /public/locales
а затем он будет скопирован в /build/locales
. Наш финал data.json
файл будет выглядеть так:
{
"en": {
"Weather.message": "Because it is sunny!",
"Day.homeLink": "Go back home",
"Home.header": "Hello, world!",
"Home.dayMessage": "It's a beautiful day outside.",
"Home.dayLink": "Click here to find out why!"
},
"es": {
"Weather.message": "¡Porque es soleado!",
"Day.homeLink": "Regresar a inicio",
"Home.header": "¡Hola Mundo!",
"Home.dayMessage": "Es un hermoso día afuera.",
"Home.dayLink": "¡Haz clic aquí para averiguar por qué!"
}
}
Мы почти на месте! Последним шагом является динамическая загрузка испаноязычной версии текста, если настройка браузера пользователя является испанской. Надо редактировать index.js
чтобы прочитать настройки языка браузера, а затем предоставить эту информацию вместе с правильными переводами <IntlProvider
/> и, в конце концов, наше приложение.
Наш финал index.js
выглядит так:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
import { BrowserRouter } from "react-router-dom";
import { IntlProvider, addLocaleData } from "react-intl";
import en from "react-intl/locale-data/en";
import es from "react-intl/locale-data/es";
import localeData from "./../build/locales/data.json";
addLocaleData([...en, ...es]);
// Define user's language. Different browsers have the user locale defined
// on different fields on the `navigator` object, so we make sure to account
// for these different by checking all of them
const language =
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
navigator.userLanguage;
// Split locales with a region code
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
// Try full locale, try locale without region code, fallback to 'en'
const messages =
localeData[languageWithoutRegionCode] ||
localeData[language] ||
localeData.en;
ReactDOM.render(
<IntlProvider locale={language} messages={messages}>
<BrowserRouter>
<App />
</BrowserRouter>
</IntlProvider>,
document.getElementById("root")
);
registerServiceWorker();
(Здесь много скопирован код по существу Прити Кассиредди)
Еще одна маленькая вещь, которую нам нужно сделать это отредактировать конфигурации веб-пакетов, чтобы разрешить импорт за пределами src
и node_modules
.
Теперь, если мы изменим настройки браузера на испанский, мы увидим наше содержимое, переведенное на испанский!

С окончательным состоянием проекта можно ознакомиться здесь.