Приложения React/Redux и многоязычия (интернационализация) - Архитектура - программирование
Подтвердить что ты не робот

Приложения React/Redux и многоязычия (интернационализация) - Архитектура

Я создаю приложение, которое должно быть доступно на нескольких языках и локалях.

Мой вопрос не чисто технический, а скорее о архитектуре и шаблонах, которые люди фактически используют в производстве для решения этой проблемы. Я не мог найти ни одной "кулинарной книги" для этого, поэтому я обращаюсь к своему любимому сайту Q/A:)

Вот мои требования (они действительно "стандартные" ):

  • Пользователь может выбрать язык (тривиальный)
  • При изменении языка интерфейс должен автоматически переводиться на новый выбранный язык.
  • Я не слишком беспокоюсь о форматировании чисел, дат и т.д. на данный момент, я хочу простое решение просто перевести строки

Вот возможные решения, которые я мог бы подумать:

Каждый компонент имеет дело с переводом в изоляции

Это означает, что каждый компонент имеет, например, набор файлов en.json, fr.json и т.д. вместе с переведенными строками. И вспомогательная функция, помогающая считывать значения от тех, которые зависят от выбранного языка.

  • Pro: более уважительно из философии React, каждый компонент является "автономным".
  • Минусы: вы не можете централизовать все переводы в файле (например, чтобы кто-то добавил новый язык)
  • Минусы: вам все равно нужно передать текущий язык в качестве опоры, в каждом кровавом компоненте и их детях.

Каждый компонент получает переводы через реквизит

Таким образом, они не знают о текущем языке, они просто берут список строк как реквизиты, которые соответствуют текущему языку

  • Pro: поскольку эти строки идут "сверху", они могут быть централизованы где-то
  • Минусы: каждый компонент теперь привязан к системе перевода, вы не можете просто повторно использовать его, вам нужно указывать правильные строки каждый раз

Вы немного обходите реквизиты и, возможно, используете context, чтобы передать текущий язык

  • Pro: он в основном прозрачный, не нужно передавать текущий язык и/или переводы через реквизиты все время
  • Минусы: он выглядит громоздким для использования

Если у вас есть другая идея, пожалуйста, скажите!

Как вы это делаете?

4b9b3361

Ответ 1

Попробовав несколько решений, я думаю, что нашел тот, который хорошо работает и должен быть идиоматическим решением для React 0.14 (т.е. он не использует mixins, а компоненты более высокого порядка) (редактирование: также отлично работает с React 15 конечно!).

Итак, здесь решение, начинающееся снизу (отдельные компоненты):

Компонент

Единственное, что понадобится вашему компоненту (по соглашению), - это реквизиты strings. Он должен быть объектом, содержащим различные строки, которые вам нужны Компоненты, но в действительности их форма зависит от вас.

Он содержит переводы по умолчанию, поэтому вы можете использовать компонент где-то еще без необходимости предоставления какого-либо перевода (он будет работать из коробки со стандартным языком, английский в этом примере)

import { default as React, PropTypes } from 'react';
import translate from './translate';

class MyComponent extends React.Component {
    render() {

        return (
             <div>
                { this.props.strings.someTranslatedText }
             </div>
        );
    }
}

MyComponent.propTypes = {
    strings: PropTypes.object
};

MyComponent.defaultProps = {
     strings: {
         someTranslatedText: 'Hello World'
    }
};

export default translate('MyComponent')(MyComponent);

Компонент более высокого порядка

В предыдущем фрагменте вы могли заметить это на последней строке: translate('MyComponent')(MyComponent)

translate в этом случае представляет собой компонент более высокого порядка, который обертывает ваш компонент и предоставляет некоторые дополнительные функции (эта конструкция заменяет микшины предыдущих версий React).

Первый аргумент - это ключ, который будет использоваться для поиска переводов в файле перевода (я использовал здесь имя компонента, но это могло быть что угодно). Второй (обратите внимание, что функция curryed, чтобы позволить декораторам ES7) - сам Компонент обернуть.

Вот код для компонента перевода:

import { default as React } from 'react';
import en from '../i18n/en';
import fr from '../i18n/fr';

const languages = {
    en,
    fr
};

export default function translate(key) {
    return Component => {
        class TranslationComponent extends React.Component {
            render() {
                console.log('current language: ', this.context.currentLanguage);
                var strings = languages[this.context.currentLanguage][key];
                return <Component {...this.props} {...this.state} strings={strings} />;
            }
        }

        TranslationComponent.contextTypes = {
            currentLanguage: React.PropTypes.string
        };

        return TranslationComponent;
    };
}

Это не волшебство: он просто прочитает текущий язык из контекста (и этот контекст не будет кровоточить по всей базе кода, просто используется здесь в этой оболочке), а затем получить соответствующий объект строк из загруженных файлов. Этот фрагмент логики довольно наивен в этом примере, можно сделать так, как вы хотите.

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

На самой вершине иерархии

В корневом компоненте вам просто нужно установить текущий язык из текущего состояния. В следующем примере используется Redux как потоковая реализация, но его можно легко преобразовать, используя любую другую структуру/шаблон/библиотеку.

import { default as React, PropTypes } from 'react';
import Menu from '../components/Menu';
import { connect } from 'react-redux';
import { changeLanguage } from '../state/lang';

class App extends React.Component {
    render() {
        return (
            <div>
                <Menu onLanguageChange={this.props.changeLanguage}/>
                <div className="">
                    {this.props.children}
                </div>

            </div>

        );
    }

    getChildContext() {
        return {
            currentLanguage: this.props.currentLanguage
        };
    }
}

App.propTypes = {
    children: PropTypes.object.isRequired,
};

App.childContextTypes = {
    currentLanguage: PropTypes.string.isRequired
};

function select(state){
    return {user: state.auth.user, currentLanguage: state.lang.current};
}

function mapDispatchToProps(dispatch){
    return {
        changeLanguage: (lang) => dispatch(changeLanguage(lang))
    };
}

export default connect(select, mapDispatchToProps)(App);

И чтобы закончить, файлы перевода:

Файлы переводов

// en.js
export default {
    MyComponent: {
        someTranslatedText: 'Hello World'
    },
    SomeOtherComponent: {
        foo: 'bar'
    }
};

// fr.js
export default {
    MyComponent: {
        someTranslatedText: 'Salut le monde'
    },
    SomeOtherComponent: {
        foo: 'bar mais en français'
    }
};

Что вы, ребята, думаете?

Я думаю, решает всю проблему, которую я пытался избежать в своем вопросе: логика перевода не исчерпывается по всему исходному коду, она довольно изолирована и позволяет повторно использовать компоненты без нее.

Например, MyComponent не нужно обертывать translate() и может быть раздельным, позволяя повторному использованию другим, желающим предоставить strings своим собственным значением.

[Редактировать: 31/03/2016]: Недавно я работал над Ретроспективным советом (для Agile Retrospectives), созданным с помощью React и Redux, и многоязычным. Поскольку в комментариях довольно много людей задавали реальный пример, вот он:

Код можно найти здесь: https://github.com/antoinejaussoin/retro-board/tree/master

Ответ 2

Исходя из моего опыта, лучший подход - это создать состояние редукции i18n и использовать его по многим причинам:

1- Это позволит вам передать начальное значение из базы данных, локального файла или даже из механизма шаблонов, такого как EJS или Jade.

2- Когда пользователь меняет язык, вы можете изменить весь язык приложения, даже не обновляя интерфейс.

3- Когда пользователь меняет язык, это также позволит вам получить новый язык из API, локального файла или даже из констант.

4- Вы также можете сохранить другие важные вещи со строками, такими как часовой пояс, валюта, направление (RTL/LTR) и список доступных языков.

5- Вы можете определить язык изменений как обычное редукционное действие.

6- Вы можете хранить строки вашего интерфейса и внешнего интерфейса в одном месте, например, в моем случае я использую i18n-node для локализации, а когда пользователь меняет язык интерфейса, я просто делаю обычный вызов API, а в backend я просто return i18n.getCatalog(req) это вернет все пользовательские строки только для текущего языка

Мое предложение для начального состояния i18n:

{
  "language":"ar",
  "availableLanguages":[
    {"code":"en","name": "English"},
    {"code":"ar","name":"عربي"}
  ],
  "catalog":[
     "Hello":"مرحباً",
     "Thank You":"شكراً",
     "You have {count} new messages":"لديك {count} رسائل جديدة"
   ],
  "timezone":"",
  "currency":"",
  "direction":"rtl",
}

Дополнительные полезные модули для i18n:

1- string-template это позволит вам вставлять значения между строками вашего каталога, например:

import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة

2- человеческий формат Этот модуль позволит вам преобразовать число в/из читаемой человеком строки, например:

import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)

3- момент - самая известная библиотека дат и времени npm, вы можете перевести момент, но она уже имеет встроенный перевод, просто вам нужно передать язык текущего состояния, например:

import moment from "moment";

const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م

Обновление (14/06/2019)

В настоящее время существует много фреймворков, реализующих одну и ту же концепцию с использованием API реагирующего контекста (без избыточности), я лично рекомендовал I18следующий

Ответ 3

Раствор Антуана работает нормально, но есть некоторые оговорки:

  • Он использует контекст React напрямую, чего я стараюсь избегать, когда уже использую Redux
  • Он напрямую импортирует фразы из файла, что может быть проблематично, если вы хотите получить нужный язык во время выполнения на стороне клиента
  • Он не использует библиотеку i18n, которая является легкой, но не дает вам доступа к удобным функциям перевода, таким как плюрализация и интерполяция.

Именно поэтому мы построили редукс-полиглот поверх Redux и AirBNB Polyglot.
(Я один из авторов)

Это обеспечивает:

  • редуктор для хранения языка и соответствующих сообщений в вашем магазине Redux. Вы можете поставить оба:
    • промежуточное ПО, которое вы можете настроить для отслеживания определенных действий, вывода текущего языка и получения/получения связанных сообщений.
    • прямая рассылка setLanguage(lang, messages)
  • селектор getP(state), который извлекает объект P который предоставляет 4 метода:
    • t(key): оригинальная функция полиглота Т
    • tc(key): перевод заглавными буквами
    • tu(key): перевод в верхнем регистре
    • tm(morphism)(key): пользовательский перевод
  • getLocale(state) для получения текущего языка
  • Компонент translate более высокого порядка для улучшения ваших компонентов React, вводя объект p в реквизит

Простой пример использования:

отправить новый язык:

import setLanguage from 'redux-polyglot/setLanguage';

store.dispatch(setLanguage('en', {
    common: { hello_world: 'Hello world' } } }
}));

в компоненте:

import React, { PropTypes } from 'react';
import translate from 'redux-polyglot/translate';

const MyComponent = props => (
  <div className='someId'>
    {props.p.t('common.hello_world')}
  </div>
);
MyComponent.propTypes = {
  p: PropTypes.shape({t: PropTypes.func.isRequired}).isRequired,
}
export default translate(MyComponent);

Пожалуйста, скажите мне, если у вас есть какие-либо вопросы/предложения!

Ответ 4

Из моих исследований в этом, как представляется, существуют два основных подхода, которые используются для i18n в JavaScript: ICU и gettext.

Я только использовал gettext, поэтому я смещен.

Что меня поражает, так это бедная поддержка. Я родом из мира PHP, CakePHP или WordPress. В обеих этих ситуациях базовым стандартом является то, что все строки просто окружены __(''), а затем вниз по линии вы легко получаете переводы с помощью файлов PO.

Gettext

Вы получаете информацию о sprintf для форматирования строк, а файлы PO будут легко переведены тысячами различных агентств.

Есть два популярных варианта:

Оба имеют поддержку стиля gettext, форматирование строк в стиле sprintf и импорт/экспорт в файлы PO.

i18next имеет React extension, разработанный ими самим. Джед не делает. Sentry.io, похоже, использует пользовательскую интеграцию Jed с React. React + Redux post, предлагает использовать

Инструменты: jed + po2json + jsxgettext

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

ICU

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

Популярным вариантом для этого является messageformat.js. Вкратце обсужден этот учебник по блогу sentry.io. messageformat.js фактически разработан тем же человеком, который написал Jed. Он предъявляет довольно жесткие претензии в отношении использования ICU:

Jed - это функция, на мой взгляд. Я рад исправить ошибки, но, как правило, мне не интересно добавлять больше в библиотеку.

Я также поддерживаю messageformat.js. Если вам не нужна конкретная реализация gettext, я могу предложить вместо этого использовать MessageFormat, так как она лучше поддерживает множественные числа/пол и имеет встроенные данные локали.

Грубое сравнение

gettext с помощью sprintf:

i18next.t('Hello world!');
i18next.t(
    'The first 4 letters of the english alphabet are: %s, %s, %s and %s', 
    { postProcess: 'sprintf', sprintf: ['a', 'b', 'c', 'd'] }
);

messageformat.js(мое лучшее предположение от чтения guide):

mf.compile('Hello world!')();
mf.compile(
    'The first 4 letters of the english alphabet are: {s1}, {s2}, {s3} and {s4}'
)({ s1: 'a', s2: 'b', s3: 'c', s4: 'd' });

Ответ 5

Если еще не сделано, посмотрев https://react.i18next.com/, это может быть хорошим советом. Он основан на i18next: узнайте один раз - переводите всюду.

Ваш код будет выглядеть примерно так:

<div>{t('simpleContent')}</div>
<Trans i18nKey="userMessagesUnread" count={count}>
  Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>

Поставляется с образцами для:

  • WebPack
  • CRA
  • expo.js
  • next.js
  • интеграция сборника рассказов
  • кутерьмы
  • Дат
  • ...

https://github.com/i18next/react-i18next/tree/master/example

Кроме того, вы должны также рассмотреть рабочий процесс во время разработки, а затем для своих переводчиков → https://www.youtube.com/watch?v=9NOzJhgmyQE

Ответ 6

Я хотел бы предложить простое решение, используя create-реакции-приложение.

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

Веб-сервер будет использовать правильный язык автоматически, в зависимости от заголовка Accept-Language, или вручную, установив файл cookie.

В основном, мы не меняем язык более одного раза, если вообще когда-либо)

Данные перевода помещаются в тот же файл компонента, который их использует, вместе со стилями, HTML и кодом.

И здесь мы имеем полностью независимый компонент, который отвечает за собственное состояние, просмотр, перевод:

import React from 'react';
import {withStyles} from 'material-ui/styles';
import {languageForm} from './common-language';
const {REACT_APP_LANGUAGE: LANGUAGE} = process.env;
export let language; // define and export language if you wish
class Component extends React.Component {
    render() {
        return (
            <div className={this.props.classes.someStyle}>
                <h2>{language.title}</h2>
                <p>{language.description}</p>
                <p>{language.amount}</p>
                <button>{languageForm.save}</button>
            </div>
        );
    }
}
const styles = theme => ({
    someStyle: {padding: 10},
});
export default withStyles(styles)(Component);
// sets laguage at build time
language = (
    LANGUAGE === 'ru' ? { // Russian
        title: 'Транзакции',
        description: 'Описание',
        amount: 'Сумма',
    } :
    LANGUAGE === 'ee' ? { // Estonian
        title: 'Tehingud',
        description: 'Kirjeldus',
        amount: 'Summa',
    } :
    { // default language // English
        title: 'Transactions',
        description: 'Description',
        amount: 'Sum',
    }
);

Добавьте переменную языковой среды в ваш package.json

"start": "REACT_APP_LANGUAGE=ru npm-run-all -p watch-css start-js",
"build": "REACT_APP_LANGUAGE=ru npm-run-all build-css build-js",

Вот и все!

Также мой оригинальный ответ включал более монолитный подход с одним файлом JSON для каждого перевода:

языки /ru.json

{"hello": "Привет"}

Lib/lang.js

export default require('../lang/${process.env.REACT_APP_LANGUAGE}.json');

SRC/App.jsx

import lang from '../lib/lang.js';
console.log(lang.hello);