i18n
Eine Internationalisierungs-Engine ohne Framework-Bindung: ein kleiner reaktiver Kern (I18nCore) mit optionalem globalem Singleton (createI18n / useI18n). Nichts hier fasst das DOM an — binde es an die Oberfläche, wie du magst.
import { createI18n, useI18n } from 'ranuts/i18n';Es wird auch aus ranuts/utils weitergereicht. Importiere aus ranuts/i18n, wenn du nur die i18n brauchst: Dieser Einstiegspunkt trägt allein die Engine und ihre beiden Helfer, statt dessen, was das breite utils-Barrel sonst noch mitzieht.
Verwendung
import { createI18n, useI18n } from 'ranuts/i18n';
createI18n({
messages: {
en: { 'hero.title': 'Hello, {name}', 'nav.docs': 'Docs' },
zh: { 'hero.title': '你好,{name}', 'nav.docs': '文档' },
},
fallbackLocale: 'en',
persist: true,
detectNavigator: true,
});
const i18n = useI18n()!;
i18n.t('hero.title', { name: 'Ada' }); // "Hello, Ada"
i18n.setLocale('zh');
i18n.t('hero.title', { name: 'Ada' }); // "你好,Ada"Die Wörterbücher sind flach: t() schlägt direkt messages[locale][key] nach, die Schlüssel sind also wörtliche Zeichenketten wie 'hero.title' und keine verschachtelten Objekte.
Anfangssprache
Einmal im Konstruktor aufgelöst, in dieser Reihenfolge:
- Die im
localStoragegespeicherte Wahl (nur bei aktivempersist, und nur wenn es für diese Sprache ein Wörterbuch gibt) config.locale- Die Sprachen des Browsers (nur bei aktivem
detectNavigator) fallbackLocale
Schritt 3 läuft über resolveLocale, das die vollständige geordnete Liste navigator.languages liest statt nur navigator.language: Wessen erste Wahl nicht unter deinen Wörterbüchern ist, bekommt trotzdem die zweite, statt gleich auf die Ersatzsprache zu fallen.
Interpolation
t(key, params) ersetzt {param}-Platzhalter in einem einzigen Durchgang von links nach rechts und folgt damit der Formatzeichenketten-Konvention von Rusts format!, Pythons str.format und .NETs String.Format:
| Eingabe | Ausgabe |
|---|---|
{{ | ein wörtliches { |
}} | ein wörtliches } |
{name} | params.name, in Text umgewandelt |
{name} ohne passenden Parameter | bleibt unangetastet, ein verirrter Platzhalter ist so sichtbar statt leer |
Ein einzelnes { / } oder eine Gruppe mit Leerzeichen wie { x } ist kein Platzhalter und wird wörtlich ausgegeben — CSS, JSON oder Codeschnipsel in einer Nachricht kommen also unversehrt durch. Um einen Wert in wörtliche Klammern zu setzen, verdopple das äußere Paar: {{{name}}}.
Typisierte Wörterbücher
Übergib die Form deines Wörterbuchs als Typargument, und jeder t()-Aufruf wird zur Übersetzungszeit geprüft. Ohne das verkommt ein umbenannter oder vertippter Schlüssel stillschweigend zu „zeichne den Schlüssel selbst": Die lesende Person sieht agentModelFirstDownlaod, wo ein Satz stehen sollte, und bis dahin schlägt nichts fehl.
interface Messages {
save: string;
cancel: string;
}
const i18n = createI18n<Messages>({
messages: {
en: { save: 'Save', cancel: 'Cancel' },
'zh-CN': { save: '保存' }, // wird noch übersetzt — das ist in Ordnung
},
fallbackLocale: 'en',
});
i18n.t('save'); // ok
i18n.t('saev'); // Übersetzungsfehler
useI18n<Messages>()?.t('cancel'); // denselben Typ zurückgeben, damit die Prüfung bleibtDrei Einzelheiten machen das brauchbar und nicht bloß vorhanden:
- Jede Sprache ist
Partial. Eine laufende Übersetzung ist der Normalfall; die Ersatzsprache deckt ab, was eine Sprache noch nicht gefüllt hat. - Der Typ kommt aus dem Typargument, nie aus den Daten.
messagesist inNoInfergehüllt, Sprachen mit unterschiedlichen Schlüsselmengen können TypeScript also nicht dazu bringen, ihre Schnittmenge abzuleiten. Sonst würde ein Schlüssel, den nur die Ersatzsprache definiert, an jeder Aufrufstelle abgelehnt, und eine unvollständige Übersetzung bräche den Build, statt zur Laufzeit zurückzufallen. - Ein
interfacefunktioniert, nicht nur eintype. Die Einschränkung lautetStringValues<T>({ [K in keyof T]: string }) stattRecord<string, string>, weil TypeScript implizite Indexsignaturen nur Typaliassen gibt: Die naheliegende Einschränkung hätte jeden Verwender gezwungen, sein Wörterbuch alstypeneu zu schreiben.
Das Typargument wegzulassen erhält das untypisierte Verhalten exakt: Das voreingestellte MessageDict ist Record<string, string>, dessen keyof string ist.
Konfiguration
| Feld | Beschreibung | Typ | Standard |
|---|---|---|---|
locale | Anfangssprache. Eine gespeicherte Wahl sticht sie, wenn persist an ist | string | - |
fallbackLocale | Sprache, die greift, wenn ein Schlüssel in der aktiven Sprache fehlt | string | 'en' |
messages | Sprache → Schlüssel → Zeichenkette | LocaleMessages | {} |
persist | Speichert die aktive Sprache im localStorage | boolean | false |
storageKey | localStorage-Schlüssel, wenn persist an ist | string | 'ran-locale' |
detectNavigator | Setzt die Anfangssprache aus den Spracheinstellungen des Browsers | boolean | false |
API
createI18n
Erzeugt und registriert das globale Singleton.
Parameter
| Parameter | Beschreibung | Typ | Standard |
|---|---|---|---|
config | Siehe Konfiguration | I18nConfig | {} |
Rückgabe
| Argument | Beschreibung | Typ |
|---|---|---|
i18n | Die neue Instanz | I18nCore |
useI18n
Gibt die aktive globale Instanz zurück, oder null, wenn keine erzeugt wurde.
Rückgabe
| Argument | Beschreibung | Typ |
|---|---|---|
i18n | Die aktive Instanz oder null | I18nCore | null |
I18nCore
| Element | Beschreibung |
|---|---|
t(key, params?) | Übersetzt; fällt auf die Ersatzsprache und dann auf den Schlüssel selbst zurück |
locale / getLocale() | Die aktive Sprache |
setLocale(locale) | Wechselt die Sprache, speichert (falls an) und benachrichtigt. Ohne Änderung passiert nichts |
addMessages(locale, dict) | Führt ein Wörterbuch in eine Sprache ein und legt sie bei Bedarf an |
getMessages(locale?) | Das Wörterbuch einer Sprache, oder {} |
availableLocales | Sprachen, für die ein Wörterbuch angemeldet ist |
onChange(fn) | Abonniert Sprachwechsel; gibt eine Funktion zum Abbestellen zurück |
destroy() | Entfernt alle Abonnenten |
SSR
Sicher. Jeder Zugriff auf localStorage und navigator ist abgesichert, eine Instanz während des Server-Renderings zu erzeugen fällt also auf config.locale oder fallbackLocale durch.