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

119

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

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

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

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

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

Каждый компонент выполняет перевод отдельно

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

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

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

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

  • Плюс: поскольку эти струны идут «сверху», их можно где-то централизовать.
  • Минусы: каждый компонент теперь привязан к системе перевода, вы не можете просто использовать его повторно, вам нужно каждый раз указывать правильные строки

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

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

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

Как ты это делаешь?

Антуан Жосен
источник
2
Я предпочитаю идею объекта ключей со строками перевода, который передается как опора, вам не нужно передавать каждую строку как опору отдельно. Изменение этого параметра на верхнем уровне должно вызвать повторный рендеринг. Я не думаю, что использование контекста - хорошая идея для этого, и каждый компонент, имеющий доступ к файлу перевода, делает их менее «тупыми» и переносимыми на самом деле imo (и труднее заставить приложение повторно отображать при смене языка).
Доминик
1
Фактически, согласно facebook.github.io/react/docs/context.html , использование контекста для совместного использования текущего языка является одним из законных вариантов использования. Подход, который я пытаюсь сейчас использовать, состоит в том, чтобы использовать это плюс Компонент более высокого порядка, чтобы иметь дело с логикой извлечения строк для этого конкретного компонента (возможно, на основе некоторого ключа)
Антуан Жосойн
1
Возможно, вы также можете взглянуть на Instant . Они решают эту проблему совершенно по-другому, решая ее во внешнем интерфейсе, а именно Optimizely (иначе говоря, изменяя DOM во время загрузки).
Марсель Пансе,
1
Совсем неплохо! Это действительно совершенно другой зверь (который связывает вас с услугой, за которую вам, возможно, придется платить, если ваш веб-сайт будет расти), но мне нравится эта идея, и она действительно, вероятно, того стоит для небольшого веб-сайта, который вам нужно быстро запустить!
Antoine Jaussoin,
4
Кроме того, вы можете упомянуть, что являетесь соучредителем Instant, вместо того, чтобы говорить «Они», как будто вы не имеете к ним никакого отношения :)
Антуан Жосойн,

Ответы:

110

Попробовав несколько решений, я думаю, что нашел тот, который хорошо работает и должен быть идиоматическим решением для React 0.14 (то есть он не использует миксины, а компоненты более высокого порядка) ( редактировать : также, конечно, отлично подходит для 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).

Первый аргумент - это ключ, который будет использоваться для поиска переводов в файле перевода (здесь я использовал имя компонента, но это могло быть что угодно). Второй (обратите внимание, что функция каррирована, чтобы позволить декораторам 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 используется в качестве реализации, подобной Flux, но его можно легко преобразовать с помощью любой другой структуры / шаблона / библиотеки.

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его своими собственными средствами.

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

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

Антуан Жосен
источник
Это отличное решение ... интересно, по-прежнему ли вы готовы к этому через несколько месяцев? Я не нашел много советов по шаблонам для этого в Интернете
Дэймон
2
На самом деле, я обнаружил, что это отлично работает (для моих нужд). Это заставляет компонент работать без перевода по умолчанию, а перевод просто выполняется поверх него, а компонент не знает об этом
Антуан Жосойн
1
@ l.cetinsoy, вы можете использовать dangerouslySetInnerHTMLопору, просто помните о последствиях (дезинфицируйте ввод вручную). См. Facebook.github.io/react/tips/dangerously-set-inner-html.html
Теодор Санду,
6
Есть ли причина, по которой вы не пробовали react-intl?
SureshCS 03
1
Очень нравится это решение. Я бы добавил одну вещь, которую мы нашли очень полезной для согласованности и экономии времени: если у вас много компонентов с общими строками, вы можете воспользоваться преимуществами переменных и распределения по объектам, напримерconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Хью Дэвис
18

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

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

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

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

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

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

6- Вы можете разместить свои бэкэнд и интерфейсные строки в одном месте, например, в моем случае я использую i18n-node для локализации, и когда пользователь меняет язык пользовательского интерфейса, я просто выполняю обычный вызов API, а в бэкенде я просто возвращаю 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- momentjs самая известная библиотека npm даты и времени, вы можете перевести момент, но у нее уже есть встроенный перевод, вам просто нужно передать текущий язык состояния, например:

import moment from "moment";

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

Обновление (14.06.2019)

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

Фарид Алнамрути
источник
Будет ли этот подход работать более чем на двух языках? Учитывая настройку каталога
темпранова 05
Проголосовал против. Это не отвечает на вопрос. OP попросил идею архитектуры, а не предложение или сравнение какой-либо библиотеки i18n.
TrungDQ
9
Я предложил каталог i18n как состояние redux, кажется, вы не понимаете redux
Фарид Алнамрути
5

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

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

Вот почему мы построили Redux-полиглот на вершине как Redux и AirBnB в Полиглот .
(Я один из авторов)

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

  • редуктор для хранения языка и соответствующих сообщений в вашем магазине Redux. Вы можете предоставить оба варианта:
    • промежуточное программное обеспечение, которое вы можете настроить для отслеживания определенных действий, определения текущего языка и получения / выборки связанных сообщений.
    • прямая отправка setLanguage(lang, messages)
  • getP(state)селектор , который извлекает Pобъект , который предоставляет 4 методы:
    • t(key): исходная функция полиглота T
    • 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);

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

Джалиль
источник
1
Намного лучше переводить оригинальные фразы. И создать инструмент, который анализирует все компоненты на наличие _()функций, например, чтобы получить все эти строки. Таким образом, вы можете переводить в языковом файле проще и не связываться с сумасшедшими переменными. В некоторых случаях целевым страницам требуется, чтобы определенная часть макета отображалась по-другому. Таким образом, должна быть доступна некоторая умная функция, позволяющая выбрать вариант по умолчанию и другие возможные варианты.
Roman M. Koss
Привет @Jalil, есть ли где-нибудь полный пример с промежуточным программным обеспечением?
ArkadyB 07
Привет @ArkadyB, Мы используем его в продакшене на нескольких проектах, которые не имеют открытого кода. Вы можете найти дополнительную информацию о модуле README: npmjs.com/package/redux-polyglot. У вас есть вопросы / трудности с его использованием?
Джалил
Моя главная проблема с этим и polyglot.js заключается в том, что он полностью заново изобретает колесо, а не строит его поверх PO-файлов. Эта альтернативная библиотека выглядит многообещающей: npmjs.com/package/redux-i18n . Я не думаю, что это сильно отличается - это просто дополнительный уровень для преобразования в файлы PO и обратно.
icc97
2

Судя по моему исследованию, к i18n в JavaScript, ICU и gettext используются два основных подхода .

Я когда-либо использовал только gettext, поэтому я предвзято.

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

Gettext

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

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

  1. i18next , использование которого описано в этом сообщении блога arkency.com
  2. Джед , с использованием описанного в сообщении sentry.io и в этом сообщении React + Redux ,

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

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

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

Однако Jed кажется более ориентированной на gettext реализацией - то есть выраженным намерением, тогда как 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 (я догадываюсь, прочитав руководство ):

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' });
icc97
источник
Проголосовал против. Это не ответ на вопрос. OP попросил идею архитектуры, а не предложение или сравнение какой-либо библиотеки i18n.
TrungDQ
@TrungDQ Это то, что спросил ОП: «Мой вопрос не чисто технический, а скорее об архитектуре и шаблонах, которые люди фактически используют в производстве для решения этой проблемы». , Это два шаблона, которые используются в производстве.
icc97
На мой взгляд, этот ответ не предоставляет информацию, которую я (и другие) ищу. Информация, которую вы предоставили, полезна, но, возможно, для другого вопроса. Я просто хочу проголосовать против, чтобы правильный ответ всплыл наверху (надеюсь).
TrungDQ
@TrungDQ Если это не то, что вы ищете, просто проголосуйте за тот, который вы использовали, и игнорируйте другие, вместо того, чтобы отрицать абсолютно правильные ответы, которые не соответствуют конкретной части интересующего вас вопроса.
icc97
1

Если вы еще не сделали этого, посмотрите 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

jamuhl
источник
Это не ответ на вопрос. OP попросил идею архитектуры, а не предложение или сравнение какой-либо библиотеки i18n.
TrungDQ
@TrungDQ as с вашим комментарием к моему ответу, который вы проголосовали против - OP запросил текущие решения, используемые в производстве. Однако я предложил i18next в своем ответе еще в феврале.
icc97
0

Я хотел бы предложить простое решение с использованием приложения create-react-app .

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

Веб-сервер будет обслуживать правильный язык автоматически, в зависимости от заголовка 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);
Игорь Сухарев
источник
Разве это не сработает только во время компиляции? Без возможности пользователю менять язык на лету? Тогда это был бы другой вариант использования.
Antoine Jaussoin
Приложение будет скомпилировано для всех необходимых языков. Веб-сервер автоматически предоставит правильную версию в зависимости от заголовка «Accept-Language» или файла cookie, установленного пользователем на лету. Таким образом, вся логика перевода может быть перемещена из приложения.
Игорь Сухарев