Mehrsprachige Webseiten mit Hugo
Hugo umfasst eine Reihe von Hilfsmitteln, um Inhalte für verschiedene Sprachregionen zu lokalisieren und so mehrsprachige Webseiten (multilingual websites) zu realisieren. Auf diese Weise können zwei oder mehr Sprachen eingebunden werden.
Inhalte sollten per Hand in die unterstützten Sprachen übersetzt werden. In Hugo repräsentieren verschiedene Markup-Dateien Übersetzungen eines Inhalts. Es stehen verschiedene Möglichkeiten zur Verfügung, um Übersetzungen als solche auszuweisen. Somit kann Nutzern die Möglichkeit gegeben werden, zwischen verschiedenen Sprachversionen umzuschalten.
Eine sachgemäße Lokalisierung stellt weitere Anforderungen. Im Folgenden werden wir die wichtigsten davon ansprechen und zeigen, wie sie in Hugo umgesetzt werden können. Zunächst muss die Webseite generell für die Unterstützung mehrerer Sprachen konfiguriert werden.
Allgemeine Konfiguration
Einstellungen für eine mit Hugo entwickelte Webseite werden in einer Konfigurationsdatei im Hauptverzeichnis festgelegt, die standardmäßig hugo.toml
, hugo.yaml
oder hugo.json
heißt. Individuelle Sprachen werden unter dem languages
-Schlüssel konfiguriert, wobei die Schlüssel für einzelne Sprachen dem
RFC 5646-Standard entnommen sind.
In YAML sieht die generelle Struktur folgendermaßen aus:
languages:
en:
...
de:
...
fr:
...
Einige Einstellungen, die die Webseite als ganze betreffen, müssen für die verschiedenen Sprachvarianten einer mehrsprachigen Webseite separat konfiguriert werden. Titel und Untertitel der Webseite sind typische Beispiele dafür. Die allgemeine Konfiguration könnte beispielsweise so aussehen:
languages:
de:
languageCode: de-DE
languageName: Deutsch
params:
subtitle: Erklärungen und Tutorials
title: Persönliche Seite
en:
languageCode: en-US
languageName: English
params:
subtitle: Explanations and Tutorials
title: Personal Site
Diese Werte können in Templates über das Site
-Objekt abgerufen werden.
Hugo macht keine Vorgabe, wie title
und ähnliche Werte implementiert werden müssen. Auf einfachen Seiten wird der Titel im <title>
-Tag des Head gesetzt, während er bei komplexeren Seiten möglicherweise Bestandteil vom Titel individueller Dokumente ist. Klar ist, dass Webseitentitel und -untertitel in der Regel übersetzt werden müssen.
Der Hauptverwendungszweck von languageName
liegt in der Implementierung eines Language Switchers. Angenommen unsere Seite unterstützt zwei Sprachen, Deutsch und Englisch. Auf Unterseiten könnte der Name der jeweils anderen Sprache in verlinkter Form angezeigt werden. Wir werden im Folgenden zeigen, wie ein solcher Switcher umgesetzt werden kann.
Der languageCode
kann im Base-Template genutzt werden, um den Wert für das
lang-Attribut im HTML-Tag automatisch zu setzen.
<html lang="{{ .Site.LanguageCode }}">
Von Hugo selbst wird der Wert lediglich in den vordefinierten RSS- und Alias-Templates verwendet.
Wir können einstellen, ob einzelne Sprachen auf der Webseite angezeigt werden. Das ist besonders hilfreich, wenn sich eine Sprachvariante noch im Aufbau befindet. Dazu dient ein weiteres Feld der Hauptkonfiguration:
languages:
de:
disabled: true
Die der Sprache zugeordneten Inhalte sind Benutzern dann nicht mehr verfügbar.
Weitere vordefinierte Felder sind darauf ausgelegt, spezifische Anforderungen der Lokalisierung zu erfüllen. Diese Felder finden keine Anwendung in Templates, sondern beeinflussen direkt den Build-Vorgang der Webseite.
Formale Unterschiede
Zeitangaben, Zahlenformate und Maßeinheiten unterscheiden sich regional. Auch beim Vokabular und in der Rechtschreibung gibt es Unterschiede, etwa bei “Jänner” für Januar (Österreich) oder “ss” statt “ß” (Schweiz). Diese Unterschiede folgen eindeutigen Mustern und lassen sich somit automatisiert übertragen.
Das
Common Locale Data Repository (CLDR) definiert hierfür einen Standard, der auch in Hugo verwendet wird. Die Regeln werden für sprachlich und regional abgegrenzte Einheiten festgelegt, die durch Kombinationen von zweistelligen
Sprachcodes und zweistelligen
Ländercodes gekennzeichnet sind. Zum Beispiel steht de-AT
für Deutsch, wie es in Österreich verwendet wird.
Hugo erkennt anhand des Schlüssels unter languages
, welche Formate auf die jeweiligen Inhalte angewendet werden sollen. Dadurch werden Werte automatisch im passenden Format ausgegeben, wenn die vordefinierten Funktionen und Methoden genutzt werden.
Darüber hinaus müssen die verwendeten Schriftarten möglicherweise verschiedene Schriftzeichen unterstützen. Sprachen wie Arabisch und Hebräisch erfordern zudem eine Schreibrichtung von rechts nach links, die wir in der Konfigurationsdatei festlegen können:
languages:
ar:
languageDirection: rtl
Bei der manuellen Übersetzung sind zudem andere formale Unterschiede zu berücksichtigen, wie die verwendete Währung oder unterschiedliche Maßsysteme.
Wie Inhalte einer Sprache zugeordnet werden
Wie erläutert hängt es von der Sprache ab, wie lokalisierbare Inhalte auf der Webseite angezeigt werden. Nun stellt sich die Frage, wie Hugo erkennt, welche Inhalte zu welcher Sprache gehören. Im nächsten Abschnitt werden wir auf den verwandten Aspekt eingehen: wie Inhalte in verschiedenen Sprachen als Übersetzungen desselben Inhalts identifiziert werden können.
Für einfache Seiten empfiehlt sich eine
Zuordnung über den Dateinamen. Dabei wird der Sprachcode zwischen Basisnamen und Dateiendung eingefügt, wobei die Komponenten durch Punkte getrennt werden. Zum Beispiel wäre about.en.md
eine englische Einzelseite, und content/posts/first-post.en.md
eine englische Einzelseite innerhalb einer Sektion. Für die entsprechende deutsche Einselseite würde content/posts/first-post.de.md
erstellt werden. Hugo erkennt in diesem Fall ohne Weiteres, dass es sich um zueinander gehörige Inhalte handelt.
Eine Zuordnung über Content-Unterverzeichnisse bietet in der Regel zusätzliche Vorteile. So lassen sich alle Inhalte einer Sprachvariante übersichtlich in einem eigenen Bereich der Verzeichnisstruktur verwalten. Welches Unterverzeichnis für welche Sprache verwendet wird, kann flexibel in der Konfiguration festgelegt werden.
languages:
en:
contentDir: content/english
fr:
contentDir: content/french
Diese Einstellungen bestimmt nicht den Aufbau der URLs, über die die Inhalte der jeweiligen Sprache verfügbar sind. Wie bei der Zuordnung über Dateinamen ist auch hier der Sprachcode ausschlaggebend:
languages:
de-AT:
contentDir: content/de
In diesem Fall wäre der in content/de/ueber-mich.md
verfasste Inhalt über domain.com/de-at/ueber-mich/
erreichbar.
Zusätzlich kann eine Haupt- oder Standardsprache definiert werden. Es lässt sich festlegen, ob der Sprachcode auch bei dieser in der URL enthalten sein soll:
defaultContentLanguage: 'de'
defaultContentLanguageInSubdir: true # Still place 'de' in the URL
Für lokalisierte URLs können Datei- und Verzeichnisnamen für diesen Fall übersetzt werden, etwa content/de/beitraege/erster-beitrag.md
. Im nächsten Abschnitt wird gezeigt, wie dieser Inhalt als Übersetzung von content/en/posts/first-post.md
deklariert wird.
Wer jeden Aspekt der Übersetzung selbst in die Hand nehmen möchte, kann die Sprache direkt in der Frontmatter der jeweiligen Inhalte spezifizieren:
---
title: "About Us"
language: "en"
---
Der Vorteil dieses Ansatzes liegt darin, dass die URL der gegebenen Verzeichnisstruktur direkt folgt. In speziellen Fällen kann es sinnvoll sein, diese Methode ergänzend zu den beiden anderen, allgemein bevorzugten Ansätzen einzusetzen.
Lokalisierung der Hauptinhalte
Nun zur Frage, wie Hugo Sprachvarianten desselben Inhalts erkennt. Eine Möglichkeit wurde bereits angesprochen: die Zuordnung über Dateinamen. Wenn sich beispielsweise ein Inhalt unter content/en/about.md
und content/de/about.md
oder unter content/about.en.md
und content/about.de.md
befindet, erkennt Hugo automatisch, dass es sich um Übersetzungen handelt.
Dieser Ansatz hat jedoch einen wesentlichen Nachteil: Standardmäßig spiegelt die URL in Hugo die Verzeichnisstruktur wider. Das bedeutet, dass der genannte Inhalt unter domain.com/de/about/
abrufbar wäre. Nicht übersetzte URLs wirken jedoch nicht nur unschön, sondern sind auch aus Sicht der Suchmaschinenoptimierung (SEO) suboptimal.
Es wurde bereits erwähnt, dass wir beim Unterverzeichnis-basierten Ansatz übersetzte Datei- und Verzeichnisnamen verwenden können, um übersetzte URLs zu erreichen. Dadurch ist jedoch nicht mehr automatisch ersichtlich, dass content/en/about.md
und content/de/ueber-uns.md
denselben Inhalt repräsentieren.
Das
translationKey-Feld in der Frontmatter stellt diesen Zusammenhang her. Hugo betrachtet alle Inhalte als Sprachvarianten desselben Inhalts, wenn sie denselben translationKey
nutzen.
translationKey: about
Die Verzeichnisstruktur spielt dabei keine Rolle. Sobald zwei Inhalte denselben translationKey
haben, werden sind als Übersetzungen voneinander betrachtet. Wie dieser Schlüssel benannt wird, bleibt dem Anwender überlassen.
Language Switcher
Nachdem die zuvor beschriebenen Schritte durchgeführt wurden, erkennt Hugo die Übersetzungen eines bestimmten Inhalts (sofern vorhanden). Diese Informationen können in Templates genutzt werden, um eine Funktion zum Umschalten der Sprache (Language Switcher) zu erstellen.
Wir möchten ein Menü umsetzen, das die anderen verfügbaren Sprachen anzeigt und direkt auf die entsprechende Übersetzung des aktuell geöffneten Inhalts verlinkt. Um den Ansatz zu verdeutlichen, betrachten wir zunächst den einfachsten Fall mit nur zwei Sprachen. Wenn wir uns auf der deutschen Version der Webseite befinden, soll EN
angezeigt werden; auf der englischen Seite hingegen DE
. Diese Auswahl könnte beispielsweise am oberen rechten Bildschirmrand positioniert werden.
{{ if .IsTranslated }}
{{ range .Translations }}
<a href="{{ .RelPermalink }}" hreflang="{{ .Lang }}">
{{ if eq .Lang "en" }}EN{{ else }}DE{{ end }}
</a>
{{ end }}
{{ else }}
{{ if eq .Lang "en" }}
<a href="{{ relURL "de/" }}" hreflang="de">DE</a>
{{ else }}
<a href="{{ relURL "en/" }}" hreflang="en">EN</a>
{{ end }}
{{ end }}
Die aktuell geöffnete Seite bildet den Kontext, der in Hugo durch .
repräsentiert wird. Mit der PAGE.IsTranslated
-Methode können wir abfragen, ob die Seite wenigstens eine Übersetzung hat. Falls keine Übersetzungen existieren, führen die Links zur Startseite der anderen Sprache.
Existieren Übersetzungen, können diese mithilfe der Methode PAGE.Translations
abgerufen werden. Die Übersetzungen werden dann im Kontext der Schleifeniterationen verarbeitet. Jede Übersetzung stellt ein Page
-Objekt dar, dessen URL Hugo automatisch speichert und die über die Methode PAGE.RelPermalink
abgerufen werden kann. Die URL nutzen wir, um auf die entsprechende Sprachvariante zu verlinken. Das
hreflang-Attribut informiert Webcrawler über die Sprache der Zielseite und unterstützt die Suchmaschinenoptimierung.
Für Webseiten mit mehr als zwei Sprachen soll das Menü alle Sprachen auflisten, in die der aktuelle Inhalt übersetzt wurde. Falls der Inhalt nicht übersetzt wurde, wird auf die Startseiten der anderen Website-Sprachen verlinkt.
Statt die Sprache der aktuellen Seite abzufragen, können wir die Sprachen der verfügbaren Übersetzungen ins Menü aufnehmen. Das ist möglich, weil es pro Sprache höchstens eine Übersetzung gibt. Auf diese Weise können wir den Code trotz der Unterstützung weiterer Sprachen vereinfachen:
{{ $currentLang := .Lang }}
{{ if .IsTranslated }}
{{ range .Translations }}
{{ $targetLang := .Lang }}
<a href="{{ .RelPermalink }}"
hreflang="{{ $targetLang }}">
{{- upper $targetLang -}}
</a>
{{ end }}
{{ else }}
{{ range .Site.Languages }}
{{ if ne $currentLang .Lang }}
{{ $targetLang := .Lang }}
<a href="{{ relURL (print .Lang "/") }}"
hreflang="{{ $targetLang }}">
{{- upper $targetLang -}}
</a>
{{ end }}
{{ end }}
{{ end }}
Es werden zwei Variablen für die aktuelle Sprache und die Zielsprache deklariert, um den Code lesbarer zu gestalten. Neu ist vor allem die Handhabung des Falls, in dem der aktuelle Inhalt nicht übersetzt wurde. Die SITE.Languages
-Methode gibt uns eine Liste aller unterstützten Sprachen der Webseite. Im Menü werden dann alle Sprachen aufgelistet, die von der aktuellen Sprache abweichen. Hierbei verwenden wir den übergeordneten Kontext, der durch $
repräsentiert wird und sich in diesem Fall auf die aktuelle Seite bezieht.
Lokalisierung geteilter Komponenten
Komponenten wie Inhaltsverzeichnisse oder Seitenleisten können für verschiedene Sprachvarianten verwendet werden. Diese festen Strukturen beinhalten jedoch häufig hardkodierte Strings, wie "Neueste Beiträge"
oder "Table of Contents"
. Hugo verfügt über einen
Mechanismus, mit dem automatisch die übersetzten Strings eingesetzt werden können.
Angenommen wir haben eine Überblicksseite, auf der die Teile eines Artikels aufgelistet werden. Im Deutschen möchten wir Teil <n>
, im Englischen Part <n>
anzeigen. Zu diesem Zweck definieren wir
Übersetzungstabellen (translation tables) unter i18n/en.yaml
und i18n/de.yaml
:
# i18n/en.yaml
part: Part
# i18n/de.yaml
part: Teil
Mit {{ i18n "part" }}
können wir den jeweiligen Wert in ein Layout oder einen Shortcode einsetzen, wobei Hugo die Sprache der jeweiligen Unterseite automatisch erkennt.
Lokalisierung der Stichwörter
Im Kontext der Verschlagwortung sollten Taxonomien, Taxonomie-Terme und deren Werte lokalisiert werden. Ähnlich wie bei der Lokalisierung der Hauptinhalte geht es dabei vor allem darum, die URLs der entsprechenden Übersichtsseiten zu übersetzen und automatisch auf die passenden Übersetzungen umschalten zu können.
Das Klassifikationssystem für Hauptinhalte muss konfiguriert werden. Bei einer mehrsprachigen Webseite erfolgt die Definition der zu verwendenden Klassen (oder Taxonomien) relativ zu den unterstützten Sprachen.
languages:
en:
taxonomies:
category: categories
tag: tags
de:
taxonomies:
category: kategorien
tag: stichwoerter
Der String-Wert (Plural) bestimmt, was in der URL angezeigt wird. Mit der gegebenen Konfiguration sind zum Beispiel domain.com/de/kategorien/
und domain.com/en/categories/
abrufbar. Der Schlüssel (Singular) wird in der Frontmatter der Inhalte verwendet und kann, falls gewünscht, übersetzt werden. Einheitliche Bezeichnungen erleichtern jedoch die Zuordnung der entsprechenden Taxonomien.
Leider erkennt Hugo die entsprechenden Überblicksseiten nicht automatisch als Übersetzungen voneinander. Um diesen Zusammenhang herzustellen, gehen wir ähnlich wie bei den Hauptinhalten vor: Unter content/en/<plural-of-taxonomy>/_index.md/
und content/de/<plural-of-taxonomy>/_index.md
erhalten beide Inhalte denselben translationKey
:
---
TranslationKey: "taxonomy-categories"
---
Möchte man auch bei Taxonomie-Termen zwischen Sprachvarianten wechseln, steigt der Verwaltungsaufwand erheblich. Um den Überblick zu behalten, empfiehlt es sich auch hier, in allen Sprachversionen denselben Term zu verwenden:
# rezepte.md
---
category:
- cooking
---
# recipes.md
---
category:
- cooking
---
Je nach Komplexität der Webseite kann es sehr viele Terme geben, für die unter content/<lang>/<plural-of-taxonomy>/<term>/
eine _index.md
angelegt werden müsste. Neben dem translationKey
sollte in dieser Datei auch die Übersetzung definiert werden, die in der URL erscheinen soll.
# content/de/kategorien/cooking/_index.md
---
translationKey: term-cooking
slug: kochen
---
Derzeit scheint es keinen Mechanismus zu geben, um diesen Prozess zu vereinfachen. Daher empfehle ich, den Language Switcher für Term-Übersichtsseiten auszublenden.
Geteilte Ressourcen
In der Regel wollen wir für verschiedene Sprachvarianten dieselben Ressourcen nutzen. Gerade geteilte Seitenressourcen werden Fragen bezüglich ihrer korrekten Verwaltung auf. Natürlich wollen wir Mediendateien nicht duplizieren, nur um in den verschiedenen Sprachen einfach auf sie zugreifen zu können. Stattdessen bieten sich zwei Methoden an.
Für Webseiten, die nur moderaten Gebrauch von Medien machen, empfiehlt sich die generelle Verwendung global verfügbarer Ressourcen. Werden sie im assets/
-Verzeichnis abgelegt, können sie sprachunabhängig abgerufen werden.
Wird häufig von Ressourcen Gebrauch gemacht, möchten wir Dateien differenzierter ablegen und mit den entsprechenden Hauptinhalten
bündeln. Werden Inhalte per Dateinamen der Sprache zugeordnet, haben wir keine Probleme. index.de.md
und index.en.md
würden im gleichen Verzeichnis liegen und somit in der gleichen Beziehung zu den gebündelten Assets stehen.
Komplizierter wird es, wenn Inhalte verschiedener Sprachen auf ihren eigenen Zweigen des Verzeichnisbaums abgespeichert werden. In diesem Fall empfiehlt es sich, Ressourcen nur mit einer Sprache zusammenzufügen. Die Übersetzungen des Inhalts können auf dieselben Ressourcen zugreifen.
Dazu muss ein Shortcode definiert werden. Für Bilder könnte das beispielsweise so aussehen:
{{- $imageName := .Get 0 }}
{{- $altText := .Get 1 }}
{{- $caption := .Get 2 }}
{{- $currentPage := .Page }}
{{- $germanPage := $currentPage }}
{{- if ne $currentPage.Language.Lang "de" }}
{{- range where .Site.AllPages "TranslationKey" $currentPage.TranslationKey }}
{{- if eq .Language.Lang "de" }}
{{- $germanPage = . }}
{{- end }}
{{- end }}
{{- end }}
{{- $image := $germanPage.Resources.GetMatch (printf "images/%s" $imageName) }}
{{- if $image }}
{{ partial "images/render-image.html" (dict "image" $image.Permalink "alt" $altText "caption" $caption) }}
{{- else }}
<div class="error">
Image not found: "images/{{ $imageName }}" in page bundle at {{ $germanPage.File.Dir }}
</div>
{{- end }}