Internationalisierung mit Eleventy 2.0 und Netlify

Veröffentlicht:
Zuletzt bearbeitet:
Lesezeit:
21 Min.

Ich komme aus Deutschland, lebe und arbeite aber schon lange in Spanien. Mein tägliches Leben ist dreisprachig, da vieles in meiner Arbeit auf Englisch stattfindet.

Als ich vor einigen Monaten meine Website (auf der du dich gerade befindest!) neu entwickelt habe, kam ich auf die Idee, die Inhalte hier auch in allen drei Sprachen zur Verfügung zu stellen. Je mehr ich darüber nachdachte, desto besser gefiel mir die Idee: Denn ist es nicht auch ein Beitrag zur Barrierefreiheit?
Okay, sagen wir, es ist zumindest ein Bemühen um Inclusive Design. Als natürliche Folge vergrößere ich auch meine Reichweite.

Also habe ich mich darauf eingelassen, und dabei viel gelernt. Zum Beispiel war mir der header accept-language bislang unbekannt, der die vom Benutzer bevorzugte natürliche Sprache angibt.

In diesem Artikel, den ich zusammen mit meinem Lightning Talk bei TheJam.dev 2023 veröffentliche, erkläre ich, wie eine grundlegende Einrichtung für die Internationalisierung mit Eleventy vorgenommen werden kann.

Eleventy bündelt ein Plugin speziell für diesen Zweck mit Version 2.0, das die kniffligen Dinge im Hintergrund für uns erledigt.

Mein Ziel ist es, ein mehrsprachiges Starterprojekt zu bauen, das so einfach und verständlich wie möglich ist.

Lasst uns anfangen!

Inhaltsverzeichnis überspringen

Inhaltsverzeichnis

Eleventy 2.0 installieren

Stelle zunächst sicher, dass du Node.js installiert hast. Über den Befehl npm init im Terminal erhältst du eine Datei namens package.json, die Metadaten zu deinem Projekt enthält und deine dependencies aufzeichnet.

Erstelle einen neuen Projektordner und wechsle dann in diesen Ordner. Ich habe mein Projekt “eleventy-i18n-starter” genannt.

Um Eleventy in deinem Projekt zu installieren, führe in der Kommandozeile folgendes aus:

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

Wir brauchen nichts anderes zu installieren! 🎉

Grundlegende Anpassungen

Zunächst erstellen wir eine Eleventy-Konfigurationsdatei. Hier geben wir an, wo sich unsere Quelldateien befinden und wie unser Ausgabeordner heißen soll. Außerdem teile ich Eleventy mit, dass wir Nunjucks" als globale template language für unsere Markdown- und HTML-Dateien verwenden.

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

Als Nächstes erstelle den Eingabeordner src und darin zwei Ordner namens _data (den werden wir später brauchen) und _includes. Innerhalb von _includes füge eine Datei namens base.njk hinzu. Sie enthält unser Hauptlayout:

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>

Für unser einfaches Beispiel brauchen wir nur ein Layout, das wir zum Einbetten aller Inhalte verwenden.

Ich habe nur einige sehr grundlegende und unbedingt erforderliche Details im Bereich head angelegt.
Der title-tag und die meta-description werden aus unseren Markdown-Vorlagendateien gezogen, für das Styling verwende ich ein CSS-Framework auf einem CDN.

Die Landmarke main enthält unseren Inhalt, der aus Markdown-Dateien generiert wird. Damit es nicht zu unübersichtlich wird, lagere ich die “landmarks” header und footer in Layout-Teilbereiche aus.

Lege header.njk und footer.njk im selben Ordner an und lass beide Dateien einfach erstmal leer.

Um Eleventy mit einem schnellen Befehl starten zu können, fügen wir das CLI-Skript in den Abschnitt “scripts” der package.json ein.

package.json:

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

Internationalisierung und Lokalisierung

Bevor wir weitermachen, müssen wir einen kurzen Blick auf die Begriffe Internationalisierung (auch bekannt als “i18n”) und Lokalisierung (“l10n”) werfen, auf die man zwangsläufig stößt.

Lokalisierung

Die Lokalisierung zielt darauf ab, Inhalte in einen strukturell und sprachlich korrekten Kontext für ein bestimmtes Gebiet zu setzen.

Lokalisierungen werden durch ISO-Sprachcodes umschrieben, die sich aus einer Basissprache und dem Land (Gebiet), in dem sie verwendet wird, zusammensetzen.

Am Beispiel von Deutsch gibt es die folgenden Sprachcodes:

de
Deutsch
de-AT
Deutsch (Österreich)
de-CH
Deutsch (Schweiz)
de-DE
Deutsch (Deutschland)
de-LI
Deutsch (Liechtenstein)
de-LU
Deutsch (Luxemburg)

Im Rahmen des Lokalisierungsprozesses werden Inhalte übersetzt, der Tonfall an die jeweilige Kultur angepasst, Währungen und Maßeinheiten der jeweiligen Region hinzugefügt, oder Multimedia-Inhalte so angepasst, dass die Botschaft in dem jeweiligen kulturellen Kontext korrekt vermittelt wird.

Im Rahmen dieses Artikels befassen wir uns vor allem mit der Internationalisierung.

Internationalisierung

Bei der Internationalisierung bereitest du deinen Code auf die Anforderungen der verschiedenen Sprachumgebungen und den gesamten Lokalisierungsprozess vor.

Wir wollen zum Beispiel sicherstellen, dass Sprachen mit einer Rechts-nach-Links-Schreibweise vom CSS korrekt behandelt werden, oder dass Textinhalte aus verschiedenen Sprachen unterschiedlich viel Platz einnehmen können, ohne das Design zu zerstören. Lange deutsche Wörter sind berüchtigt!

Um das zu illustrieren habe ich eine Liste langer deutscher Wörter für euch, die ich im Laufe der Jahre gesammelt habe. Endlich ist sie für etwas zu gebrauchen:

Lange Liste mit langen deutschen Wörtern
  • 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

Eine ernsthafte Internationalisierung und Lokalisierung ist keine leichte Aufgabe.

Auf meiner persönlichen Website (und im Rahmen dieses Projekts) nehme ich mir die Freiheit zu bestimmen, dass es nur eine englische oder spanische Sprache für alle Besucher gibt, so dass die Leser es hinnehmen müssen, dass ich arglos britisches und amerikanisches Englisch in Rechtschreibung und Audruck vermische, weil ich es einfach nicht besser weiß.

Wie auf dieser Website hier, verwende ich für das Startprojekt Englisch, Spanisch und Deutsch, weil ich diese Sprachen beherrsche und automatischen Übersetzungen nicht traue. Ansonsten wäre eine RTL-Sprache natürlich gut gewesen.

Beginnen wir damit, uns eine Struktur für unsere drei Sprachen zu überlegen!

Beispiele für lokalisierte URLs

Es gibt verschiedene Möglichkeiten, Identifikatoren in die URLs einzubauen.

Du kannst länderspezifische Domains kaufen (deinewebsite.es), Subdomains einrichten (es.deinewebsite.com), URL-Parameter hinzufügen (deinewebsite.com?lang=es) oder lokalisierte Unterverzeichnisse erstellen (deinewebsite.com/es).

Bei Google Search Central wurde eine Tabelle mit einigen Vor- und Nachteilen zusammengstellt.

Eleventy kompiliert auf der Grundlage der Ordnerstruktur, so dass lokalisierte Unterverzeichnisse perfekt funktionieren. Dies ist auch die von Eleventy empfohlen Vorgehensweise.

Explizite und implizite Lokalisierung

Als nächstes müssen wir überlegen, ob wir explizit oder implizit lokalisierte Unterverzeichnisse haben wollen. Das heißt, wollen wir einen aus zwei Buchstaben bestehenden Gebietsschema-Code für alle Sprachen in der URL (explizit), oder soll unsere Hauptsprache in der URL ohne Kennung angezeigt werden (implizit)?

Beide Varianten sind möglich. Wenn du eine sehr eindeutige Hauptsprache auf der Website hast und vielleicht nicht alle Inhalte übersetzen willst, ist es sinnvoll, keinen Sprachcode in der URL für deine Hauptsprache anzuzeigen. In den Docs des Plugins wird erklärt, wie man das so einrichtet.

Auf meiner Website sind alle Inhalte übersetzt. Und obwohl ich Englisch als Hauptsprache betrachte, sind mir alle drei Sprachen gleich wichtig. Deshalb habe ich mich für das explizite Schema entschieden - und so wollen wir es auch in diesem Startprojekt machen.

Erstellen der Ordnerstruktur

Fügen wir drei Ordner hinzu, die nach dem zweistelligen ISO-Code jeder Sprache benannt sind, und erstellen wir in jedem Ordner eine Datei namens index.md.

Unsere Ordnerstruktur sieht nun wie folgt aus:

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

Wir fügen in jede Datei einige lokalisierte Inhalte ein:

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.

Wenn wir die Permalink-Struktur in Eleventy nicht ändern, wird unsere Ordnerstruktur im Eingabeverzeichnis unverändert übernommen. Die Datei “index.md” wird dabei als “root” des Sprachverzeichnisses erkannt und direkt als “index.html” übertragen.

Fügen wir nun eine Unterseite hinzu, die klassische “Über mich” Seite, und fügen auch hier lokalisierte Inhalte hinzu.

Ich nenne diese Seite in beiden Sprachen about.md. Das hat verschiedene Vorteile:

  • Das Eleventy i18n-Plugin erkennt die zusammengehörigen Seiten
  • alle Seiten werden in der gleichen Reihenfolge angezeigt
  • es hält mein Startprojekt so schlank und einfach wie möglich.

Wenn wir in unserem Ausgabeordner schauen, sehen wir, dass nun drei neue Unterordner erstellt wurden, die alle “about” heißen.

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

Beachte dass dies finale Struktur unserer Webseite darstellt, dass der Link für die deutsche “Über mich”-Seite also folgendermaßen aussähe: www.mywebsite.com/de/about

Lokalisierte URL-Slugs

Ich ziehe es vor, dass die Sprachen in sich konsistent sind. Passen wir also die Permalinks an.

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.

Wenn du das automatisieren und intuitiver machen willst, gibt es eine clevere Lösung, die ich im Eleventy Base Blog gefunden habe.

Du musst dafür Parent Directory Data Files innerhalb aller lokalisierten Unterordner hinzufügen.

Hier wird die jeweilige Sprache im Permalink angegeben und es wird geprüft, ob ein optionaler Frontmatter-Dateneintrag im Template vorhanden ist, der zu einem “slug” umgewandelt wird. “Slugifiziert”? 😶

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.

In unserem Fall könnten wir nur den Titel “slugifizieren”, aber ich ziehe es vor, diese Dinge getrennt zu behandeln, damit ich den Titel-String anpassen kann, während der Permalink aber unverändert bleibt.

Trennung von Code und Übersetzung

Um zu verstehen, wie das alles funktioniert, ist es wichtig, in Eleventys Data Cascade einzutauchen.

Wir wollen immer den Code von der Übersetzung trennen. Wir trennen bereits Code und Inhalt: Unser Code befindet sich in Layout-Dateien (in diesem Fall einfach base.njk), und unsere Inhalte werden von Markdown in Template-Dateien erstellt. Wir müssen auch einige lokalisierte strings für das Layout speichern. Man könnte sie im Frontmatter der Layout-Datei speichern, aber ich ziehe es vor, alles an einem Ort zu haben.

Hier kommen die globalen Daten ins Spiel!

Globale Daten

Im Ordner _data legen wir alles ab, was in unserem Projekt global verfügbar sein soll. Gültig sind alle *.json und module.exports Werte aus *.js Dateien.

Erstelle die folgenden Datendateien:

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/'
    }
  ]
};

Ich versuche immer, unnötige Komplexität zu vermeiden. Ich lege alle meine lokalisierten strings in meinem global data Ordner in der Datei languages.js ab und greife auf sie mittels dot notation zu.

Wenn ich weitere lokalisierte strings benötige, füge ich sie einfach dort hinzu. Das Gleiche gilt für die Navigation. Ich möchte maximale Kontrolle darüber haben, welche Seiten ich wo anzeige und wie sie im Menü heißen sollen.

Verwendung des Eleventy Internationalisierungs-Plugins

Um das mitgelieferte i18n-Plugin zu aktivieren, füge es zu deiner eleventy.js hinzu:

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

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

  // other settings
};

Wir müssen eine Standardsprache angeben. Ich habe Englisch gewählt, aber es kann jede Sprache sein: defaultLanguage: 'en'

Das Plugin bietet uns eine Ergänzung zur page variable (page.lang) sowie zwei neue universelle Filter (locale_url und locale_links) für die gängigsten Template-Sprachen.

Werfen wir zunächst einen Blick auf page.lang.

Verwendung des “language tags” für das aktuelle Template

page.lang stellt das “language tag” für das aktuelle Seiten-Template dar und wird standardmäßig auf den Wert gesetzt, den wir als defaultLanguage im Plugin angegeben haben.

Zunächst verwenden wir es für das html lang-Attribut und um auf den Richtungswert (dir) zuzugreifen, der in languages.js gesetzt werden kann.

base.njk:

<!doctype html>

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

Es nimmt den lokalisierten Verzeichnisnamen und legt ihn als Sprache des aktuellen Dokuments fest. Im Falle des Attributs dir “loopt” es durch die Datei languages.js in _data und erhält die entsprechende Richtung (RTL oder, als Fallback, LTR).

Das ist sehr wichtig.

Das lang-Attribut wird verwendet, um die Sprache eines Elements zu definieren. Es wird von Screenreadern verwendet, um den richtigen Akzent und die richtige Aussprache zu gewährleisten, der User-Agent kann die richtigen Glyph-Varianten und Anführungszeichen, Silbentrennung, Ligaturen und Abstände auswählen.
Wie alles im Bereich der Barrierefreiheit kommt dies auch den Suchmaschinen zugute.

In diesem Fall legen wir die Sprache des gesamten Dokuments fest, aber du kannst die Sprache auch für jedes beliebige Element festlegen, z.B. <p lang="fr">Bonjour mon ami</p>, innerhalb eines Dokuments, das in einer anderen Sprache geschrieben ist.

Als Nächstes verwenden wir es für unsere Navigation im header und als einfachen Sprachumschalter im 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>

Diese Komponente durchläuft unsere globale Navigationsdatei und zeigt auf der Grundlage des durch page.lang festgelegten Wertes das Menü in der aktuellen Sprache an.

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>

Im footer machen wir zuerst einen Loop durch languages.js, um den String in der aktuellen Sprache zu erhalten, dann verwenden wir den ersten der neu verfügbaren Universalfilter: locale_links.

locale_links gibt ein Array mit den alternativen Sprachversionen für eine bestimmte URL zurück. Die ursprüngliche Seite, die dem Filter übergeben wurde, ist dort nicht enthalten.
Wenn man dieses Array in einem Loop durchläuft, erhält man einen einwandfreien Sprachumschalter!

Du kannst auch einen Sprachumschalter mit zweistelligen Locale-Codes erstellen, der die aktuelle Sprache zuerst anzeigt:

<!-- 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>

Im Element head in base.njk teilen wir dem User-Client mit, dass es alternative Versionen des aktuellen Dokuments gibt. Das Attribut hreflang gibt an, in welcher Sprache die verlinkte Ressource vorliegt.

Beachte, dass jede Sprachversion sowohl sich selbst als auch alle anderen Sprachversionen auflisten muss. Alternative URLs müssen fully-qualified sein, einschließlich der Transportmethode - also http oder https.

Wir fügen zuerst das Link-Attribut zur aktuellen URL hinzu, dann loopen über die alternativen Versionen.

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 -->

Die Tatsache, dass die aktuelle Sprache aus dem Filter “herausgefiltert” wird, ist für diesen Anwendungsfall sehr hilfreich! Aber jetzt möchte ich sehr streng sein und auch x-default hreflang integrieren. x-default hreflang wird verwendet, um die Standardsprache oder regionale Ausrichtung einer Seite anzugeben, wenn keine andere Sprache oder Region explizit angegeben ist. Ich bin mir nicht ganz sicher, was das bedeutet, aber ich werde es mit der Strategie im Hinterkopf aufnehmen, dass Englisch meine Standardsprache ist. Bitte korrigiert mich, wenn ich hier falsch liege.

Damit dies funktioniert, brauche ich den canonical Link zu meiner Standardsprache, Englisch, auf jeder Seite. Ein loop durch den Filter locale_links hilft mir also nicht weiter.

Gut, dass es noch einen weiteren Filter gibt, über den wir noch nicht gesprochen haben.

Der ‘locale_url’ Filter

In den Worten der Eleventy-Dokumentation ausgedrückt, akzeptiert locale_url eine beliebige URL-Zeichenkette und wandelt sie unter Verwendung des Lokalisierungscodes der aktuellen Seite um. Funktioniert wie erwartet, wenn die URL bereits einen Sprachcode enthält. Dies ist besonders nützlich bei gemeinsam genutztem Code, der von internationalisierten Inhalten verwendet wird (Layouts, Partials, Includes usw.).

In unserem Startprojekt brauchen wir es nicht, aber stell dir vor, du verwendest es als Layout-Teil für einen call to action, der den Besucher z. B. auf die lokalisierte Blog-Seite schickt.

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

Es scheint aber nur zu funktionieren, wenn man die Permalinks nicht verändert, deshalb mache ich im Moment keinen Gebrauch davon - zumindest nicht von der herkömmlichen Anwendung.

‘locale_url’ override für ‘x-default hreflang’

Wer einen [Blick in die Dokumentation] (https://www.11ty.dev/docs/plugins/i18n/#locale_url-filter) wirft, findet auch den Hinweis, dass das Überschreiben der Root-Locale mit einem zweiten Argument möglich ist.

Die docs sagen, dass es unwahrscheinlich sei, dass jemand das machen müsste, aber in unserem Fall ist das genau das, was wir brauchen.

Unter unserem loop für alternative URLs fügen wir nun noch dieses “link”-Attribut hinzu:

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

Es ist gut möglich, dass sich das EleventyI18n-Plugin jetzt in der Konsole beschwert. In der Eleventy-Konfigurationsdatei müssen wir den errorMode von default “strict” auf “allow-fallback” setzen.

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

Weitere Anpassungen der Barrierefreiheit

Wir müssen einige wichtige Anpassungen in Bezug auf die Barrierefreiheit vornehmen. Navigations-“Landmarks” (nav) sollten eine Beschriftung (label) haben, und diese Beschriftungen müssen ebenfalls übersetzt werden. Das gleiche gilt für den Link um die Navigation zu überspringen (für Keyboard Navigation).
Beide Zeichenfolgen können in languages.js gesetzt werden.

Du kannst das im GitHub Repo für die header.njk und languages.js umgesetzt finden.

Umleitung des Besuchers zu seinem bevorzugten Sprachverzeichnis

Bevor wir unseren Code auf einer Live-Site verwenden können, müssen wir noch ein sehr wichtiges Detail anpassen.

Im Moment erhalten wir eine 404 in der URL-Root, da wir die Besucher noch nicht zu ihren bevorzugten Sprachverzeichnissen umleiten.

Wie wird die bevorzugte Sprache ermittelt?

Die sprachbasierten Weiterleitungen, die wir einrichten, werden mit der ==ersten Sprache abgeglichen, die der Browser im Accept-Language-Header angibt.

Dies kann in den Voreinstellungen des Browsers angepasst werden. Wenn du deine IP-Adresse nicht verschleierst, erfährt der Benutzer-Client außerdem, wo du dich auf Landesebene befindest.

Theoretisch könnte ich mich selbst als deutschsprachige Person mit Wohnsitz in Spanien ansprechen und einige sehr spezifische Inhalte für diesen Anwendungsfall anzeigen lassen, solange ich diese Information an meinen Browser weitergebe. Richtig?

In den Developer Tools kannst du auf der Registerkarte Netzwerk sehen, welche Werte eingestellt sind. Meine verschiedenen Browser haben unterschiedliche Einstellungen, aber eine meiner eine Chrome-Instanzen zeigt mir dies:

Wie du siehst, ist es keine gute Idee, Leute auf der Grundlage dieser Indikatoren anzusprechen.
Meine bevorzugte Sprache ist auf (britisches) Englisch eingestellt.

In unserem Projekt wollen wir nicht nur eine einfache Möglichkeit für Benutzer bieten, zu ihrer tatsächlich bevorzugten Sprache zu wechseln, sondern auch immer zu einer Basissprache zurückkehren (falls die bevorzugte Sprache des Besuchers z. B. auf Französisch eingestellt ist).

Regeln für die Weiterleitung mit Netlify festlegen

Im Rahmen unseres Beispielprojekts arbeiten wir mit Netlify Hosting.

Netlify bietet eine einfache Implementierung für Redirect-Regeln, entweder über die Datei _redirects oder über die Netlify-Konfigurationsdatei.

Ich persönlich ziehe es vor, die _redirects-Datei nur für automatisierte interne Weiterleitungen zu verwenden, wenn eine Seite dauerhaft den Standort gewechselt hat (eine Art separation of concerns).

Da ich ohnehin eine Netlify-Konfigurationsdatei (netlify.toml) erstelle, um anzugeben, welches mein Ausgabeverzeichnis und welches mein Build-Skript ist, lege ich meine drei sprachbasierten Weiterleitungen ebenfalls dort ab. Ich mag auch die saubere Syntax!

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

Wir leiten im Root-Verzeichnis auf der Grundlage der bevorzugten Sprache um - derjenigen, die an erster Stelle im Accept-Language header steht (conditions = {Language = ["de"]}). Dies ist ein Array, so dass du mehrere Werte setzen kannst.

Lege so viele Umleitungen fest, wie du Sprachen erstellt hast. Die letzte sollte die Fallback-Sprache sein, d.h. die Sprache, die du als defaultLanguage im i18n-Plugin eingestellt hast.

Schlussbemerkungen

Das Projekt ist bereit für Deployment!

Meine bevorzugte Vorgehensweise ist es, ein neues Repo auf GitHub zu erstellen und es auf Netlify einzulesen.

Es gibt noch viele Dinge, die fehlen. Zum Beispiel brauchst du vielleicht lokalisierte collections und einen lokalisierten Datumsfilter für deinen Blog.

Ich wollte diesen Starter so einfach und überschaubar wie möglich halten. Man kann viele Prozesse verfeinern und automatisieren, sei es die Navigation, die Permalinks oder die Unterseiten der jeweiligen Sprachen, die auch über Frotnmatter-keys einander zugeordnet werden können, anstatt denselben Ordnernamen zu verwenden.

Leider hatte CSS in diesem Artikel keinen Platz. Bitte ersetze das CDN-Stylesheet durch dein eigenes, es dient nur dazu, ein einigermaßen ansprechendes Erscheinungsbild zu erreichen, ohne dass CSS-Klassen notwendig sind.

Eine kleine CSS-Erwähnung

Die Pseudoklasse :lang ordnet Elemente auf der Grundlage der Sprache zu, in der sie sich befinden. Eine wirklich schöne Möglichkeit, den Sprachen eine eigene gestalterische Note zu geben!

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

Logical properties bieten automatische Unterstützung für die Internationalisierung und helfen dir dabei stabile und inklusive Frontends zu bauen. Ich empfehle das Kapitel dazu auf web.dev (Learn CSS).

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

Repository “forken”

Du kannst die Repository forken, die ich zusammen mit dem Artikel erstellt habe. Ich habe einige zusätzliche Funktionen hinzugefügt, wie z.B. ein Hilfsskript, das Screenreadern die aktuelle Seite anzeigt, Weiterleitungen zu lokalisierten 404-Seiten und globale Datenstrings für verbesserte Barrierefreiheit (übersetzte Aria-Labels, skip-link…). Um Rendering-Blockaden zu vermeiden, habe ich das minimierte CSS nach dem Entfernen von Stilen, die nicht verwendet werden, inline gesetzt.

Ich würde mich freuen, wenn weitere Sprachen hinzugefügt würden! Hier ist eine Erklärung, wie man zu Projekten beiträgt. Füge einen neuen Ordner mit deinem Regionalcode in src hinzu, und erstelle die entsprechenden Templates in deiner Sprache. Füge dann noch das neue Navigations-Array zu navigation.js, die lokalisierten Strings zu languages.js und die Weiterleitungen zu netlify.toml hinzu.

Ganz zum Schluss eine kleine Entschuldigung: Ich kann mich nur in den englischsprachigen Begriffen ausdrücken und habe noch nie ein deutsches Fachbuch über Web-Technologien gelesen. Ich habe ich eine Menge seltsamer eingedeutschter Formulierungen verwendet oder manche Begriffe gar nicht erst versucht zu übersetzten.

TheJam.dev 2023 Lightning Talk


Ich versuche meine Artikel aktuell zu halten, und natürlich kann ich mich auch irren, oder es gibt eine bessere Lösung. Wenn du etwas siehst was so nicht (mehr) stimmt, oder etwas das noch erwähnt werden sollte, kannst du den Artikel gerne bearbeiten: GitHub.

Webmentions

Hast du eine Antwort veröffentlicht? Lass mich wissen, wo: