Как настроить интернационализацию в React от начала до конца

1656632414 kak nastroit internaczionalizacziyu v react ot nachala do koncza

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

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

1*6lJJiXiCnX2peIeLG3oIZg
Фото Артема Бали на Unsplash

Что такое интернационализация?

Учитывая, что вы решили нажать ссылку на эту публикацию, скорее всего, у вас есть определенное представление о том, что такое интернационализация (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
1*T-74w-twF7GYNn7eeFkumg

Состояние проекта к этому моменту можно посмотреть здесь.

Настраивая react-intl

Теперь начинается самое интересное. Будем устанавливать react-intl и к работе!

$ npm install react-intl

Главная цель позади react-intl заключается в обеспечении поддержки i18n, минимизируя влияние на обычный поток кодирования. Конечно, у вас есть содержимое во многих местах в вашей веб-приложении. У вас есть текст, числа и даты в абзацах, таблицах и заголовках.

Что бы вы сделали, если бы вам пришлось создать библиотеку i18n? Ну, у вас есть эти фрагменты содержимого по всему веб-приложению. И вы хотите, чтобы все это легко переводилось. Если бы вы собирались передать содержимое переводчику, вы бы не давали ему свой код и не говорили «желаю успеха, к работе».

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

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

И почти все!

Первый шаг – завернуть вашу заявку в <IntlProvider> компонент:

<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 <span> , поэтому нам нужно будет завернуть это в оригинал <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 .

Теперь, если мы изменим настройки браузера на испанский, мы увидим наше содержимое, переведенное на испанский!

1*4DNdd7o70MWMetI9vgR0gw

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *