Mehrsprachige Websites mit Jekyll
Nach etlichen Jahren wollte ich vor kurzem endlich das Projekt Website von der
TODO-Liste holen. Da ich nicht mehr bei Imperia{:target=_blank
}
arbeite, musste ich mich nach einer leichtgewichtigen Alternative umschauen.
PHP wollte ich mir nicht antune, was die Auswahl stark einschränkte. Ein Kollege brachte mich
schließlich auf Jekyll{:target=_blank
}, das mich mit seinem
simplen semi-statischen Ansatz an Imperia erinnerte, und ich beschloss, es
auszuprobieren.
Das Thema Mehrsprachigkeit ist eins der Dinge, bei denen sich im CMS-Markt
schnell die Spreu vom Weizen trennt, und von Imperia war ich in dieser
Hinsicht natürlich sehr verwöhnt, weil Mehrsprachigkeit bei Imperia im ganzen System fest
eingebaut ist, und deshalb mehr oder weniger einfach so
funktioniert.
Mehrsprachigkeits-Optionen bei Jekyll
Für Jekyll gibt es verschiedene Plug-Ins, die dem System Mehrsprachigkeit
beibringen sollen. Nach einiger Zeit stieß ich jedoch auf den ausgezeichneten
Artikel Making Jekyll multilingual{:target=_blank
}
von Sylvain Durand{:target=_blank
},
der einen Ansatz ohne Plug-Ins beschreibt.
Ich stelle hier nur die Lösungen dar, bei denen ich von Sylvains Empfehlungen abgewichen bin. Wer alles nachvollziehen will, sollte also zuerst seinen Artikel lesen.
Grundsatzfragen
Bevor die Struktur der mehrsprachigen Site entschieden werden kann, musste
das Thema auch noch webserverseitig angegangen werden. Best Practice ist
hier Content-Negotiation, bei der die Auswahl der Sprachversion einer
Seite vom Webserver mit dem Browser der Besucherin gleichsam ausgehandelt
wird. Das ist auch das Grundprinzip für Mehrsprachigkeit in Imperia.
Ich wollte aber als Webserver Nginx{:target=_blank
} statt
Apache{:target=_blank
} einsetzen. Content-Negotiation ist für
Nginx noch immer nur als Patch für den Sourcecode verfügbar. Das wollte ich mir
nicht antun, und beschloss das Thema mit einem eigenen Handler in Perl
anzugehen.
Der Perl-Handler handelt nur für die Startseite /
die Sprache aus, und
triggert dann einen Redirect auf die sprachspezifische Startseite /en/
,
/de/
und so weiter. That is described in the post
Einfache
Content-Negotiation für Nginx.
Sprachumschalter
Sylvain{:target=_blank
} schlägt vor, im Vorspann die
Variable name
auf einen eindeutigen, sprachunabhängigen Identifier für jedes
Dokument zu setzen, um zu einem Dokument die anderen Sprachvarianten finden
zu können. Das Menü zur Sprachumschaltung sieht bei ihm so aus:
{% assign posts=site.posts | where:"name", page.name | sort: 'path' %}
<ul>
{% for post in posts %}
<li class="lang">
<a href="{{ post.url }}" class="{{ post.lang }}">{{ post.lang }}</a>
</li>
{% endfor %}
</ul>
In Zeile 1 sucht er aus allen Posts die Posts heraus, die das gleiche
Property name
haben. Für jedes dieser Posts erzeugt er in der Schleife von
Zeile 3 bis 7 einen Link.
Das heißt aber, dass für Sprachen, für die der jeweilige Artikel nicht übersetzt ist, kein Eintrag im Sprachmenü angezeigt wird. Ich bevorzuge in solchen Fällen, einen Link auf eine übergeordnete Seite, im Zweifel die Startseite zu setzen:
{% for lang in site.languages %}
{% if page.type == 'posts' %}
{% assign other=site.posts | where: "name", page.name
| where: "lang", lang | first %}
{% else %}
{% assign other=site.pages | where: "pageid", page.pageid
| where: "lang", lang | first %}
{% endif %}
<li class="lang">
<a href="{% if other.url %}{{ other.url }}{% else %}/{{ lang }}/{% endif %}"
class="{{ lang }}">{{ lang | upcase }}</a></li>
{% endfor %}
In Zeile 1 iteriere ich über die Sprachen der Site. Die hole ich mir aus
der Variablen site.languages
, die aus _config.yml
kommt:
languages: [en, de]
Auch die Fallunterscheidung in Zeile 2 ist neu. Das Property name
soll die
verschiedenen Sprachversionen verlinken. Das funktioniert bei mir nur für
Seiten vom Typ posts
, nicht aber zum Beispiel für page
. In Seiten, die
keine Posts sind, definiere ich deshalb eine analoge Variable pageid
mit
dem gleichen Zweck. Es wäre wahrscheinlich schlauer, durchgehend pageid
statt name
zu verwenden, und dann die Fallunterscheidungen zu sparen.
Nachtrag: Statt name
benutzt Sylvain{:target=_blank
}
mittlerweile ref
, was den gleichen Effekt hat, wie durchgehend pageid
zu
verwenden.
Effekt ist jedenfalls, dass die Variable other
jeweils das Seitenobjekt
in der jeweiligen Sprache enthält, sofern dieses vorhanden ist. In Zeile 10
wird der Link für die Sprache entweder auf die Version der Seite in dieser
Sprache oder hilfsweise auf die Startseite gesetzt, genau so, wie wir es
haben wollten.
Verlinkung der Sprachvarianten
Der Code für die sitemap.xml
muss analog angepasst werden:
---
layout:
permalink: /sitemap.xml
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
{% for page in site.pages %}
<url>
<loc>{{ site.url }}{{ page.url }}</loc>
{% assign versions=site.pages | where:"pageid", page.pageid %}
{% for version in versions %}
<xhtml:link rel="alternate" hreflang="{{ version.lang }}"
href="{{ site.url }}{{ version.url }}" />
{% endfor %}
{% if page.date %}
<lastmod>{{ page.date | date_to_xmlschema }}</lastmod>
{% endif %}
<changefreq>monthly</changefreq>
</url>
{% endfor %}
{% for post in site.posts %}
<url>
<loc>{{ site.url }}{{ post.url }}</loc>
{% assign versions=site.posts | where:"name", post.name %}
{% for version in versions %}
<xhtml:link rel="alternate" hreflang="{{ version.lang }}"
href="{{ site.url }}{{ version.url }}" />
{% endfor %}
<lastmod>{{ post.date | date_to_xmlschema }}</lastmod>
<changefreq>weekly</changefreq>
</url>
{% endfor %}
</urlset>
Auch hier wird wieder zwischen Seiten vom Typ page
und post
unterschieden.
Bei Posts wird über die Eigenschaft name
verlinkt, bei Pages via pageid
.
Die Verlinkung im <head>
des HTMLs verläuft analog:
{% for lang in site.languages %}
{% if page.type == 'posts' %}
{% assign other=site.posts | where: "name", page.name
| where: "lang", lang | first %}
{% else %}
{% assign other=site.pages | where: "pageid", page.pageid
| where: "lang", lang | first %}
{% endif %}
{% if other and page.lang != other.lang %}
<link rel="alternate" hreflang="{{other.lang}}" href="{{other.url}}" />
{% endif %}
{% endfor %}
Diesmal fallen wir natürlich nicht auf den Link zur Startseite zurück, weil wir ja wirklich nur auf Ressourcen mit dem gleichen Inhalt in einer anderen Sprache verweisen wollen.
Übersetzung von Template-Texten
Nicht nur die eigentlichen Inhalte müssen übersetzt werden, sondern auch
die Templates enthalten Strings, die übersetzt werden müssen. Das mache ich noch immer mit dem
Ansatz, den auch Sylvain Durand vorschlägt. Die Übersetzungen werden in
_config.yml
eingepflegt:
# Boilerplate translations.
t:
en:
home: Home
toggle_navigation: "Toggle navigation
categories: Categories
featured_posts: "Featured Posts"
ads: Ads
de:
home: Start
toggle_navigation: "Navigation ein-/ausklappen"
categories: Rubriken
featured_posts: "Mehr zu lesen"
ads: Werbung
In den Templates wird dann so auf die Variablen zugegriffen:
<span class="sr-only">{{site.t[page.lang].toggle_navigation}}</span>
Das ist potthässlich. Platzhalter für übersetzbare Strings zu verwenden ist ein Rezept für Probleme. Ich würde eine Markierung der Strings in der Primärspache bevorzugen:
<span class="sr-only">{% gettext "Toggle navigation" %}</span>
Im Moment habe ich nur sehr wenige solche Strings in meiner Konfigurationsdatei und deshalb habe ich mich damit abgefunden. Werden es mehr Strings, werde ich mir eine bessere Lösung überlegen.
blog comments powered by Disqus