React / Redux e app multilingue (internazionalizzazione) - Architettura


119

Sto creando un'app che dovrà essere disponibile in più lingue e localizzazioni.

La mia domanda non è puramente tecnica, ma piuttosto sull'architettura e sui modelli che le persone stanno effettivamente utilizzando nella produzione per risolvere questo problema. Non sono riuscito a trovare da nessuna parte alcun "libro di cucina" per questo, quindi mi rivolgo al mio sito web di domande / risposte preferito :)

Ecco i miei requisiti (sono davvero "standard"):

  • L'utente può scegliere la lingua (banale)
  • Dopo aver cambiato la lingua, l'interfaccia dovrebbe tradursi automaticamente nella nuova lingua selezionata
  • Al momento non sono troppo preoccupato per la formattazione di numeri, date ecc., Voglio una soluzione semplice per tradurre solo le stringhe

Ecco le possibili soluzioni che potrei pensare:

Ogni componente si occupa della traduzione in modo isolato

Ciò significa che ogni componente ha ad esempio un insieme di file en.json, fr.json ecc. Accanto alle stringhe tradotte. E una funzione di supporto per aiutare a leggere i valori da quelli che dipendono dalla lingua selezionata.

  • Pro: più rispettoso della filosofia React, ogni componente è "autonomo"
  • Contro: non puoi centralizzare tutte le traduzioni in un file (per fare in modo che qualcun altro aggiunga una nuova lingua per esempio)
  • Contro: è comunque necessario far passare il linguaggio corrente come oggetto di scena, in ogni componente sanguinosa e nei propri figli

Ogni componente riceve le traduzioni tramite gli oggetti di scena

Quindi non sono consapevoli della lingua corrente, prendono solo un elenco di stringhe come oggetti di scena che corrispondono alla lingua corrente

  • Pro: poiché quelle stringhe provengono "dall'alto", possono essere centralizzate da qualche parte
  • Contro: ogni componente è ora collegato al sistema di traduzione, non puoi semplicemente riutilizzarne uno, devi specificare ogni volta le stringhe corrette

Ignori un po 'gli oggetti di scena e possibilmente usi il contesto per trasmettere la lingua corrente

  • Pro: è per lo più trasparente, non è necessario passare la lingua corrente e / o le traduzioni tramite oggetti di scena tutto il tempo
  • Contro: sembra ingombrante da usare

Se hai altre idee, per favore dillo!

Come si fa?


2
Preferisco l'idea di un oggetto di chiavi con stringhe di traduzione che viene tramandato come sostegno, non è necessario passare singolarmente ogni stringa come sostegno. La modifica di questo a un livello superiore dovrebbe attivare un nuovo rendering. Non penso che l'uso del contesto sia una buona idea per questo, e ogni componente che ha accesso al file di traduzione li rende meno "stupidi" e portabili in realtà imo (e più difficile da far rieseguire l'app per il cambio di lingua).
Dominic

1
In realtà, secondo facebook.github.io/react/docs/context.html , l'utilizzo del contesto per condividere la lingua corrente è uno dei casi d'uso legittimi. L'approccio che sto provando ora è quello di utilizzare questo più un componente di ordine superiore per gestire la logica di estrazione delle stringhe per quel particolare componente (probabilmente basato su qualche chiave)
Antoine Jaussoin

1
Forse puoi anche dare un'occhiata a Instant . Affrontano questo problema in un modo completamente diverso affrontandolo nel frontend ala Optimizely (ovvero alterando il DOM durante il caricamento).
Marcel Panse

1
Non è affatto male! È davvero una bestia completamente diversa (il che ti lega a un servizio che potresti dover pagare se il tuo sito web cresce), ma mi piace l'idea e probabilmente ne vale la pena per un piccolo sito web che devi far funzionare velocemente!
Antoine Jaussoin

4
Inoltre, potresti menzionare che sei un co-fondatore di Instant, invece di dire "Loro" come se non avessi nulla a che fare con loro :)
Antoine Jaussoin

Risposte:


110

Dopo aver provato alcune soluzioni, penso di averne trovata una che funziona bene e dovrebbe essere una soluzione idiomatica per React 0.14 (cioè non usa mixin, ma componenti di ordine superiore) ( modifica : anche perfettamente bene con React 15 ovviamente! ).

Quindi ecco la soluzione, partendo dal basso (i singoli componenti):

Il componente

L'unica cosa di cui il tuo componente avrebbe bisogno (per convenzione), è un file strings oggetto di scena. Dovrebbe essere un oggetto contenente le varie stringhe di cui il tuo Componente ha bisogno, ma in realtà la forma dipende da te.

Contiene le traduzioni predefinite, quindi puoi utilizzare il componente da qualche altra parte senza la necessità di fornire alcuna traduzione (funzionerebbe immediatamente con la lingua predefinita, l'inglese in questo esempio)

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

Il componente di ordine superiore

Nello snippet precedente, potresti averlo notato nell'ultima riga: translate('MyComponent')(MyComponent)

translate in questo caso è un componente di ordine superiore che avvolge il componente e fornisce alcune funzionalità extra (questa costruzione sostituisce i mixin delle versioni precedenti di React).

Il primo argomento è una chiave che verrà utilizzata per cercare le traduzioni nel file di traduzione (ho usato il nome del componente qui, ma potrebbe essere qualsiasi cosa). Il secondo (notare che la funzione è curry, per consentire ai decoratori ES7) è il Componente stesso di avvolgere.

Ecco il codice per il componente di traduzione:

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

Non è magico: leggerà semplicemente la lingua corrente dal contesto (e quel contesto non si spande su tutta la base di codice, usata solo qui in questo wrapper), e quindi otterrà l'oggetto stringhe pertinente dai file caricati. Questo pezzo di logica è abbastanza ingenuo in questo esempio, potrebbe essere fatto nel modo in cui vuoi davvero.

La parte importante è che prende la lingua corrente dal contesto e la converte in stringhe, data la chiave fornita.

Al vertice della gerarchia

Sul componente root, devi solo impostare la lingua corrente dal tuo stato attuale. L'esempio seguente utilizza Redux come implementazione simile a Flux, ma può essere facilmente convertito utilizzando qualsiasi altro framework / pattern / libreria.

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

E per finire, i file di traduzione:

File di traduzione

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

Che cosa ne pensate?

Penso che risolva tutto il problema che stavo cercando di evitare nella mia domanda: la logica di traduzione non sanguina in tutto il codice sorgente, è abbastanza isolata e consente di riutilizzare i componenti senza di essa.

Ad esempio, MyComponent non ha bisogno di essere racchiuso da translate () e potrebbe essere separato, consentendo il suo riutilizzo da parte di chiunque altro desideri fornire il stringscon le proprie intenzioni.

[Modifica: 31/03/2016]: Recentemente ho lavorato su una Retrospective Board (per Agile Retrospectives), costruita con React & Redux, ed è multilingue. Poiché molte persone hanno chiesto un esempio di vita reale nei commenti, eccolo qui:

Puoi trovare il codice qui: https://github.com/antoinejaussoin/retro-board/tree/master


Questa è una soluzione interessante .. ti chiedi se sei ancora d'accordo con questo dopo pochi mesi? Non ho trovato molti consigli in termini di consigli sui modelli per questo online
Damon

2
In realtà lo sono, ho scoperto che funziona perfettamente (per le mie esigenze). Fa funzionare il componente senza traduzione per impostazione predefinita, e la traduzione viene semplicemente sopra senza che il componente se ne
accorga

1
@ l.cetinsoy puoi usare l' dangerouslySetInnerHTMLelica, fai solo attenzione alle implicazioni (disinfetta manualmente l'input). Vedi facebook.github.io/react/tips/dangerously-set-inner-html.html
Teodor Sandu

6
C'è una ragione per cui non hai provato React-Intl?
SureshCS

1
Piace molto questa soluzione. Una cosa che aggiungerei che abbiamo trovato molto utile per la coerenza e il risparmio di tempo è che se hai molti componenti con stringhe comuni potresti trarre vantaggio dalle variabili e dalla diffusione su oggetti, ad esempioconst formStrings = { cancel, create, required }; export default { fooForm: { ...formStrings, foo: 'foo' }, barForm: { ...formStrings, bar: 'bar' } }
Huw Davies

18

Dalla mia esperienza l'approccio migliore è creare uno stato redux i18n e usarlo, per molte ragioni:

1- Questo ti consentirà di passare il valore iniziale dal database, file locale o anche da un motore di modelli come EJS o jade

2- Quando l'utente cambia la lingua, è possibile modificare l'intera lingua dell'applicazione senza nemmeno aggiornare l'interfaccia utente.

3- Quando l'utente cambia la lingua, questo ti permetterà anche di recuperare la nuova lingua dall'API, dal file locale o anche dalle costanti

4- Puoi anche salvare altre cose importanti con le stringhe come fuso orario, valuta, direzione (RTL / LTR) e l'elenco delle lingue disponibili

5- È possibile definire il cambio di lingua come una normale azione di redux

6- Puoi avere le stringhe di backend e front-end in un unico posto, ad esempio nel mio caso uso i18n-node per la localizzazione e quando l'utente cambia la lingua dell'interfaccia utente eseguo semplicemente una normale chiamata API e nel backend, torno semplicemente i18n.getCatalog(req)questo restituirà tutte le stringhe utente solo per la lingua corrente

Il mio suggerimento per lo stato iniziale di 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",
}

Moduli extra utili per i18n:

1- string-template questo ti permetterà di inserire valori tra le stringhe del tuo catalogo, ad esempio:

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

2- in formato umano questo modulo ti consentirà di convertire un numero in / da una stringa leggibile dall'uomo, ad esempio:

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

3- momentjs la libreria npm di date e orari più famose, puoi tradurre moment ma ha già una traduzione incorporata, devi solo passare la lingua di stato corrente, ad esempio:

import moment from "moment";

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

Aggiornamento (14/06/2019)

Attualmente, ci sono molti framework che implementano lo stesso concetto usando l'API per il contesto di reazione (senza redux), personalmente ho consigliato I18next


Questo approccio funzionerebbe anche per più di due lingue? Considerando l'allestimento del catalogo
tempranova

Ho votato verso il basso. Questo non risponde alla domanda. OP ha chiesto un'idea di architettura, non un suggerimento o un confronto con nessuna libreria i18n.
TrungDQ

9
Ho suggerito il catalogo i18n come stato redux, sembra che tu non capisca redux
Fareed Alnamrouti

5

La soluzione di Antoine funziona bene, ma ci sono alcuni avvertimenti:

  • Usa direttamente il contesto React, cosa che tendo ad evitare quando uso già Redux
  • Importa direttamente le frasi da un file, il che può essere problematico se si desidera recuperare la lingua necessaria in fase di esecuzione, lato client
  • Non utilizza alcuna libreria i18n, che è leggera, ma non ti dà accesso a utili funzionalità di traduzione come la pluralizzazione e l'interpolazione

Ecco perché abbiamo creato redux-polyglot su Redux e Polyglot di AirBNB .
(Sono uno degli autori)

Fornisce :

  • un riduttore per memorizzare la lingua e i messaggi corrispondenti nel tuo negozio Redux. Puoi fornire entrambi:
    • un middleware che è possibile configurare per rilevare un'azione specifica, detrarre la lingua corrente e ottenere / recuperare i messaggi associati.
    • invio diretto di setLanguage(lang, messages)
  • un getP(state)selettore che recupera un Poggetto che espone 4 metodi:
    • t(key): funzione T poliglotta originale
    • tc(key): traduzione in maiuscolo
    • tu(key): traduzione in maiuscolo
    • tm(morphism)(key): traduzione personalizzata con morph
  • un' getLocale(state) selettore per ottenere la lingua corrente
  • un translatecomponente di ordine superiore per migliorare i tuoi componenti React iniettando l' poggetto in oggetti di scena

Esempio di utilizzo semplice:

invia nuova lingua:

import setLanguage from 'redux-polyglot/setLanguage';

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

in componente:

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

Per favore dimmi se hai qualche domanda / suggerimento!


1
Frasi originali molto migliori da tradurre. E per creare uno strumento che analizzi tutti i componenti per le _()funzioni, ad esempio per ottenere tutte quelle stringhe. Quindi puoi tradurlo in un file di lingua più facilmente e non fare confusione con variabili pazze. In alcuni casi, le pagine di destinazione richiedono una parte specifica del layout per essere visualizzata in modo diverso. Quindi dovrebbero essere disponibili anche alcune funzioni intelligenti su come scegliere l'impostazione predefinita rispetto ad altre scelte possibili.
Roman M. Koss

Ciao @Jalil, c'è da qualche parte un esempio completo con middleware?
ArkadyB

Ciao @ArkadyB, lo usiamo in produzione su diversi progetti che non sono open-source. Puoi trovare maggiori informazioni sul modulo README: npmjs.com/package/redux-polyglot Hai qualche domanda / difficoltà ad usarlo?
Jalil

Il mio problema principale con questo e polyglot.js è che sta reinventando completamente la ruota piuttosto che costruire sopra i file PO. Questa libreria alternativa sembra promettente npmjs.com/package/redux-i18n . Non penso che stia andando molto diversamente: sta solo fornendo un livello aggiuntivo per la conversione da e verso i file PO.
icc97

2

Dalla mia ricerca su questo aspetto sembrano esserci due approcci principali utilizzati per i18n in JavaScript, ICU e gettext .

Ho sempre usato solo gettext, quindi sono di parte.

Quello che mi stupisce è quanto sia scarso il supporto. Vengo dal mondo PHP, CakePHP o WordPress. In entrambe le situazioni, è uno standard di base da cui tutte le stringhe sono semplicemente circondate__('') , quindi più in basso si ottengono traduzioni utilizzando file PO molto facilmente.

gettext

Ottieni la familiarità di sprintf per la formattazione delle stringhe e i file PO verranno tradotti facilmente da migliaia di agenzie diverse.

Ci sono due opzioni popolari:

  1. i18next , con l'utilizzo descritto in questo post del blog arkency.com
  2. Jed , con l'utilizzo descritto dal post sentry.io e da questo post React + Redux ,

Entrambi supportano lo stile gettext, la formattazione di stringhe in stile sprintf e importano / esportano in file PO.

i18next ha un'estensione React sviluppata da loro stessi. Jed no. Sentry.io sembra utilizzare un'integrazione personalizzata di Jed con React. Il post React + Redux suggerisce di utilizzare

Strumenti: jed + po2json + jsxgettext

Tuttavia, Jed sembra un'implementazione più focalizzata su gettext - ovvero è intenzione espressa, mentre i18next lo ha solo come opzione.

ICU

Questo ha più supporto per i casi limite intorno alle traduzioni, ad esempio per trattare con il genere. Penso che ne vedrai i benefici se avrai lingue più complesse in cui tradurre.

Un'opzione popolare per questo è messageformat.js . Discusso brevemente in questo tutorial del blog sentry.io . messageformat.js è in realtà sviluppato dalla stessa persona che ha scritto Jed. Fa affermazioni piuttosto forti per l'utilizzo di ICU :

Jed è una funzionalità completa secondo me. Sono felice di correggere i bug, ma generalmente non sono interessato ad aggiungere altro alla libreria.

Gestisco anche messageformat.js. Se non hai specificamente bisogno di un'implementazione di gettext, potrei suggerire di usare MessageFormat, poiché ha un supporto migliore per plurali / genere e ha dati locali incorporati.

Confronto approssimativo

gettext con 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 (la mia ipotesi migliore dalla lettura della guida ):

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

Ho votato verso il basso. Questo non risponde alla domanda. OP ha chiesto un'idea di architettura, non un suggerimento o un confronto con nessuna libreria i18n.
TrungDQ

@TrungDQ Questo è ciò che l'OP ha chiesto: "La mia domanda non è puramente tecnica, ma piuttosto sull'architettura e sui modelli che le persone stanno effettivamente utilizzando in produzione per risolvere questo problema." . Questi sono due modelli che vengono utilizzati nella produzione.
icc97

Secondo me questa risposta non fornisce le informazioni che sto (e altri stanno cercando). Le informazioni che hai fornito sono utili, ma forse per un'altra domanda. Voglio solo contribuire con il mio voto negativo per far apparire la risposta giusta in cima (spero).
TrungDQ

@TrungDQ Se non è quello che stai cercando, dai un voto positivo a quello che hai usato e ignora gli altri invece di votare per le risposte perfettamente valide che non corrispondono alla parte specifica della domanda che ti interessa.
ICC97

1

Se non l'hai ancora fatto, dare un'occhiata a https://react.i18next.com/ potrebbe essere un buon consiglio. Si basa su i18next: impara una volta - traduci ovunque.

Il tuo codice avrà un aspetto simile a:

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

Viene fornito con campioni per:

  • webpack
  • cra
  • expo.js
  • next.js
  • integrazione del libro di fiabe
  • razzle
  • dat
  • ...

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

Oltre a ciò, dovresti anche considerare il flusso di lavoro durante lo sviluppo e successivamente per i tuoi traduttori -> https://www.youtube.com/watch?v=9NOzJhgmyQE


Questo non risponde alla domanda. OP ha chiesto un'idea di architettura, non un suggerimento o un confronto con nessuna libreria i18n.
TrungDQ

@TrungDQ come con il tuo commento sulla mia risposta che hai svalutato - l'OP ha chiesto le soluzioni correnti utilizzate nella produzione. Tuttavia avevo suggerito i18next nella mia risposta di ritorno a febbraio
ICC97

0

Vorrei proporre una soluzione semplice utilizzando l' app create-react .

L'applicazione verrà creata per ogni lingua separatamente, quindi l'intera logica di traduzione verrà spostata fuori dall'applicazione.

Il server web servirà automaticamente la lingua corretta, a seconda dell'intestazione Accept-Language , o manualmente impostando un cookie .

Per lo più, non cambiamo lingua più di una volta, se mai del tutto)

I dati di traduzione vengono inseriti nello stesso file componente, che lo utilizza, insieme a stili, html e codice.

E qui abbiamo una componente completamente indipendente che è responsabile del proprio stato, vista, traduzione:

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

Aggiungi la variabile d'ambiente della lingua al tuo 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",

Questo è tutto!

Anche la mia risposta originale includeva un approccio più monolitico con un singolo file json per ogni traduzione:

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

Non funzionerebbe solo in fase di compilazione? Senza la possibilità per l'utente di cambiare la lingua al volo? Sarebbe quindi un caso d'uso diverso.
Antoine Jaussoin

L'app verrà compilata per ogni lingua necessaria. Il server web fornirà automaticamente la versione corretta, a seconda dell'intestazione "Accept-Language" o da un cookie impostato dall'utente al volo. In questo modo, l'intera logica di traduzione potrebbe essere spostata fuori dall'app.
Igor Sukharev
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.