Implementare multi-lingua (i18n) senza nessuna libreria in React Native

Home/Stories/Multi lingua senza nessuna libreria in React Native

Franco Berton - Sep 07, 2019

#reactnative#multilanguage#redux#i18n

Questo articolo spiega come implementare un multi lingua in React Native manualmente senza appoggiarsi a moduli pre-esistenti.

Index

Introduzione: Questo articolo vuole essere una guida introduttiva su come realizzare un multi lingua (i18n) in React Native senza l'appoggio di librerie terze, descrivendo gli step necessari e concludendo attraverso un'analisi riflessiva della soluzione proposta.

Pre-requisiti: La seguente guida ha come prerequiito l'integrazione del modulo react-redux per la condivisione degli stati e delle traduzioni

1. Definizione dei file di traduzione

Il primo passo per l'implementazione di un sistema di internazionalizzazione è definire una cartella, che chiameremo i18n, contenente i file JSON con le varie traduzioni.

Nel nostro caso defineremo due file JSON per supportare la lingua inglese e la lingua italiana. Di seguito presentiamo la struttura della nostra cartella:

ios
android
src
└── i18n
    ├── en.json // English
    └── it.json // Italian

All'interno di ogni file definiamo le proprietà identificative delle traduzioni:

//en.json
{
  "selectLanguage": "Select language",
  "languageEn": "English",
  "languageIt": "Italian",
  "welcome": "Welcome",
  "hello": "Hello",
  "howareyou": "How are you?"
}

//it.json
{
  "selectLanguage": "Seleziona il linguaggio",
  "languageEn": "Inglese",
  "languageIt": "Italiano",
  "welcome": "Benvenuto",
  "hello": "Ciao",
  "howareyou": "Come stai?"
}

2. Implementazione di un servizio di internazionalizzazione

A questo punto è necessario definire un servizio di internazionalizzazione tramite la definizione di un reducer e di una action. L'implementazione del servizio richiederà una struttura architetturale come questa:

ios
android
src
└── i18n
└── services
    └── Intl
    │   ├── reducers.js
    │   ├── actions.js
    │   └── index.js
    ├── mapDispatchToProps.js
    ├── mapStateToProps.js
    └── index.js

Il reducer fornirà un metodo per impostare le traduzioni della lingua selezionata. Questo metodo sarà invocata da una action per gestire le traduzioni della lingua.

Di seguito, presentiamo il metodo per impostare le traduzioni:

const setLanguage = language => {
  let messages = {};
  switch (language) {
    case 'it':
      messages = Object.assign(messages, require(`../../i18n/it.json`));
      break;
    default:
    case 'en':
      messages = Object.assign(messages, require(`../../i18n/en.json`));
      break;
  }
  return messages;
};

Il metodo precedente verrà invocato da un altro metodo per gestire l'action di aggiornamento della lingua:

const intlData = (state = initialState, action) => {
  if (action === undefined) return state;
  switch (action.type) {
    case 'UPDATE_LANGUAGE':
      return {
        locale: action.language,
        messages: setLanguage(action.language)
      };
    default:
      return state;
  }
};

Ricapitolando, il reducer si presenterà così:

//reducers.js
const setLanguage = language => {
  let messages = {};
  switch (language) {
    case 'it':
      messages = Object.assign(messages, require(`../../i18n/it.json`));
      break;
    default:
    case 'en':
      messages = Object.assign(messages, require(`../../i18n/en.json`));
      break;
  }
  return messages;
};

const initialState = {
  locale: 'it',
  messages: setLanguage('it')
};

const intlData = (state = initialState, action) => {
  if (action === undefined) return state;
  switch (action.type) {
    case 'UPDATE_LANGUAGE':
      return {
        locale: action.language,
        messages: setLanguage(action.language)
      };
    default:
      return state;
  }
};
export default intlData;

L'action di aggiornamento della lingua si presenterà come segue:

//actions.js
const updateLanguage = language => {
  return dispatch => {
    dispatch({
      type: 'UPDATE_LANGUAGE',
      language
    });
  };
};

export default {
  updateLanguage
};

Infine, all'interno di un file index.js esportiamo il reducer e l'action:

//index.js
export { default as reducers } from './reducers';
export { default as actions } from './actions';

e registriamo il reducer all'interno della funzione combineReducers di redux per catturare le action invocate:

//services/index.js
import { combineReducers } from 'redux';
import { reducers as IntlReducers } from './Intl';

// Functions to catch actions dispatched
export default combineReducers({
  IntlReducers
});

3. Definizione di un componente per la connessione a redux

Affinchè il servizio di internazionalizzazione possa essere condiviso fra le varie features, è necessario creare un componente di appoggio per facilitare la connessione a redux delle features applicative.

Questo componente permette la condivisione delle actions e degli stati relativi all'invocazione delle actions.

//ConnectedComponent/index.js
import { connect } from 'react-redux';

import mapStateToProps from '../../services/mapStateToProps';
import mapDispatchToProps from '../../services/mapDispatchToProps';

export default InputComponent => {
  // High Order Component
  return connect(
    mapStateToProps,
    mapDispatchToProps
  )(InputComponent);
};

La mappatura delle actions e degli stati viene definita in due file separati:

//services/mapDispatchToProps.js
import { actions as IntlAction } from './Intl';
const mapDispatchToProps = {
  IntlAction
};
export default mapDispatchToProps;
//services/mapStateToProps.js
const mapStateToProps = state => {
  return {
    intlData: state.IntlReducers
  };
};
export default mapStateToProps;

4. Integrazione di un componente per il cambio lingua

Ora, andiamo a creare il componente di cambio lingua all'interno della cartella dei componenti components, definendo un'alberatura come questa:

ios
android
src
└── components
    └── LanguageSwitcher
    │   └── index.js
    └── index.js

Nella classe di definizione del componente, andiamo a definire il metodo di aggiornamento della lingua, che invocherà l'action, estratta dalle props, per aggiornare lo stato globale contenente la lingua e le relative traduzioni.

_updateLanguage = lang => {
  const { updateLanguage } = this.props;

  updateLanguage(lang);
};

La classe verrà arrichita con il componente Picker per selezionare la lingua desiderata.

//LanguageSwitcher/index.js
import React, { Component } from 'react';
import { View, Picker } from 'react-native';

class LanguageSwitcher extends Component {
  constructor(props) {
    super(props);
  }

  _updateLanguage = lang => {
    const { updateLanguage } = this.props;

    updateLanguage(lang);
  };

  render() {
    const { intlData } = this.props;
    const languages = [
      { code: 'en', name: intlData.messages['languageEn'] },
      { code: 'it', name: intlData.messages['languageIt'] }
    ];
    const options = languages.map(language => {
      return (
        <Picker.Item
          value={language.code}
          key={language.code}
          label={language.name}
        />
      );
    });

    return (
      <View>
        <Text>{intlData.messages['selectLanguage']}</Text>
        <Picker
          selectedValue={intlData.locale}
          onValueChange={itemValue => this._updateLanguage(itemValue)}
        >
          {options}
        </Picker>
      </View>
    );
  }
}

export default LanguageSwitcher;

5. Connessione di una feature a redux per la condivisione dei contenuti tradotti

Attraverso questa sezione, andiamo a creare una feature chiamata Settings a cui verrà incluso il componente di cambio lingua e dei contenuti derivanti dai file di traduzione La feature verrà connessa a redux affinchè possa disporre dello stato contenente le traduzioni.

Di seguito presentiamo l'implementazione del componente Settings:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { ConnectedComponent as connect } from '../../components';

class Settings extends Component {
  render() {
    const { intlData } = this.props;
    return (
      <View>
        <LanguageSwitcher {...this.props} />
        <Text>{intlData.messages['welcome']}</Text>
        <Text>{intlData.messages['hello']}</Text>
        <Text>{intlData.messages['howareyou']}</Text>
      </View>
    );
  }
}

export default connect(Settings);

6. Risultato

7. Conclusioni

La soluzione proposta vuole evidenziare com'è possibile in pochi semplici step realizzare un servizio di internazionalizzazione senza l'utilizzo di librerie esterne.

Questo tipo di approccio non deve essere visto come un'alternativa all'utilizzo di moduli come react-native-localize, ma bensì una soluzione volta a realizzare un multi-lingua semplice ed immediato.

Il codice della soluzione proposta è visualizzabile in questo repository Github.

Se vi è un piaciuto il mio articolo, vi invito a condividerlo.

Il vostro supporto e feedback significa molto per noi.