Construyendo un sitio web multilingüe más inteligente

Me gusta que las cosas funcionan sin problemas. Como desarrollador, por tanto, me gusta cuando una funcionalidad compleja es sencilla de usar.

Por desgracia, esto también significa que algunas cosas que codifico pasan desapercibidas a pesar de ser, en mi opinión, muy tuanis.

Un ejemplo es la barra de selección dinámica de idiomas que ves en la parte superior de este sitio web (las banderas). El número de banderas cambia dependiendo de la página que visites. Este blog tiene dos banderas porque está disponible en dos idiomas: inglés y español. Otros blogs tienen tres, si están disponibles en todos los idiomas, o sólo una bandera, si sólo escribí en un idioma.

Algunos sitios web multilingües logran todo esto, pero no de forma tan efectiva.

¿Por qué? ¿Cómo?

Los sitios web multilingües son difíciles de gestionar. Muchos operan como sitios separados para cada idioma, con un mega-diccionario de links intermediarios para la selección de idioma. Esto genera desafíos de mantenimiento y una experiencia de usuario fragmentada, lo que resulta en errores comunes cuando los usuarios cambian de idioma en una página que no existe en otro idioma.

Mi objetivo era construir una única plataforma multilingüe y cohesiva.

Para ello, utilicé una combinación de capacidades preexistentes en el framework web que uso para este sitio y algunos trucos que programé yo mismo.

La base de Astro

Mi sitio web está construido con Astro, un framework web moderno que ofrece robustas funcionalidades integradas para la internacionalización (i18n). Esto me permite organizar el contenido para diferentes idiomas dentro del mismo proyecto, en lugar de tener que gestionar sitios web distintos.

Mi contenido usa directorios anidados:

  • Las páginas de nivel superior residen bajo /src/pages/en/..., las de español bajo /src/pages/es/..., y las de finés bajo /src/pages/fi/...,
  • Las colecciones de contenido (ejemplo: blogs) tienen una estructura similar: <collection>/en/..., <collection>/es/..., <collection>/fi/....

Astro se encarga del enrutamiento del contenido utilizando una convención <folder>/<language>/<filename> para los URLs de las páginas y <collection>/<language>/<slug> para los URLs de las colecciones.

Importante: las páginas y el contenido en diferentes idiomas son independientes. Mi página de inicio en inglés podría decir y verse completamente diferente a mi página de inicio en español. Del mismo modo, no necesito traducir cada blog a todos los idiomas.

Validación dinámica de idiomas

Es genial poder tener contenido diferente en distintos idiomas, pero esto también crea un desafío. Las páginas en un idioma no necesariamente existen en otros.

Para evitar una sopa interminable de enlaces rotos, la barra de idiomas superior se adapta al contenido.

Gestión de URLs

El primer paso es determinar si la URL prevista para una página es similar en todos los idiomas (ejemplo: .../blog/en/eu-ai/, .../blog/es/eu-ai/, .../blog/fi/eu-ai/) o si las URLs son específicas de cada idioma (ejemplo: .../blog/en/dynamic-langs/, .../blog/es/barra-lenguaje-dinamica/).

Para esto, utilizo un pequeño diccionario de “rutas” que incluye sólo las páginas con URLs diferentes entre idiomas.


export const routes = [
  // ...
  { en: "digital-transformation-currents", es: "corrientes-transformacion-digital", fi: "digitaalisen-kehityksen-virrat" },
  { en: "risk-in-platform-economy", es: "gestion-riesgo-digital", fi: "" }, // A propósito: no hay equivalente en finlandés para esta página específica
  // ...
]

Antes de renderizar las banderas, el sitio verifica si una página está en este diccionario. Si lo está, toma los slugs del diccionario; de lo contrario, asume que lo único que cambia en la URL es el prefijo del idioma.

Gestionando contenido inexistente

Después, el sitio verifica si las páginas existen en diferentes idiomas. Por ejemplo, si una entrada de blog existe en inglés y español, pero no en finlandés, el sitio detectará la ausencia del archivo de contenido en finlandés.

El código relevante se muestra a continuación. Sólo los idiomas en valid_langs tendrán sus banderas renderizadas.


// Recoge el objeto con todos los idiomas que el sitio soporta
let valid_langs = Object.keys(languages);

// Determina los slugs donde estaría el contenido si existe
// Nota: El objeto "routes" (recogido anteriormente en el código) será una cadena si los slugs son iguales en todos los idiomas
let lang_slugs =
  typeof routes === "string" ? [slug, slug, slug] : Object.values(routes);

// Determina los idiomas aplicables
if ( ... ) {   // Condiciones para evitar que el bloque se ejecute en secciones donde el contenido SÍ existirá en todos los idiomas

  // Variables auxiliares
  valid_langs = [];
  let items: (object | undefined)[] = [];

  // Para cada idioma soportado, comprueba si existe el contenido
  for (let i = 0; i < Object.keys(languages).length; i++) {
    const post_in_lang = await getEntry(
      "blog",
      `${Object.keys(languages)[i]}/${lang_slugs[i]}`,
    );
    items.push(post_in_lang);
  }

  const do_posts_exist = items.every((item) => !item)
    ? [true, true, true]
    : items.map((post) => {
        return post !== undefined;
      });

  // Si el contenido existe, añadir a la lista de banderas a mostrar
  for (let i = 0; i < Object.keys(languages).length; i++) {
    if (do_posts_exist[i]) {
      valid_langs.push(Object.keys(languages)[i]);
    }
  }
}

Conclusión

Este método de validación dinámica de la disponibilidad de contenido ofrece ventajas significativas:

  • Ofrece la libertad de tener contenido muy diferente en distintos idiomas.
  • Evita el engorro de tener que actualizar innumerables enlaces o gestionar instancias de sitios separadas.
  • Reduce las posibilidades de que los usuarios terminen en páginas de error debido a enlaces de idioma rotos.
  • Facilita la adición de nuevos idiomas en el futuro.

Y probablemente ni siquiera te darás cuenta de que las banderas cambian dependiendo de la página.

¡Una experiencias de usuario fluida!


Ps. El código completo para la barra de lenguaje está disponible en GitHub.