it-swarm.com.de

React / Redux und mehrsprachige (Internationalisierung) Apps - Architektur

Ich erstelle eine App, die in mehreren Sprachen und Ländereinstellungen verfügbar sein muss.

Meine Frage ist nicht rein technisch, sondern betrifft eher die Architektur und die Muster, die die Leute in der Produktion tatsächlich verwenden, um dieses Problem zu lösen. Ich konnte dafür nirgendwo ein "Kochbuch" finden, also wende ich mich an meine Lieblings-Q/A-Website :)

Hier sind meine Anforderungen (sie sind wirklich "Standard"):

  • Der Benutzer kann die Sprache wählen (trivial)
  • Nach dem Ändern der Sprache sollte die Benutzeroberfläche automatisch in die neu ausgewählte Sprache übersetzen
  • Im Moment mache ich mir keine Sorgen um die Formatierung von Zahlen, Datumsangaben usw., ich möchte eine einfache Lösung, um nur Zeichenfolgen zu übersetzen

Hier sind die möglichen Lösungen, die ich mir überlegen könnte:

Jede Komponente behandelt die Übersetzung isoliert

Dies bedeutet, dass jede Komponente zum Beispiel eine Reihe von en.json-, fr.json- usw. -Dateien mit den übersetzten Zeichenfolgen enthält. Und eine Hilfsfunktion, die das Ablesen der Werte in Abhängigkeit von der ausgewählten Sprache erleichtert.

  • Pro: Respektvoller gegenüber der React Philosophie, jede Komponente ist "Standalone"
  • Nachteile: Sie können nicht alle Übersetzungen in einer Datei zentralisieren (damit beispielsweise jemand eine neue Sprache hinzufügt)
  • Nachteile: Sie müssen immer noch die aktuelle Sprache als Requisite in jeder verdammten Komponente und ihren Kindern weitergeben

Jede Komponente erhält die Übersetzungen über die Requisiten

Sie kennen also nicht die aktuelle Sprache, sondern nehmen nur eine Liste von Zeichenfolgen als Requisiten, die zufällig mit der aktuellen Sprache übereinstimmen

  • Pro: Da diese Saiten "von oben" kommen, können sie irgendwo zentralisiert werden
  • Nachteile: Jede Komponente ist jetzt in das Übersetzungssystem eingebunden. Sie können eine Komponente nicht einfach wiederverwenden. Sie müssen jedes Mal die richtigen Zeichenfolgen angeben

Sie umgehen die Requisiten ein wenig und verwenden möglicherweise das Kontext -Dingy, um die aktuelle Sprache weiterzugeben

  • Pro: Es ist größtenteils transparent und muss nicht ständig die aktuelle Sprache und/oder die Übersetzungen über Requisiten weitergeben
  • Nachteile: Es sieht umständlich in der Anwendung aus

Wenn Sie eine andere Idee haben, sagen Sie es bitte!

Wie machst du das?

112

Nachdem ich einige Lösungen ausprobiert habe, denke ich, dass ich eine gefunden habe, die gut funktioniert und eine idiomatische Lösung für React 0.14) sein sollte (dh es werden keine Mixins, sondern Komponenten höherer Ordnung verwendet) (edit: auch perfekt mit React 15!).

Also hier die Lösung, beginnend von unten (die einzelnen Komponenten):

Die Komponente

Das einzige, was Ihre Komponente (gemäß Konvention) benötigen würde, ist ein strings Requisiten. Es sollte ein Objekt sein, das die verschiedenen Zeichenfolgen enthält, die Ihre Komponente benötigt, aber in Wirklichkeit liegt die Form bei Ihnen.

Es enthält die Standardübersetzungen, sodass Sie die Komponente an einer anderen Stelle verwenden können, ohne dass eine Übersetzung erforderlich ist (in diesem Beispiel wird die Standardsprache Englisch verwendet).

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);

Die Komponente höherer Ordnung

Im vorherigen Snippet haben Sie möglicherweise Folgendes in der letzten Zeile bemerkt: translate('MyComponent')(MyComponent)

translate ist in diesem Fall eine Komponente höherer Ordnung, die Ihre Komponente umschließt und einige zusätzliche Funktionen bereitstellt (diese Konstruktion ersetzt die Mixins früherer Versionen von React).

Das erste Argument ist ein Schlüssel, der zum Nachschlagen der Übersetzungen in der Übersetzungsdatei verwendet wird (ich habe hier den Namen der Komponente verwendet, aber es könnte alles Mögliche sein). Die zweite (beachten Sie, dass die Funktion curryed ist, um ES7-Dekoratoren zu ermöglichen) ist die Komponente selbst, die verpackt werden soll.

Hier ist der Code für die Übersetzungskomponente:

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;
    };
}

Es ist keine Zauberei: Es wird nur die aktuelle Sprache aus dem Kontext gelesen (und dieser Kontext überschneidet sich nicht in der gesamten Codebasis, die hier in diesem Wrapper verwendet wird), und dann wird das relevante Zeichenfolgenobjekt aus den geladenen Dateien abgerufen. Diese Logik ist in diesem Beispiel ziemlich naiv und könnte so gemacht werden, wie Sie es wirklich wollen.

Wichtig ist, dass die aktuelle Sprache aus dem Kontext entnommen und unter Berücksichtigung des angegebenen Schlüssels in Zeichenfolgen konvertiert wird.

Ganz oben in der Hierarchie

In der Root-Komponente müssen Sie lediglich die aktuelle Sprache aus Ihrem aktuellen Status festlegen. Im folgenden Beispiel wird Redux als Flux-ähnliche Implementierung verwendet, sie kann jedoch problemlos mit einem anderen Framework/Muster/einer anderen Bibliothek konvertiert werden.

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);

Und zum Abschluss die Übersetzungsdateien:

Übersetzungsdateien

// 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'
    }
};

Was denkt ihr?

Ich denke, dies löst all das Problem, das ich in meiner Frage zu vermeiden versuchte: Die Übersetzungslogik blutet nicht im gesamten Quellcode, sondern ist ziemlich isoliert und ermöglicht die Wiederverwendung der Komponenten ohne sie.

Zum Beispiel muss MyComponent nicht mit translate () umschlossen werden und kann separat sein, sodass es von allen anderen wiederverwendet werden kann, die strings auf eigene Faust bereitstellen möchten.

[Edit: 31/03/2016]: Ich habe kürzlich an einem Retrospective Board (für Agile Retrospectives) gearbeitet, das mit React & Redux) erstellt wurde und mehrsprachig ist Ein reales Beispiel in den Kommentaren, hier ist es:

Den Code finden Sie hier: https://github.com/antoinejaussoin/retro-board/tree/master

107

Nach meiner Erfahrung besteht der beste Ansatz darin, einen i18n-Redux-Status zu erstellen und ihn aus verschiedenen Gründen zu verwenden:

1- Hiermit können Sie den Anfangswert aus der Datenbank, der lokalen Datei oder sogar aus einer Template-Engine wie EJS oder Jade übergeben

2- Wenn der Benutzer die Sprache ändert, können Sie die gesamte Anwendungssprache ändern, ohne die Benutzeroberfläche zu aktualisieren.

3- Wenn der Benutzer die Sprache ändert, können Sie die neue Sprache auch von der API, der lokalen Datei oder sogar von Konstanten abrufen

4- Sie können auch andere wichtige Dinge mit den Zeichenfolgen wie Zeitzone, Währung, Richtung (RTL/LTR) und Liste der verfügbaren Sprachen speichern

5- Sie können die Änderungssprache als normale Redux-Aktion definieren

6- Sie können Ihre Back-End- und Front-End-Zeichenfolgen an einer Stelle haben, zum Beispiel in meinem Fall verwende ich i18n-node zur Lokalisierung und wenn der Benutzer die Sprache der Benutzeroberfläche ändert, führe ich einfach einen normalen API-Aufruf durch und Im Backend gebe ich nur i18n.getCatalog(req) zurück. Dadurch werden alle Benutzerzeichenfolgen nur für die aktuelle Sprache zurückgegeben

Mein Vorschlag für den i18n-Ausgangszustand ist:

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

Zusätzliche nützliche Module für i18n:

1- string-template Damit können Sie Werte zwischen Ihre Katalogzeichenfolgen einfügen, zum Beispiel:

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

2- Human-Format Mit diesem Modul können Sie eine Zahl in/aus einer von Menschen lesbaren Zeichenfolge konvertieren. Beispiel:

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

3- momentjs die bekannteste Datums- und Uhrzeitbibliothek, Sie können moment übersetzen, aber es ist bereits eine Übersetzung integriert, die Sie benötigen, um die aktuelle Staatssprache zu übergeben, zum Beispiel:

import moment from "moment";

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

Aktualisierung (14/06/2019)

Gegenwärtig gibt es viele Frameworks, die dasselbe Konzept mithilfe der React Context API (ohne Redux) implementieren. Ich persönlich empfehle I18next

17

Die Lösung von Antoine funktioniert einwandfrei, weist jedoch einige Einschränkungen auf:

  • Es wird der Kontext React direkt verwendet, den ich vermeide, wenn ich Redux bereits verwende
  • Es importiert direkt Phrasen aus einer Datei, was problematisch sein kann, wenn Sie die benötigte Sprache zur Laufzeit clientseitig abrufen möchten
  • Es wird keine i18n-Bibliothek verwendet, die leichtgewichtig ist, Sie haben jedoch keinen Zugriff auf praktische Übersetzungsfunktionen wie Pluralisierung und Interpolation

Deshalb haben wir redux-polyglot auf Redux- und AirBNBs polyglot aufgebaut.
(Ich bin einer der Autoren)

Es bietet :

  • ein Reduzierer zum Speichern der Sprache und der entsprechenden Nachrichten in Ihrem Redux-Shop. Sie können beides angeben, indem Sie
    • eine Middleware, die Sie so konfigurieren können, dass bestimmte Aktionen abgefangen, die aktuelle Sprache abgezogen und zugehörige Nachrichten abgerufen werden.
    • direkter Versand von setLanguage(lang, messages)
  • ein Selektor getP(state), der ein P -Objekt abruft, das 4 Methoden verfügbar macht:
    • t(key): ursprüngliche polyglotte T-Funktion
    • tc(key): Übersetzung mit Großbuchstaben
    • tu(key): Übersetzung in Großbuchstaben
    • tm(morphism)(key): benutzerdefinierte verwandelte Übersetzung
  • ein getLocale(state) Selektor, um die aktuelle Sprache abzurufen
  • eine translate Komponente höherer Ordnung, um Ihre React durch Injizieren des p Objekts in Requisiten zu verbessern

Einfaches Anwendungsbeispiel:

neue Sprache versenden:

import setLanguage from 'redux-polyglot/setLanguage';

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

im Bauteil:

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);

Bitte sagen Sie mir, wenn Sie Fragen/Anregungen haben!

5
Jalil

Aus meiner Forschung in diesem Bereich scheint es zwei Hauptansätze zu geben, die in JavaScript verwendet werden: ICU und gettext .

Ich habe bisher nur gettext verwendet, daher bin ich voreingenommen.

Was mich erstaunt ist, wie schlecht der Support ist. Ich komme aus der PHP Welt, entweder CakePHP oder WordPress. In beiden Situationen ist es ein grundlegender Standard, dass alle Zeichenfolgen einfach von __('') umgeben werden und dann weiter unten In dieser Zeile erhalten Sie sehr einfach Übersetzungen mit PO-Dateien.

gettext

Sie werden mit sprintf vertraut, wenn es darum geht, Zeichenfolgen zu formatieren, und PO-Dateien können von Tausenden verschiedener Agenturen problemlos übersetzt werden.

Es gibt zwei beliebte Optionen:

  1. i18next , wobei die Verwendung in diesem arkency.com-Blogpost beschrieben wird
  2. Jed , mit Verwendung beschrieben durch sentry.io post und dies React + Redux post ,

Beide unterstützen Gettext-Stile, formatieren Strings im Sprintf-Stil und importieren/exportieren PO-Dateien.

i18next hat eine React-Erweiterung von sich selbst entwickelt. Jed nicht. Sentry.io scheint eine benutzerdefinierte Integration von Jed mit React zu verwenden. Der React + Redux post , schlägt vor, mit

Werkzeuge: jed + po2json + jsxgettext

Wie auch immer, Jed scheint eine zielgerichtetere Implementierung zu sein - das ist die ausdrückliche Absicht, die i18next als Option hat.

ICU

Dies bietet mehr Unterstützung für die Edge-Fälle rund um Übersetzungen, z. für den Umgang mit Geschlecht. Ich denke, Sie werden die Vorteile davon sehen, wenn Sie komplexere Sprachen zum Übersetzen haben.

Eine beliebte Option hierfür ist messageformat.js . In diesem sentry.io-Blog-Tutorial kurz besprochen. messageformat.js wird tatsächlich von derselben Person entwickelt, die Jed geschrieben hat. Er macht ziemlich starke Behauptungen für die Verwendung der Intensivstation :

Jed ist meiner Meinung nach komplett ausgestattet. Ich bin glücklich, Fehler zu beheben, bin aber im Allgemeinen nicht daran interessiert, der Bibliothek mehr hinzuzufügen.

Ich pflege auch messageformat.js. Wenn Sie keine spezielle Gettext-Implementierung benötigen, kann MessageFormat verwendet werden, da es Plural/Gender besser unterstützt und integrierte Gebietsschemadaten enthält.

Grober Vergleich

gettext mit 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 (meine beste Vermutung aus dem Lesen der Anleitung ):

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' });
2
icc97

Falls noch nicht geschehen, ist ein Blick auf https://react.i18next.com/ möglicherweise ein guter Rat. Es basiert auf i18next: Einmal lernen - überall übersetzen.

Ihr Code sieht ungefähr so ​​aus:

<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>

Kommt mit Mustern für:

  • webpack
  • cra
  • expo.js
  • next.js
  • storybook-Integration
  • knurren
  • dat
  • ...

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

Außerdem sollten Sie den Workflow während der Entwicklung und später für Ihre Übersetzer berücksichtigen -> https://www.youtube.com/watch?v=9NOzJhgmyQE

1
jamuhl

Ich möchte eine einfache Lösung mit create-react-app vorschlagen.

Die Anwendung wird für jede Sprache separat erstellt, daher wird die gesamte Übersetzungslogik aus der Anwendung entfernt.

Der Webserver stellt automatisch die richtige Sprache bereit, abhängig von der Überschrift Accept-Language , oder manuell, indem ein Cookie gesetzt wird .

Meistens ändern wir die Sprache nicht mehr als einmal, wenn überhaupt.

Übersetzungsdaten werden in derselben Komponentendatei abgelegt, in der sie verwendet werden, zusammen mit Stilen, HTML und Code.

Und hier haben wir eine völlig unabhängige Komponente, die für den eigenen Zustand, die Ansicht und die Übersetzung verantwortlich ist:

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',
    }
);

Füge eine Sprachumgebungsvariable zu deinem package.json hinzu

"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",

Das ist es!

Meine ursprüngliche Antwort enthielt auch einen monolithischeren Ansatz mit einer einzelnen JSON-Datei für jede Übersetzung:

lang/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);
0
Igor Sukharev