← Volver al listado de artículos
Imagen de perfil de Lucía Aldana Castillo

Autor

Lucía Aldana Castillo

¡Suscríbete!

y mantente al tanto

Puedes darte de baja en el momento que lo desees

Comparte en tus redes

Internacionalización en Next.js 13 con I18Next

La internacionalización es un elemento esencial en el desarrollo de aplicaciones web, ya que permite llegar a audiencias globales y ofrecer una experiencia personalizada a usuarios de diferentes partes del mundo.

En este artículo, exploraremos cómo integrar I18Next a un proyecto Next.js 13 con app directory, gestionando de manera eficiente la traducción y adaptación de contenidos y elementos de la interfaz de usuario a varios idiomas.

Pero si necesitas agregar internacionalización a un proyecto sencillo que no requiere de funciones avanzadas, como la interpolación que ofrece I18Next, puedes intentar con está guía Internacionalización en NextJs-13 que te permitirá obtener un código más simple y fácil de mantener.

Crear el proyecto

Empecemos creando un proyecto con Next.js 13. Si ya tienes uno, puedes saltarte este paso.

npx create-next-app@latest
  
// What is your project named? … next13-internationalization
// Would you like to use TypeScript? … No / Yes ✅
// Would you like to use ESLint? … No / Yes ✅
// Would you like to use Tailwind CSS? … No / Yes ❌
// Would you like to use src/ directory? … No / Yes ✅
// Would you like to use App Router? (recommended) … No / Yes ✅
// Would you like to customize the default import alias? … No / Yes ❌

cd next13-internationalization
Read-only

Open browser consoleTests


Instalamos I18Next y creamos la nueva estructura

npm install i18next
Read-only

Open browser consoleTests

Crearemos la nueva estructura del proyecto donde usaremos el idioma para crear rutas dinámicas.

Estas rutas se crean agregando segmentos dinámicos a partir de datos dinámicos que se completan en el momento de la solicitud o se renderizan previamente en el momento de la compilación.

Este segmento dinámico lo creamos colocando el nombre de la carpeta entre corchetes, que en nuestro caso será [lng], y lo tendremos disponible como parámetros en layout, page, route y generateMetadata.

En este proyecto usaré los idiomas español e inglés, pero puedes agregar tantos como quieras.

La ruta de nuestro proyecto se verá cómo:

Ruta => app/[lng]/page.tsx

URL => http://localhost:3000/[lng] => [lng] será reemplazado por el lenguaje que usemos.
por ejemplo http://localhost:3000/es o http://localhost:3000/en donde 'es' y 'en' son nuestros datos dinámicos para la url en español e inglés respectivamente.

Parámetros => {lng: ['es', 'en']}
Read-only

Open browser consoleTests

La estructura quedaría de esta forma:

.src
  |__ app
      |__[lng]
          |__second-page
              |__page.tsx
          |__layout.tsx
          |__page.tsx
Read-only

Open browser consoleTests

Ahora agregamos la lista de idiomas para que esté disponible en el html en el archivo layout.tsx:

// src/app/[lng]/layout.tsx
  
import { dir } from 'i18next'; // i18n para agregar el lenguaje al html

const languages = ['en', 'de']; // define los lenguajes que necesites

export async function generateStaticParams() {
  return languages.map((lng) => ({ lng }));
}

export default function RootLayout({
  children,
  params: { lng }, // lng estará disponible por parámetro
}: {
  children: React.ReactNode,
  params: {
    lng: string,
  },
}) {
  return (
    // pasamos lng
    <html lang={lng} dir={dir(lng)}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}
Read-only

Open browser consoleTests

Ahora nuestra URL funcionará utilizando /es o /en que son los lenguajes que definimos en el archivo layout.tsx, ejemplo http://localhost:3000/es.

❗️WARNING❗️Nos encontraremos con un error 404 si utilizamos la ruta sin ese parámetro (http://localhost:3000), corrigamos eso en el siguiente paso.

Detectar el idioma


Primero instalamos la dependencia accept-language:

npm install accept-language --save
Read-only

Open browser consoleTests

Esta dependencia analiza la cabecera "Accept-Language" de las solicitudes HTTP para obtener información sobre las preferencias de idioma del cliente y luego ayuda a seleccionar la respuesta adecuada en el idioma preferido del usuario.

Ahora agregamos los archivos settings.ts dentro de una carpeta i18n y middleware.ts en la dentro de src:

.src
  |__ app
      |__i18n
          |__settings.ts
  |__middleware.ts
Read-only

Open browser consoleTests

  • settings.ts:

Definimos el idioma por defecto y la lista de idiomas que usaremos (en este proyecto será español e inglés).

// src/app/i18n/settings.ts

export const fallbackLng = 'es';
export const languages = [fallbackLng, 'en'];
Read-only

Open browser consoleTests

  • middleware.ts:

Este middleware nos permitirá redireccionar a la url adecuada.

// src/middleware.ts

import { NextResponse, NextRequest } from 'next/server';
import acceptLanguage from 'accept-language';
import { fallbackLng, languages } from './app/i18n/settings';

acceptLanguage.languages(languages);

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)'],
};

const cookieName = 'i18next';

export function middleware(req: NextRequest) {
  let lng;
  if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName)?.value);
  if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'));
  if (!lng) lng = fallbackLng;

  if (
    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url));
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer') as string | URL);
    const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`));
    const response = NextResponse.next();
    if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
    return response;
  }

  return NextResponse.next();
}
Read-only

Open browser consoleTests

Ahora si no agregamos el parámetro a la URL, nos redireccionará al idioma de nuestro navegador.

En mi caso http://localhost:3000/ me redirecciona a http://localhost:3000/es.

Probemos cambiar nuestro parámetro a 'en' y navegar.

Esto guardará la cookie i18next que definimos con el valor del parámetro. Luego, si borramos el parámetro de la URL nos redirecciona al último lenguaje utilizado, el guardado en la cookie i18next: en.


Crear los diccionarios


El diccionario es la base de nuestra internacionalización que nos permite ofrecer la experiencia de usuario en diferentes idiomas.

Dentro de la carpeta i18n que creamos, agregamos una carpeta locales, donde organizaremos los archivos de internacionalización de una manera estructurada. Creamos carpetas dentro de locales con el nombre de un idioma específico, como 'es' para español, 'en' para inglés, como definimos en languages en el archivo settings.ts.

Dentro de cada una de estas carpetas de idioma, crearemos los archivos .json que contendrán las traducciones para cada sección, como 'home' y 'second-page'. Todos los json deben respetar la misma estructura en cada carpeta.

// la carpeta `en` y `es` serán el diccionario para las traducciones en inglés y español respectivamente.
// Dentro tendremos las secciones separadas por archivos json:

.src
  |__ app
      |__i18n
          |__settings.ts
          |__locales
              |__en
                  |__common.json
                  |__home.json
                  |__second-page.json
              |__es
                  |__common.json
                  |__home.json
                  |__second-page.json
Read-only

Open browser consoleTests


I18Next en Server Components


Preparemos el hook useTranslation para las traducciones en Server Components.

Primero instalamos los siguientes paquetes:

npm install react-i18next i18next-resources-to-backend
Read-only

Open browser consoleTests

El primero es una integración de React con I18Next, permite la traducción y localización de aplicaciones React de manera sencilla. Y el segundo paquete ayuda a transformar recursos a un backend i18next.

En el archivo settings.ts agregamos las opciones de configuración, donde defaultNS define qué archivo .json del diccionario que creamos en locales se usará por defecto cuando no especificamos ninguno:

// src/app/i18n/settings.ts

export const fallbackLng = 'es';
export const languages = [fallbackLng, 'en'];
export const defaultNS = 'common'; // nombre del archivo .json de locales que usaremos por defecto

export function getOptions(lng = fallbackLng, ns: string | string[] = defaultNS) {
  return {
    supportedLngs: languages,
    fallbackLng,
    lng,
    fallbackNS: defaultNS,
    defaultNS,
    ns,
  };
}
Read-only

Open browser consoleTests

Ahora agregamos un archivo index.ts dentro de la carpeta i18n, aquí crearemos el hook useTranslation.

Estaremos creando una nueva instancia de i18n en cada llamado al hook useTranslation, que recibe el lenguaje que estamos requiriendo mostrar (lng) y el nombre del archivo .json que queremos usar (ns). Este último lo definimos en locationNS al final del index.ts, que no es más que una lista de los nombres de los archivos .json que tienen las traducciones (Esto nos evitará errores de tipeo).

// src/app/i18n/index.ts

import { createInstance, Namespace, KeyPrefix } from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next/initReactI18next';
import { getOptions } from './settings';

const initI18next = async (lng: string, ns: string | string[]) => {
  const i18nInstance = createInstance();
  await i18nInstance
    .use(initReactI18next)
    .use(resourcesToBackend((language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)))
    .init(getOptions(lng, ns));
  return i18nInstance;
};

export async function useTranslation<N extends Namespace, TKPrefix extends KeyPrefix<N>>(
  lng: string,
  ns?: any,
  options: { keyPrefix?: TKPrefix } = {}
) {
  const i18nextInstance = await initI18next(lng, ns);
  return {
    t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? (ns[0] as string[]) : (ns as string), options.keyPrefix),
    i18n: i18nextInstance,
  };
}

export const locationNS = {
  COMMON

Read-only

Open browser consoleTests

Ahora usaremos el hook useTranslation en la page Home para obtener las traducciones y configuraciones necesarias para mostrar contenido multilingüe en nuestra aplicación. Para asegurarnos de que la aplicación funcione de manera fluida y que no se bloquee mientras espera que las traducciones se carguen, utilizamos async/await al llamar a useTranslation.

El hook useTranslation requiere dos parámetros:

  • El lenguaje: estará disponible por parámetros.
  • La sección: la importamos desde la definición de locationNS en el archivo i18n/index.ts para evitar errores tipográficos.

Obtendremos la función t que, con la key definida en el archivo json, nos devolverá el string que queremos mostrar:

// src/app/[lng]/page.tsx

import { locationNS, useTranslation } from '../i18n';
import styles from './page.module.css';

const Home = async ({ params: { lng } }: { params: { lng: string } }) => {
  const { t } = await useTranslation(lng, locationNS.HOME); // Cambiar a la sección necesaria

  return (
    <main className={styles.main}>
      <div className={styles.description}>
        <p>{t('note')}</p>
      </div>
    </main>
  );
};

export default Home;

// http://localhost:3000 || http://localhost:3000/es || http://localhost:3000/en
Read-only

Open browser consoleTests

// src/app/i18n.locales/es/home.json
  
  {
    "note": "Esta es la página de inicio"
  }
  
// src/app/i18n.locales/en/home.json
  
  {
    "note": "This is the home page"
  }
Read-only

Open browser consoleTests


I18Next en Client Components


Usar nuestro hook useTranslation con async/await en un client component nos dará este error:

Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server.

Preparemos useTranslation para las traducciones en Client Components.

Instalamos el siguiente paquete que nos servirá para detectar automáticamente el idioma preferido del usuario en su navegador:

npm install i18next-browser-languagedetector
Read-only

Open browser consoleTests

Creamos un archivo client.ts dentro de la carpeta i18n, donde crearemos nuevamente useTranslation pero para las traducciones en client component.

// src/app/i18n/client.ts

'use client';
import { useEffect, useState } from 'react';
import i18next, { Namespace, KeyPrefix } from 'i18next';
import {
  initReactI18next,
  useTranslation as useTranslationOrg,
  UseTranslationOptions,
  UseTranslationResponse,
} from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { getOptions, languages } from './settings';

const runsOnServerSide = typeof window === 'undefined';

i18next
  .use(initReactI18next)
  .use(LanguageDetector)
  .use(resourcesToBackend((language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)))
  .init({
    ...getOptions(),
    lng: undefined,
    detection: {
      order: ['path', 'htmlTag', 'cookie', 'navigator'],
    },
    preload: runsOnServerSide ? languages : [],
  });

export function useTranslation<N extends Namespace, TKPrefix extends KeyPrefix<N> = undefined>(
  lng: string,
  ns?: N | Readonly<N>,
  options?: UseTranslationOptions<TKPrefix>
): UseTranslationResponse<N, TKPrefix> {
  const ret = useTranslationOrg(ns, options);
  const { i18n } = ret;
  if (runsOnServerSide && lng && i18next.resolvedLanguage !== lng) {
    i18next.changeLanguage(lng);
  } else {
    const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage);
    useEffect(() => {
      if (activeLng === i18n.resolvedLanguage) return;
      setActiveLng(i18n.resolvedLanguage);
    }, [activeLng, i18n.resolvedLanguage]);
    useEffect(() => {

Read-only

Open browser consoleTests

Usamos el nuevo useTranslation para client component en second-page.tsx:

'use client';
import { useState } from 'react';
import { locationNS } from '../../i18n';
import { useTranslation } from '../../i18n/client';
import styles from '../page.module.css';

const SecondPage = ({ params: { lng } }: { params: { lng: string } }) => {
  const { t } = useTranslation(lng, locationNS.SECOND_PAGE);

  const [color, setColor] = useState('orange');

  const changeColor = () => {
    const availableColours = ['red', 'green', 'blue', 'orange', 'purple'];
    const newColor = availableColours[Math.floor(Math.random() * availableColours.length)];
    setColor(newColor);
  };

  return (
    <main className={styles.main}>
      <div className={styles.description}>
        <p style={{ color: color }}>{t('note')}</p>
        <button className={styles.button} onClick={changeColor}>
          {t('action')}
        </button>
      </div>
    </main>
  );
};

export default SecondPage;

// http://localhost:3000 || http://localhost:3000/es/second-page || http://localhost:3000/en/second-page
Read-only

Open browser consoleTests

// src/app/i18n.locales/es/second-page.json
  
  {
    "note": "Esta es la segunda página"
    "action": "cambiar color"
  }
  
// src/app/i18n.locales/en/home.json
  
  {
    "note": "This is the second page"
    "action": "Change color"
  }
Read-only

Open browser consoleTests


Crear un switch de idioma


Para cambiar de idioma, lo que hacemos es usar la ruta dinámica que creamos con la carpeta [lng]. Entonces debemos navegar a la ruta del idioma que queramos.

El componente será un client component porque usaremos el hook usePathname.

Este hook (usePathname) es necesario para hacer la navegación en caso de que tengamos una url con más partes que el dominio, por ejemplo /second-page que tenemos definida en esta estructura. Tomamos el path completo y luego con una expresión regular, reemplazamos el idioma por el que elegimos en el switch.

En langRegex modificamos nuestro array de idiomas definido en settings.ts para usarlo en la expresión regular y no tener que agregar manualmente al switch algún idioma extra.

En el atributo href del elemento Link, sustituimos el idioma actual en la ruta de acceso (pathname) con el idioma que hemos seleccionado en el switch. Esto nos permite navegar a la misma página con el nuevo idioma elegido.

// src/components/SwitchLng/index.tsx

'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { languages } from '../../i18n/settings';
import styles from './styles.module.css';

const SwitchLng = ({ lng }: { lng: string }) => {
  const pathname = usePathname();
  const langRegex = languages.join('|');

  return (
    <div className={styles.languageSwitch}>
      {languages.map((lang, index) => (
        <Link
          href={pathname.replace(new RegExp(`^/(${langRegex})\b`), `/${lang}`)}
          key={lang}
          className={`${styles.link} ${styles[index === 0 ? 'firstLink' : '']} ${
            styles[index === languages.length - 1 ? 'lastLink' : '']
          } ${styles[lang === lng ? 'activeLng' : '']}`}
        >
          <span className={styles.language}>{lang.toUpperCase()}</span>
        </Link>
      ))}
    </div>
  );
};
export default SwitchLng;
Read-only

Open browser consoleTests


Para obtener más detalles y acceder al proyecto completo, te invito a visitar el repositorio del proyecto en GitHub.

¡Espero que esta guía te haya sido útil!


Referencias


Next.js Internationalization

Ejemplo de proyecto