Я создаю приложение, которое должно быть доступно на нескольких языках и локалях.
Мой вопрос не чисто технический, а скорее о архитектуре и шаблонах, которые люди фактически используют в производстве для решения этой проблемы. Я не мог найти ни одной "кулинарной книги" для этого, поэтому я обращаюсь к своему любимому сайту Q/A:)
Вот мои требования (они действительно "стандартные" ):
Вот возможные решения, которые я мог бы подумать:
Каждый компонент имеет дело с переводом в изоляции
Это означает, что каждый компонент имеет, например, набор файлов en.json, fr.json и т.д. вместе с переведенными строками. И вспомогательная функция, помогающая считывать значения от тех, которые зависят от выбранного языка.
Каждый компонент получает переводы через реквизит
Таким образом, они не знают о текущем языке, они просто берут список строк как реквизиты, которые соответствуют текущему языку
Вы немного обходите реквизиты и, возможно, используете context, чтобы передать текущий язык
Если у вас есть другая идея, пожалуйста, скажите!
Как вы это делаете?
Попробовав несколько решений, я думаю, что нашел тот, который хорошо работает и должен быть идиоматическим решением для 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
Из моего опыта лучший подход заключается в создании состояния i18n redux и использования его по многим причинам:
1- Это позволит вам передать начальное значение из базы данных, локального файла или даже из механизма шаблона, такого как ejs или jade
2- Когда пользователь меняет язык, вы можете изменить весь язык приложения, даже не обновляя интерфейс.
3- Когда пользователь меняет язык, это также позволит вам извлечь новый язык из api, локального файла или даже из констант
4- Вы также можете сохранить другие важные вещи со строками, такими как часовой пояс, валюта, направление (RTL/LTR) и список доступных языков
5- Вы можете определить язык изменений как нормальное действие сокращения
6- Вы можете иметь свои внутренние и внешние строки в одном месте, например, в моем случае я использую i18n-node для локализации и когда пользователь меняет язык ui, я просто делаю обычный вызов api, а в backend я возвращаю 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- human-format этот модуль позволит вам преобразовать число в/из удобочитаемой строки, например:
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'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م
Решение антуан работает отлично, но есть некоторые оговорки:
Вот почему мы построили redux-polyglot поверх Redux и AirBNB Polyglot.
(Я один из авторов)
setLanguage(lang, messages)
getP(state)
, который извлекает объект P
, который предоставляет 4 метода:
t(key)
: оригинальная функция многоугольника Ttc(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);
Скажите, пожалуйста, если у вас есть вопрос/предложение!
_()
например, чтобы получить все эти строки. Таким образом, вы можете перевести это в языковой файл проще и не связываться с сумасшедшими переменными. В некоторых случаях целевые страницы нуждаются в определенной части макета, чтобы отображаться по-разному. Таким образом, должна быть доступна некоторая умная функция выбора по умолчанию против других возможных вариантов.
Из моих исследований в этом, как представляется, существуют два основных подхода, которые используются для i18n в JavaScript: ICU и gettext.
Я только использовал gettext, поэтому я смещен.
Что меня поражает, так это бедная поддержка. Я родом из мира PHP, CakePHP или WordPress. В обеих этих ситуациях базовым стандартом является то, что все строки просто окружены __('')
, а затем вниз по линии вы легко получаете переводы с помощью файлов PO.
Вы получаете информацию о sprintf для форматирования строк, а файлы PO будут легко переведены тысячами различных агентств.
Есть два популярных варианта:
Оба имеют поддержку стиля gettext, форматирование строк в стиле sprintf и импорт/экспорт в файлы PO.
i18next имеет React extension, разработанный ими самим. Джед не делает. Sentry.io, похоже, использует пользовательскую интеграцию Jed с React. React + Redux post, предлагает использовать
Инструменты: jed + po2json + jsxgettext
Однако Jed кажется более ориентированной на контекстно-зависимую реализацию - то есть она выражает намерение, где, поскольку i18next просто имеет это как вариант.
Это больше поддерживает случаи кросс вокруг переводов, например. для борьбы с гендерной проблематикой. Я думаю, вы увидите преимущества этого, если у вас есть более сложные языки для перевода.
Популярным вариантом для этого является 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' });
Если еще не сделано, посмотрев 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>
Поставляется с образцами для:
https://github.com/i18next/react-i18next/tree/master/example
Кроме того, вы должны также рассмотреть рабочий процесс во время разработки, а затем для своих переводчиков → https://www.youtube.com/watch?v=9NOzJhgmyQE