i18n con solo React Hooks

Home/Stories/i18n react hooks

Matteo Granzotto - Jul 20, 2020

#React Hooks#Multi language#no-redux#I18n#translations#localizations#traduzioni#localizzazioni

In questo articolo spieghiamo come creare un sistema di internalizzazione/i18n utilizzando solamente React Hooks. Questo articolo segue il primo articolo della serie Come implementare un sistema di internalizzazione senza nessuna libreria.

Il primo articolo della serie Come Implementare Un Sistema Di Internalizzazione Senza Nessuna Libreria tratta React Native e può essere letto qui.

Puoi vedere il codice di questo tutorial qui. Puoi provare la demo qui.

Ricorda di adattare il codice alle tue best practice preferite e lo stile che ritieni più opportuno.

Impostare l'ambiente di lavoro

Esegui i seguenti comandi:

npx create-react-app i18n-only-with-react-hooks
cd i18n-only-with-react-hooks
npm run eject

Rispondi yes alla domanda che ti viene proposta:

? Are you sure you want to eject? This action is permanent. 

Dopo queste azioni ti ritroverai con la seguente struttura:

i18n-only-with-react-hooks
├── README.md
├── node_modules
├── package.json
├── package-lock.json
├── .gitignore
├── config
│   ├── webpack.config.js
│   ├── ...
│   └── Altre cartelle e file
├── scripts
│   ├── build.js
│   ├── start.js
│   └── test.js
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── serviceWorker.js
    └── setupTests.js

Esegui poi:

npm i

Creare le seguenti cartelle all'interno di src:

  • assets;
  • components;
  • screens;
  • translate.

e dentro ad ognuna di queste cartelle, crea un file index.js. In ogni file index.js andremo ad esportare le sotto cartelle. La sintassi per gli export che andremo ad utilizzare è:

export { default as ComponentName/ServiceName/etc } from "./ComponentNameFolder/ServiceNameFolder/etc";

Modifica il file config/webpack.config.js - in particolare l'array alias dell'oggetto resolve - come segue:

'Assets': path.resolve(__dirname, '../src/assets/'), 
'Components': path.resolve(__dirname, '../src/components/'),
'Screens': path.resolve(__dirname, '../src/screens/'),
'Translate': path.resolve(__dirname, '../src/translate/'),

in questo modo saremo capaci di fare come segue in ogni componente:

import { ComponentName } from 'Components';
import { ServiceName } from 'Services';
...

ed anche di espostare i file di internalizzazione - il modulto Translate. Se preferisci puoi continuare ad utilizzare i path relativi, la logica rimane la stessa.

Ora riorganizzeremo i file generati dal comando npm run eject.

Iniziando dalla cartella assets, spostiamo logo.svg dentro una nuova cartella chiamata images. Nel file index, esportiamo i file:

export { default as Logo } from './images/logo.svg';

Ora, per i componenti, spostiamo App.css, App.js e App.test.js dentro una nuova cartella App. Poi, rinominiamoli in style.css, index.js and index.test.js. Dentro il nuovo file App/index.js modifichiamo come segue:

  • import './App.css'; in import './style.css';;
  • import logo from './logo.svg';in import { Logo as logo } from 'Assets';.

Alla fine otterremo:

src/index.js:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import { App } from "Components";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(<App />, document.getElementById("root"));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Il servizio di traduzione

Andremo, ora, a creare il servizio di traduzione con tutte le funzioni necessarie a tradurre la nostra applicazione.

La funzionalità principali utilizzate di React hooks sono:

  • createContext;
  • useContext;
  • useReducer.

Dentro la cartella translate, creiamo due nuove sotto cartelle:

  • Languages;
  • Translate.

Languages - File di traduzione

La nuova cartella Languages conterrà i file JSON con tutte le label della lingua interessata:

// src/translate/Languages/en.json 

{
  "Application.title": "Wavelop",
  "Application.subTitle": "i18n internalization with only React hooks - translate",

  "Application.footer": "Developed by Wavelop",

  "LanguageSwitcher.used": "Lang selected:",
  "LanguageSwitcher.it": "Italiano",
  "LanguageSwitcher.en": "English",
  "LanguageSwitcher.fr": "Français"
}
// src/translate/Languages/it.json

{
  "Application.title": "Wavelop",
  "Application.subTitle": "i18n internalizzazione with only React hooks - traduzioni",

  "Application.footer": "Sviluppato da Wavelop",
  "LanguageSwitcher.used": "Lingua selezionata: "
}
// src/translate/Languages/fr.json 

{
  "Application.title": "Wavelop (French translation)",
  "Application.subTitle": "i18n internalization with only React hooks - translate (French translation)",

  "Application.footer": "Developed by Wavelop (French translation)",
  "LanguageSwitcher.used": "Lang selected: (French translation) "
}

Come si può vedere, ci sono delle label che non sono presenti nei file di traduzione per l'italiano e il francese. Questo non sarà un problema perché il sistema utilizzerà una lingua di fallback per le label mancanti.

Translate - funzioni di traduzione

La nuova cartella Translate conterrà tutte le funzioni di utilità che permetteranno al servizio di eseguire le traduzioni. Il file sarà come segue:

// src/translate/Translate/index.js

let _currentLanguage = "";
let _fallbackLanguage = "";
let _languages = [];
let _translations = {};

export const getCurrentLanguage = () => {
  return _currentLanguage;
};

export const setCurrentLanguage = currentLanguage => {
  _currentLanguage = currentLanguage;
};

export const getFallbackLanguage = () => {
  return _fallbackLanguage;
};

export const setFallbackLanguage = fallbackLanguage => {
  _fallbackLanguage = fallbackLanguage;
};

export const getLanguages = () => {
  return _languages;
};

export const setLanguages = languages => {
  _languages = languages;

  _languages.forEach(language => {
    const loadedLanguage = require(`../Languages/${language}.json`);
    _translations[language] = loadedLanguage;
  });
};

export const getTranslations = () => {
  return _translations;
};

export const setTranslations = translations => {
  _translations = translations;
};

export const t = label => {
  return _translations[_currentLanguage] &&
    _translations[_currentLanguage][label]
    ? _translations[_currentLanguage][label]
    : _translations[_fallbackLanguage] &&
      _translations[_fallbackLanguage][label]
    ? _translations[_fallbackLanguage][label]
    : label;
};

Le integrazioni degli Hooks

Usando la combinazione di createContext, useContext e useReducer saremo capaci di creare un sistema che aggiornerà le label di tutta l'applicazione.

// src/translate/index.js

import React, { createContext, useContext, useReducer } from "react";

import {
  getCurrentLanguage,
  setCurrentLanguage,
  getFallbackLanguage,
  setFallbackLanguage,
  getLanguages,
  setLanguages,
  getTranslations,
  setTranslations,
  t
} from "./Translate";

// Configuration
const { language, fallBacklanguage, languages } = {
  language: "en",
  fallBacklanguage: "en",
  languages: ["it", "fr", "en"]
};

// Init language properties

setCurrentLanguage(language);
setFallbackLanguage(fallBacklanguage);
setLanguages(languages);

// Contexts
const TranslateContext = createContext();
const TranslateStateContext = createContext();
const TranslateDispatchContext = createContext();

// Reducers
function translateReducer(state, action) {
  switch (action.type) {
    case "CHANGE_LANGUAGE": {
      setCurrentLanguage(action.language);
      return { ...state, language: action.language };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

// Initial state
const initialState = {
  language
};

export const TranslateProvider = props => {
  const value = {
    getCurrentLanguage: props.getCurrentLanguage || getCurrentLanguage,
    setCurrentLanguage: props.setCurrentLanguage || setCurrentLanguage,
    getFallbackLanguage: props.getFallbackLanguage || getFallbackLanguage,
    setFallbackLanguage: props.setFallbackLanguage || setFallbackLanguage,
    getLanguages: props.getLanguages || getLanguages,
    setLanguages: props.setLanguages || setLanguages,
    getTranslations: props.getTranslations || getTranslations,
    setTranslations: props.setTranslations || setTranslations,
    t: props.t || t
  };
  const [state, dispatch] = useReducer(translateReducer, initialState);

  return (
    <TranslateContext.Provider value={value}>
      <TranslateStateContext.Provider value={state}>
        <TranslateDispatchContext.Provider value={dispatch}>
          {props.children}
        </TranslateDispatchContext.Provider>
      </TranslateStateContext.Provider>
    </TranslateContext.Provider>
  );
};

export const useTranslate = () => {
  // You can use the function of provider
  const context = useContext(TranslateContext);
  if (context === undefined) {
    throw new Error("useTranslate must be used within a TranslateProvider");
  }
  return context;
};

export const useTranslateState = () => {
  const context = useContext(TranslateStateContext);
  if (context === undefined) {
    throw new Error("useTranslateState must be used within a TranslateProvider");
  }
  return context;
};

export const useTranslateDispatch = () => {
  const context = useContext(TranslateDispatchContext);
  if (context === undefined) {
    throw new Error("useTranslateDispatch must be used within a TranslateProvider");
  }
  return context;
};

Creiamo tre contesti per poter inniettare all'interno dell'applicazione le funzioni di utilità, lo stato e la funzione dispatch. Lo stato espone tutte le label per la lingua corrente e la funzione di dispatch permetterà di eseguire il cambio di lingua. Le funzioni di utilità saranno utilizzare per scopi diversi, quello principale sarà la funzione per ottenere le traduzioni a partire da una determinata stringa.

Il componente per cambiare lingua

Per poter cambiare lingua, sarà necessario creare un componente che, mediante la funzione dispatch, esegua l'azione di switch. Creiamo una nuova cartella all'interno di components che chiameremo LanguageSwitcher. Dentro la nuova cartella, creiamo due nuovi file - index.js e style.js:

// src/components/LanguageSwitcher/index.js

// NPM dependencies
import React from "react";

// Application dependencies
import {
  useTranslate,
  useTranslateDispatch,
  useTranslateState
} from "Translate";
import "./style.css";

function LanguageSwitcher() {
  const { language } = useTranslateState(); // we get the current language
  const i18n = useTranslate(); // we get the utils functions
  const { t, getLanguages } = i18n;
  const dispatch = useTranslateDispatch();

  const items = getLanguages().map(key => {
    return key !== language ? (
      <button
        key={key}
        onClick={() => {
          dispatch({ type: "CHANGE_LANGUAGE", language: key });
        }}
      >
        {t(`LanguageSwitcher.${key}`)}
      </button>
    ) : (
      ""
    );
  });

  return (
    <section>
      <span>{t(`LanguageSwitcher.used`)}  {t(`LanguageSwitcher.${language}`)}</span>
      <span>{items}</span>
    </section>
  );
}

export default LanguageSwitcher;

Possiamo lasciare vuoto il file di stile src/components/LanguageSwitcher/style.js.

Infine, aggiungiamo in src/components/index.js l'export di quanto fatto:

export { default as LanguageSwitcher } from "./LanguageSwitcher";

Uniamo quanto fatto grazie al TranslateProvider

Creiamo una pagina per mostrare quanto fatto: creaimo un componente HelloWorld dentro la cartella screen. All'interno di ques'ultima cartella, creiamo due nuovi file - index.js e style.js:

// src/screens/HelloWorld/index.js

import React from "react";
import { Logo as logo } from "Assets";
import "./style.css";
import { useTranslate } from "Translate";
import { LanguageSwitcher } from "Components";

function HelloWorld() {

  const i18n = useTranslate();
  const { t } = i18n;

  return (
      <span className="HelloWorld">
        <header>
          <h1>{t("Application.title")}</h1>
          <h2>{t("Application.subTitle")}</h2>
          <img src={logo} className="HelloWorld-logo" alt="logo" />
        </header>
        <main>
          <LanguageSwitcher></LanguageSwitcher>
        </main>

        <footer>{t("Application.footer")}</footer>
      </span>
  );
}

export default HelloWorld;

Possiamo lasciare vuoto il file di stile src/screens/HelloWorld/style.js.

Infine, aggiungiamo in src/screens/index.js l'export di quanto fatto:

export {default as HelloWorld} from './HelloWorld';

A questo punto torniamo nel file src/components/App/index.js ed aggiorniamo come segue:

// src/components/App/index.js

import React from "react";
import "./style.css";
import { TranslateProvider } from "Translate";
import { HelloWorld } from "Screens";

function App() {
  return (
    <TranslateProvider>
      <HelloWorld />
    </TranslateProvider>
  );
}

export default App;

Lo stile del componente App non è più necessario, cancelliamo il contenuto del file style.js.

La struttura finale del progetto sarà:

i18n-only-with-react-hooks
├── README.md
├── node_modules
├── package.json
├── package-lock.json
├── .gitignore
├── config
│   ├── webpack.config.js
│   ├── ...
│   └── Other folder and files
├── scripts
│   ├── build.js
│   ├── start.js
│   └── test.js
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── index.css
    ├── index.js
    ├── serviceWorker.js
    ├── setupTests.js
    ├── assets
    │   ├── images
    |   │   └── logo.svg   
    │   └── index.js
    ├── components
    │   ├── App
    |   │   ├── index.js   
    |   │   └── style.css   
    │   ├── LanguageSwitcher
    |   │   ├── index.js   
    |   │   └── style.css  
    │   └── index.js  
    ├── screens
    │   ├── HelloWorld
    |   │   ├── index.js   
    |   │   └── style.css  
    │   └── index.js
    └── translate
        ├── Languages
        │   ├── en.json  
        │   ├── fr.json   
        │   └── it.json  
        ├── Translate
        │   └── index.js  
        └── index.js

Ora che tutto è pronto, eseguiamo npm run start e visualizziamo il risultato in localhost:3000.

Demo

Riferimenti

Conclusioni

Con la combinazione delle API di React Hooks è semplice creare un sistema di internalizzazione i18n per qualsiasi sito o applicazione.

Il primo articolo della serie Come Implementare Un Sistema Di Internalizzazione Senza Nessuna Libreria tratta React Native e può essere letto qui.

Puoi vedere il codice di questo tutorial qui. Puoi provare la demo qui.

Se hai qualsiasi domanda, scrivici nella chat o via emil a info@wavelop.com.