Internacionalización con Eleventy 2.0 y Netlify

Publicado:
Última edición:
Tiempo de lectura:
22 min.

Soy de Alemania, y ahora vivo y trabajo en España. Mi vida cotidiana es trilingüe, ya que el inglés es el idioma en el que se desarrolla gran parte de mi actividad profesional.

Cuando hace unos meses rehice mi página web personal (¡ahí es donde estás ahora mismo!), se me ocurrió la idea de que todo el contenido estuviera disponible en los tres idiomas. Si lo piensas bien, ¡es realmente una contribución a la accesibilidad!

Vale, digamos que al menos es un esfuerzo de Diseño Inclusivo. Como consecuencia natural, estoy aumentando mi alcance.

Así que me puse manos a la obra. Y he aprendido mucho por el camino. Por ejemplo, nunca había visto la cabecera accept-language, que indica el idioma natural y la configuración regional que prefiere el usuario.

En este artículo, que publico junto con mi charla relámpago en TheJam.dev 2023, explico cómo se puede hacer una configuración básica para la internacionalización con Eleventy.

Eleventy incluye un plugin específico para este propósito con la versión 2.0 que hace las cosas difíciles por nosotros en segundo plano.

Mi objetivo es construir un proyecto multilingüe de arranque, que sea lo más simple y comprensible posible.

¡Comencemos!

Saltar el índice de contenidos

índice de contenidos

Instalación de Eleventy 2.0

Asegúrate de que tienes Node.js instalado primero. Después de ejecutar npm init, obtendrás un archivo package.json que contendrá metadatos sobre tu proyecto y registrará tus dependencias.

Crea una nueva carpeta de proyecto, luego “cd” en ella. Yo he llamado a la mía “eleventy-i18n-starter”.

Para instalar Eleventy en tu proyecto, en la línea de comandos, ejecuta:

npm install @11ty/eleventy --save-dev

¡No necesitamos instalar nada más! 🎉

Ajustes básicos

Primero, creamos un Eleventy config file. Aquí especificamos dónde se encuentran nuestros archivos fuente, y cómo debe llamarse nuestra carpeta de salida. También hago saber a Eleventy que usamos Nunjucks` como lenguaje de plantilla global por defecto para nuestros archivos markdown y HTML.

module.exports = function (eleventyConfig) {
  return {
    dir: {
      input: 'src',
      output: 'dist'
    },
    markdownTemplateEngine: 'njk',
    htmlTemplateEngine: 'njk'
  };
};

A continuación, crea la carpeta de entrada src, y en ella dos carpetas, llamadas _data (la necesitaremos más adelante) e _includes. Dentro de _includes añade un archivo llamado base.njk. Contiene nuestro “layout” principal:

base.njk:

<!doctype html>
<html lang="" dir="">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>{{ title }}</title>

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/kimeiga/bahunya/dist/bahunya.min.css"
    />

    <link rel="canonical" href="{{ meta.url }}{{ page.url }}" />
    <meta name="description" content="{{ description }}" />
  </head>

  <body>
    {% include "header.njk" %}
    <main>
      <h1>{{ title }}</h1>
      {{ content | safe }}
    </main>

    {% include "footer.njk" %}
  </body>
</html>

Para nuestro sencillo ejemplo, sólo necesitamos un layout que utilizamos para envolver todo el contenido.

Sólo he puesto algunos detalles muy básicos y necesarios en la sección head.
La etiqueta title y la meta description serán extraídas de nuestros archivos de plantilla markdown, para el estilo utilizo un framework CSS sin clases de 10KB en un CDN, ya que este artículo no trata sobre CSS.

El landmark main contiene nuestro contenido, que es generado por archivos markdown. Para evitar que se llene demasiado, externalizo los puntos de referencia header y footer en layout partials.

Añade los parciales header.njk y footer.njk en la misma carpeta.
Por ahora, déjalos vacíos.

Para poder iniciar Eleventy con un comando rápido, añadimos el script CLI en la sección scripts del package.json.

package.json:

  "scripts": {
    "start": "eleventy --serve --watch",
    "build": "eleventy"
  },

Internacionalización y Localización

Antes de continuar, echemos un vistazo rápido a los términos internacionalización (también conocido como “i18n”) y localización (“l10n”), con los que inevitablemente te encontrarás.

Localización

El objetivo de la localización es situar los contenidos en un contexto estructural y lingüísticamente correcto para una localización determinada.

Las localizaciones están circunscritas por códigos de idioma ISO, compuestos por un idioma base y el país (territorio) de uso.
Tomando el alemán como ejemplo, existen los siguientes códigos de localización:

de
Alemán
de-AT
Alemán (Austria)
de-CH
Alemán (Suiza)
de-DE
Alemán (Alemania)
de-LI
Alemán (Liechtenstein)
de-LU
Alemán (Luxemburgo)

Como parte del proceso de localización, hay que traducir el contenido, ajustar el tono de voz para adaptarlo a la cultura, añadir las divisas y unidades de medida utilizadas en esa región o ajustar los elementos multimedia para que el mensaje se transmita correctamente en ese contexto cultural.

En el ámbito de este artículo nos preocupamos más de la internacionalización.

Internacionalización

Con la internacionalización preparas tu código para los requisitos de las diferentes localizaciones y todo el proceso de localización.

Por ejemplo, queremos asegurarnos de que los idiomas que se escriben de derecha a izquierda se manejen correctamente con CSS, o que los contenidos de texto de diferentes idiomas puedan ocupar diferentes cantidades de espacio sin romper el diseño. Las palabras alemanas pueden ser bastante largas.

Si quieres saber más al respecto, aquí tienes algunas palabras largas en alemán.
Por alguna razón las he ido recopilando a lo largo de los años.

Larga lista de palabras largas en alemán
  • Pfarrgemeinderatsmitglied
  • Liegenschaftskataster
  • Kunstfahndungsdienststelle
  • Nichtregierungsorganisationen
  • Mobilfunkanrufaufzeichnungen
  • Säbelscheidenschienbein
  • Strukturentwicklungszulagen
  • nordwestmecklenburgisch
  • Unabhängigkeitsreferendum
  • Junggesellinnenabschiedsparty
  • Reiseschnäppchenportal
  • Selbstverwirklichungsversprechen
  • Unabhängigkeitsbestrebungen
  • Nachwuchsunternehmerin
  • Desinformationskampagne
  • Bundesprogrammkommission
  • Hauptstadtbüroleiterin
  • Altmedienbesitzstandswahrung
  • Weltanschauungsvereinigungen
  • aufmerksamkeitsökonomisch
  • Majestätsbeleidungsparagrafen
  • Zweckentfremdungsverbot
  • Suchmaschinenergebnisseiten
  • Lebensmittelverteilungszentrum
  • Rassekaninchenzüchterverein
  • Bundespsychotherapeutenkammer
  • Auslandsdirektinvestitionen
  • Nachwuchsleistungszentren
  • Wildwasserkajakstrecke
  • Satellitenbeobachtungsprogramm
  • Onlinelebensmittellieferdienst
  • Jahresdurchschnittstemperaturen
  • Androgenisierungserscheinungen
  • Urananreicherungszentrum
  • Senatsuntersuchungsausschuss
  • Energieforschungskooperationen
  • Antiglobalisierungstendenzen
  • Renationalisierungstendenzen
  • Eiskunstlaufenthusiastinnen
  • Antikorruptionsstaatsanwältin
  • Selbstzerfleischungsprozess
  • Unterbringungsmöglichkeiten
  • Heimatschutzministerium
  • Verkehrsüberwachungsmaßnahmen
  • Geschwindigkeitsbegrenzungsschild
  • Kunsthandwerkergenossenschaft
  • Fruchstaftgetränkehersteller
  • Wahlkampffinanzierungsgesetze
  • Unabhängigkeitsbefürworterin
  • Vizepräsidentschaftskandidatin
  • Geldwäscheverdachtsmeldungen

La internacionalización y la localización serias no son tarea fácil.

En mi página web personal (y en el ámbito de este proyecto) me tomo la libertad de determinar que sólo haya un idioma inglés o español para todos los visitantes, así que todos mis lectores deben aguantarme mezclando alegremente el inglés británico y el americano en ortografía y expresiones, porque no sé hacerlo mejor.

Como en esta página web, utilizo el inglés, el español y el alemán para el proyecto inicial, simplemente porque sé hablar estos idiomas y no me fío de las traducciones automáticas. De lo contrario, un lenguaje RTL, por supuesto, habría sido intrigante.

Empecemos a pensar en una estructura para nuestros tres idiomas.

Patrones para URLs específicas de la localización

Hay varios patrones para construir identificadores en las URLs.

Puedes optar por comprar dominios específicos de un país (tuweb.de), crear subdominios (de.tuweb.com), añadir parámetros de URL (tuweb.com?lang=de) o crear subdirectorios localizados (tuweb.com/de).
Google Search Central ofrece una tabla con algunos pros y contras.

Eleventy compila basándose en la estructura de carpetas, por lo que los subdirectorios localizados funcionan perfectamente. Este es también el procedimiento recomendado por Eleventy.

Localización explícita e implícita

A continuación, tenemos que considerar si queremos tener subdirectorios localizados explícitos o implícitos. Es decir, ¿queremos un código de localización de dos letras para todos los idiomas en la URL (explícito), o debe mostrarse nuestro idioma principal en la URL sin un identificador (implícito)?

Ambas variantes son posibles. Si tienes un idioma primario muy claro en el sitio web y tal vez ni siquiera quieres traducir todos los contenidos, tiene sentido no mostrar un código de idioma en la URL para tu idioma primario. En los docs del plugin se explica cómo se configura esto.

En mi sitio personal todo el contenido está traducido. Y aunque considero que el inglés es el idioma principal, valoro los tres idiomas por igual. Así que opté por el esquema explícito, y así es como queremos hacerlo también en este proyecto starter.

Creando la estructura de carpetas

Vamos a añadir tres carpetas, nombradas por el código ISO de dos caracteres de cada idioma, y en cada una crearemos un archivo llamado index.md.

Nuestra estructura de carpetas tiene ahora este aspecto:

│
├── src
│  │
│  ├── _data
│  ├── _includes
│  ├── de
│  │  └── index.md
│  ├── en
│  │  └── index.md
│  ├── es
│  │  └── index.md
│

Pondremos algunos contenidos localizados en cada archivo:

en/index.md:

---
title: 'English Page'
description: 'This is the english version of the homepage'
---

This is a minimal starter for localized content with Eleventy.

de/index.md:

---
title: 'Deutsche Seite'
description: 'Dies ist die deutsche Version der Startseite'
---

Dies ist ein minimaler Starter für lokalisierte Inhalte mit Eleventy.

es/index.md:

---
title: 'Página en español'
description: 'Esta es la versión en español de la página inical'
---

Este es un starter mínimo para contenido localizado con Eleventy.

Si no cambiamos la estructura de enlaces permanentes en Eleventy, tomará nuestra estructura de carpetas dentro del directorio de entrada tal cual. El archivo index.md es reconocido como la raíz del directorio de idioma y transferido directamente como index.html.

Añadamos una subpágina, la clásica “sobre mí”, y añadamos algunos contenidos localizados.

Llamo a esta página about.md en ambos idiomas. Esto tiene varias ventajas:

  • El plugin i18n Eleventy reconoce las páginas relacionadas
  • odas las páginas se muestran en el mismo orden
  • Mantiene mi proyecto lo más mínimo y simple posible.

Si miramos en nuestra carpeta de salida, vemos que ahora se han creado tres nuevas subcarpetas, todas llamadas “about”.

│
├── dist
│  │
│  ├── de
│  │  └── index.html
│  │  └── about
│  │     └── index.html
│  ├── en
│  │  └── index.html
│  │  └── about
│  │     └── index.html
│  ├── es
│  │  └── index.html
│  │  └── about
│  │     └── index.html
│

Ten en cuenta que esta es la estructura final de nuestra página web, por lo que el enlace para la página “Sobre mí” en alemán sería: www.mywebsite.com/de/about.

Slugs de URL localizadas

Prefiero que los idiomas sean coherentes entre sí. Así que vamos a ajustar los permalinks.

de/about.md:

---
title: 'Über mich'
description: 'Eine deutsche Unterseite'
permalink: /de/ueber-mich/index.html
---

Ich bin Webentwicklerin und Designerin, geboren in Berlin, zu Hause in Madrid.

Si quieres automatizar esto un poco más y hacerlo más intuitivo de usar, aquí hay una solución inteligente que encontré en el Eleventy Base Blog.

Tienes que añadir Parent Directory Data Files dentro de todas las subcarpetas localizadas.

Aquí, pasas el idioma respectivo en el permalink y compruebas si hay una entrada de datos frontmatter opcional en tu plantilla, que se slugifica (¿existe esa palabra? 😶).

es/es.11tydata.js:

module.exports = {
  lang: 'es',
  permalink: function (data) {
    // Slug override for localized URL slugs
    if (data.slugOverride) {
      return `/${data.lang}/${this.slugify(data.slugOverride)}/`;
    }
  }
};

es/about.md:

---
title: 'Sobre mí'
description: 'Una subpágina en español'
slugOverride: sobre mi
---

Soy una desarrolladora y diseñadora nacida en Berlín, viviendo en Madrid.

En nuestro caso podríamos simplemente slugificar el título, pero prefiero tratar estas cosas por separado, ya que puedo optar por ajustar el string del título, manteniendo intacto el permalink.

Separar el código de la traducción

Para entender cómo funciona todo esto es crucial que te sumerjas en Eleventys Data Cascade.

Siempre queremos separar el código de la traducción. Ya separamos el código de los contenidos: nuestro código vive en archivos de diseño (en este caso sólo base.njk), y nuestros contenidos son creados por Markdown en archivos de plantilla. También necesitamos almacenar algunos strings localizados para el diseño. Podrías almacenarlas en el Frontmatter del archivo de diseño, pero yo prefiero tener todo en un solo lugar.

¡Los datos globales entran en juego!

Datos globales

En la carpeta _data ponemos todo lo que queremos que esté disponible globalmente en nuestro proyecto. Válidos son todos los valores *.json y module.exports de los archivos *.js.

Crea los siguientes ficheros de datos:

meta.js:

// holds all our meta data
module.exports = {
  url: process.env.URL || 'http://localhost:8080',
  siteName: '18n-starter',
  siteDescription:
    "Minimal starter for localized content, using Eleventy's own Internationalization (I18n) plugin"
};

languages.js:

// same locale codes as in your localized subdirectories
module.exports = {
  en: {
    dir: '', // stands for the direction of the language set in the head, defaults to LTR (left to right)
    availableText: 'This page is also available in:'
  },
  de: {
    availableText: 'Diese Seite ist auch verfügbar in:'
  },
  es: {
    availableText: 'Esta página también está disponible en:'
  }
};

layout.js:

// sets a global layout for all templates, can be overwritten later in the Eleventy Data Cascade
module.exports = 'base.njk';

navigation.js:

// just my personal preference for creating navigation in Eleventy
module.exports = {
  en: [
    {
      text: 'Home',
      url: '/en/'
    },
    {
      text: 'About me',
      url: '/en/about-me/'
    }
  ],
  de: [
    {
      text: 'Startseite',
      url: '/de/'
    },
    {
      text: 'Über mich',
      url: '/de/ueber-mich/'
    }
  ],
  es: [
    {
      text: 'Inicio',
      url: '/es/'
    },
    {
      text: 'Sobre mi',
      url: '/es/sobre-mi/'
    }
  ]
};

Siempre intento evitar complejidades innecesarias. Pongo todas mis strings localizados en mi carpeta de datos globales dentro de languages.js, y accedo a ellos usando “dot notation”.

Si necesito más strings localizados, los añado allí. Lo mismo ocurre con la navegación. Me gusta tener el máximo control sobre qué páginas muestro dónde, y cómo deberían llamarse en el menú.

Usando el plugin de internacionalización de Eleventy

Para activar el paquete i18n-Plugin, añádelo a tu eleventy.js:

const {EleventyI18nPlugin} = require('@11ty/eleventy');

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(EleventyI18nPlugin, {
    defaultLanguage: 'en' // Required
  });

  // other settings
};

Necesitamos especificar un idioma por defecto. Yo elijo el inglés, pero puede ser cualquier idioma: defaultLanguage: 'en'

El plugin nos ofrece una adición a la variable “page” (page.lang) así como dos nuevos filtros universales (locale_url y locale_links) para los “template languages” más comunes.

Veamos primero page.lang.

Uso de la etiqueta de idioma para la plantilla de página actual

page.lang representa la etiqueta de idioma para la plantilla de página actual, y por defecto tendrá el valor que hayamos pasado como defaultLanguage en el plugin.

En primer lugar lo usamos para el atributo html lang y para acceder al valor de dirección (dir) que se puede establecer en languages.js.

base.njk:

<!doctype html>

<html lang="{{ page.lang }}" dir="{{ languages[page.lang].dir or 'ltr' }}">
  <!-- rest of the template -->
</html>

Toma tu nombre de directorio localizado y lo establece como el idioma del documento actual, y en caso del atributo dir, hace un bucle a través de languages.js en _data y obtiene la dirección correspondiente (RTL, o por defecto LTR).

Esto es muy importante.

El atributo lang se utiliza para definir el idioma de un elemento. Es utilizado por los lectores de pantalla para proporcionar el acento y la pronunciación correctos, el User Agent (tu browser) puede seleccionar las variantes correctas de glifos y comillas, separación silábica, ligaduras y espaciado.
Como todo en accesibilidad, también beneficia a los motores de búsqueda.

En este caso, definimos el idioma de todo el documento, pero también se puede establecer el idioma en cualquier elemento, por ejemplo <p lang="fr">Bonjour mon ami</p>, dentro de un documento que esté escrito en otro idioma.

Ya lo he hecho antes con la lista de palabras en alemán. Imagina un lector de pantalla intentando leer en voz alta esta molesta lista cuando está configurado un idioma que no es el alemán.

A continuación, lo utilizamos para nuestra navegación en el punto de referencia header y como un simple conmutador de idioma en el landmark footer.

header.njk:

{% set activePage = page.url %}

<header>
  <nav aria-label="Primary">
    <ul>
      {% for item in navigation[page.lang] %}
      <li>
        <a href="{{ item.url }}">{{ item.text }}</a>
      </li>
      {% endfor %}
      <li>
        <a href="https://github.com/madrilene/eleventy-i18n">GitHub</a>
      </li>
    </ul>
  </nav>
</header>

Este parcial realiza un bucle a través de nuestro archivo de navegación global, y basándose en el valor establecido por page.lang, muestra el menú en el idioma actual.

footer.njk:

<footer>
  {{ languages[page.lang].availableText }}

  {% for link in page.url | locale_links %} {%-if not loop.first %}/{% endif %}
  <a href="{{ link.url }}" lang="{{ link.lang }}" hreflang="{{ link.lang }}"
    >{{ link.label }}</a
  >
  {% endfor %}
</footer>

En el footer primero hacemos un bucle a través de languages.js para obtener el string en el idioma actual, luego usamos el primero de los nuevos filtros universales disponibles: locale_links.

locale_links devuelve un array con el contenido alternativo de una URL especificada. La página original pasada al filtro no se incluye en los resultados.
Haciendo un bucle a través de este array se obtiene un selector de idioma perfecto.

También puedes crear un selector de idioma con códigos de localización de dos caracteres que muestre primero el idioma actual:

<!-- alternative language switcher -->
<footer>
  <nav>
    <a href="{{ page.url }}" lang="{{ page.lang }}" hreflang="{{ page.lang }}">
      {{ page.lang | upper }}
    </a>

    {% for link in page.url | locale_links %}
    <a href="{{ link.url }}" lang="{{ link.lang }}" hreflang="{{ link.lang }}">
      {{ link.lang | upper }}</a
    >
    {% endfor %}
  </nav>
</footer>

En el elemento head de base.njk hacemos saber al “user agent” que existen versiones alternativas del documento actual. El atributo hreflang indica en qué idioma está el recurso enlazado.

Ten en cuenta que cada versión lingüística debe listarse a sí misma, así como todas las demás versiones lingüísticas. Las URL alternativas deben ser completamente cualificadas, incluyendo el método de transporte - es decir http o https.

Primero añadimos el atributo “link” a la URL actual y, a continuación, hacemos un bucle sobre las versiones alternativas.

base.njk:

<!-- stylesheet here -->

<link rel="alternate" hreflang="{{ page.lang }}" href="{{ meta.url }}{{ page.url }}" />

    {% for link in page.url | locale_links %}
    <link
      rel="alternate"
      hreflang="{{ link.lang }}"
      href="{{ meta.url }}{{ link.url }}"
    />
    {% endfor %}

<!-- Canonical URL here -->

El hecho de que el idioma actual sea “filtrado” del filtro es muy útil para este caso de uso. Pero ahora quiero ser muy estricto y tener en cuenta también la anotación x-default hreflang. La anotación hreflang x-default se utiliza para indicar el idioma o la región por defecto de una página cuando no se especifica explícitamente ningún otro idioma o región. No estoy completamente seguro de lo que esto significa, pero lo incluiré con la estrategia en mente, que el inglés es mi idioma por defecto. Por favor, corregidme si me equivoco.

Para que esto funcione necesito el enlace canónico a mi idioma por defecto, inglés, en cada página. Así que pasar por el filtro locale_links no me ayuda.

Lo bueno es que hay otro filtro del que aún no hemos hablado.

Usando el filtro “locale_url”

En palabras de la documentación de Eleventy, locale_url acepta cualquier string de URL arbitrario y lo transforma usando la configuración regional de la página actual. Funciona como se espera si la URL ya contiene un código de idioma. Esto es muy útil en cualquier código compartido usado por contenido internacionalizado (layouts, partials, includes, etc).

En nuestro proyecto inicial no lo necesitamos, pero imagínese usarlo como un parcial de diseño para una call to action, enviando al visitante a la página localizada del blog, por ejemplo. La sintaxis es la siguiente:

<a href="{{ "/blog/" | locale_url }}">Blog</a>

Sin embargo, parece que sólo funciona si no cambias los permalinks, así que de momento no lo uso, al menos no la aplicación convencional.

‘locale_url’ para ‘x-default hreflang’

Si echas un vistazo a la documentación, también verás que es posible anular la configuración regional raíz con un segundo argumento.

Los documentos dicen que es “improbable que lo necesites”, pero en nuestro caso es exactamente lo que necesitamos.

Bajo nuestro bucle para versiones alternativas, añadimos ahora este atributo link:

<link
  rel="alternate"
  hreflang="x-default"
  href="{{ meta.url }}{{ page.url | locale_url('en') }}"
/>

Es muy posible que tu plugin EleventyI18n se esté quejando ahora en la consola. En el fichero de configuración de Eleventy tenemos que cambiar el errorMode de “strict” por defecto a “allow-fallback”.

eleventyConfig.addPlugin(EleventyI18nPlugin, {
  defaultLanguage: 'en',
  errorMode: 'allow-fallback'
});

Más ajustes de accesibilidad

Necesitamos hacer algunos ajustes importantes en términos de accesibilidad. Los puntos de referencia de navegación (nav) deben tener una etiqueta, y estas etiquetas deben ser traducidas también. Lo mismo ocurre con el enlace “saltar navegación”.
Ambos strings pueden establecerse en languages.js.

Puedes encontrarlo implementado en el repositorio de GitHub para header.njk y languages.js.

Redirigir al visitante a su directorio de idioma preferido

Antes de que podamos utilizar nuestro código en un sitio en vivo, tenemos que ajustar un detalle muy importante.

Ahora mismo, obtenemos un 404 en la raíz de la URL, ya que todavía no estamos redirigiendo al visitante a su directorio de idioma preferido.

¿Cómo se determina el idioma preferido?

Las redirecciones basadas en el idioma que vamos a configurar coinciden con el primer idioma informado por el navegador en la cabecera Accept-Language.

Esto puede ajustarse en la configuración de preferencias del navegador. Además, si no oculta su dirección IP, informa a los clientes usuarios sobre su geolocalización a nivel de país.

En teoría, podría segmentarme por ser un hablante de alemán residente en España y mostrar un contenido muy específico para ese caso de uso, siempre que comparta esa información con mi navegador. ¿Verdad?

Puedes ver qué valores están configurados en las herramientas de desarrollo mirando la pestaña de “network”. Mis diferentes navegadores muestran diferentes preferencias, pero Chrome me da esto:

accept-language: en-GB,en;q=0.9,es-ES;q=0.8,es;q=0.7,de-DE;q=0.6,de;q=0.5,en-US;q=0.4

Como puedes ver, dirigirse a la gente basándose en estos indicadores no es una buena idea.
Mi idioma preferido es el inglés (británico).

En nuestro proyecto, además de proporcionar una manera fácil para que los usuarios cambien a su idioma preferido real, queremos siempre volver por defecto a un idioma base (en caso de que el idioma preferido del los visitantes se establezca en francés, por ejemplo).

Estableciendo reglas de redirección con Netlify

En el ámbito de nuestro proyecto de ejemplo estamos trabajando con Netlify Hosting.

Netlify provee una implementación directa para las reglas de redirección, ya sea usando el archivo _redirects, o el archivo de configuración de Netlify.

Personalmente, prefiero utilizar el archivo _redirects sólo para redirecciones internas automatizadas cuando una página ha cambiado permanentemente de ubicación (algún tipo de separación de preocupaciones).

Dado que creo un archivo de configuración de Netlify (netlify.toml) de todos modos para informar cuál es mi directorio de salida y cuál es mi script de construcción, pongo mis tres redirecciones basadas en el idioma allí también. ¡También me gusta la sintaxis limpia!

netlify.toml:

# tell netlify about your build script and output directory

[build]
  command = "npm run build"
  publish = "dist"

# redirect to english, spanish or german landing pages

[[redirects]]
  from = "/"
  to = "/de"
  status = 302
  force= true
  conditions = {Language = ["de"]}

[[redirects]]
  from = "/"
  to = "/es"
  status = 302
  force = true
  conditions = {Language = ["es"]}

[[redirects]]
  from = "/"
  to = "/en"
  status = 302
  force = true

Redirigimos el directorio raíz en función del idioma preferido, el que aparece en primer lugar en el string de cabecera Accept-Language (conditions = {Language = ["de"]}). Se trata de un array, por lo que puede establecer varios valores.

Establece tantas redirecciones como idiomas hayas creado. El último debe ser el idioma alternativo, es decir, el idioma que has establecido como defaultLanguage en el i18n-Plugin.

Notas finales

¡El proyecto está listo para ser desplegado!

Mi forma preferida de hacerlo es crear un nuevo repositorio en GitHub y desplegarlo en Netlify.

Todavía faltan muchas cosas. Por ejemplo, podrías necesitar colecciones localizadas y un filtro de fecha localizado para tu blog.

He querido mantener este starter lo más simple y directo posible. Puedes refinar y automatizar muchos procesos, ya sea la navegación, los permalinks, o las subpáginas de los respectivos idiomas, que pueden ser asignados entre sí con claves en el frontmatter, en lugar de utilizar el mismo nombre de carpeta.

Lamentablemente, el CSS no tiene cabida en este artículo. Por todos los medios, por favor reemplace la hoja de estilos CDN con la suya propia, sólo sirve para dar una apariencia razonablemente agradable sin tener que establecer clases.

Mención honorífica CSS

La pseudo-clase :lang empareja elementos basándose en el idioma en el que se determina que están. Una buena forma de dar un toque creativo a tus idiomas.

nav:lang(de) {
  background-color: darkseagreen;
}

Logical properties proporcionan soporte automático para la internacionalización y te ayudan a construir frontends estables e inclusivos. Recomiendo leer el capítulo sobre el tema en el curso Aprende CSS en web.dev.

.element {
  padding-block-start: 2em;
  padding-block-end: 2em;
}

Bifurcar el repositorio

Puedes bifurcar el repo que creé junto con el artículo. He añadido algunas características extra, como un script de ayuda para indicar la página actual a los lectores de pantalla, redirecciones a páginas 404 localizadas y strings de datos globales para mejorar la accesibilidad (etiquetas aria traducidas, skip-link…). Para evitar el bloqueo de renderizado, he inlineado el CSS minificado después de eliminar los estilos que no se utilizan.

Me encantaría que se añadieran más idiomas. Aquí tienes una explicación de cómo contribuir a los proyectos. Añade una nueva carpeta con tu localización en src, y crea los archivos de plantilla correspondientes en tu idioma. Añade el array de navegación a navigation.js, los strings localizados a languages.js y añade las redirecciones a netlify.toml.

Por último, una pequeña disculpa: Sólo puedo expresarme en términos ingleses y nunca he leído un libro de referencia en español sobre tecnologías web. Utilicé muchas frases extrañas en pseudoespañol o ni siquiera intenté traducir algunos términos.

Algunos enlaces relacionados con el tema

TheJam.dev 2023 Lightning Talk


Intento mantener mis artículos actualizados y, por supuesto, podría equivocarme o podría haber una solución mejor. Si ves algo que (ya) no es correcto, o algo que debería mencionarse, no dudes en editar el artículo en GitHub.

Webmentions

¿Has publicado una respuesta? Dime dónde: