Construyendo una app multi-lenguaje con React. 馃寪

Franklin Martinez - Feb 17 '23 - - Dev Community

En la actualidad, crear una app que soporte varios idiomas se vuelve mas indispensable para llegar a un gran alcance con los usuarios. Asi que en esta ocasi贸n, con ayuda de React vamos a construirlo.

Tabla de contenido.

馃搶 Tecnolog铆as a utilizar.

馃搶 Creando el proyecto.

馃搶 Primeros pasos.

馃搶 Configurando i18n.

馃搶 Usando useTranslation.

馃搶 Mover las traducciones a archivos separados.

馃搶 Conclusi贸n.

馃搶 Demostraci贸n.

馃搶 C贸digo fuente.

馃埖 Tecnolog铆as a utilizar. m

  • 鈻讹笍 React JS 18.2.0
  • 鈻讹笍 i18next 22.4.9
  • 鈻讹笍 Vite JS 4.0.0
  • 鈻讹笍 TypeScript 4.9.3
  • 鈻讹笍 CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

馃埖 Creando el proyecto.

Al proyecto le colocaremos el nombre de: multi-lang-app (opcional, tu le puedes poner el nombre que gustes).

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.

Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

cd multi-lang-app
Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode Exit fullscreen mode

Despu茅s abrimos el proyecto en un editor de c贸digo (en mi caso VS code).

code .
Enter fullscreen mode Exit fullscreen mode

馃埖 Primeros pasos.

Primero vamos a instalar una librer铆a para poder crear rutas en nuestra app. En este caso usaremos react-router-dom

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Creamos una carpeta src/pages y dentro creamos 2 archivos que ser谩n nuestras paginas y ser谩n muy sencillas

  1. Home.tsx
export const Home = () => {

    return (
        <main>
            <h1>Multi-language app</h1>
            <span>Select another language!</span>
        </main>
    )
}
Enter fullscreen mode Exit fullscreen mode
  1. About.tsx
export const About = () => {

    return (
        <main>
            <h1>About</h1>
        </main>
    )
}
Enter fullscreen mode Exit fullscreen mode

Tambi茅n crearemos un componente Menu sencillo para que se puedan move entre rutas y cambiar el idioma desde cualquier ruta.

Pero antes, vamos a definir los lenguajes a usar, en un archivo aparte. En mi caso los creare en una carpeta src/constants creamos un archivo index.ts y agregamos:

export const LANGUAGES = [
    { label: 'Spanish', code: 'es' },
    { label: 'English', code: 'en' },
    { label: 'Italian', code: 'it' },
]
Enter fullscreen mode Exit fullscreen mode

Ahora si, creamos una carpeta src/components y dentro el archivo Menu.tsx y agregamos lo siguiente:

import { NavLink } from 'react-router-dom';
import { LANGUAGES } from '../constants';

const isActive = ({ isActive }: any) => `link ${isActive ? 'active' : ''}`

export const Menu = () => {

    return (
        <nav>
            <div>
                <NavLink className={isActive} to='/'>Home</NavLink>
                <NavLink className={isActive} to='/about'>About</NavLink>
            </div>

            <select defaultValue={'es'} >
                {
                    LANGUAGES.map(({ code, label }) => (
                        <option
                            key={code}
                            value={code}
                        >{label}</option>
                    ))
                }
            </select>
        </nav>
    )
}
Enter fullscreen mode Exit fullscreen mode

Finalmente crearemos nuestro router en el archivo src/App.tsx, agregando las paginas y el componente Menu.

import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { Menu } from './components/Menu';
import { About } from './pages/About';
import { Home } from './pages/Home';

const App = () => {

  return (
    <BrowserRouter>
      <Menu />
      <Routes>
        <Route path='/' element={<Home />} />
        <Route path='/about' element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Y listo, ya tenemos una aplicaci贸n sencilla de dos rutas.

馃埖 Configurando i18n.

Primero vamos a instalar estas dependencias.

npm install i18next react-i18next
Enter fullscreen mode Exit fullscreen mode

react-i18next es el paquete que nos ayudar谩 a traducir nuestras paginas en un proyecto de React de una forma m谩s f谩cil, pero para ello necesita otro paquete que es i18next para realizar la configuraci贸n de la internacionalizaci贸n

Asi que b谩sicamente, i18next es el ecosistema en si, y react-i18next es el plugin para complementarlo.

Ahora vamos a crear un nuevo archivo nombrado i18n.ts lo crearemos en la dentro de la carpeta src (src/i18n.ts).
Dentro vamos a importar el paquete de i18next y vamos a acceder al m茅todo use porque vamos a cargar el plugin de initReactI18next para usar la internacionalizaci贸n con React mas f谩cil.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n.use(initReactI18next)

export default i18n;
Enter fullscreen mode Exit fullscreen mode

Ahora accederemos a su m茅todo init para agregar un objeto de configuraci贸n.

  • lng: Lenguaje por defecto.
  • fallbackLng: Lenguaje que se cargara en caso de que las traducciones que el usuario busca no est谩n disponibles.
  • resources: un objeto con las traducciones que se usaran en la aplicaci贸n.
  • interpolation.escapeValue: sirve para escapar los valores y evitar ataques XSS, lo pondremos en false, porque React ya lo hace por defecto.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18
    .use(initReactI18next)
    .init({
        lng: 'en',
        fallbackLng: 'en',
        interpolation:{
            escapeValue: false
        },
        resources: {}
    });

export default i18n;
Enter fullscreen mode Exit fullscreen mode

En la parte de resources, tiene que crearse de la siguiente manera:

La llave del objeto debe ser el c贸digo del lenguaje, en este caso "en" de "English" y luego dentro un objeto translation que dentro vendr谩n todas las traducciones, identificadas por llave-valor.

Y es importante, mantener el mismo nombre de la llave de los objetos, lo 煤nico que cambia es su valor. Nota como en ambos objetos translation, dentro tienen la misma clave de title

resources:{
    en: {
        translation: {
            title: 'Multi-language app',
        }
    },
    es: {
        translation: {
            title: 'Aplicaci贸n en varios idiomas',
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

Asi quedar铆a nuestro archivo una vez agregada las traducciones.

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n
    .use(i18nBackend)
    .use(initReactI18next)
    .init({
        fallbackLng: 'en',
        lng: getCurrentLang(),
        interpolation:{
            escapeValue: false
        },
        resources: {
            en: {
                translation: {
                    title: 'Multi-language app',
                    label: "Select another language!",
                    about: 'About',
                    home: 'Home'
                }
            },
            es: {
                translation: {
                    title: 'Aplicaci贸n en varios idiomas',
                    label: "Selecciona otro lenguaje!",
                    about: 'Sobre m铆',
                    home: 'Inicio'
                }
            },
            it: {
                translation: {
                    title: 'Applicazione multilingue',
                    label: "Selezionare un'altra lingua ",
                    about: 'Su di me',
                    home: 'Casa'
                }
            },
        }
    });

export default i18n;
Enter fullscreen mode Exit fullscreen mode

Finalmente este archivo solo lo importaremos en el archivo src/main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

import './i18n'

import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
Enter fullscreen mode Exit fullscreen mode

馃埖 Usando useTranslation.

Bueno ahora que terminamos la configuraci贸n de i18n, vamos a usar las traducciones que creamos. Asi que en el archivo src/components/Menu.tsx

Vamos a usar el hook que nos da react-i18next que es el useTranslation

De este hook, recuperamos el objeto i18nm y la funci贸n t

const { i18n, t } = useTranslation()
Enter fullscreen mode Exit fullscreen mode

Para usar las traducciones es de la siguiente manera:

Mediante brackets ejecutamos la funci贸n t que recibe como par谩metro un string que hace referencia a la llave de alg煤n valor que esta dentro del objeto translation que configuramos anteriormente. (Verifica en tu configuraci贸n del archivo i18n.ts exista un objeto con la llave home y que contenga un valor).

Dependiendo de lenguaje por defecto que coloques, este se mostrara.

<NavLink className={isActive} to='/'>
    {t('home')}
</NavLink>
Enter fullscreen mode Exit fullscreen mode

Bueno, ahora vamos a cambiar entre idiomas.

  • Primero una funci贸n que se ejecute cada vez que el select cambie
  • Accedemos al valor del evento.
  • Mediante el objeto i18n accedemos al m茅todo changeLanguage y le pasamos el valor por par谩metro.
    const onChangeLang = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const lang_code = e.target.value
        i18n.changeLanguage(lang_code)
    }
Enter fullscreen mode Exit fullscreen mode

Ahora si cambias entre idiomas veras como cambian los textos de tu app.

El archivo Menu.tsx quedar铆a asi.

import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router-dom';
import { LANGUAGES } from '../constants/index';

const isActive = ({ isActive }: any) => `link ${isActive ? 'active' : ''}`

export const Menu = () => {

    const { i18n, t } = useTranslation()

    const onChangeLang = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const lang_code = e.target.value
        i18n.changeLanguage(lang_code)
    }

    return (
        <nav>
            <div>
                <NavLink className={isActive} to='/'>{t('home')}</NavLink>
                <NavLink className={isActive} to='/about'>{t('about')}</NavLink>
            </div>

            <select defaultValue={i18n.language} onChange={onChangeLang}  >
                {
                    LANGUAGES.map(({ code, label }) => (
                        <option
                            key={code}
                            value={code}
                        >{label}</option>
                    ))
                }
            </select>
        </nav>
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a las otras paginas para agregar la traducci贸n a los textos.

Home.tsx

import { useTranslation } from 'react-i18next';

export const Home = () => {

    const { t } = useTranslation()

    return (
        <main>
            <h1>{t('title')}</h1>
            <span>{t('label')} </span>
        </main>
    )
}
Enter fullscreen mode Exit fullscreen mode

About.tsx

import { useTranslation } from 'react-i18next';

export const About = () => {

    const { t } = useTranslation()

    return (
        <main>
            <h1>{t('about')}</h1>
        </main>
    )
}
Enter fullscreen mode Exit fullscreen mode

Bueno, ahora digamos r谩pidamente te mostrare como interpolar variables.

Dentro de la funci贸n t, el segundo par谩metro es un objeto, el cual le puedes especificar la variable a interpolar.

Nota que yo le agrego la propiedad name. Bueno entonces esta propiedad name, la tengo que tener muy en cuenta

import { useTranslation } from 'react-i18next';

export const About = () => {

    const { t } = useTranslation()

    return (
        <main>
            <h1>{t('about')}</h1>
            <span>{t('user', { name: 'Bruce Wayne 馃' })}</span>

        </main>
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a un archivo json (pero lo que sea haga en uno, se tiene que replicar en todos los archivos json de traducciones).

  • Primero agrego la nueva propiedad user, ya que no la tenia antes.
  • Luego mediante corchetes dobles agrego el nombre de la propiedad que le asigne antes, el cual era name.
{
    "title": "Multi-language app",
    "label": "Select another language!",
    "about": "About me",
    "home": "Home",
    "user": "My name is: {{name}}"
}
Enter fullscreen mode Exit fullscreen mode

Y de esa manera interpolamos valores.

馃埖 Mover las traducciones a archivos separados.

Pero que pasa cuando las traducciones son demasiadas, entonces tu archivo i18n.ts se saldr铆a de control. Lo mejor sera moverlas a archivos separados.

Para esto necesitaremos instalar otro plugin.

npm install i18next-http-backend
Enter fullscreen mode Exit fullscreen mode

Este plugin cargara los recursos desde un servidor, por lo que sera bajo demanda.

Ahora vamos a crear dentro de la carpeta public una carpeta i18n (public/i18n).
Y dentro vamos a ir creando archivos .json que ser谩n nombrados seg煤n sea su traducci贸n, por ejemplo.
El archivo es.json sera para las traducciones en Espa帽ol, el archivo it.json sera solo para las traducciones en italiano, etc.
Al final tendremos 3 archivos porque en esta app solo manejamos 3 idiomas.

Luego, movemos cada contenido del objeto translation del archivo i18n.ts a su archivo JSON correspondiente.
Por ejemplo el archivo en.json.

{
    "title": "Multi-language app",
    "label": "Select another language!",
    "about": "About",
    "home": "Home"
}
Enter fullscreen mode Exit fullscreen mode

Una vez echo eso con los 3 archivos, vamos al i18n.ts y vamos a modificar algunas cosas.

  • Primero la propiedad resources la vamos a quitar.
  • Vamos a importar el paquete de i18next-http-backend y mediante el m茅todo use, se lo pasamos como par谩metro para que ejecute ese plugin.
import i18n from 'i18next';
import i18nBackend from "i18next-http-backend";
import { initReactI18next } from 'react-i18next';

i18n
    .use(i18nBackend)
    .use(initReactI18next)
    .init({
        fallbackLng: 'en',
        lng: 'en',
        interpolation:{
            escapeValue: false
        }
    });

export default i18n;
Enter fullscreen mode Exit fullscreen mode

Finalmente, necesitamos agregar una nueva propiedad, la cual es backend que recibe un objeto, al cual accederemos a la propiedad loadPath.

La propiedad loadPath, recibe una funci贸n que contiene el lenguaje y debe retornar un string.
Pero una manera mas sencilla es interpolando la variable lng.

Asi tendremos nuestro path de donde se obtendr谩n las traducciones, nota que estoy apuntando a la carpeta public.

Ahora cuando quieras agregar un nuevo idioma, solo agregas el archivo json en la carpeta i18n dentro de public.

import i18n from 'i18next';
import i18nBackend from "i18next-http-backend";
import { initReactI18next } from 'react-i18next';

i18n
    .use(i18nBackend)
    .use(initReactI18next)
    .init({
        fallbackLng: 'en',
        lng: 'en',
        interpolation:{
            escapeValue: false
        },
        backend: {
            loadPath: 'http://localhost:5173/i18n/{{lng}}.json',
        }
    });

export default i18n;
Enter fullscreen mode Exit fullscreen mode

Pero hay un paso mas que hacer, si notas en la propiedad loadedPath, el host es http://localhost:5173 y cuando lo suba a producci贸n, no funcionaran las traducciones por lo cual debemos validar si estamos en modo desarrollo o no, para poder agregar el host correcto.

import i18n from 'i18next';
import i18nBackend from "i18next-http-backend";
import { initReactI18next } from 'react-i18next';

const getCurrentHost = import.meta.env.MODE === 'development' ? 'http://localhost:5173' : 'LINK TO PROD'

i18n
    .use(i18nBackend)
    .use(initReactI18next)
    .init({
        fallbackLng: 'en',
        lng: 'en',
        interpolation:{
            escapeValue: false
        },
        backend: {
            loadPath: `${getCurrentHost}/i18n/{{lng}}.json`,
        }
    });

export default i18n;
Enter fullscreen mode Exit fullscreen mode

Un tip m谩s, es que las traducciones como est谩n en el backend podr铆an seguir siendo cargadas mientras la pagina ya esta lista, por lo que es aconsejable manejar un Suspense en la app.

import { Suspense } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { Menu } from './components/Menu';
import { About } from './pages/About';
import { Home } from './pages/Home';

const App = () => {

    return (

    <Suspense fallback='loading'>
        <BrowserRouter>
        <Menu />
        <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/about' element={<About />} />
        </Routes>
      </BrowserRouter>
    </Suspense>

  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

El componente Suspense pone en pausa la app hasta que este lista, y en la propiedad fallback es lo que se le muestra al usuario mientras espera a que la aplicaci贸n este lista, aqu铆 es un lugar perfecto para poner un loading o spinner.

Probablemente no se note una mejora considerable, ya que nuestra tiene muy pocas traducciones. Pero es una buena practica.

馃埖 Conclusi贸n.

Crear una app multi-idioma ahora resulta ser mas sencillo gracias a i18n y su plugins.

Espero que te haya gustado esta publicaci贸n y que tambi茅n espero haberte ayudado a entender como realizar este tipo de aplicaciones de una manera mas f谩cil. 馃檶

Si conoces alguna otra forma distinta o mejor de realizar esta aplicaci贸n con gusto puedes comentar todas tus observaciones y sugerencias, te lo agradecer铆a bastante!.

Te invito a que revises mi portafolio en caso de que est茅s interesado en contactarme para alg煤n proyecto! Franklin Martinez Lucas

馃數 No olvides seguirme tambi茅n en twitter: @Frankomtz361

馃埖 Demostraci贸n simple.

https://multi-lang-app-react.netlify.app/

馃埖 C贸digo fuente.

https://github.com/Franklin361/multi-lang-app

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player