La guía definitiva para la localización de JavaScript

Pon en marcha la localización de JavaScript para tu navegador con esta guía completa y prepara tu aplicación para usuarios internacionales.

Raro, maduro, expresivo y asombroso, JavaScript es el lenguaje de programación más utilizado en la actualidad. Siendo el lenguaje del navegador—y trabajando en el servidor con Node—JavaScript está en todas partes en las tecnologías web actuales.
Y con muchos frameworks móviles multiplataforma, envolturas de escritorio, motores de juegos e incluso frameworks de internet de las cosas (IoT), realmente es el mundo de JavaScript — nosotros solo vivimos en él.
Ahora, por supuesto, estás aquí porque quieres tomar todo eso 🔥 y aprender a localizar aplicaciones de JavaScript, preparándolas para una audiencia global. No tengas miedo: Esta guía cubrirá todo lo que necesitas saber para comenzar la localización de JavaScript en el navegador.
¡Vamos a rock && roll!

🔗 Recurso » Obtén todo el código que acompaña este artículo de nuestro repositorio de GitHub.

🗒 Nota » Internet Explorer (IE), con una participación de mercado global del 2,15%, puede considerarse un navegador obsoleto. Por brevedad, estamos omitiendo soluciones específicas de IE en esta guía. Si estás dando soporte a IE, asegúrate de verificar si las características integradas de JavaScript que cubrimos en este artículo requieren bifurcaciones (forks) o polyfills.

Overview

¿Cómo localizas una página web con JavaScript?

Si bien puede ser tentador tomar una biblioteca de internacionalización (i18n) lista para usar para tus necesidades de localización—y esa podría ser la elección correcta para tu proyecto—verás que JavaScript puro te servirá perfectamente bien para proyectos más pequeños. Crear una propia también te dará un buen recetario de técnicas de i18n que puedes usar con cualquier biblioteca que elijas.

🤿 Profundiza más » Nuestro artículo, Qué es I18n: Una definición simple de internacionalización, entra en más detalles sobre lo que son la internacionalización (i18n) y la localización (l10n).

✋🏽 Atención » Si estás construyendo una MPA (aplicación multipágina) tradicional, a menudo ocurre que gran parte de la localización se realiza en el propio servidor. Solo estamos trabajando con la localización del navegador aquí. Sin embargo, te lo ponemos fácil del lado del servidor, con un tutorial de i18n de Node y una guía de i18n de JavaScript de pila completa.

Está bien, digamos que tenemos una página que queremos localizar.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <!-- ... -->
  <title>Mi Appy Apperson</title>
</head>
<body>
  <div class="container">
    <h1>Mi Appy Apperson</h1>
    <p>¡Bienvenido a mi pequeño rincón en la web!</p>
  </div>
  <script src="js/scripts.js"></script>
</body>
</html>

Versión en inglés de una aplicación de demostración de JavaScript | Phrase

🔗 Recurso » Puedes obtener todo el código de la aplicación que estamos construyendo en esta sección en la carpeta vanilla en nuestro repositorio de GitHub.
🔗 Recurso » Estoy usando la biblioteca Skeleton CSS en caso de que te lo preguntes.

Esto se ve bien, pero no está exactamente listo para un entorno global, ¿verdad? Todo el contenido está integrado directamente en inglés. Hagamos un poco de i18n básico.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <!-- ... -->
  <title>Mi Appy Apperson</title>
</head>
<body>
  <div class="container">
    <h1 data-i18n-key="app-title">Mi Appy Apperson</h1>
    <p data-i18n-key="lead">¡Bienvenido/a a mi pequeño rincón en la web!</p>
  </div>
  <script src="js/scripts.js"></script>
</body>
</html>

Nota los atributos data-i18n-key que añadimos arriba a nuestros contenedores de texto. Podemos acceder a ellos cuando se carga el documento y reemplazar su texto con traducciones. De hecho, hagamos exactamente eso.

// El idioma activo
const locale = "en";
// Podemos tener tantos idiomas aquí como queramos,
// y usar cualquier idioma o configuración regional que queramos. Tenemos el idioma inglés
// y árabe como idiomas aquí a modo de ejemplo.
const translations = {
  // Traducciones al inglés
  "en": {
    "app-title": "Mi Appy Apperson",
    "lead": "¡Bienvenido a mi pequeño rincón en la web!",
  },
  // Traducciones en árabe
  "ar": {
    "app-title": "تطبيقي المطبق",
    "lead": "أهلاً بك في مكاني الصغير على النت.",
  },
};
// Cuando el contenido de la página esté listo...
document.addEventListener("DOMContentLoaded", () => {
  document
    // Encuentra todos los elementos que tienen el atributo key
    .querySelectorAll("[data-i18n-key]")
    .forEach(translateElement);
});
// Reemplaza el texto interno del elemento HTML dado
// con la traducción en el idioma activo,
// correspondiente a la clave i18n de datos del elemento
function translateElement(element) {
  const key = element.getAttribute("data-i18n-key");
  const translation = translations[locale][key];
  element.innerText = translation;
}

Ahora que ya lo tienes, cambia la segunda línea de arriba a const locale = "ar"; y recarga la página. Cuando se activa el evento DOMContentLoaded, nuestra página muestra las traducciones al árabe.
Versión árabe de una aplicación de demostración de JavaScript | Phrase

🗒 Nota » "en" y "ar" arriba son los códigos ISO 639-1 para inglés y árabe, respectivamente. Es estándar usar códigos ISO para idiomas y países al localizar.

Cargando traducciones de forma asíncrona

Hemos empezado con buen pie con nuestra solución i18n. Sin embargo, agregar idiomas y traducciones no escala bien en este momento. A medida que nuestra aplicación crece, probablemente querríamos dividir nuestras traducciones en archivos separados por idioma. El archivo de traducción correspondiente al idioma activo podría cargarse sin el coste de cargar los demás idiomas. Podemos implementar esto sin demasiado esfuerzo.
Primero, movamos nuestras traducciones fuera de nuestro script principal y a archivos JSON, uno para cada idioma que admitimos.

{
  "app-title": "Mi Appy Apperson",
  "lead": "¡Bienvenido a mi pequeño rincón en la web!"
}
{
  "app-title": "تطبيقي المطبق",
  "lead": "Bienvenido/a a mi pequeño rincón en Internet."
}

Vamos a reestructurar nuestro script para cargar los archivos JSON de forma asíncrona cuando los necesitemos.

// El idioma que nuestra aplicación muestra primero
const defaultLocale = "en";
// El idioma activo
let locale;
// Se rellena con las traducciones del idioma activo
let translations = {};
// Cuando el contenido de la página esté listo...
document.addEventListener("DOMContentLoaded", () => {
  // Traducir la página al idioma predeterminado
  setLocale(defaultLocale);
});
// Cargar traducciones para el idioma (o configuración regional) dado y traducir
// la página a este idioma
async function setLocale(newLocale) {
  if (newLocale === locale) return;
  const newTranslations =
    await fetchTranslationsFor(newLocale);
  locale = newLocale;
  translations = newTranslations;
  translatePage();
}
// Recupera el objeto JSON de traducciones para el elemento dado
// locale a través de la red
función asíncrona fetchTranslationsFor(newLocale) {
  const response = await fetch(`/lang/${newLocale}.json`);
  return await response.json();
}
// Reemplazar el texto interno de cada elemento que tiene
// atributo data-i18n-key con la traducción correspondiente
// a su data-i18n-key
function translatePage() {
  document
    .querySelectorAll("[data-i18n-key]")
    .forEach(translateElement);
}
// Reemplaza el texto interno del elemento HTML dado
// con la traducción en el idioma activo,
// correspondiente a la clave data-i18n del elemento
function translateElement(element) {
  const key = element.getAttribute("data-i18n-key");
  const translation = translations[key];
  element.innerText = translation;
}

Si recargamos nuestra página ahora, se ve exactamente como antes. Sin embargo, internamente, hemos hecho que nuestra aplicación sea mucho más escalable y mantenible.

🗒 Nota » Usamos la práctica Fetch API integrada en los navegadores modernos para obtener nuestros archivos JSON mediante la red.

Creando un selector de idioma

Nuestros usuarios aún no tienen forma de hacer uso de nuestras asombrosas habilidades asíncronas. ¿Creamos un menú desplegable para que puedan cambiar de idioma?
Agregaremos una barra de navegación y alojaremos nuestro conmutador en dicha barra.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
  <title>Mi Appy Apperson</title>
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        <ul class="navbar-list navbar-left">
          <!-- Enlaces de navegación -->
        </ul>
        <div class="navbar-right">
          
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">Árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <h1 data-i18n-key="app-title">Mi Appy Apperson</h1>
    <p data-i18n-key="lead">¡Bienvenido a mi pequeño rincón en internet!</p>
  </div>
  <script src="js/scripts.js"></script>
</body>
</html>

Una simple <select> bastará aquí. Podemos usar un atributo data-i18n-switcher para enganchar desde nuestro JavaScript y cargar la localización seleccionada por el usuario.

const defaultLocale = "en";
let locale;

// Cuando el contenido de la página esté listo...
document.addEventListener("DOMContentLoaded", () => {
  setLocale(defaultLocale);
  bindLocaleSwitcher(defaultLocale);
});

// Siempre que el usuario seleccione un nuevo idioma,
// carga las traducciones del locale y actualiza
// la página
function bindLocaleSwitcher(initialValue) {
  const switcher =
    document.querySelector("[data-i18n-switcher]");
  switcher.value = initialValue;
  switcher.onchange = (e) => {
    // Establece la configuración regional en la opción seleccionada [value]
    setLocale(e.target.value); // Establece el idioma según el valor seleccionado
  };
}

El controlador de eventos onchange nos permite actualizar las traducciones de nuestra página según el valor de la <option> seleccionada. ¡Y ya está! Ahora los visitantes de nuestro sitio pueden seleccionar su propio idioma o configuración regional.
Demo app con controlador del evento onchange | Phrase

📣 Un saludo » a Hary Murdiono JS del Noun Project por su icono de traducción.

Detectando los idiomas preferidos del usuario desde el navegador

A veces es una buena idea intentar adivinar el idioma o la configuración regional preferida del usuario antes de darle la opción de seleccionar manualmente la que prefiera. La mayoría de las personas tienen la interfaz de usuario de su navegador configurada en su idioma preferido, a menudo el idioma del sistema operativo.
Este idioma de la interfaz de usuario del navegador se puede encontrar en el objeto navigator, concretamente en la cadena navigator.language.
El también estándar—aunque experimental mientras escribo esto—navigator.languages debería contener el idioma de la interfaz de usuario como su primera entrada, además de cualquier idioma que la persona usuaria haya configurado explícitamente en la configuración de idiomas preferidos de su navegador.
Una pequeña función que consulta navigator.languages puede ponernos en marcha con la detección del idioma del navegador.

 * Recupera los idiomas preferidos del usuario desde el navegador
 
 * @param {boolean} languageCodeOnly - cuando es verdadero, devuelve
 * ["en", "fr"] en lugar de ["en-US", "fr-FR"]
 * @returns array | undefined
 
function browserLocales(languageCodeOnly = false) {
  return navigator.languages.map((locale) =>
    languageCodeOnly ? locale.split("-")[0] : locale,
  );
}

Ahora digamos que el usuario tiene francés (Canadá) y chino (Simplificado) en la configuración de su navegador.
Configuración de idioma del navegador | Phrase
En este caso, browserLocales() devolverá ["fr-CA", "zh-CN"]. Si llamamos a browserLocales(true), obtendremos ["fr", "zh"].
Ahora podemos usar esta nueva función para detectar los idiomas preferidos del usuario cuando cargamos nuestra página por primera vez.

// El idioma que nuestra aplicación muestra primero
const defaultLocale = "en";
const supportedLocales = ["en", "ar"];

document.addEventListener("DOMContentLoaded", () => {
  const initialLocale =
    supportedOrDefault(browserLocales(true));
  setLocale(initialLocale);
  bindLocaleSwitcher(initialLocale);
});

function isSupported(locale) {
  devuelve true si supportedLocales.indexOf(locale) es mayor que -1;
}
// Recuperar el primer idioma que soportamos de la dada
// array, o devolver nuestro idioma por defecto
function supportedOrDefault(locales) {
  return locales.find(isSupported) || defaultLocale;
}

function browserLocales(languageCodeOnly = false) {
  return navigator.languages.map((locale) =>
    languageCodeOnly ? locale.split("-")[0] : locale,
  );
}

Ten en cuenta que hemos introducido el concepto de supportedLocales; estos son los únicos locales para los que tenemos traducciones. Con ellas, podemos recurrir a nuestro idioma predeterminado si ninguno de los idiomas preferidos del usuario está en nuestra lista de idiomas compatibles.
Nuestra aplicación ahora se traducirá al primer idioma en la lista de preferencias del usuario, con una elegante alternativa en caso necesario.

🤿 Descubre más a fondo » Cubrimos la detección de idioma tanto en el navegador como en el servidor en profundidad en Detectando la preferencia de idioma del navegador con JavaScript.

Manejo de dirección: de derecha a izquierda y lenguajes de derecha a izquierda

Árabe, hebreo, persa, urdu y otros idiomas utilizan escrituras que se escriben de derecha a izquierda. Mientras que los idiomas de izquierda a derecha (LTR) superan en número a los de derecha a izquierda (RTL), es bueno saber cómo dar soporte a estos últimos. Afortunadamente, gran parte del trabajo lo realiza el navegador aquí; solo tenemos que establecer el <html dir> atributo en nuestras páginas.

// Cargar traducciones para el idioma dado y traducir
// la página para este idioma
async function setLocale(newLocale) {
  si (newLocale === locale) return;
  const newTranslations = await fetchTranslationsFor(
    nuevo locale,
  );
  locale = newLocale;
  translations = newTranslations;
  // Establecer el atributo <html dir>
  document.documentElement.dir = dir(newLocale);
  // No es necesario para el flujo de dirección, pero por si acaso…
  document.documentElement.lang = newLocale;
  translatePage();
}

function dir(locale) {
  return locale === "ar" ? "rtl" : "ltr";
}

El <html dir> atributo puede tomar los valores "ltr" o "rtl". Proporcionamos este valor mediante una función muy simple dir() y asignamos el atributo cada vez que cambiamos de idioma.

Atributos HTML de las herramientas de desarrollo del navegador | Phrase
Si abrimos las herramientas de desarrollo del navegador, podemos ver cómo se actualizan los atributos <html> al cambiar de idioma

El navegador mostrará el documento de derecha a izquierda automáticamente cuando establezcamos <html dir="rtl">. Sin embargo, cualquiera de nuestros estilos direccionales personalizados, p. ej. margin-left: 20px, requerirá algo de CSS específico para RTL, incluyendo estilos volteados. Eso generalmente no es demasiado complicado; simplemente está un poco fuera del alcance de este artículo.

🤿 Adéntrate más » Lee más sobre CSS localizado en ¿Cómo usar un archivo CSS para la localización del sitio?

¡Con nuestro nuevo código en su lugar, obtenemos una página árabe que Avicena aprobaría!
Aplicación de demostración con alineación a la derecha de texto árabe | Phrase

Mensajes de traducción básicos

Antes de pasar a mensajes más complejos, como los que tienen valores interpolados y plurales, vamos a repasar brevemente cómo hemos implementado los mensajes de traducción.

// En nuestra página HTML
<h1 data-i18n-key="app-title">Mi Appy Apperson</h1>
// En nuestro JavaScript
function translateElement(element) {
  const key = element.getAttribute("data-i18n-key");
  const translation = translations[key];
  element.innerText = translation;
}
// Dado que hemos cargado traducciones en árabe desde ar.json:
translations = {
  "app-title": "تطبيقي المطبق",
};
translateElement(document.querySelector("[data-i18n-key='lead']"));
// renderiza como:
<h1 data-i18n-key="app-title">تطبيقي المطبق</h1>

Ese es nuestro sistema de traducción en pocas palabras.

Interpolación

¿Qué sucede cuando tenemos valores que cambian durante la ejecución y necesitan ser insertados en nuestros mensajes? Un ejemplo común es el nombre del usuario que ha iniciado sesión actualmente. Tendremos que actualizar nuestro sistema de traducción para manejar casos como estos.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <!-- ... -->
    <h1 data-i18n-key="app-title">Mi Appy Apperson</h1>
    <p
      data-i18n-key="lead"
      data-i18n-opt='{"username": "Swoodesh"}'
    >
      ¡Bienvenido a mi pequeño rincón en internet, {username}!
    </p>
  </div>
  <script src="js/scripts.js"></script>
</body>
</html>

Indicamos marcadores de posición para los valores que queremos interpolar en nuestros mensajes con la sintaxis {variable}. Un nuevo atributo data-i18n-opt almacena pares clave/valor de interpolación en un objeto JSON válido.
Por supuesto, necesitaremos los marcadores de posición en nuestros archivos de idioma.

{
  "lead": ¡Bienvenido a mi pequeño rincón en internet, {username}!
}
{
  "lead": "¡Hola! Bienvenido a mi rinconcito en la red, {username}."
}

Ahora podemos modificar la función translateElement para manejar interpolaciones.

// ...
function translateElement(element) {
  const key = element.getAttribute("data-i18n-key");
  const translation = translations[key];
  const options = JSON.parse(
    element.getAttribute("data-i18n-opt")
  );
  element.innerText = options
    ? interpolate(translation, options)
    : translation;
}
// Convertir un mensaje como "Hola, {name}" a "Hola, Chad"
// dado el objeto de interpolación {name: "Chad"}
function interpolate(message, interpolations) {
  return Object.keys(interpolations).reduce(
    (interpolated, key) =>
      interpolated.replace(
        new RegExp(`{\s*${key}\s*}`, "g"),
        interpolaciones[key],
      ),
    message,
  );
}
// ...

Si detectamos un atributo data-i18n-opt en el elemento dado a translateElement(), ejecutamos su mensaje traducido a través de una nueva función interpolate() antes de actualizar el elemento. Ahora, cuando cargamos nuestra página, vemos el mensaje con el valor interpolado.
App de demostración con interpolación en inglés | Phrase
App de demostración con interpolación en la configuración regional árabe | Phrase
Por supuesto, tener valores estáticos en el HTML tiene un uso limitado para nosotros. Lo ideal es que podamos interpolar dinámicamente con JavaScript. Eso no es muy difícil de programar.

Traducción dinámica después de cargar la página

Vamos a extraer una función de traducción general de translateElement().

// ...
function translateElement(element) {
  const key = element.getAttribute("data-i18n-key");
  const options =
    JSON.parse(element.getAttribute("data-i18n-opt")) || {};
  element.innerText = translate(key, options);
}
function translate(key, interpolations = {}) {
  return interpolate(translations[key], interpolations);
}
// ...

Simplemente hemos extraído el código que se encarga de obtener un mensaje en el idioma activo, con interpolaciones, a una nueva función translate(). Ahora podemos usar esta función para actualizar la traducción de un elemento después de cargar la página. Digamos que queremos actualizar nuestro mensaje principal después de que el usuario inicie sesión. No es gran cosa.

const element =
  document.querySelector("[data-i18n-key='lead']");
// Nuestra nueva función nos está sirviendo bien aquí
element.innerText =
  translate("lead", { username: "Maggie" });
// Almacenar las interpolaciones actualizadas en el documento
// en caso de que el elemento se vuelva a renderizar en el futuro
element.setAttribute(
  "data-i18n-opt",
  JSON.stringify({ username: "Maggie" }),
);

Demo app con traducción dinámica al inglés | Phrase
Ahora podemos actualizar las traducciones de los elementos en cualquier momento desde nuestro JavaScript.

Plurales

No solo necesitamos presentar diferentes mensajes basados en un contador—como “1 seguidor” o “20.000 seguidores”—los distintos idiomas tienen diferentes reglas de plural. Mientras que el inglés tiene dos formas plurales: one y other, el árabe tiene seis formas plurales, por ejemplo. Históricamente, esto significaba que implementar soporte de plurales en aplicaciones de front-end no era muy fácil. Afortunadamente, el ahora-objeto estándar Intl.PluralRules hace que manejar los plurales sea mucho más fácil.
Digamos que somos escritores prolíficos, y queremos hacer saber al mundo cuántos artículos hemos redactado de verdad.

<p
  data-i18n-key="article-plural"
  data-i18n-opt='{"count": 122}'
>
  {count} artículos escritos y contando.
</p>

Ten en cuenta que estamos usando una convención de terminar la clave de mensaje plural con -plural. Y por supuesto, necesitamos un count entero requerido para seleccionar la forma plural correcta. Hablando de formas plurales, añadámoslas.

{
  // El inglés tiene dos formas plurales
  "article-plural": {
    "one": "{count} artículo y contando",
    "other": "{count} artículos y contando"
  }
}
{
  // El árabe tiene seis formas plurales
  "article-plural": {
    "zero": "لا توجد مقالات",
    "one": "مقال {count}",
    "two": "مقالان",
    "few": "{count} مقالات",
    "many": "{count} مقال",
    "other": "{count} مقال"
  }
}

Ahora actualicemos nuestra función translate para manejar mensajes plurales.

// ...
function translate(key, interpolations = {}) {
  const message = translations[key];
  if (key.endsWith("-plural")) {
    return interpolate(
      pluralFormFor(message, interpolations.count),
      interpolaciones,
    );
  }
  return interpolate(message, interpolations);
}


  Dado un objeto de formularios como
  {
    "zero": "No hay artículos",
    "one": "Un artículo",
    "other": "{count} artículos"
  } y una cantidad de 3, devuelve "3 artículos"
*/
function pluralFormFor(forms, count) {
  const matchingForm = new Intl.PluralRules(locale).select(count);
  return forms[matchingForm];
}
// ...

El truco aquí es la parte que lee new Intl.PluralRules(locale).select(...). El objeto incorporado Intl.PluralRules, dado un idioma, conoce las reglas de plural de ese idioma. Por ejemplo, pasar "ar" al constructor, luego llamar a select(5) en el objeto devuelto, devuelve "few" — la forma correcta aquí.
Así que con muy pocas líneas de código, tenemos soporte para plurales totalmente global 🙌

Pluralización en árabe e inglés | PhraseNuestro mensaje plural mostrado en inglés y árabe, dadas diferentes cantidades

Formato de números

Trescientos mil euros son “€300,000.00” en inglés (Estados Unidos), “300.000,00 €” en alemán (Alemania), “€3,00,000.00” en hindi (indio)—nota las comas en ese último—y “٣٠٠٬٠٠٠٫٠٠ €” en árabe (Egipto). ¿Cómo gestionamos todos estos formatos? No te preocupes; otro objeto Intl que es parte del estándar moderno de JavaScript es justo lo que necesitabas. Intl.NumberFormat ¡al rescate!
Siendo los emprendedores que somos, imaginemos que queremos iniciar una web para seguir NFTs, con estadísticas numéricas, por supuesto.

<p
  data-i18n-key="nyan-cat-price"
  data-i18n-opt='{"price": {"number" : 5300}}'
>
  Nyan Cat (Oficial) NFT: {price}
</p>

Nuestra data-i18n-opt identifica el valor numérico del {price} en nuestros archivos de localización. Buscaremos esa clave number cuando actualicemos nuestro código de interpolación en un momento. Primero, proporcionemos las traducciones de los mensajes.

{
  "nyan-cat-price": "Nyan Cat (Oficial) NFT: {price}"
}
{
  "nyan-cat-price": "نيان كات NFT: {price}"
}

OK, actualicemos nuestro JavaScript para que esto funcione.

// ...
const fullyQualifiedLocaleDefaults = {
  en: "en-US",
  ar: "ar-EG",
};

function interpolate(message, interpolations) {
  return Object.keys(interpolations).reduce(
    (interpolated, key) => {
      const value = formatNumber(interpolations[key]);
      return interpolated.replace(
        new RegExp(`{\s*${key}\s*}`, "g"),
        value,
      );
    },
    message,
  );
}

  Dado un objeto de valor como
  {
    Número : 300.000,
    "estilo": "moneda",
    Moneda: EUR
  } y que la configuración regional activa es "en", devuelve "€300,000.00"

function formatNumber(value) {
  if (typeof value === "object" && value.number) {
    const { number, ...options } = value;
    return new Intl.NumberFormat(
      fullyQualifiedLocaleDefaults[locale],
      options,
    ).format(number);
  } else {
    return value;
  }
}

Cuando interpolamos nuestros mensajes traducidos, primero pasamos el valor que vamos a intercambiar por un formateador de números, que a su vez utiliza el objeto incorporado Intl.NumberFormat. Así que, de nuevo, con muy pocas líneas de código, tenemos un formato de números localizado.
Demo app with US number formatting | Phrase
Demo app with Arabic number formatting | Phrase

✋🏽 Heads up » Es mejor pasar una configuración regional completamente especificada, como "en-US", al constructor Intl.NumberFormat(). Si pasamos solo un código de idioma, como "en", cada navegador decidirá qué región usar para formatear sus números: Un navegador podría establecer por defecto "en-US", mientras que otro podría establecer por defecto "en-UK". Así que usamos un mapa fullyQualifiedLocaleDefaults en nuestra función formatNumber() para obtener un formato coherente en todos los navegadores.

Debido a que estamos pasando todas las opciones definidas en nuestro objeto de interpolaciones al constructor Intl.NumberFormat(), podemos hacer uso de sus múltiples opciones de formato en cualquier momento que queramos.

<p
  data-i18n-key="nyan-cat-price"
  data-i18n-opt='{"price": {"
    Número : 5300,
    "estilo": "moneda",
    Moneda: EUR
  }}'
 
  Nyan Cat (Oficial): el NFT {price}
</p>

Ejemplo de formato de número de EE.UU. | PhraseEjemplo de formato de número árabe | Phrase

🔗 Recurso » Puede que te guste nuestra Guía concisa de localización de números.

Formato de fecha

Al igual que los números, el formato de fecha es específico de la región. El 5 de diciembre de 2021 en su forma corta se escribe como «12/5/2021» en inglés de EE. UU. y «5.12.2021» en alemán de Alemania, por ejemplo. Y, de nuevo, un práctico objeto incorporado Intl.DateTimeFormat puede encargarse de la parte más complicada cuando se trata de formatear fechas.
Digamos que queremos mostrar la fecha y hora de publicación de un artículo nuestro.

<p
  data-i18n-key="publish-date"
  data-i18n-opt='{"publishDate": {"
    Fecha: 2021-12-05 15:29:00
  }}'

  Publicado el {publishDate}
</p>

La clave especial date en nuestro objeto data-i18n-opt contiene el valor de fecha y hora que queremos formatear. Como de costumbre, vamos a agregar nuestros mensajes localizados.

{
  
  "fecha-publicación": "Publicado {publishDate}"
}
{
  
  "fecha-publicación": "publicado {publishDate}"
}

Ahora actualicemos nuestro sistema de traducción para buscar claves date y formatear sus valores como fechas localizadas.

function interpolate(message, interpolations) {
  return Object.keys(interpolations).reduce(
    (interpolated, key) => {
      const value = formatDate(
        formatearNúmero(interpolaciones[clave]),
      );
      return interpolated.replace(
        new RegExp(`{\s*${key}\s*}`, "g"),
        Valor,
      );
    },
    Mensaje,
  );
}


  Dado un objeto de valor como
  {
    "date": "2021-12-05 15:29:00",
    "dateStyle": "long",
    "timeStyle": "short"
  } y que el idioma actual es "en",
  devuelve "5 de diciembre de 2021 a las 3:29 PM"

function formatDate(value) {
  if (typeof value === "object" && value.date) {
    const { date, ...options } = value;
    const parsedDate =
      typeof date === "string" ? Date.parse(date) : date;
    return new Intl.DateTimeFormat(
      fullyQualifiedLocaleDefaults[locale],
      options,
    ).format(parsedDate);
  } else {
    return value;
  }
}

Después de pasar nuestro objeto de valor a nuestro formateador de números, volvemos a pasar por nuestro nuevo formateador de fechas. Las opciones de formateo de fechas se pasan al constructor de Intl.DateTimeFormat, permitiendo una bastante flexibilidad en el formateo de fechas.

✋🏽 Heads up » We use Date.parse() to make sure that our string date is converted to a Date object, otherwise Intl.DateTimeFormat will throw an error.

Así, tenemos un formato de fecha localizado 👍

<p
  data-i18n-key="publish-date"
  data-i18n-opt='{"publishDate": {"
    "date": 2021-12-05 15:29:00
    "dateStyle": "long",
    "timeStyle": "short"
  }}'

  Publicado el {publishDate}
</p>

Aplicación de demostración con formateo de fechas de EE. UU. | Phrase
Aplicación de demostración con formato de fecha en árabe | Phrase

🤿 Profundiza más » Si buscas características de formateo de fechas más robustas, echa un vistazo a nuestro resumen, ¿Cuál es la mejor biblioteca de fechas y horas de JavaScript? Y nuestra Forma amigable para mostrar fechas en TypeScript/JavaScript te permite mostrar fechas como «hace 1 hora.»

🔗 Recurso » Si estás usando un marco declarativo como React, puedes llevar lo que construimos en esta sección más allá y Crea tu propia biblioteca de i18n en JavaScript con TypeScript.

¿Cuáles son buenas bibliotecas de JavaScript para i18n que puedo usar?

Hemos cubierto cómo crear tu propia biblioteca de i18n en JavaScript anteriormente. Sin embargo, podría tener más sentido para tu proyecto adoptar una biblioteca de i18n lista para usar. No hay escasez de opciones, y en este artículo veremos cómo usar las bibliotecas Polyglot, i18next y Globalize.
Para una selección aún más amplia, nuestros artículos populares pueden ayudarte a comenzar con buen pie:

🗒 Nota » Si por casualidad estás trabajando con gettext heredado, echa un vistazo a la biblioteca Jed.

¿Cómo localizo una página web con Polyglot?

Mantenida por Airbnb, Polyglot es una pequeña biblioteca de i18n que resuelve algunos problemas de localización que anteriormente no estaban soportados por las bibliotecas estándar de JavaScript. Lo más notable entre las características de Polyglot es su excelente manejo de los plurales. Sin embargo, como mencionamos anteriormente, el ahora incorporado Intl.PluralRules constructor es compatible con todos los navegadores modernos y resuelve de manera efectiva el problema de pluralización. Aun así, la última versión de este artículo daba un gran protagonismo a Polyglot, así que queríamos incluir esta sección en caso de que algunos de nuestros lectores aún quieran una guía de Polyglot.

🗒 Nota » A menos que tu caso de uso requiera Polyglot, consulta la alternativa biblioteca i18next en la siguiente sección antes de decidirte por una solución de localización.

Sin más preámbulos, localicemos tu pequeña aplicación de demostración con la biblioteca de localización de Airbnb.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        <ul class="navbar-list navbar-start">
          <li class="navbar-item">
            <a href="#" data-i18n-key="home" class="navbar-link">
              Inicio
            </a>
          </li>
          <li class="navbar-item">
            <a href="#" data-i18n-key="about" class="navbar-link">
              Sobre
            </a>
          </li>
        </ul>
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">Árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <h1 data-i18n-key="app-title">Con Polyglot</h1>
    <p data-i18n-key="lead" data-i18n-opt='{"username": "Cadence"}'>
      ¡Bienvenido a mi pequeño rincón en internet, %{username}!
    </p>
    <p
      data-i18n-key="artículos-plural"
      data-i18n-opt='{"smart_count": 2}'
    >
      %{smart_count} artículos escritos y contando.
    </p>
  </div>
</body>
</html>

Aplicación de demostración de JavaScript con Polyglot | Phrase
Vale, ¡pongámonos a localizar esta aplicación con Polyglot!

Instalación

Polyglot tiene algunas dependencias de NPM, así que necesita Node instalado localmente. Con Node en su lugar, podemos inicializar un package.json para nuestra app de demostración ejecutando lo siguiente desde la línea de comandos.

npm init -y

Para agrupar nuestras dependencias de NPM en un archivo que los navegadores puedan leer, instalaremos el empaquetador de módulos Webpack como una dependencia de desarrollo. El servidor de desarrollo de Webpack nos ayudará con la actualización en caliente del paquete en el navegador mientras desarrollamos.

npm install --save-dev webpack webpack-cli webpack-dev-server

Vale, ahora vamos a instalar a la estrella del espectáculo: Polyglot.

npm install node-polyglot

Un index.js servirá como el punto de entrada para nuestra aplicación, y podemos usarlo para hacer una prueba rápida de las instalaciones de la librería.

import Polyglot from "node-polyglot";
console.log({ Polyglot });

Un script start en nuestro package.json facilitará nuestro desarrollo al poner en marcha el servidor de desarrollo con nuestra configuración personalizada.

{
  "name": "polyglot-demo",
  // ...
  "scripts": {
    "start": "webpack-dev-server --config webpack.config.js"
  },
  // ...
  "devDependencies": {
    "webpack": "^5.65.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.6.0"
  },
  "dependencies": {
    "node-polyglot": "^2.4.2"
  }
}

🗒 Nota » Estamos utilizando un archivo webpack.config.js relativamente simple para agrupar nuestra aplicación y configurar el servidor de desarrollo. Échale un vistazo en nuestro repo de Git en GitHub.

Ahora vamos a insertar nuestro JS empaquetado justo antes de la etiqueta de cierre </body> en nuestro archivo public/index.html.

<script src="./bundle.js"></script>

Con esto listo, ya podemos ejecutar nuestro script start desde la línea de comandos para iniciar el servidor de desarrollo de Webpack.

npm start

Si todo va bien, nuestra aplicación debería abrirse automáticamente en el navegador. Si abres las herramientas de desarrollador de tu navegador, verás mensajes en la consola similares a los siguientes.
Registros de consola de herramientas de desarrollador del navegador | Phrase

Traducciones básicas

Pasemos al uso básico de Polyglot. Aquí está la receta:

// 1. Importa la biblioteca
import Polyglot from "node-polyglot";
// 2. Crear una instancia
const polyglot = new Polyglot();
// 3. Agregar mensajes de traducción para el idioma activo
polyglot.extend({
  "app-title": "Con Polyglot",
});
// 4. Usa los mensajes para traducir los elementos de la página
const element = document.querySelector(
  "[data-i18n-key='app-title']",
);
// polyglot.t() resuelve un mensaje de traducción dado
// una clave
element.innerHTML = polyglot.t("app-title");

Para cambiar de idioma, podemos recargar la página con los mensajes del nuevo idioma.

✋🏽 Atención » Usamos innerHTML en este artículo para poner el contenido de un elemento. Ten cuidado con este atributo en producción; asegúrate de sanear cualquier HTML que inyectes usando innerHTML para evitar ataques de secuencias de comandos en sitios cruzados (XSS).

import Polyglot from "node-polyglot";
const polyglot = new Polyglot();
polyglot.extend({
  "app-title": "مع بوليجلوت",
});
const element = document.querySelector(
  "[data-i18n-key='app-title']",
);
element.innerHTML = polyglot.t("app-title");

Carga asíncrona de archivos de traducción

Mientras que lo anterior funciona bien para las aplicaciones más pequeñas, podríamos hacerlo un poco mejor separando nuestros mensajes de traducción en archivos JSON separados, uno por cada idioma.

{
  "app-title": "Con Polyglot",
  "home": "Inicio",
  "about": "Sobre"
}
{
  "app-title": "مع بوليجلوت",
  "home": "الرئيسية",
  "about": "نبذة عنا"
}

Ahora podemos configurar un idioma predeterminado para nuestra aplicación y cargar sus traducciones desde la red cuando se carga la página.

import Polyglot from "node-polyglot";
const defaultLocale = "en";
const polyglot = new Polyglot();
// Cargar mensajes de traducción desde la red
async function loadTranslations(locale) {
  return await fetch(`/lang/${locale}.json`).then(
    (respuesta) => respuesta.json(),
  );
}
// Traduce todos los elementos de la página que tengan nuestro elemento personalizado
// atributo data-i18n-key
function traducirPagina() {
  const elementosTraducibles = document.querySelectorAll(
    "[data-i18n-key]",
  );
  elementosTraducibles.forEach((el) => {
    const key = el.getAttribute("data-i18n-key");
    el.innerHTML = polyglot.t(key);
  });
}
// Init
(async function () {
  const traducciones = await cargarTraducciones(
    idioma predeterminado,
  );
  polyglot.extend(translations);
  traducePágina();
})();

Con eso, obtenemos el siguiente renderizado en el navegador.
Aplicación de demostración en inglés con Polyglot y elementos de menú y título principal faltantes | Phrase
Nuestros elementos del menú de navegación y el título principal están traducidos a nuestro idioma predeterminado, inglés. Sin embargo, observa los errores de Polyglot en la consola, y cómo las claves faltantes (lead y article-plural) muestran el valor de las claves mismas. Añadiremos traducciones para estas claves en breve para solucionarlo.
Si cambiamos defaultLocale a "ar", obtenemos el siguiente resultado.
Aplicación de demostración de localización árabe con Polyglot y con elementos de menú y título principal que faltan | Phrase

Selector de idioma

Nuestra aplicación ahora es más escalable porque solo se cargan las traducciones del idioma activo. Usa esto para crear un selector de idioma. Ya tenemos el HTML para el selector en nuestra aplicación:

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
		
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <!-- ... -->
  <script src="./bundle.js"></script>
</body>
</html>

Conectarnos a este elemento <select> desde nuestro JavaScript nos permite agregar nuestro comportamiento para cambiar de idioma.

import Polyglot from "node-polyglot";
const defaultLocale = "en";
const polyglot = new Polyglot();

// Cargar traducciones para el idioma (o configuración regional) dado y traducir
// elementos de página para este idioma
async function loadAndTranslate(locale) {
  const translations = await loadTranslations(locale);
  polyglot.replace(translations);
  translatePage();
}
// Siempre que el usuario cambie el idioma activo, carga
// los mensajes de este idioma en esta página
function bindLocaleSwitcher(initialValue) {
  const switcher = document.querySelector(
    "[data-i18n-switcher]",
  );
  switcher.value = initialValue;
  switcher.onchange = (e) => {
    loadAndTranslate(e.target.value);
  };
}
// Init
.init({
loadAndTranslate(defaultLocale);
bindLocaleSwitcher(defaultLocale);

Hemos refactorizado el código que carga nuestros mensajes de traducción y traduce los elementos de nuestra página a una función reutilizable loadAndTranslate(). Una nueva función bindLocaleSwitcher() se conecta al selector de idioma <select>; utiliza loadAndTranslate() para actualizar nuestras traducciones según el idioma seleccionado por el usuario.

✋🏽 Atención » polyglot.extend() agregará mensajes de traducción a los ya cargados, así que, usamos polyglot.replace() en su lugar para asegurarnos de que solo cargamos las traducciones de la localización activa.

Esto debería poner en marcha nuestro elegante cambiador de idioma.
Aplicación de demostración usando Polyglot con selector de idioma fijo | Phrase

Interpolación

Nuestro texto principal incluye el nombre del usuario que ha iniciado sesión (ficticio, por supuesto). Este tipo de valor interpolado es manejado por Polyglot, por defecto, usando una sintaxis especial %{variable}.

🗒 Nota » Puedes cambiar los caracteres que denotan valores interpolados usando la interpolación opción pasada al constructor de Polyglot.

<!-- ... -->
    <p data-i18n-key="lead" data-i18n-opt='{"username": "Cadence"}'>
      ¡Bienvenido/a a mi pequeño rincón en internet, %{username}!
    </p>
<!-- ... -->
{
  // ...
  "lead": ¡Bienvenido a mi pequeño rincón en Internet, %{username}!
}
{
  
  "lead": "أهلاً بك في مكاني الصغير على النت يا %{username}."
}

Se pueden agregar algunas líneas de código a la función translatePage() para acomodar interpolaciones.

// ...
// Traducir todos los elementos de la página que tienen un
// atributo data-i18n-key
function traducePágina() {
  const elementosTraducibles = document.querySelectorAll(
    "[data-i18n-key],"
  );
  elementosTraducibles.forEach((el) => {
    const key = el.getAttribute("data-i18n-key");
    // Extraer claves/valores de interpolación del HTML y
    // conviértelos a JSON
    const interpolations = el.getAttribute("data-i18n-opt");
    const parsedInterpolations = interpolations
      ? JSON.parse(interpolations)
      : {};
    // Pasa las interpolaciones procesadas a polyglot.t().
    // que maneja automáticamente las sustituciones
    el.innerHTML = polyglot.t(key, parsedInterpolations);
  });
}
// ...

Con el código anterior en su lugar, ahora tenemos nuestro párrafo de introducción interpolado mostrado en el idioma activo.
Aplicación de demostración en inglés con Polyglot con párrafo principal fijo | Phrase
Aplicación de demostración de localización árabe con Polyglot con párrafo principal fijo | Phrase

Plurales

Digamos que queremos mostrarte, como usuario que ha iniciado sesión, cuántos mensajes has recibido: “Tienes 1 nuevo mensaje” o “Tienes 12 nuevos mensajes”, por ejemplo. Polyglot maneja los plurales así de bien. Solo necesitamos agregar nuestros mensajes de traducción con el valor numérico interpolado especial, smart_count.

<!-- ... -->
    <p
      data-i18n-key="new-messages"
      data-i18n-opt='{"smart_count": 12}'
    
      Tienes %{smart_count} nuevos mensajes
    </p>
<!-- ... -->

Polyglot usa smart_count para seleccionar la forma de plural correcta de un mensaje de traducción dependiendo del idioma activo. Las formas plurales se separan usando cuatro barras verticales |||| en nuestros mensajes. English tiene one y other formas de plural, y necesitamos proporcionarlas en orden:

{
  
  "new-messages": "Tienes %{smart_count} nuevo mensaje |||| Tienes %{smart_count} nuevos mensajes"
}

El árabe tiene seis formas plurales, y las añadimos a nuestros mensajes de la misma manera.

{
  
  "new-messages": "No tienes mensajes nuevos |||| Tienes un mensaje nuevo |||| Tienes dos mensajes nuevos |||| Tienes %{smart_count} mensajes nuevos |||| Tienes %{smart_count} mensaje nuevo |||| Tienes %{smart_count} mensaje nuevo"
}

🗒 Nota » Consulta la sección ¿Cómo localizo una página web con JavaScript? ➞ Plurals para obtener más detalles sobre las formas plurales.

Una cosa más: por defecto, Polyglot está completamente ajeno al idioma activo, por lo que no conocerá las reglas de pluralización del idioma activo a menos que especifiquemos explícitamente el idioma al cargar.

// Cargar traducciones para el idioma dado y traducir
// elementos de página para este idioma
async function loadAndTranslate(locale) {
  const translations = await loadTranslations(locale);
  polyglot.locale(locale);
  polyglot.replace(translations);
  traducePágina();
}

¡Y eso es todo! Ahora tenemos soporte avanzado de plurales en nuestra app.
Aplicación de demostración en inglés con conteo inteligente de Polyglot | Phrase
Aplicación de demostración en árabe con conteo inteligente de Polyglot | Phrase

🔗 Recurso » Obtén todo el código para nuestra aplicación Polyglot desde nuestro repositorio de GitHub.

🔗 Recurso » La documentación oficial de Polyglot es tan ajustada como la propia biblioteca.

¿Cómo localizo una página web con i18next?

Mientras escribo esto, i18next es una de las bibliotecas de i18n de JavaScript más populares. La biblioteca “aprende una vez, [usa] en todas partes” funciona de forma independiente y con numerosos frameworks de JavaScript. Un amplio ecosistema de plugins significa que a menudo estás a un npm install de resolver un problema común de i18n. Todo esto hace que i18next sea una recomendación fácil.
Vale, basta de cháchara. Volvamos a nuestra pequeña demostración y localicémosla con i18next.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        <ul class="navbar-list navbar-start">
          <li class="navbar-item">
            <a href="#" data-i18n-key="home" class="navbar-link">
              Inicio
            </a>
          </li>
          <li class="navbar-item">
            <a href="#" data-i18n-key="about" class="navbar-link">
              Sobre
            </a>
          </li>
        </ul>
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <h1 data-i18n-key="app-title">Con i18next</h1>
    <p data-i18n-key="lead" data-i18n-opt='{"username": "Zelda"}'>
      ¡Bienvenido/a a mi pequeño rincón en internet, {{username}}!
    </p>
    <p data-i18n-key="new-messages" data-i18n-opt='{"count": 12}'>
      Tienes {{count}} nuevos mensajes
    </p>
  </div>
  <script src="./bundle.js"></script>
</body>
</html>

No hay mucho nuevo aquí. ¡Vamos a localizar!

Instalación

Usaremos Node y su gestor de paquetes NPM para instalar i18next. Primero, vamos a crear un archivo package.json para rastrear las dependencias de nuestro proyecto y los scripts de NPM, ejecutando el siguiente comando en la línea de comandos.

npm init -y

La herramienta de empaquetado Webpack nos permitirá agrupar i18next, sus complementos y nuestro JavaScript personalizado, y servirlos en un solo archivo al navegador. Instalemos Webpack, junto con su servidor de desarrollo, que tiene una útil función de recarga en caliente que facilita el desarrollo:

npm install --save-dev webpack webpack-cli webpack-dev-server

No podemos olvidar nuestra biblioteca i18n, por supuesto.

npm install i18next

Un práctico script npm start puede facilitar el arranque de nuestro servidor de desarrollo.

{
  "name": "i18next-demo",
  
  "scripts": {
    "start": "webpack-dev-server --config webpack.config.js"
  },
  
  "devDependencies": {
    "webpack": "^5.65.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.6.0"
  },
  "dependencies": {
    "i18next": "^21.6.3"
  }
}

🗒 Nota » Estamos usando un archivo webpack.config.js relativamente simple para empaquetar nuestra aplicación y configurar el servidor de desarrollo. Échale un vistazo a nuestro repositorio de Git en GitHub.

Vamos a crear un punto de entrada index.js para nuestra app y hacer una prueba rápida con i18next para asegurarnos de que esté instalado correctamente.

import i18next from "i18next";
console.log({ i18next });

Ahora, cuando ejecutas npm start desde la línea de comandos, verás que el servidor de desarrollo de Webpack se inicia y carga nuestra app en el navegador automáticamente.
Demo App con servidor de desarrollo Webpack cargado | Phrase

Mensajes de traducción básicos

i18next es flexible en cómo acepta mensajes de traducción. Hacemos la mayor parte de nuestra configuración al inicializar la biblioteca con i18next.init(...). La configuración más básica consiste en incluir nuestros mensajes de traducción directamente bajo la opción resources.

import i18next from "i18next";
i18next.init({
  // El idioma activo
  lng: "en",
  // Salida de consola útil habilitada durante el desarrollo
  depuración: true.
  // Mensajes de traducción, asociados por código de localización
  Recursos:
    en: {
      // Por defecto, i18next espera mensajes bajo el.
      // espacio de nombres "traducción"
      traducción: {
        "app-title": "Con Polyglot,"
        Inicio: Inicio,
        Acerca de: Acerca de,
      },
    },
    ar: {
      traducción: {
        "app-title": "مع بوليجلوت",
        home: "الرئيسية",
        about: "نبذة عنا",
      },
    },
  },
});
// Traducir elementos de la página
const translatableElements = document.querySelectorAll(
  "[data-i18n-key]",
);
elementosTraducibles.forEach((el) => {
  const key = el.getAttribute("data-i18n-key");
  el.innerHTML = i18next.t(key);
});

Con lo anterior, no deberías ver cambios cuando tu aplicación se recarga en el navegador. Sin embargo, si cambiamos lng a "ar", vemos las siguientes traducciones en árabe.
Aplicación de demostración de localización árabe con clave faltante | Phrase

🗒 Nota » La opción debug: true activa registros de consola muy útiles en el navegador. Nota los mensajes clave que faltan arriba, por ejemplo.

Carga asíncrona de traducciones

Siendo desarrolladores con visión de futuro, hagamos que nuestra aplicación sea más escalable dividiendo nuestras traducciones en archivos separados, uno por idioma.

{
  "app-title": "Con i18next",
  "home": Inicio,
  "about": "Sobre"
}
{
  "app-title": "مع آي أيتين نيكست",
  "home": "الرئيسية",
  "about": "نبذة عنا"
}

i18next es maduro y tiene muchos aspectos cubiertos; por lo que no necesitamos escribir nuestro propio código para cargar archivos de traducción desde la red. El backend HTTP oficial se integra en la biblioteca y hace todo el trabajo por nosotros. Instálalo.

npm install i18next-http-backend

Ahora podemos integrar el backend en nuestro index.js y use() mientras inicializamos i18next.

import i18next from "i18next";
import HttpApi from "i18next-http-backend";
// Hacemos la función asíncrona para poder usar 'await'
// el archivo de traducción a medida que se transmite hacia abajo
// Red
async function initI18next() {
  // Usamos() el backend y esperamos a que se cargue
  // la traducciones de la red
  await i18next.use(HttpApi).init({
    lng: "en",
    depuración: true,
    // Eliminar `resources` integrados
    // Deshabilitar la carga de la configuración regional de desarrollo
    fallbackLng: false,
    // Configurar el backend HTTP
    backend: {
      loadPath: "/lang/{{lng}}.json",
    },
  });
}
// Refactorización rápida del código de traducción de la página
// a una función
function translatePageElements() {
  const translatableElements = document.querySelectorAll(
    "[data-i18n-key]",
  );
  elementosTraducibles.forEach((el) => {
    const key = el.getAttribute("data-i18n-key");
    el.innerHTML = i18next.t(key);
  });
}
// Init
(async function () {
  await initI18next();
  translatePageElements();
})();

La opción backend.loadPath anula la ruta de archivo de traducción predeterminada del backend. Un marcador de posición especial {{lng}} se reemplaza con el idioma activo. Por ejemplo, cuando nuestra aplicación se carga por primera vez, el backend buscará /lang/en.json, ya que especificamos el idioma predeterminado como en anteriormente en la configuración.
Eso es todo. Nuestras traducciones ahora se cargan desde la red en lugar de estar integradas en nuestro código.

Idiomas soportados y un idioma de respaldo

A menudo queremos especificar una lista de idiomas (locales) que nuestra aplicación soporta y un idioma de respaldo cuando falta una traducción. Podemos usar las opciones de configuración supportLngs y fallbackLng de i18next, respectivamente, para conseguir esto.

async function initI18next() {
  await i18next.use(HttpApi).init({
    lng: "en",
    debug: true,
    supportedLngs: ["en", "ar"],
    fallbackLng: "en",
    backend: {
      loadPath: "/lang/{{lng}}.json",
    },
  });
}

✋🏽 Atención » El idioma de respaldo se cargará siempre, independientemente del idioma activo.

Detectando automáticamente el idioma del usuario

Es común querer detectar los ajustes del navegador del usuario y usar su idioma si lo soportamos. Esto suele ser complicado, pero i18next tiene un plugin oficial que puede solucionarlo rápidamente. Empieza por instalarlo.

npm install i18next-browser-languagedetector

Al igual que el backend HTTP, importamos el plugin detector y lo usamos() al inicializarlo.

import i18next from "i18next";
import HttpApi from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
async function initI18next() {
  await i18next
    .use(HttpApi)
    .use(LanguageDetector)
    .init({
      debug: true,
      supportedLngs: ["en", "ar"],
      fallbackLng: "en",
      // Permitir que "en" se use para
      // "en-US", "en-CA", etc.
      nonExplicitSupportedLngs: true,
      backend: {
        loadPath: "/lang/{{lng}}.json",
      },
    });
}

Y eso es todo lo que necesitas para lograr una detección automática de idioma sólida con i18next.

🔗 Recurso » Te estarás preguntando qué criterios utiliza el detector de idioma para determinar el idioma del usuario. Cubrimos esto en detalle en nuestra Guía de localización de React con i18next .

✋🏽 Atención » El detector de idioma almacenará el idioma que detecte en el almacenamiento de idioma del navegador por defecto, y usará ese valor cuando el usuario visite nuestro sitio de nuevo.

🤿 Profundiza » Tenemos una guía dedicada a Detectar la preferencia de idioma del navegador con JavaScript, que te puede interesar.

Selector de idioma

La detección automática del idioma está bien y todo, pero a menudo necesitamos tener una interfaz para que los usuarios puedan establecer explícitamente su idioma preferido. Ya tenemos el marcado para un selector de idioma configurado.

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
      <!-- ... -->
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">Árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <!-- ... -->
  </div>
  <script src="./bundle.js"></script>
</body>
</html>

Conectemos este HTML y usemos la función i18next.changeLanguage() para establecer nuestro idioma activo según la elección del usuario. Después de que se carguen los mensajes del idioma, podemos encadenar translatePageElements para volver a renderizar la página con traducciones actualizadas.

function bindLocaleSwitcher(initialValue) {
  const switcher = document.querySelector(
    "[data-i18n-switcher]",
  );
  switcher.value = initialValue;
  switcher.onchange = (e) => {
    i18next
      .changeLanguage(e.target.value)
      .then(translatePageElements);
  };
}
// Init
(async function () {
  await initI18next();
  translatePageElements();
  bindLocaleSwitcher(i18next.resolvedLanguage);
})();

✋🏽 Atención » El detector de idioma podría haber detectado un idioma que no admitimos, y ese valor existirá en i18next.language. Usamos i18next.resolvedLanguage arriba para asegurarnos de que usamos el idioma activo, soportado al inicializar nuestro selector de idioma.

¡Y voilà! Una interfaz para cambiar de idioma:
Aplicación de demostración Polyglot con una interfaz de usuario para cambiar de idioma | Phrase

Interpolación

Por defecto, i18next utiliza una sintaxis {{variable}} para denotar valores interpolados en los mensajes de traducción.

<!-- ... -->
    <p data-i18n-key="lead" data-i18n-opt='{"username": "Zelda"}'>
      ¡Bienvenido/a a mi pequeño rincón en la web, {{username}}!
    </p>
<!-- ... -->
{
  
  "lead": "Welcome to my little spot on the interwebs, {{username}}!" → "¡Bienvenido a mi pequeño rincón en la web, {{username}}!"
}
{
  
  "lead": "أهلاً بك في مكاني الصغير على النت يا {{username}}."
}

Podemos extraer estos valores dinámicos de nuestros atributos HTML y alimentarlos a i18next.t(), que maneja la interpolación por nosotros.

function translatePageElements() {
  const translatableElements = document.querySelectorAll(
    "[data-i18n-key]",
  );
  translatableElements.forEach((el) => {
    const key = el.getAttribute("data-i18n-key");
    const interpolations = el.getAttribute("data-i18n-opt");
    const parsedInterpolations = interpolations
      ¿? JSON.parse(interpolations)
      : {};
    el.innerHTML = i18next.t(key, parsedInterpolations);
  });
}

Con esto, nuestros valores dinámicos se reemplazan en nuestros mensajes.
Aplicación de demostración con Polyglot en inglés y párrafo principal interpolado | Phrase
Aplicación de demostración con Polyglot en configuración regional árabe y párrafo principal interpolado | Phrase

Plurales

Bajo el capó, i18next intenta usar el estándar Intl.PluralRules para gestionar los plurales. Una variable interpolada especial count determina la elección de la forma plural, dependiendo del idioma activo.

    <p data-i18n-key="new-messages" data-i18n-opt='{"count": 12}'>
      Tienes {{count}} nuevos mensajes
    </p>

i18next utiliza la convención message_form para las claves de mensaje en plural. Por ejemplo, para manejar las formas one y other en la traducción al inglés de new-messages, podemos especificar lo siguiente.

{
  
  "new-messages_one": Tienes {{count}} nuevo mensaje
  "new-messages_other": "Tienes {{count}} nuevos mensajes"
}

El árabe tiene seis formas plurales, y podemos especificarlas de forma muy similar.

{
  
  "new-messages_zero": "No tienes mensajes nuevos",
  "new-messages_one": "لديك رسالة جديدة",
  "new-messages_two": "Tienes dos mensajes nuevos",
  "new-messages_few": "Tienes {{count}} mensajes nuevos",
  "new-messages_many": "Tienes {{count}} mensajes nuevos",
  "new-messages_other": "Tienes {{count}} mensajes nuevos"
}

Con poco esfuerzo, nuestra aplicación puede mostrar mensajes plurales.
App de demostración con Polyglot en inglés y pluralización | Phrase
App de demostración con Polyglot en configuración regional árabe y pluralización | Phrase

🔗 Recurso » Obtén todo el código que hemos cubierto anteriormente de nuestro repositorio de GitHub.

¿Cómo localizo una aplicación de React, Angular o Vue?

En los últimos años, frameworks declarativos como React, Angular, Vue.js y otros han revolucionado el mundo del front-end web. Cubrimos estos frameworks en profundidad en nuestro blog. Como React es el más popular entre los grandes, proporcionaremos una guía rápida para localizar aplicaciones de React a continuación. Y para otros frameworks declarativos, consulta los siguientes artículos en profundidad.

Artículos de localización de Angular

Artículos de localización de Vue.js

Artículos de localización para otros frameworks

Nos damos cuenta de que algunos de nuestros lectores pueden estar usando Svelte, Next u otro framework, así que siempre escribimos sobre las últimas novedades y lo más destacado. Aquí hay una selección de guías para localizar la aplicación que estás construyendo en tu framework favorito:

🗒 Nota » De manera más específica: Si vas a trabajar con números de teléfono internacionales, asegúrate de echar un vistazo a la libphonenumber library. ¡Es agnóstico respecto al framework!

¿Cómo localizo una aplicación de React con i18next?

Como prometimos, vamos a repasar rápidamente una guía para localizar nuestras aplicaciones de React con la biblioteca i18next. Hemos cubierto i18next anteriormente en este artículo, así que nos centraremos en su integración con React.

🤿 Profundiza más » Guía de localización de React con i18next es más exhaustiva que nuestro breve resumen aquí.

Primero, tomaremos nuestra querida aplicación de demostración y la descompondremos en componentes de React.

import "./App.css";
import Navbar from "./layout/Navbar";
function App() {
  return (
    <div className="container">
      <Navbar />
      <h1>React i18n</h1>
      <p>
        Bienvenido/a a mi pequeño rincón en internet, ¡tú!
      </p>
      <p>Tienes {count} nuevos mensajes</p>
    </div>
  );
}
export default App;
import LocaleSwitcher from "../features/LocaleSwitcher";
function Navbar() {
  return (
    <nav className="navbar">
      <div className="container">
        <ul className="navbar-list navbar-start">
          <li className="navbar-item">
            <a href="#" className="navbar-link">
              Inicio
            </a>
          </li>
          <li className="navbar-item">
            <a href="#" className="navbar-link">
              Sobre
            </a>
          </li>
        </ul>
        <div className="navbar-end">
          <LocaleSwitcher />
        </div>
      </div>
    </nav>
  );
}
export default Navbar;
function LocaleSwitcher() {
  return (
    <>
      <img
        alt="Icono de traducción"
        src="img/translation-icon@2x.png"
        className="translation-icon"
      />
      <select className="locale-switcher">
        <option value="en">Inglés</option>
        <option value="ar">árabe (العربية)</option>
      </select>
    </>
  );
}
export default LocaleSwitcher;

El selector de idioma hace poco en este momento, pero pronto lo mejoraremos. Por ahora, tenemos una buena base para localizar.
Aplicación de demostración de React en inglés con la biblioteca i18n | Phrase

Instalación de la biblioteca

Además de i18next, vamos a usar el marco de integración oficial react-i18next , que hace que usar i18next con React sea muy sencillo. Desde la raíz del proyecto, ejecutemos lo siguiente en la línea de comandos para instalar las bibliotecas.

npm install i18next react-i18next

A continuación, inicialicemos i18next, usando() la integración de React como lo hacemos. La forma más básica de proporcionar traducciones a i18next es usando la opción resources al inicializar.

import i18next from "i18next";
import { initReactI18next } from "react-i18next";
i18next.use(initReactI18next).init({
  Recursos:
    en: {
      {Traducción
        "app-title": "Con React",
      },
    },
    ar: {
      Traducción{
        "app-title": "مع ريأكت",
      },
    },
  },
  lng: "en",
  depuración: true,
  interpolation: {
    escapeValue: false,
  },
});
export default i18next;

🗒 Nota » Establecemos interpolation.escapeValue en false para deshabilitar el escape predeterminado que i18next hace para proteger contra ataques XSS, ya que React lo hace por nosotros de todas formas.

Traigamos nuestro módulo a nuestro index.js raíz para que podamos inicializar i18next cuando nuestra aplicación se cargue.

import React from "react";
import ReactDOM from "react-dom";
import "./lib/skeleton/normalize.css";
import "./lib/skeleton/skeleton.css";
import "./services/i18n";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root"),
);
// ...

Traducciones básicas

La función de traducción i18next.t() familiar se puede usar en nuestros componentes de React. Solo necesitamos importar el hook de React useTranslation para que t esté disponible.

import { useTranslation } from "react-i18next";
import Navbar from "./layout/Navbar";
import "./App.css";
function App() {
  const { t } = useTranslation();
  return (
    <div className="container">
      <Navbar />
      <h1>{t("app-title")}</h1>
      // ...
    </div>
  );
}
export default App;

Cuando nuestra app se recarga, todo debería verse igual. Sin embargo, si cambiamos el valor de lng a "ar" en nuestro archivo inicializador src/services/i18n.js, deberíamos ver el título de nuestra aplicación localizado al árabe.
Aplicación de demostración de React con biblioteca i18n y título en árabe | Phrase

Carga asíncrona de archivos de traducción

Hagamos que nuestra aplicación sea más escalable dividiendo nuestras traducciones en archivos por idioma. El práctico y oficial complemento i18next-http-backend hace que esto sea un trabajo rápido para nosotros. Instálalo.

npm install i18next-http-backend

El backend buscará archivos en /locales/{{lng}}/{{ns}}.json por defecto, donde {{lng}} se resuelve en la localización activa, y {{ns}} se resuelve en el espacio de nombres activo. Dado que el espacio de nombres por defecto es translation, podemos poner nuestros mensajes de traducción en inglés en public/locales/en/translation.json.

{
  "app-title": "Con React",
  "home": Inicio,
  "about": "Sobre"
}

Nuestro archivo en árabe sigue la misma convención:

{
  "app-title": "مع ريأكت",
  "home": "الرئيسية",
  "about": "نبذة عنا"
}

Ahora solo tenemos que importar el backend y usar() al inicializar i18next. También deberíamos eliminar nuestras traducciones en línea bajo la clave resources, ya que ahora el plugin cargará nuestros mensajes de traducción desde la red.

import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import HttpApi from "i18next-http-backend";
i18next
  .use(initReactI18next)
  .use(HttpApi)
  .init({
    // Eliminar `resources`
    lng: "en",
    debug: true,
    interpolation: {
      escapeValue: false,
    },
  });
export default i18next;

Cuando nuestra aplicación se recargue, deberíamos ver exactamente el mismo renderizado localizado. Por supuesto, las traducciones de la localización activa ahora están fluyendo por la red, por lo que nuestra aplicación es más ligera de carga y más fácil de escalar y mantener.
Aplicación de demostración de React con carga de archivos de traducción asíncrona | Phrase

Selector de idioma

Construir una interfaz para cambiar de idioma es pan comido con React e i18next. Podemos actualizar nuestro componente LocaleSwitcher, controlando el <select> en su interior para cambiar el idioma activo al que el usuario elija.

import { useTranslation } from "react-i18next";
function LocaleSwitcher() {
  const { i18n } = useTranslation();
  return (
    <>
      <img
        src="img/translation-icon@2x.png"
        alt="Icono de traducción"
        className="translation-icon"
      />
      <select
        className="locale-switcher"
        value={i18n.language}
        onChange={(e) =>
          i18n.changeLanguage(e.target.value)
        }
      >
        <option value="en">Inglés</option>
        <option value="ar">Árabe (العربية)</option>
      </select>
    </>
  );
}
export default LocaleSwitcher;

La integración de i18next React garantiza que las traducciones se vuelvan a renderizar cuando se cambia el idioma activo.
Aplicación de demostración de React con un selector de idioma funcional | Phrase

🔗 Recurso » Obtén el código de todo lo que hemos construido anteriormente desde nuestro repositorio de GitHub.

Artículos de localización de React

Nos encanta escribir sobre React en nuestro blog, así que estamos felices de ofrecerte una selección de nuestros análisis en profundidad y tutoriales basados en React, todos centrados en la localización:

¿Cómo localizas una página web con jQuery e i18next?

Aunque no está tan de moda como hace algunos años, jQuery sigue siendo una de las bibliotecas de JavaScript más populares actualmente en uso. Encontrarás que localizar aplicaciones jQuery es bastante fácil con la biblioteca i18next. Un plugin oficial de i18next para jQuery requiere muy poco trabajo para configurarse, así que vamos a usarlo para traducir nuestra fiable aplicación de demostración.

🗒 Nota » Hemos cubierto i18next en más detalle anteriormente en este artículo, así que nos centraremos en la integración de jQuery aquí.

Si has estado siguiendo la lectura, la siguiente aplicación de ejemplo te resultará familiar. Sirve como una buena base para nuestro trabajo de localización.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        <ul class="navbar-list navbar-start">
          <li class="navbar-item">
            <a href="#" data-i18n="home" class="navbar-link">
              Inicio
            </a>
          </li>
          <li class="navbar-item">
            <a href="#" data-i18n="about" class="navbar-link">
              Sobre
            </a>
          </li>
        </ul>
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <h1 data-i18n="app-title">jQuery i18n</h1>
    <p data-i18n="lead" data-i18n-options='{"username": "Jackie"}'>
      ¡Bienvenido/a a mi pequeño rincón en internet, {{username}}!
    </p>
    <p data-i18n="new-messages" data-i18n-options='{"count": 3}'>
      Tienes {{count}} nuevos mensajes
    </p>
  </div>
</body>
</html>

🗒 Nota » Por defecto, el complemento jQuery de i18next utiliza data-i18n (no nuestro anterior data-i18n-key) para sus claves de traducción. Este se puede cambiar en las opciones del plugin.

app de demostración de jQuery | Phrase

¿Es hora de localizar? ¡Vamos!

Instalación

La forma más sencilla de instalar i18next y su plugin jQuery es descargar sus archivos de distribución minificados e incluirlos en nuestro HTML. Puedes obtener los archivos en las siguientes ubicaciones.

🔗 Recurso » Consulta la documentación oficial del plugin jQuery de i18next en GitHub.

Después de descargar los archivos anteriores, podemos colocarlos en un directorio js/lib en nuestro proyecto y cargarlos en nuestra página HTML principal.

<!DOCTYPE html>
<html lang="es">
<head>
   <!-- ... -->
</head>
<body>
  <div class="container">
    
  </div>
  <script src="./js/lib/jquery-3.6.0.min.js"></script>
  <script src="./js/lib/i18next.min.js"></script>
  <script src="./js/lib/jquery-i18next.min.js"></script>
  <!-- Nuestro JavaScript personalizado, en un momento... -->
  <script src="./js/scripts.js"></script>
</body>
</html>

Traducciones básicas

Con nuestras bibliotecas instaladas, ahora podemos escribir algo de código de configuración para hacer que el trabajo de localización básico funcione.

// Inicializar i18next
i18next.init({
  lng: "es",     // Idioma inicial
  debug: true, // Proporciona mensajes útiles en la consola
  resources: {   // Traducciones
    en: {
      translation: {
        "app-title": "jQuery + i18next",
      },
    },
    ar: {
      translation: {
        "app-title": "جي كويري + آي إيتين نيكست",
      },
    },
  },
});
// Inicializar el plugin jQuery i18next
jqueryI18next.init(i18next, $);
// Traducir elementos de la página
$("body").localize();

Hemos cubierto i18next.init(...) anteriormente en este artículo. Nota que aquí también tenemos una llamada a jQueryI18next.init(...). En su forma más básica, la función init del plugin jQuery i18next toma la instancia activa de i18next, así como una referencia al objeto jQuery, $.
Cuando se inicializa el plugin, agrega una función localize() a jQuery. Llamar a $(selector).localize() hace que el plugin localice todos los elementos bajo la jerarquía seleccionada. Para cada elemento, si se encuentra un atributo data-i18n, su traducción correspondiente, en el idioma activo, se sustituye.
Por ejemplo:

// En nuestro JavaScript
i18next.init({
  lng: "en",
  resources: {
    en: {
      translation: {
        "app-title": "jQuery + i18next",
      },
    },
    ar: {
      translation: {
        "app-title": "جي كويري + آي إيتين نيكست",
      },
    },
  },
});
jqueryI18next.init(i18next, $);
$("#main-title").localize();
// En nuestro HTML
<h1 id="main-title" data-i18n="app-title"></h1>
// Renderiza como:
<h1 id="main-title" data-i18n="app-title">jQuery + i18next</h1>

Sencillo y agradable 😊

Demo de jQuery con la biblioteca i18next cargada en inglés | Phrase

Y si cambiamos nuestro idioma inicial a árabe cambiando lng a "ar", obtendremos un título en árabe.

Aplicación de demostración de jQuery con la biblioteca i18next con la localización árabe cargada | Phrase

Carga asíncrona de archivos de traducción

¿Qué tal si dividimos nuestros archivos de traducción en archivos separados, uno por idioma? Sé que te lo preguntas. No te preocupes, el plugin oficial de backend HTTP de i18next nos respalda.
Para instalar el plugin, podemos obtener el script de distribución minificado de GitHub y colocarlo en nuestro directorio js/lib. Por supuesto, también querremos incluirlo en nuestro HTML.

<!DOCTYPE html>
<html lang="es">
<head>
   <!-- ... -->
</head>
<body>
  <div class="container">
    <!-- ... -->
  </div>
  <script src="./js/lib/jquery-3.6.0.min.js"></script>
  <script src="./js/lib/i18next.min.js"></script>
  <script src="./js/lib/i18nextHttpBackend.min.js"></script>
  <script src="./js/lib/jquery-i18next.min.js"></script>
  <script src="./js/scripts.js"></script>
</body>
</html>

Ahora podemos hacer que nuestra aplicación sea más escalable moviendo nuestras traducciones a archivos JSON separados.

{
  "app-title": "Con jQuery + i18next",
  "home": Inicio,
  "about": "Acerca de"
}
{
  "app-title": "مع جي كويري و آي إيتين نيكست",
  "home": "الرئيسية",
  "about": "نبذة عنا"
}

Tendremos que rehacer nuestro código de configuración para usar() el plugin HTTP al inicializar i18next. También querremos esperar a que se descargue el archivo de traducción de nuestro idioma inicial antes de intentar traducir los elementos de nuestra página.

// Esperar a que las traducciones lleguen a través de la red
// antes de inicializar el plugin de jQuery
async function initI18n() {
  // Usar el plugin de backend HTTP para descargar traducciones
  await i18next.use(i18nextHttpBackend).init({
    lng: "en",
  // Eliminar la opción `resources`, ya que nuestras traducciones
  // ahora están en archivos JSON
  });
  jqueryI18next.init(i18next, $);
}
// Refactorizar a función
function translatePage() {
  $("body").localize();
}
// Init
(async function () {
  // Espera a que i18next se inicialice antes
  // traduciendo los elementos de la página
  await initI18n();
  translatePage();
})();

Si recargamos nuestra aplicación, no notamos ninguna diferencia en la salida. Sin embargo, si miras más de cerca la pestaña Red de las herramientas de desarrollo, verás una solución más mantenible para los archivos de traducción: «descárgalo cuando lo necesites».

Demo de jQuery con carga asíncrona de archivos de traducción | Phrase

Selector de idioma

Es posible que hayas notado que tenemos algo de HTML que parece una interfaz de cambio de idioma en la demo.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        <!-- ... -->
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">Árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <!-- ... -->
  </div>
  <!-- ... -->
</body>
</html>

Conectemos el elemento <select> de arriba para que nuestra aplicación cambie sus traducciones para que coincidan con el idioma seleccionado por el usuario.

// ...
function bindLocaleSwitcher() {
  const $switcher = $("[data-i18n-switcher]");
  // Valor inicial
  $switcher.val(i18next.language);
  $switcher.on("change", async function () {
    // Cambiar el idioma activo hará que su
    // para que las traducciones se carguen desde la red, así que
    // esperamos esa carga antes de actualizar
    // elementos de la página
    await i18next.changeLanguage($switcher.val());
    translatePage();
  });
}
(async function () {
  await initI18n();
  translatePage();
  bindLocaleSwitcher();
})();

Así de fácil, tenemos un selector de idioma funcional.

demo de jQuery con conmutador de idioma | Phrase

Interpolación

Manejar valores dinámicos en nuestros mensajes de traducción viene de serie con i18next. Solo necesitamos proporcionar un mapa JSON data-i18n-options en el elemento en el que tenemos interpolaciones.

    <p data-i18n="lead" data-i18n-options='{"username": "Jackie"}'>
      ¡Bienvenid@ a mi pequeño rincón en internet, {{username}}!
    </p>
<!-- ... -->

El plugin de jQuery de i18next no busca data-i18n-options por defecto; tenemos que usar su useOptionsAttr opción de configuración para habilitar la interpolación automática.

async function initI18n() {
  await i18next.use(i18nextHttpBackend).init({ lng: "en" });
  jqueryI18next.init(i18next, $, { useOptionsAttr: true });
}
// ...

Por supuesto, asegurémonos de que tenemos los {{variable}} marcadores de posición que i18next espera en nuestros mensajes de traducción.

{
  "app-title": "Con jQuery + i18next",
  "home": "Inicio",
  "about": "Acerca de",
  "lead": "¡Bienvenido a mi pequeño rincón en internet, {{username}}!"
}
{
  "app-title": "مع جي كويري و آي إيتين نيكست",
  "home": "الرئيسية",
  "about": "نبذة عنا",
  "lead": "أهلاً بك في مكاني الصغير على النت يا {{username}}."
}

¡Allá vamos! Interpolaciones interpoladas.

Párrafo principal con interpolación | Phrase
Párrafo introductorio en árabe con interpolación | Phrase

Plurales

Usamos el mismo atributo data-i18n-options para proporcionar una variable numérica especial count cuando tenemos mensajes en plural.

🗒 Nota » Consulta la sección anterior i18next ➞ Plurales para obtener más detalles sobre cómo la biblioteca gestiona los plurales.

<!-- ... -->
    <p data-i18n="new-messages" data-i18n-options='{"count": 3}'>
      Tienes {{count}} nuevos mensajes
    </p>
<!-- ... -->

Usamos una convención de key_form al especificar nuestros mensajes en plural. El inglés tiene dos formas plurales, one y other.

{
  // ...
  "new-messages_one": "Tienes {{count}} nuevo mensaje",
  "new-messages_other": "Tienes {{count}} nuevos mensajes"
}

El árabe tiene seis formas plurales, y se gestionan automáticamente en i18next.

{
  // ...
  "new-messages_zero": "لا توجد لديك رسائل جديدة",
  "new-messages_one": "لديك رسالة جديدة",
  "new-messages_two": "لديك رسالتان جداد",
  "new-messages_few": "لديك {{count}} رسائل جديدة",
  "new-messages_many": "لديك {{count}} رسالة جديدة",
  "new-messages_other": "لديك {{count}} رسالة جديدة"
}

Así que sin código adicional, nuestra aplicación tiene un soporte avanzado para plurales.

Aplicación de demostración de jQuery en inglés con pluralización | Phrase
Aplicación de demostración de jQuery en inglés con pluralización | Phrase

🔗 Recurso » Obtén el código completo en funcionamiento de la app de arriba desde nuestro repositorio de GitHub.

🔗 Recurso » Si buscas una alternativa a i18next, consulta la Guía avanzada de jQuery i18n, que usa la biblioteca jQuery.i18n.

¿Cómo puedo localizar una página web utilizando el formato ICU con Globalize?

Si deseas la cobertura de localización casi exhaustiva de los Componentes Internacionales para Unicode (ICU) y el Repositorio de Datos de Locales Comunes (CLDR) en tu aplicación JavaScript, la biblioteca Globalize seguramente cumplirá con creces su función. Vamos a repasar cómo instalar Globalize y localizar nuestra humilde aplicación de demostración usándola.

🔗 Recurso » La Guía imprescindible del Formato de Mensaje ICU cubre qué es ICU y CLDR con más detalle.

🗒 Nota » A diferencia de muchas otras bibliotecas que cubrimos en este artículo, Globalize es compatible con Internet Explorer 9+.

¿Qué demostración? dices. Nuestro confiable one-pager, por supuesto.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        <ul class="navbar-list navbar-start">
          <li class="navbar-item">
            <a href="#" data-i18n-key="home" class="navbar-link">
              Inicio
            </a>
          </li>
          <li class="navbar-item">
            <a href="#" data-i18n-key="about" class="navbar-link">
              Sobre
            </a>
          </li>
        </ul>
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    <h1 data-i18n-key="app-title">Con Globalize</h1>
    <p data-i18n-key="lead" data-i18n-opt='{"username": "Stella"}'>
      ¡Bienvenido a mi pequeño rincón en los interwebs, {username}!
    </p>
    <p data-i18n-key="new-messages" data-i18n-opt='{"count": 100}'>
      Tienes # nuevos mensajes
    </p>
  </div>
</body>
</html>
Aplicación de demostración de Globalize | Phrase

Empecemos a localizar.

Instalación

Globalize es muy modular, por lo que puede ser un poco engorroso instalarlo con JavaScript puro. Aún así, es una receta relativamente simple.

Esto debería darnos tres archivos ZIP. Descomprimamos, renombremos los directorios de nivel superior descomprimidos y los movamos a nuestro directorio de proyecto. He colocado el mío en un directorio /lib en mi proyecto, así que mi proyecto ahora se ve así:

.
├── css/
├── img/
├── lib/
│   ├── cldr/          << renombrado de cldr-x.x.x
│   ├── cldr-json/     << renombrado desde cldr-x.x.x-json-full
│   └── globalize/  << anteriormente llamado globalize.x.x
└── index.html

🔗 Recurso » La documentación oficial repasa diferentes formas de instalar Globalize.

Usando la herramienta de requisitos para determinar los scripts necesarios

Siempre necesitamos algunas partes de Globalize y cldr.js; otras dependerán de las funcionalidades de localización de nuestra aplicación. Una herramienta útil, So What’cha Want, nos permite saber qué archivos incluir en nuestro proyecto dependiendo de las características seleccionadas. Podemos comenzar deseleccionando todas las características excepto message.

So What’cha Want Globalize Captura de pantalla | Phrase

Nota las dos listas de archivos cerca de la parte inferior de la página. La lista de la izquierda indica qué archivos incluir de Globalize y cldr.js; podemos usar la etiqueta <script> para estos.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <!-- ... -->
  </div>
  <!-- Requisitos de Globalize -->
  <script src="./lib/cldr/dist/cldr.js"></script>
  <script src="./lib/cldr/dist/cldr/event.js"></script>
  <script src="./lib/cldr/dist/cldr/supplemental.js"></script>
  <script src="./lib/globalize/dist/globalize.js"></script>
  <script src="./lib/globalize/dist/globalize/message.js"></script>
  <!-- Nuestro punto de entrada de la aplicación -->
  <script src="./index.js"></script>
</body>
</html>

La lista en la esquina inferior derecha de So What’cha Want tiene datos JSON de CLDR que necesitamos. Este JSON lo necesitaremos obtener en nuestro código y pasarlo a Globalize mediante su función load().

// Agregaremos más a esta lista a medida que avancemos
const supplementals = ["likelySubtags"];
async function loadIntoGlobalize(featureUrls) {
  await Promise.all(
    featureUrls.map((url) => fetchJson(url))
  ).then((downloaded) =>
    downloaded.forEach((feature) => Globalize.load(feature))
  );
}
function supplementalUrlsFor(options) {
  return options.map(
    (feature) =>
      `/lib/cldr-json/cldr-core/supplemental/${feature}.json`
  );
}
async function fetchJson(url) {
  const response = await fetch(url);
  return await response.json();
}
(async function () {
  // Cargar requisitos suplementarios
  await loadIntoGlobalize(
    supplementalUrlsFor(supplementals)
  );
})();

Eso es todo por la configuración. Bueno, no es la biblioteca más fácil de instalar, pero para un proyecto que necesita una localización infalible, vale la pena el esfuerzo.

Traducciones básicas

Usar Globalize para traducir elementos de la página es un proceso sencillo de tres pasos.

// ...
(async function () {
  await loadIntoGlobalize(
    supplementalUrlsFor(supplementals)
  );
  // 1. Cargar mensajes de traducción
  Globalize.loadMessages({
    en: {
      "app-title": "¡Hola Globalize!",
    },
    ar: {
      "app-title": "أهلاً جلوبالايز",
    },
  });
  // 2. Establece el idioma predeterminado
  Globalize.locale("en");
  // 3. Usa formatMessage() para obtener la traducción por clave
  document.querySelector(
    "[data-i18n-key='app-title']"
  ).textContent = Globalize.formatMessage("app-title");
})();

🔗 Recurso » La documentación oficial de Globalize tiene una práctica sección de la API que enumera las funciones disponibles.

Disco. Nuestro título se muestra utilizando las traducciones de nuestro idioma predeterminado.

Traducción al inglés de la aplicación de demostración de Globalize | Phrase

Si cambiamos la llamada anterior para que diga Globalize.locale("ar"), veremos el título de nuestra aplicación en árabe.

Traducción al árabe de la aplicación de demostración de Globalize | Phrase

Manejo de errores por mensajes faltantes

Generalicemos el código que traduce los elementos de la página escribiendo una translatePageElements() función. A diferencia de otras bibliotecas de i18n, Globalize lanzará un error y se detendrá si encuentra un mensaje que falta para una clave dada en formatMessage(). Nada que un poco de try/catch no pueda suavizar.

function translatePageElements() {
  const elements = document.querySelectorAll(
    "[data-i18n-key]"
  );
  elements.forEach((element) => {
    const key = element.getAttribute("data-i18n-key");
    try {
      element.innerHTML = Globalize.formatMessage(key);
    catch (error) {
      if (error.code === "E_MISSING_MESSAGE") {
        // Mostrar advertencias en la consola sobre mensajes faltantes
        // en lugar de detenerse por completo.
        console.warn(error.message);
        // Mostrar el valor de la clave en la página para el mensaje faltante
        element.innerHTML = key;
      } else {
        console.error(error);
      }
    }
  });
}
(async function () {
  
  Globalize.loadMessages(/* ... */);
  Globalize.locale("en");
  translatePageElements();
})();

Menos «fallo en caso de mensaje faltante», más «mostrar la clave y su valor y mostrar una advertencia en la consola».

Aplicación de demostración de Globalize con advertencia en el código | Phrase

Carga asíncrona de archivos de traducción

Como hemos hecho con otras soluciones en este artículo, dividamos nuestros mensajes de traducción en archivos JSON por cada localización para escalabilidad y mantenimiento.

{
  // Globalize espera que el código de idioma sea la clave principal
  "en": {
    "app-title": "Con Globalize",
    "home": Inicio,
    "about": Sobre
  }
}
{
  "ar": {
    "app-title": "مع جلوبالايز",
    "home": "الرئيسية",
    "about": "نبذة عنا"
  }
}

Una función reutilizable setLocale() puede cargar nuestro archivo de traducción, configurar Globalize y renderizar de nuevo los elementos de la página.

const defaultLocale = "en";

 async function setLocale(locale) {
  const mensajes = await fetchJson(`/lang/${locale}.json`);
  Globalize.loadMessages(mensajes);
  Globalize.locale(locale);
  translatePageElements();
}

(async function () {
  await loadIntoGlobalize(
    supplementalUrlsFor(suplementos)
  );
  setLocale(defaultLocale);
})();

Carga de archivo de traducción asíncrona: listo.

Selector de idioma

Podemos usar la función setLocale() que acabamos de escribir para que nuestra interfaz de usuario de cambio de idioma funcione. Puede que recuerdes que nuestra aplicación de demostración ya tiene algo de HTML para el selector.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <nav class="navbar">
      <div class="container">
        
        <div class="navbar-end">
          <img src="img/translation-icon@2x.png" class="translation-icon" />
          <select data-i18n-switcher class="locale-switcher">
            <option value="en">Inglés</option>
            <option value="ar">árabe (العربية)</option>
          </select>
        </div>
      </div>
    </nav>
    
  </div>
  
  <script src="./index.js"></script>
</body>
</html>

Algo de JavaScripting hará que el interruptor cambie.

const defaultLocale = "en";

function bindLocaleSwitcher() {
  const switcher = document.querySelector(
    "[data-i18n-switcher]"
  );
  // Globalize.locale() devuelve la configuración regional activa
  switcher.value = Globalize.locale().locale;
  switcher.onchange = (e) => {
    setLocale(e.target.value); // Establece el idioma según el valor seleccionado
  };
}
(async function () {
  await loadIntoGlobalize(
    supplementalUrlsFor(supplementals)
  );
  await setLocale(defaultLocale);
  bindLocaleSwitcher(defaultLocale);
})();

Y con eso, nuestros usuarios pueden elegir su idioma preferido.

Aplicación de demostración de Globalize con selector de idioma | Phrase

Mostrando los nombres de los idiomas en el idioma activo

Una de las características más poderosas de usar una biblioteca ICU como Globalize es el acceso a una variedad de datos de localización CLDR inmensamente rica. Por ejemplo, podemos mostrar los idiomas en nuestro selector de idiomas en el idioma activo—el inglés sería “الإنجليزية” en árabe, por ejemplo. Necesitaríamos obtener los datos principales de CLDR y luego actualizar nuestro select > option texto.

const defaultLocale = "ar";
const mains = {
    localenames: ["languages"],
};
const supplementals = "likelySubtags"];
async function setLocale(locale) {
  const mensajes = await fetchJson(`/lang/${locale}.json`);
  Globalize.loadMessages(mensajes);
  await loadIntoGlobalize(mainUrlsFor(mains, locale));
  Globalize.locale(locale);
  translatePageElements();
  setLocaleSwitcherDisplayNames();
}
// Opciones dadas = {
//   nombresLocales: ["idiomas"],
//   fechas: ["ca-generic", "ca-gregorian"],

// y, locale = "en", devuelve un array como
// [
//    "/lib/cldr-json/cldr-localenames-full/main/en/languages.json",
//    "/lib/cldr-json/cldr-dates-full/main/en/ca-generic.json",
//    "/lib/cldr-json/cldr-dates-full/main/en/ca-gregorian.json"
// ]
function mainUrlsFor(opciones, idioma) {
  const resultado = [];
  Object.keys(opciones).forEach((clave) => {
    opciones[clave].forEach((colección) => {
      resultado.push(
        `/lib/cldr-json/cldr-${key}-full/main/${locale}/${collection}.json`
      );
    });
  });
  return resultado;
}
function setLocaleSwitcherDisplayNames() {
  const options = document.querySelectorAll(
    "[data-i18n-switcher] opción"
  );
  options.forEach((option) => {
    const localeCode = option.value;
    // Obtener datos principales de CLDR por ruta
    option.textContent = Globalize.cldr.main(
      `localeDisplayNames/languages/${localeCode}`
    );
  });
}

Podemos usar nuestro código principal de carga de JSON siempre que queramos cargar los datos principales de CLDR en JSON. De lo contrario, solo un poco de manipulación del DOM cuando se elige un nuevo idioma nos da nombres de idiomas localizados (¡tan meta!).

Aplicación de demostración de Globalize con nombres de localización localizados | Phrase

🔗 Recurso » La documentación oficial de cldr.js explica cómo recuperar datos JSON de CLDR con más detalle.

Interpolación

El manejo de valores dinámicos en nuestros mensajes se gestiona mediante el formato de mensaje ICU mediante la sintaxis {variable}.

{
  "en": {
    
    "lead": "¡Bienvenido/a a mi pequeño rincón en la web, {username}!"
  }
}
{
  "ar": {
    
    "lead": "أهلاً بك في مكاني الصغير على النت يا {username}."
  }
}

Podemos proporcionar pares de sustitución clave/valor en nuestro HTML.

    <p data-i18n-key="lead" data-i18n-opt='{"username": "Stella"}'>
      ¡Bienvenido/a a mi pequeño rincón en la web, {username}!
    </p>
<!-- ... -->

Y ahora solo necesitamos leer esos pares clave/valor y alimentarlos a Globalize.formatMessage().

function translatePageElements() {
  const elements = document.querySelectorAll(
    "[data-i18n-key]"
  );
  elements.forEach((element) => {
    const key = element.getAttribute("data-i18n-key");
    const interpolaciones =
      element.getAttribute("data-i18n-opt");
    const parsedInterpolations = interpolaciones
      ? JSON.parse(interpolations)
      : {};
    try {
      element.innerHTML = Globalize.formatMessage(
        key,
        parsedInterpolations
      );
    } catch (error) {
      if (error.code === "E_MISSING_MESSAGE") {
        console.warn(error.message);
      } else {
        console.error(error);
      }
    }
  });
}

No hay problema.

Aplicación de demostración de Globalize con interpolación en la localización en inglés | Phrase
Aplicación de demostración de Globalize con interpolación en la localización en árabe | Phrase

Plurales

El manejo de plurales en el formato ICU es insuperable y cubre formas plurales complejas como las del ruso o el árabe. Tenemos que configurar la función de plural antes de poder usarla.

Añadir requisitos de plural

Vayamos a So What’cha Want para averiguar qué necesitaremos incluir si habilitamos plural.

¿Qué quieres para la captura de pantalla de Globalize | Phrase?

No está tan mal: primero necesitaremos agregar una etiqueta <script> para la función.

<!DOCTYPE html>
<html lang="es">
<head>
  <!-- ... -->
</head>
<body>
  <div class="container">
    <!-- ... -->
  </div>
  <script src="./lib/cldr/dist/cldr.js"></script>
  <script src="./lib/cldr/dist/cldr/event.js"></script>
  <script src="./lib/cldr/dist/cldr/supplemental.js"></script>
  <script src="./lib/globalize/dist/globalize.js"></script>
  <script src="./lib/globalize/dist/globalize/message.js"></script>
  <script src="./lib/globalize/dist/globalize/plural.js"></script>
  <script src="./index.js"></script>
</body>
</html>

También necesitaremos el JSON suplementario de plurales, y probablemente el de ordinales si queremos cubrir formatos como «3.º» y «4.º» en nuestros mensajes. Solo necesitaremos agregar los requisitos a nuestro arreglo de configuración supplementals. Nuestra aplicación ya está configurada para cargar el JSON correspondiente al cargar.

const defaultLocale = "ar";
const supplementals = [
  "likelySubtags",
  "plurals",
  "ordinals",
 ];
// ...

Ahora podemos agregar nuestros mensajes plurales. La sintaxis plural de ICU es en gran medida intuitiva: una variable count determina la forma plural elegida, y un símbolo especial # se reemplaza con el valor count al mostrarse.

{
  "en": {
    // ...
    "new-messages": [
      // Podemos llamar a `count` como queramos, siempre que
      // coincide con la clave en nuestra llamada a formatMessage()
      "Tienes {count, plural,
          one {# new message}",
        other {# new messages}",
      "}"
    ]
  }
}

🗒 Nota » Globalize nos permite dividir mensajes multilínea utilizando arrays.

Las seis formas plurales en árabe se gestionan mediante el formato ICU.

{
  "ar": {
    // ...
    "new-messages": [
      "{count, plural, ",
         zero {لا توجد لديك رسائل جديدة}",
          one {لديك رسالة جديدة}",
          two {لديك رسالتان جداد}",
          few {لديك # رسائل جديدة}",
         many {لديك # رسالة جديدة}",
        other {لديك # رسالة جديدة}",
      "}"
    ]
  }
}

🤿 Profundiza » Cubrimos los plurales de ICU con más detalle en La Guía Perdida del Formato de Mensaje ICU.

Por supuesto, necesitamos asegurarnos de que count se suministre a Globalize.formatMessage().

<!-- ... -->
    <p data-i18n-key="new-messages" data-i18n-opt='{"count": 110}'>
      Tienes # nuevos mensajes
    </p>
<!-- ... -->

Y eso es básicamente todo sobre los plurales.

Aplicación de demostración de Globalize con pluralización | Phrase
Aplicación de demostración de Globalize en árabe con pluralización | Phrase
🔗 Recurso » Obtén todo el código demo de Globalize de nuestro repo de GitHub.

🗒 Nota » La implementación de ICU de Globalize abarca los formatos completos de fechas y números. Echa un vistazo a la documentación oficial para más información.

🔗 Recurso » Te mostramos más opciones de instalación de Globalize y casos de uso en JS I18n con Globalize.js.

Conclusión de nuestra guía de localización de JavaScript

Y con esto terminamos por aquí. Esperamos que hayas disfrutado de esta incursión en algunas de las soluciones de localización de JavaScript más populares.

🔗 Recurso » Si trabajas con Ruby on Rails, te puede interesar Cómo localizar JavaScript en aplicaciones Rails.

Y si buscas llevar tu proceso de localización al siguiente nivel, echa un vistazo a Phrase. Phrase soporta el formato ICU, todos los demás formatos de traducción que hemos cubierto aquí y muchos más. Con su CLI y la sincronización con Bitbucket, GitHub y GitLab, tu i18n puede estar en piloto automático. La consola web de Phrase, totalmente equipada, con aprendizaje automático y sugerencias inteligentes, es un placer de usar para los traductores. Una vez que las traducciones estén listas, se pueden sincronizar automáticamente con tu proyecto. Lo configuras y te olvidas, permitiéndote centrarte en el código que amas.

Consulta todas las características de Phrase para desarrolladores y comprueba tú mismo cómo puede agilizar tus flujos de trabajo de localización de software.

Publicaciones relacionadas

Software localization blog category featured image | Phrase

Blog post

La Guía Definitiva para la Localización de Flutter

Descodifiquemos los secretos de la localización de Flutter para que puedas hablar el idioma de tus usuarios y seguir codificando tu camino hacia la dominación global.

Software localization blog category featured image | Phrase

Blog post

Usa el mejor plugin traductor de Phrase Strings para WordPress

Aprende a traducir páginas, publicaciones y más de WordPress a múltiples idiomas con la integración de Phrase Strings para WordPress.

Software localization blog category featured image | Phrase

Blog post

Detectando el idioma de un usuario en una aplicación web

Uno de los problemas más comunes en el desarrollo de aplicaciones web es detectar la configuración regional de un usuario. Así se hace de la manera correcta.

Software localization blog category featured image | Phrase

Blog post

Traducción de la app de iOS de forma inalámbrica con Phrase Strings

Si quieres asegurarte de que todos los usuarios de tu aplicación obtengan el texto correcto, la función Over the Air de Phrase puede ayudarte a publicar tus actualizaciones de traducción al instante.