Format.js позволяет вам переводить элементы пользовательского интерфейса, сообщения и даты в вашем приложении React. В этом руководстве показано, как перевести ваше приложение React с помощью Format.JS. Чтобы следовать этой статье, вам потребуются базовые знания React, терминала и редактора кода.

Обеспечение того, чтобы ваше приложение React было доступно на нескольких языках, является одним из наиболее важных способов гарантировать, что люди во всем мире могут получить к нему доступ в современном глобально интегрированном обществе. Локализация помогает в этой ситуации. Format.js, набор инструментов с открытым исходным кодом, предлагает набор инструментов для локализации вашего приложения React.

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

Чтобы следовать этой статье, вам понадобятся базовые знания React, терминал и редактор кода (рекомендую VS Code). Давайте начнем!

Забежать вперед:

Начало работы с React

Сначала мы используем Create React App для создания приложения React:

npx create-react-app translate-app

Затем мы можем перейти к вновь созданному проекту после установки с помощью команды cd translate-app/. Давайте быстро настроим очень простое приложение для электронной коммерции, в котором у нас есть куча продуктов, отображаемых в карточках. Наше простое приложение также будет иметь компонент корзины, который показывает продукты, которые мы добавили в нашу корзину, и позволяет нам удалять эти продукты.

Создание компонентов ProductCard и Cart

Теперь создайте новый файл ./src/components/ProductItem.jsx и введите следующий код:

// ./src/components/ProductItem.jsx
const ProductItem = ({ product, addToCart }) => {
  return (
    <li className="product">
      <div className="product-image img-cont">
        <img src={product.thumbnail} alt="" />
      </div>
      <div className="product-details">
        <h3>{product.title}</h3>
        <p>{product.description}</p>
        <span className="product-price">${product.price}</span>
      </div>
      <div className="product-actions">
        <button
          disabled={product?.isInCart}
          onClick={() => addToCart(product)}
          className="cta"
        >
          Add to cart
        </button>
      </div>
    </li>
  );
};
export default ProductItem;

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

Теперь, чтобы создать компонент Cart, создайте новый файл ./src/components/Cart.jsx и введите следующий код:

// ./src/components/Cart.jsx
const Cart = ({ cart, removeItem }) => {
  return (
    <div className="dropdown">
      <div className="trigger group">
        <button className="cta">Cart</button>
        <span className="badge"> {cart?.length}</span>
      </div>
      <div className="content">
        <aside className="cart">
          <header className="cart-header">
            <h2>Your Cart</h2>
            <p>
              You have <span>{cart.length}</span> items in your cart
            </p>
          </header>
          <ul className="items">
            {cart.map((item) => {
              return (
                <li tabIndex={0} key={item.id} className="item">
                  <div className="item-image img-cont">
                    <img src={item.thumbnail} alt={item.name} />
                  </div>
                  <div className="item-details">
                    <h3>{item.title}</h3>
                    <p className="item-price">${item.price}</p>
                    <button onClick={() => removeItem(item)} className="cta">
                      Remove
                    </button>
                  </div>
                </li>
              );
            })}
          </ul>
        </aside>
      </div>
    </div>
  );
};
export default Cart;

В этом компоненте мы также принимаем два реквизита. Свойство cart содержит список товаров, которые были добавлены в корзину. Свойство removeItem будет использоваться для передачи удаленного элемента родительскому компоненту.

Создание функций для добавления в наше приложение React

Далее, в ./src/App.js, мы импортируем наши компоненты и создадим несколько функций для получения продуктов из dummyjson.com. Они будут использоваться для наших фиктивных товаров, для добавления товаров в корзину и для удаления товаров из корзины. Сначала введите следующий код в файл ./src/App.js:

// ./src/App.js
import "./App.css";
import { useEffect, useState } from "react";
import ProductItem from "./components/ProductItem";
import Cart from "./components/Cart";
// function to fetch products from dummyjson.com
const getProducts = async () => {
  try {
    const res = await fetch("https://dummyjson.com/products");
    const data = await res.json();
    return data;
  } catch (error) {
    console.log({
      error,
    });
    return [];
  }
};
function App() {
  // set up state for products and cart
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);
  // function to add product to cart
  const handleAddToCart = (product) => {
    console.log("product", product);
    setCart((cart) => {
      return [...cart, product];
    });
    setProducts((products) => {
      return products.map((p) => {
        if (p.id === product.id) {
          return {
            ...p,
            isInCart: true,
          };
        }
        return p;
      });
    });
  };

 // function to remove product from cart
const handleRemoveFromCart = (product) => {
    setCart((cart) => {
      return cart.filter((p) => p.id !== product.id);
    });
    setProducts((products) => {
      return products.map((p) => {
        if (p.id === product.id) {
          return {
            ...p,
            isInCart: false,
          };
        }
        return p;
      });
    });
  };
  // fetch products on component mount
  useEffect(() => {
    getProducts().then((data) => {
      setProducts(data.products);
    });
  }, []);
  return (
    <div className="app">
      <header className="app-header">
        <div className="wrapper">
          <div className="app-name">Simple store</div>
          <div>
            <Cart cart={cart} removeItem={handleRemoveFromCart} />
          </div>
        </div>
      </header>
      <main className="app-main">
        <div className="wrapper">
          <section className="products app-section">
            <div className="wrapper">
              <header className="section-header products-header">
                <div className="wrapper">
                  <h2 className="caption">Browse our products</h2>
                  <p className="text">
                    We have a wide range of products to choose from. Browse our
                    products and add them to your cart.
                  </p>
                </div>
              </header>
              <ul className="products-list">
                {products.map((product) => (
                  <ProductItem
                    key={product.id}
                    product={product}
                    addToCart={handleAddToCart}
                  />
                ))}
              </ul>
            </div>
          </section>
        </div>
      </main>
    </div>
  );
}
export default App;

Потрясающий! Для целей этой статьи стили, используемые для создания этого примера проекта, будут помещены в файл ./src/App.css проекта и доступны на GitHub. Вы также можете скопировать стили из Pastebin или использовать собственные стили.

Теперь, если мы запустим наше приложение, выполнив команду npm start, мы должны увидеть что-то вроде этого:

Хороший! Далее мы установим и настроим Format.js, чтобы начать перевод нашего приложения React.

Настройка Format.js в React

Чтобы начать настройку Format.js в React, используйте следующие команды:

Install react-intl, a Format.js package for React:
npm i -S react react-intl

После установки мы можем получить доступ к различным helper функциям, компонентам и хукам из Format.js, которые мы можем использовать в нашем приложении React.

Добавление IntlProvider

Этот компонент помогает нам добавить функциональность i18n в наше приложение, предоставляя такие конфигурации, как текущая локаль и набор переведенных строк/сообщений, в корень приложения. Это делает эти конфигурации доступными для различных <Formatted /> компонентов, используемых во всем приложении.

В нашем файле ./src/App.js мы обернем наше приложение компонентом <IntlProvider />:

// ./src/App.js
// ...
import { IntlProvider } from "react-intl";
function App() {
  // ...
  // set up state for locale and messages
  const [locale, setLocale] = useState("es");
  const [messages, setMessages] = useState({
    "app.name": "Tienda sencilla",
  })
  // ...
  return (
    <IntlProvider messages={messages} key={locale} locale={locale}>
      {/* ... */}
    </IntlProvider>
  );
export default App;

На основе приведенного выше кода мы импортируем компонент IntlProvider из библиотеки react-intl.
Мы также устанавливаем состояние для переменных locale и messages с начальными значениями "es" и {"app.name": "Tienda sencilla"} соответственно.

Затем компонент IntlProvider используется для переноса содержимого компонента App. Мы также передаем переменные messages и locale в качестве реквизита для файла IntlProvider. Свойство key устанавливается в состояние locale, чтобы заставить React повторно отображать компонент при изменении значения locale.

Компонент IntlProvider обеспечивает поддержку интернационализации компонента-оболочки, снабжая его переводами для текущей локали. Используя переменные состояния messages и locale, содержимое упакованного компонента можно перевести на основе выбранной локали. В этом примере ключ сообщения app.name преобразуется в "Tienda sencilla" для испанской локали. Далее мы воспользуемся компонентом <FormattedMesage />, чтобы увидеть перевод в действии.

Использование компонента FormattedMessage

Во-первых, мы будем использовать компонент FormattedMessage для преобразования имени приложения, которое появляется в app-header в ./src/App.js, используя свойство "app.name" из нашего messages, как показано ниже:

// ./src/App.js
// ...
import { IntlProvider } from "react-intl";
function App() {
  // ...
  // set up state for locale and messages
  const [locale, setLocale] = useState("es");
  const [messages, setMessages] = useState({
    "app.name": "Tienda sencilla",
  })
  // ...
  return (
    <IntlProvider messages={messages} key={locale} locale={locale}>
      <div className="app">
        <header className="app-header">
          <div className="wrapper">
            <div className="app-name">
              <FormattedMessage id="app.name" defaultMessage={"Simple Store"} />
            </div>
            {/* ... */}
          </div>
        </header>
        {/* ... */}
      </div>
    </IntlProvider>
  );
export default App;

Здесь мы используем компонент FormattedMessage из библиотеки react-intl для перевода имени приложения, которое появляется в файле app-header. Компонент FormattedMessage принимает два реквизита: ID и defaultMessage. Свойство ID используется для идентификации сообщения перевода в объекте messages. В данном случае установлено значение "app.name".

Подсказка defaultMessage используется в качестве запасного сообщения на случай, если перевод для указанного ID не найден. В данном случае установлено значение "Simple Store".

При использовании FormattedMessage имя приложения переводится в соответствии с текущим выбранным языковым стандартом. Когда переменная состояния locale изменится, компонент IntlProvider перерендерится и предоставит FormattedMessage переводы для новой локали.

При этом у нас должно получиться что-то вроде этого:

Точно так же мы можем перевести другой текст в нашем файле .src/App.js, добавив дополнительные свойства к объекту messages и используя <FormattedMessage /> для отображения значений следующим образом:

// ./src/App.js
// ...
import { IntlProvider } from "react-intl";
function App() {
  // ...
  // set up state for locale and messages
  const [locale, setLocale] = useState("es");
  const [messages, setMessages] = useState({
    "app.name": "Tienda sencilla",
    "app.description": "Una tienda sencilla con React",
    "app.products.caption": "Explora nuestros productos",
    "app.products.text":
      "Tenemos una amplia gama de productos para elegir. Explora nuestros productos y agrégalos a tu carrito.",
  })
  // ...
  return (
    <IntlProvider messages={messages} key={locale} locale={locale}>
      <div className="app">
        {/* ... */}
        <main className="app-main">
          <div className="wrapper">
            <section className="products app-section">
              <div className="wrapper">
                <header className="section-header products-header">
                  <div className="wrapper">
                    <h2 className="caption">
                      <FormattedMessage
                        id="app.products.caption"
                        defaultMessage={"Browse our products"}
                      />
                    </h2>
                    <p className="text">
                      <FormattedMessage
                        id="app.products.text"
                        defaultMessage={"We have a wide range of products to choose from. Browse our products and add them to your cart."}
                      />
                    </p>
                  </div>
                </header>
                <ul className="products-list">
                  {products.map((product) => (
                    <ProductItem
                      key={product.id}
                      product={product}
                      addToCart={handleAddToCart}
                    />
                  ))}
                </ul>
              </div>
            </section>
          </div>
        </main>
      </div>
    </IntlProvider>
  );
export default App;

Здесь мы видим, как использовать <FormattedMessage /> для перевода другого текста в файле ./src/App.js путем добавления дополнительных свойств к объекту messages. В этом примере messages содержит свойства для имени приложения, описания, заголовка продукта и текста продукта.

Компонент отображает эти значения, передавая соответствующий ID в FormattedMessage вместе со значением по умолчанию message для отображения, если перевод недоступен. В этом примере компонент отображает заголовок продукта и текст, используя ID, соответствующий ключам свойств в объекте messages, с резервным текстом, переданным в defaultMessage. При этом у нас должно получиться что-то вроде этого:

Хороший!

Перевод компонентов Cart и ProductItem

Мы можем пойти еще дальше, переведя наши компоненты Cart и ProductItem. Во-первых, нам нужно добавить переводы в объект messages в ./src/App.js с помощью кода ниже:

const [messages, setMessages] = useState({
  "app.name": "Tienda sencilla",
  "app.description": "Una tienda sencilla con React",
  "app.products.caption": "Explora nuestros productos",
  "app.products.text":
    "Tenemos una amplia gama de productos para elegir. Explora nuestros productos y agrégalos a tu carrito.",
  "app.cart": "Carrito",
  "app.cart.title": "Tu carrito",
  "app.cart.empty": "El carrito está vacío",
  "app.cart.items":
    "{count, plural, =0 {No tienes artículos} one {# articulo} other {# artículos }} en tu carrito",
  "app.cart.remove": "Eliminar",
  "app.cart.add": "Añadir a la cesta",
  "app.item.price": "{price, number, ::currency/EUR}",
});

Здесь мы обновляем объект messages в ./src/App.js, добавляя переводы для новых компонентов. Переводы включают такие строки, как "app.cart.title", "app.cart.empty", "app.cart.items" и "app.item.price". Эти переводы будут отображать правильный текст в компонентах Cart и ProductItem.

Перевести Cart

Двигаясь вперед, мы переведем компонент Cart. Продолжайте и добавьте приведенный ниже код в ./src/components/Cart.jsx:

// ./src/components/Cart.jsx
import { FormattedMessage } from "react-intl";
const Cart = ({ cart, removeItem }) => {
  return (
    <div className="dropdown">
      <div className="trigger group">
        <button className="cta">
          <FormattedMessage id="app.cart" defaultMessage="Cart" />
        </button>
        <span className="badge"> {cart?.length}</span>
      </div>
      <div className="content">
        <aside className="cart">
          <header className="cart-header">
            <h2>
              <FormattedMessage
                id="app.cart.title"
                defaultMessage="Your Cart"
              />
            </h2>
            <p>
              <FormattedMessage
                id="app.cart.items"
                defaultMessage={`You have {count, plural, =0 {no items} one {one item} other {# items}} in your cart`}
                values={{ count: cart.length }}
              />
            </p>
          </header>
          <ul className="items">
            {cart.map((item) => {
              return (
                <li tabIndex={0} key={item.id} className="item">
                  <div className="item-image img-cont">
                    <img src={item.thumbnail} alt={item.name} />
                  </div>
                  <div className="item-details">
                    <h3>{item.title}</h3>
                    <p className="item-price">
                      <FormattedMessage
                        id="app.item.price"
                        defaultMessage={`{price, number, ::currency/USD}`}
                        values={{ price: item.price }}
                      />
                    </p>
                    <button onClick={() => removeItem(item)} className="cta">
                      <FormattedMessage
                        id="app.cart.remove"
                        defaultMessage="Remove"
                      />
                    </button>
                  </div>
                </li>
              );
            })}
          </ul>
        </aside>
      </div>
    </div>
  );
};
export default Cart;

В приведенном выше коде мы используем FormattedMessage для отображения переведенных строк. Компонент FormattedMessage принимает реквизит ID, соответствующий ключу перевода объекта message. Он также принимает реквизит defaultMessage, который отображает значение по умолчанию, если перевод не найден.

Более 200 000 разработчиков используют LogRocket для создания лучшего цифрового опыта

Подробнее →

Например, FormattedMessage с id="app.cart.title" и defaultMessage="Your Cart" отображает текст "Tu carrito" на испанском языке, когда locale установлено на "es".

Внимательно посмотрите на блок кода ниже:

<p>
  <FormattedMessage
    id="app.cart.items"
    defaultMessage={`You have {count, plural, =0 {no items} one {one item} other {# items}} in your cart`}
    values={{ count: cart.length }}
  />
</p>
Here, "app.cart.items" corresponds to:
const [messages, setMessages] = useState({
  "app.cart.items":
    "{count, plural, =0 {No tienes artículos} one {# articulo} other {# artículos }} en tu carrito",
});

Обратите внимание, что шаблон message использует count в качестве переменной, представляющей количество товаров в корзине. Имеет три возможных варианта в зависимости от значения count:

  • =0 {no items}:: если значение count равно нулю, используется этот параметр, и в сообщении будет указано "no items".
  • one {one item}:: если значение count равно единице, используется этот параметр, и в сообщении будет указано "one item".
  • other {# items}:: если значение count отличается от нуля или единицы, используется этот параметр, и в сообщении будет указано # items, где # заменяется значением count.

Это для сообщения, указанного в defaultMessage, и сообщения, указанного в состоянии messages. Это строка message, которая включает форму множественного числа на испанском языке. Сообщение содержит переменную count, используемую для определения правильной множественной формы сообщения. Синтаксис формы множественного числа: {count, plural, ...}.

В этом синтаксисе первый аргумент — это имя переменной (в данном случае count), а второй — тип используемого множественного числа. Внутри аргумента множественного числа есть три падежа:

  • =0 {No tienes artículos}: Это тот случай, когда переменная count равна нулю, значит корзина пуста. Сообщение в данном случае No tienes artículos (у вас нет предметов)
  • one {# articulo}: это когда переменная count равна единице. Сообщение в данном случае "# articulo" (один элемент)
  • other {# artículos}: случай по умолчанию для всех остальных счетчиков. Сообщение в этом случае будет "# artículos" (X элементов), где X — значение переменной count.

Таким образом, полное сообщение на испанском языке будет выглядеть так: "No tienes artículos" (у вас нет товаров) для пустой корзины, "1 artículo" (1 товар) для корзины с одним товаром и "# artículos" для корзин с двумя или более товарами. Это следует за Intl MessageFormat, о котором вы можете узнать больше в документации.

Перевести ProductItem

Для компонента ProductItem в ./src/components/ProductItem.jsx добавьте следующий код:

// ./src/components/ProductItem.jsx
import { FormattedMessage } from "react-intl";
const ProductItem = ({ product, addToCart }) => {
  return (
    <li className="product">
      <div className="product-image img-cont">
        <img src={product.thumbnail} alt="" />
      </div>
      <div className="product-details">
        <h3>{product.title}</h3>
        <p>{product.description}</p>
        <span className="product-price">
          <FormattedMessage
            id="app.item.price"
            defaultMessage={`{price, number, ::currency/USD}`}
            values={{ price: product.price }}
          />
        </span>
      </div>
      <div className="product-actions">
        <button
          disabled={product?.isInCart}
          onClick={() => addToCart(product)}
          className="cta"
        >
          <FormattedMessage id="app.cart.add" defaultMessage="Add to Cart" />
        </button>
      </div>
    </li>
  );
};
export default ProductItem;

Одна вещь, на которую мы должны обратить внимание, это product-price, как показано ниже:

<span className="product-price">
  <FormattedMessage
    id="app.item.price"
    defaultMessage={`{price, number, ::currency/USD}`}
    values={{ price: product.price }}
  />
</span>
"app.item.price" corresponds to:
const [messages, setMessages] = useState({
  "app.item.price": "{price, number, ::currency/EUR}",
});

В приведенном выше коде "{price, number, ::currency/EUR}" — это шаблон формата сообщения, используемый в библиотеке react-intl. Он указывает, как форматировать переменную price как число с символом валюты EUR.

Фигурные скобки {} обозначают заполнители в шаблоне сообщения. Между тем, price — это имя переменной, которую следует подставить в шаблон. Ключевое слово number указывает, что переменная должна быть отформатирована как число.

Параметр ::currency/EUR указывает, что число должно быть отформатировано как значение валюты с использованием символа валюты EUR. После всего этого наше приложение должно быть полностью переведено:

Потрясающий!

Переключение между языками

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

Создание файлов JSON локали

Сначала мы создадим файлы JSON для каждой локали. Для испанской локали мы создаем новый файл ./src/locales/es.json, как показано ниже:

{
  "app.name": "Tienda sencilla",
  "app.description": "Una tienda sencilla con React",
  "app.products.caption": "Explora nuestros productos",
  "app.products.text": "Tenemos una amplia gama de productos para elegir. Explora nuestros productos y agrégalos a tu carrito.",
  "app.cart": "Carrito",
  "app.cart.title": "Tu carrito",
  "app.cart.empty": "El carrito está vacío",
  "app.cart.items": "{count, plural, =0 {No tienes artículos} one {# articulo} other {# artículos }} en tu carrito",
  "app.cart.remove": "Eliminar",
  "app.cart.add": "Añadir a la cesta",
  "app.item.price": "{price, number, ::currency/EUR}"
}
For the English locale, we create a ./src/locales/en.json file:
{
  "app.name": "Simple store",
  "app.description": "A simple store with React",
  "app.products.caption": "Explore our products",
  "app.products.text": "We have a wide range of products to choose from. Explore our products and add them to your cart.",
  "app.cart": "Cart",
  "app.cart.title": "Your Cart",
  "app.cart.empty": "Your cart is empty",
  "app.cart.items": "{count, plural, =0 {You have no items} one {You have one item} other {You have # items}} in your cart",
  "app.cart.remove": "Remove",
  "app.cart.add": "Add to cart",
  "app.item.price": "{price, number, ::currency/USD}"
}

Браво!

Динамически импортировать сообщения локали

Теперь мы будем использовать useEffect Hook для асинхронной загрузки сообщений для выбранной локали при монтировании компонента или при изменении состояния locale. Вот код:

// ./src/App.js
// ...
function App() {
  // ....
  const [locale, setLocale] = useState("es");
  const [messages, setMessages] = useState({
    // ...
  });
  // ...
  // function to dynamically import messages depending on locale
  useEffect(() => {
    import(`./locales/${locale}.json`).then((messages) => {
      console.log({
        messages,
      });
      setMessages(messages);
    });
  }, [locale]);
  return (
    // ...
  )
};
export default App;

В приведенном выше коде используется динамический импорт для загрузки файла JSON, содержащего сообщения для выбранной локали. Как только сообщения загружены, он устанавливает состояние messages с загруженными сообщениями. Наконец, добавьте ввод select для переключения между локалями, как показано ниже:

// ./src/App.js
// ...
function App() {
  // ...
  return (
    <IntlProvider messages={messages} key={locale} locale={locale}>
      <div className="app">
        <header className="app-header">
          <div className="wrapper">
            <div className="app-name">
              <FormattedMessage id="app.name" defaultMessage={"Simple Store"} />
            </div>
            <div style={{ display: "flex", gap: "1rem" }}>
              <Cart cart={cart} removeItem={handleRemoveFromCart} />
              <select
                onChange={(e) => {
                  setLocale(e.target.value);
                }}
                value={locale}
                name="language-select"
                id="language-select"
                className="select-input"
              >
                <option value="es">Español</option>
                <option value="en">English</option>
              </select>
            </div>
          </div>
        </header>
        {/* ... */}
      </div>
    </IntlProvider>
  );
}
export default App;

При этом у нас должно получиться что-то вроде этого:

Потрясающий.

Заключение

В этой статье мы узнали, как использовать Format.js для перевода вашего приложения React. В конечном итоге, следуя инструкциям в этой статье, вы сможете быстро перевести свое React-приложение с помощью Format.js, расширив свою аудиторию и улучшив UX своего приложения.