Übersetzung von Stimulus-Apps mit I18Next
() translation by (you can also view the original English article)
In meinem vorherigen Artikel habe ich Stimulus behandelt - ein bescheidenes JavaScript-Framework, das von Basecamp erstellt wurde. Heute werde ich über die Internationalisierung einer Stimulus-Anwendung sprechen, da das Framework keine sofort einsatzbereiten I18n-Tools bereitstellt. Die Internationalisierung ist ein wichtiger Schritt, insbesondere wenn Ihre App von Menschen aus der ganzen Welt verwendet wird. Daher kann ein grundlegendes Verständnis der Vorgehensweise sehr nützlich sein.
Natürlich liegt es an Ihnen, zu entscheiden, welche Internationalisierungslösung implementiert werden soll, sei es jQuery.I18n, Polyglot oder eine andere. In diesem Tutorial möchte ich Ihnen ein beliebtes I18n-Framework namens I18next zeigen, das viele coole Funktionen bietet und viele zusätzliche Plugins von Drittanbietern bietet, um den Entwicklungsprozess noch weiter zu vereinfachen. Trotz all dieser Funktionen ist I18next kein komplexes Tool, und Sie müssen nicht viele Dokumentationen studieren, um loszulegen.
In diesem Artikel erfahren Sie, wie Sie die I18n-Unterstützung in Stimulus-Anwendungen mithilfe der I18next-Bibliothek aktivieren. Im Einzelnen werden wir über Folgendes sprechen:
- I18next Konfiguration
- Übersetzungsdateien und asynchrones Laden
- Übersetzungen durchführen und die ganze Seite auf einmal übersetzen
- Arbeiten mit Pluralformen und Geschlechtsinformationen
- Wechseln zwischen Gebietsschemas und Beibehalten des ausgewählten Gebietsschemas im Parameter GET
- Festlegen des Gebietsschemas basierend auf den Einstellungen des Benutzers
Der Quellcode ist im Tutorial GitHub Repo verfügbar.
Bootstrapping einer Stimulus-App
Um zu beginnen, klonen wir das Stimulus Starter-Projekt und installieren alle Abhängigkeiten mit dem Yarn-Paketmanager:
1 |
git clone https://github.com/stimulusjs/stimulus-starter.git |
2 |
cd stimulus-starter
|
3 |
yarn install
|
Wir werden eine einfache Webanwendung erstellen, die Informationen über die registrierten Benutzer lädt. Für jeden Benutzer zeigen wir sein Login und die Anzahl der Fotos an, die er bisher hochgeladen hat (es spielt keine Rolle, um welche Fotos es sich handelt).
Außerdem werden wir oben auf der Seite einen Sprachumschalter präsentieren. Wenn eine Sprache ausgewählt wird, sollte die Benutzeroberfläche sofort ohne erneutes Laden der Seite übersetzt werden. Darüber hinaus sollte die URL mit einem Gebietsschema angehängt werden ?locale
GET-Parameter, der angibt, welches Gebietsschema derzeit verwendet wird. Wenn die Seite mit diesem bereits bereitgestellten Parameter geladen wird, sollte natürlich automatisch die richtige Sprache eingestellt werden.
Okay, lassen Sie uns mit dem Rendern unserer Benutzer fortfahren. Fügen Sie der Datei public/index.html die folgende Codezeile hinzu:
1 |
<div data-controller="users" data-users-url="/api/users/index.json"></div> |
Hier verwenden wir den users
-Controller und geben eine URL an, über die unsere Benutzer geladen werden können. In einer realen Anwendung hätten wir wahrscheinlich ein serverseitiges Skript, das Benutzer aus der Datenbank abruft und mit JSON antwortet. Für dieses Tutorial platzieren wir jedoch einfach alle erforderlichen Daten in der Datei public/api/users/index.json:
1 |
[
|
2 |
{
|
3 |
"login": "johndoe", |
4 |
"photos_count": "15", |
5 |
"gender": "male" |
6 |
},
|
7 |
{
|
8 |
"login": "annsmith", |
9 |
"photos_count": "20", |
10 |
"gender": "female" |
11 |
}
|
12 |
]
|
Erstellen Sie nun eine neue Datei src/controllers/users_controller.js:
1 |
import { Controller } from "stimulus" |
2 |
|
3 |
export default class extends Controller { |
4 |
connect() { |
5 |
this.loadUsers() |
6 |
}
|
7 |
}
|
Sobald der Controller mit dem DOM verbunden ist, laden wir unsere Benutzer mithilfe der loadUsers()
-Methode asynchron:
1 |
loadUsers() { |
2 |
fetch(this.data.get("url")) |
3 |
.then(response => response.text()) |
4 |
.then(json => { |
5 |
this.renderUsers(json) |
6 |
})
|
7 |
}
|
Diese Methode sendet eine Abrufanforderung an die angegebene URL, erfasst die Antwort und rendert schließlich die Benutzer:
1 |
renderUsers(users) { |
2 |
let content = '' |
3 |
JSON.parse(users).forEach((user) => { |
4 |
content += `<div>Login: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>` |
5 |
})
|
6 |
this.element.innerHTML = content |
7 |
}
|
renderUsers()
analysiert wiederum JSON, erstellt eine neue Zeichenfolge mit dem gesamten Inhalt und zeigt diesen Inhalt zuletzt auf der Seite an (this.element
gibt den tatsächlichen DOM-Knoten zurück, mit dem der Controller verbunden ist und in den div
unterteilt ist unser Fall).
I18next
Jetzt werden wir I18next in unsere App integrieren. Fügen Sie unserem Projekt zwei Bibliotheken hinzu: I18next selbst und ein Plugin, um das asynchrone Laden von Übersetzungsdateien aus dem Back-End zu ermöglichen:
1 |
yarn add i18next i18next-xhr-backend |
Wir werden alle I18next-bezogenen Inhalte in einer separaten Datei src/i18n/config.js speichern. Erstellen Sie sie jetzt:
1 |
import i18next from 'i18next' |
2 |
import I18nXHR from 'i18next-xhr-backend' |
3 |
|
4 |
const i18n = i18next.use(I18nXHR).init({ |
5 |
fallbackLng: 'en', |
6 |
whitelist: ['en', 'ru'], |
7 |
preload: ['en', 'ru'], |
8 |
ns: 'users', |
9 |
defaultNS: 'users', |
10 |
fallbackNS: false, |
11 |
debug: true, |
12 |
backend: { |
13 |
loadPath: '/i18n/{{lng}}/{{ns}}.json', |
14 |
}
|
15 |
}, function(err, t) { |
16 |
if (err) return console.error(err) |
17 |
});
|
18 |
|
19 |
export { i18n as i18n } |
Gehen wir von oben nach unten, um zu verstehen, was hier vor sich geht:
-
use(I18nXHR)
aktiviert das i18next-xhr-Backend-Plugin. -
fallbackLng
weist es an, Englisch als Fallback-Sprache zu verwenden. - Mit
whitelist
können nur englische und russische Sprachen eingestellt werden. Natürlich können Sie auch andere Sprachen wählen. -
preload
weist Übersetzungsdateien an, die vom Server vorgeladen werden sollen, anstatt sie zu laden, wenn die entsprechende Sprache ausgewählt ist. -
ns
bedeutet "namespace" und akzeptiert entweder eine Zeichenfolge oder ein Array. In diesem Beispiel haben wir nur einen Namespace, aber für größere Anwendungen können Sie andere Namespaces wieadmin
,cart
,profile
usw. einführen. Für jeden Namespace sollte eine separate Übersetzungsdatei erstellt werden. -
defaultNS
legt fest, dassusers
der Standard-Namespace sind. -
fallbackNS
deaktiviert den Namespace-Fallback. - Mit
debug
können Debugging-Informationen in der Browserkonsole angezeigt werden. Insbesondere wird angegeben, welche Übersetzungsdateien geladen werden, welche Sprache ausgewählt ist usw. Möglicherweise möchten Sie diese Einstellung deaktivieren, bevor Sie die Anwendung für die Produktion bereitstellen. -
backend
bietet die Konfiguration für das I18nXHR-Plugin und gibt an, von wo Übersetzungen geladen werden sollen. Beachten Sie, dass der Pfad den Titel des Gebietsschemas enthalten sollte, während die Datei nach dem Namespace benannt sein und die Erweiterung .json haben sollte -
function(err, t)
ist der Rückruf, der ausgeführt wird, wenn I18next bereit ist (oder wenn ein Fehler aufgetreten ist).
Danach erstellen wir Übersetzungsdateien. Übersetzungen für die russische Sprache sollten in die Datei public/i18n/ru/users.json gestellt werden:
1 |
{
|
2 |
"login": "Логин" |
3 |
}
|
login
hier ist der Übersetzungsschlüssel, während Логин
der anzuzeigende Wert ist.
Englische Übersetzungen sollten wiederum in die Datei public/i18n/en/users.json gehen:
1 |
{
|
2 |
"login": "Login" |
3 |
}
|
Um sicherzustellen, dass I18next funktioniert, können Sie dem Rückruf in der Datei i18n/config.js die folgende Codezeile hinzufügen:
1 |
// config goes here...
|
2 |
function(err, t) { |
3 |
if (err) return console.error(err) |
4 |
console.log(i18n.t('login')) |
5 |
}
|
Hier verwenden wir eine Methode namens t
, die "übersetzen" bedeutet. Diese Methode akzeptiert einen Übersetzungsschlüssel und gibt den entsprechenden Wert zurück.
Möglicherweise müssen jedoch viele Teile der Benutzeroberfläche übersetzt werden, und die Verwendung der t
-Methode wäre ziemlich mühsam. Stattdessen schlage ich vor, dass Sie ein anderes Plugin namens loc-i18next verwenden, mit dem Sie mehrere Elemente gleichzeitig übersetzen können.
Übersetzen in One Go
Installieren Sie das loc-i18next-Plugin:
1 |
yarn add loc-i18next |
Importieren Sie es oben in die Datei src/i18n/config.js:
1 |
import locI18next from 'loc-i18next' |
Geben Sie nun die Konfiguration für das Plugin selbst an:
1 |
// other config
|
2 |
|
3 |
const loci18n = locI18next.init(i18n, { |
4 |
selectorAttr: 'data-i18n', |
5 |
optionsAttr: 'data-i18n-options', |
6 |
useOptionsAttr: true |
7 |
});
|
8 |
|
9 |
export { loci18n as loci18n, i18n as i18n } |
Hier sind einige Dinge zu beachten:
-
locI18next.init(i18n)
erstellt eine neue Instanz des Plugins basierend auf der zuvor definierten Instanz von I18next. -
selectorAttr
gibt an, welches Attribut zum Erkennen von Elementen verwendet werden soll, für die eine Lokalisierung erforderlich ist. Grundsätzlich wird loc-i18next nach solchen Elementen suchen und den Wert desdata-i18n
-Attributs als Übersetzungsschlüssel verwenden. -
optionsAttr
gibt an, welches Attribut zusätzliche Übersetzungsoptionen enthält. -
useOptionsAttr
weist das Plugin an, die zusätzlichen Optionen zu verwenden.
Unsere Benutzer werden asynchron geladen, daher müssen wir warten, bis dieser Vorgang abgeschlossen ist, und erst danach eine Lokalisierung durchführen. Stellen Sie zunächst einfach einen Timer ein, der zwei Sekunden warten soll, bevor Sie die localize()
-Methode aufrufen - das ist natürlich ein vorübergehender Hack.
1 |
import { loci18n } from '../i18n/config' |
2 |
|
3 |
// other code...
|
4 |
|
5 |
loadUsers() { |
6 |
fetch(this.data.get("url")) |
7 |
.then(response => response.text()) |
8 |
.then(json => { |
9 |
this.renderUsers(json) |
10 |
setTimeout(() => { // <--- |
11 |
this.localize() |
12 |
}, '2000') |
13 |
})
|
14 |
}
|
Codieren Sie die localize()
-Methode selbst:
1 |
localize() { |
2 |
loci18n('.users') |
3 |
}
|
Wie Sie sehen, müssen wir nur einen Selektor an das loc-i18next-Plugin übergeben. Alle darin enthaltenen Elemente (für die das Attribut data-i18n
festgelegt ist) werden automatisch lokalisiert.
Optimieren Sie nun die renderUsers
-Methode. Lassen Sie uns vorerst nur das Wort "Login" übersetzen:
1 |
renderUsers(users) { |
2 |
let content = '' |
3 |
JSON.parse(users).forEach((user) => { |
4 |
content += `<div class="users">ID: ${user.id}<br><span data-i18n="login"></span>: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>` |
5 |
})
|
6 |
this.element.innerHTML = content |
7 |
}
|
Nett! Laden Sie die Seite neu, warten Sie zwei Sekunden und stellen Sie sicher, dass für jeden Benutzer das Wort "Login" angezeigt wird.
Plural und Geschlecht
Wir haben einen Teil der Benutzeroberfläche lokalisiert, was wirklich cool ist. Dennoch hat jeder Benutzer zwei weitere Felder: die Anzahl der hochgeladenen Fotos und das Geschlecht. Da wir nicht vorhersagen können, wie viele Fotos jeder Benutzer haben wird, sollte das Wort "Foto" basierend auf der angegebenen Anzahl richtig pluralisiert werden. Dazu benötigen wir ein zuvor konfiguriertes data-i18n-options
-Attribut. Um die Anzahl bereitzustellen, sollten data-i18n-options
das folgende Objekt zugewiesen werden: {"count": YOUR_COUNT}
.
Geschlechtsspezifische Informationen sollten ebenfalls berücksichtigt werden. Das Wort "hochgeladen" auf Englisch kann sowohl für Männer als auch für Frauen verwendet werden, aber auf Russisch wird es entweder zu "загрузил" oder "загрузила", sodass wir wieder data-i18n-options
benötigen, die {"context": "GENDER"}
haben als Wert. Beachten Sie übrigens, dass Sie diesen Kontext verwenden können, um andere Aufgaben zu erfüllen, nicht nur um geschlechtsspezifische Informationen bereitzustellen.
1 |
renderUsers(users) { |
2 |
let content = '' |
3 |
JSON.parse(users).forEach((user) => { |
4 |
content += `<div class="users"><span data-i18n="login"></span>: ${user.login}<br><span data-i18n="uploaded" data-i18n-options="{ 'context': '${user.gender}' }"></span> <span data-i18n="photos" data-i18n-options="{ 'count': ${user.photos_count} }"></span></div><hr>` |
5 |
})
|
6 |
this.element.innerHTML = content |
7 |
}
|
Aktualisieren Sie nun die englischen Übersetzungen:
1 |
{
|
2 |
"login": "Login", |
3 |
"uploaded": "Has uploaded", |
4 |
"photos": "one photo", |
5 |
"photos_plural": "{{count}} photos" |
6 |
}
|
Nichts komplexes hier. Da wir uns für Englisch nicht um die Geschlechtsinformationen kümmern (was der Kontext ist), sollte der Übersetzungsschlüssel einfach uploaded
werden. Um korrekt pluralisierte Übersetzungen bereitzustellen, verwenden wir die Tasten photos
und photos_plural
. Der Teil {{count}}
ist eine Interpolation und wird durch die tatsächliche Nummer ersetzt.
Was die russische Sprache betrifft, sind die Dinge komplexer:
1 |
{
|
2 |
"login": "Логин", |
3 |
"uploaded_male": "Загрузил уже", |
4 |
"uploaded_female": "Загрузила уже", |
5 |
"photos_0": "одну фотографию", |
6 |
"photos_1": "{{count}} фотографии", |
7 |
"photos_2": "{{count}} фотографий" |
8 |
}
|
Beachten Sie zunächst, dass wir für zwei mögliche Kontexte sowohl die Schlüssel uploaded_male
als auch uploaded_female
haben. Als nächstes sind die Pluralisierungsregeln auf Russisch komplexer als auf Englisch, daher müssen wir nicht zwei, sondern drei mögliche Sätze angeben. I18next unterstützt viele sofort einsatzbereite Sprachen. Mit diesem kleinen Tool können Sie besser verstehen, welche Pluralisierungsschlüssel für eine bestimmte Sprache angegeben werden sollten.
Gebietsschema wechseln
Wir sind mit der Übersetzung unserer Anwendung fertig, aber Benutzer sollten in der Lage sein, zwischen Gebietsschemas zu wechseln. Fügen Sie daher der Datei public/index.html eine neue Komponente "language switcher" hinzu:
1 |
<ul data-controller="languages" class="language-switcher"></ul> |
Erstellen Sie den entsprechenden Controller in der Datei src/controller/language_controller.js:
1 |
import { Controller } from "stimulus" |
2 |
import { i18n, loci18n } from '../i18n/config' |
3 |
|
4 |
export default class extends Controller { |
5 |
initialize() { |
6 |
let languages = [ |
7 |
{title: 'English', code: 'en'}, |
8 |
{title: 'Русский', code: 'ru'} |
9 |
]
|
10 |
|
11 |
this.element.innerHTML = languages.map((lang) => { |
12 |
return `<li data-action="click->languages#switchLanguage" |
13 |
data-lang="${lang.code}">${lang.title}</li>` |
14 |
}).join('') |
15 |
}
|
16 |
}
|
Hier verwenden wir den Rückruf initialize()
, um eine Liste der unterstützten Sprachen anzuzeigen. Jedes li
hat ein data-action
-Attribut, das angibt, welche Methode (in diesem Fall switchLanguage
) ausgelöst werden soll, wenn auf das Element geklickt wird.
Fügen Sie nun die switchLanguage()
-Methode hinzu:
1 |
switchLanguage(e) { |
2 |
this.currentLang = e.target.getAttribute("data-lang") |
3 |
}
|
Es nimmt einfach das Ziel des Ereignisses und erfasst den Wert des data-lang
-Attributs.
Ich möchte auch einen Getter und Setter für das currentLang
-Attribut hinzufügen:
1 |
get currentLang() { |
2 |
return this.data.get("currentLang") |
3 |
}
|
4 |
|
5 |
set currentLang(lang) { |
6 |
if(i18n.language !== lang) { |
7 |
i18n.changeLanguage(lang) |
8 |
}
|
9 |
|
10 |
if(this.currentLang !== lang) { |
11 |
this.data.set("currentLang", lang) |
12 |
loci18n('body') |
13 |
this.highlightCurrentLang() |
14 |
}
|
15 |
}
|
Der Getter ist sehr einfach - wir rufen den Wert der aktuell verwendeten Sprache ab und geben ihn zurück.
Der Setter ist komplexer. Zunächst verwenden wir die changeLanguage
-Methode, wenn die aktuell eingestellte Sprache nicht der ausgewählten entspricht. Außerdem speichern wir das neu ausgewählte Gebietsschema unter dem Attribut data-current-lang
(auf das im Getter zugegriffen wird), lokalisieren den Hauptteil der HTML-Seite mithilfe des Plugins loc-i18next und markieren zuletzt das aktuell verwendete Gebietsschema.
Lassen Sie uns das highlightCurrentLang()
codieren:
1 |
highlightCurrentLang() { |
2 |
this.switcherTargets.forEach((el, i) => { |
3 |
el.classList.toggle("current", this.currentLang === el.getAttribute("data-lang")) |
4 |
})
|
5 |
}
|
Hier iterieren wir über ein Array von Gebietsschema-Switchern und vergleichen die Werte ihrer data-lang
-Attribute mit dem Wert des aktuell verwendeten Gebietsschemas. Wenn die Werte übereinstimmen, wird dem Umschalter eine current
CSS-Klasse zugewiesen, andernfalls wird diese Klasse entfernt.
Damit das Konstrukt this.switcherTargets
funktioniert, müssen Stimulusziele wie folgt definiert werden:
1 |
static targets = [ "switcher" ] |
Fügen Sie außerdem data-target
attribute mit switcher
-Werten für die li
hinzu:
1 |
initialize() { |
2 |
// ...
|
3 |
this.element.innerHTML = languages.map((lang) => { |
4 |
return `<li data-action="click->languages#switchLanguage" |
5 |
data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>` |
6 |
}).join('') |
7 |
// ...
|
8 |
}
|
Ein weiterer wichtiger Punkt ist, dass das Laden von Übersetzungsdateien einige Zeit in Anspruch nehmen kann. Wir müssen warten, bis dieser Vorgang abgeschlossen ist, bevor das Gebietsschema umgeschaltet werden kann. Nutzen wir daher den loaded
callback:
1 |
initialize() { |
2 |
i18n.on('loaded', (loaded) => { // <--- |
3 |
let languages = [ |
4 |
{title: 'English', code: 'en'}, |
5 |
{title: 'Русский', code: 'ru'} |
6 |
]
|
7 |
|
8 |
this.element.innerHTML = languages.map((lang) => { |
9 |
return `<li data-action="click->languages#switchLanguage" |
10 |
data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>` |
11 |
}).join('') |
12 |
|
13 |
this.currentLang = i18n.language |
14 |
})
|
15 |
}
|
Vergessen Sie nicht, setTimeout
aus der loadUsers()
-Methode zu entfernen:
1 |
loadUsers() { |
2 |
fetch(this.data.get("url")) |
3 |
.then(response => response.text()) |
4 |
.then(json => { |
5 |
this.renderUsers(json) |
6 |
this.localize() |
7 |
})
|
8 |
}
|
Anhaltendes Gebietsschema in der URL
Nach dem Umschalten des Gebietsschemas möchte ich der URL, die den Code der ausgewählten Sprache enthält, einen ?lang
GET-Parameter hinzufügen. Das Anhängen eines GET-Parameters ohne erneutes Laden der Seite kann mithilfe der Verlaufs-API problemlos durchgeführt werden:
1 |
set currentLang(lang) { |
2 |
if(i18n.language !== lang) { |
3 |
i18n.changeLanguage(lang) |
4 |
window.history.pushState(null, null, `?lang=${lang}`) // <--- |
5 |
}
|
6 |
|
7 |
if(this.currentLang !== lang) { |
8 |
this.data.set("currentLang", lang) |
9 |
loci18n('body') |
10 |
this.highlightCurrentLang() |
11 |
}
|
12 |
}
|
Gebietsschema erkennen
Das Letzte, was wir heute implementieren werden, ist die Möglichkeit, das Gebietsschema basierend auf den Benutzereinstellungen festzulegen. Ein Plugin namens LanguageDetector kann uns helfen, diese Aufgabe zu lösen. Fügen Sie ein neues Garnpaket hinzu:
1 |
yarn add i18next-browser-languagedetector |
Importieren Sie LanguageDetector
in die Datei i18n/config.js:
1 |
import LngDetector from 'i18next-browser-languagedetector' |
Optimieren Sie nun die Konfiguration:
1 |
const i18n = i18next.use(I18nXHR).use(LngDetector).init({ // <--- |
2 |
// other options go here...
|
3 |
detection: { |
4 |
order: ['querystring', 'navigator', 'htmlTag'], |
5 |
lookupQuerystring: 'lang', |
6 |
}
|
7 |
}, function(err, t) { |
8 |
if (err) return console.error(err) |
9 |
});
|
Die order
-Option listet alle Techniken (sortiert nach ihrer Wichtigkeit) auf, die das Plugin versuchen sollte, um das bevorzugte Gebietsschema zu "erraten":
-
querystring
bedeutet, einen GET-Parameter zu überprüfen, der den Code des Gebietsschemas enthält. -
lookupQuerystring
legt den Namen des zu verwendenden GET-Parameters fest, der in unserem Falllang
ist. -
navigator
bedeutet, Gebietsschemadaten aus der Benutzeranforderung abzurufen. - Bei
htmlTag
wird das bevorzugte Gebietsschema aus demlang
-Attribut deshtml
-Tags abgerufen.
Abschluss
In diesem Artikel haben wir uns I18next angesehen - eine beliebte Lösung, um JavaScript-Anwendungen mühelos zu übersetzen. Sie haben gelernt, wie Sie I18next in das Stimulus-Framework integrieren, konfigurieren und Übersetzungsdateien asynchron laden. Außerdem haben Sie gesehen, wie Sie zwischen Gebietsschemas wechseln und die Standardsprache basierend auf den Benutzereinstellungen festlegen.
I18next bietet einige zusätzliche Konfigurationsoptionen und viele Plugins. Lesen Sie daher unbedingt die offizielle Dokumentation, um mehr zu erfahren. Beachten Sie auch, dass Stimulus Sie nicht zwingt, eine bestimmte Lokalisierungslösung zu verwenden. Sie können daher auch versuchen, etwas wie jQuery.I18n oder Polyglot zu verwenden.
Das ist alles für heute! Vielen Dank fürs Mitlesen und bis zum nächsten Mal.