- i18n-Kern im gettext-Stil (t/msg, Wörterbuch en.ts); Startsprache aus localStorage, sonst Browsersprache; <html lang> setzt auch das Inline-Skript in index.html. Ein Test prüft t()/msg() und en.ts in beide Richtungen. - Kopfzeile: GitHub-Link und Sprachknopf (Zielsprache, Beschriftung und lang in der Zielsprache), Fußzeile aus mountFooter, Banner und Seitentitel übersetzt, statisches index.html englisch. - Sprachwechsel ohne Neuladen (main.ts): Kopfzeile, Banner, Konto-Menü und Fußzeile neu aufgebaut, rerender() des Routers baut die Seite neu auf, der Editor-Store bleibt; der Appwrite-Client bekommt die Sprache. - Kopfzeile bleibt in einer Zeile: einzeilig ab 1000 px (vorher 900), GitHub-Text ab 1230 px, auf schmalen Telefonen steht „Anmelden“ neben der Suche. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
// Sprache der Seite (Deutsch/Englisch) im gettext-Stil: Der deutsche Text ist zugleich der Schlüssel.
|
|
// `t('Deutscher Text', { name })` schlägt ihn bei Englisch in EN (en.ts) nach und setzt die Platzhalter `{name}` ein;
|
|
// fehlt der Eintrag, bleibt der deutsche Text. Übersetzt wird beim Aufruf, ein Sprachwechsel baut die Oberfläche neu auf (main.ts).
|
|
import { EN } from './en';
|
|
|
|
export type Lang = 'de' | 'en';
|
|
|
|
export const LANG_KEY = 'aircard.lang';
|
|
|
|
/**
|
|
* Startsprache: die gespeicherte Wahl (`de` oder `en`), sonst Deutsch, wenn die erste Browsersprache mit `de` beginnt,
|
|
* andernfalls Englisch. Dieselbe Regel steht im Skript in index.html, das <html lang> vor dem ersten Paint setzt.
|
|
*/
|
|
export function initialLang(stored: string | null, browserLanguages: readonly string[]): Lang {
|
|
if (stored === 'de' || stored === 'en') return stored;
|
|
return (browserLanguages[0] ?? '').toLowerCase().startsWith('de') ? 'de' : 'en';
|
|
}
|
|
|
|
function readStored(): string | null {
|
|
try {
|
|
return localStorage.getItem(LANG_KEY);
|
|
} catch {
|
|
return null; // Speicher gesperrt (z. B. privates Fenster): es gilt die Browsersprache
|
|
}
|
|
}
|
|
|
|
/** Die bevorzugten Browsersprachen: `navigator.languages`, ersatzweise `[navigator.language]`. */
|
|
function browserLanguages(): readonly string[] {
|
|
if (navigator.languages?.length) return navigator.languages;
|
|
return navigator.language ? [navigator.language] : [];
|
|
}
|
|
|
|
// Die Sprache der Seite; null, bis sie zum ersten Mal gebraucht wird.
|
|
let lang: Lang | null = null;
|
|
const listeners = new Set<(lang: Lang) => void>();
|
|
|
|
/** Die Sprache der Seite; beim ersten Aufruf aus der gespeicherten Wahl bzw. der Browsersprache bestimmt. */
|
|
export function currentLang(): Lang {
|
|
lang ??= initialLang(readStored(), browserLanguages());
|
|
return lang;
|
|
}
|
|
|
|
/**
|
|
* Stellt die Sprache um: setzt <html lang>, merkt sie sich (außer mit `persist: false`) und meldet einen echten Wechsel an
|
|
* die Hörer. Das Neuzeichnen von Kopfzeile und Seite ist Sache der Hörer.
|
|
*/
|
|
export function setLang(next: Lang, opts: { persist?: boolean } = {}): void {
|
|
const changed = next !== currentLang();
|
|
lang = next;
|
|
document.documentElement.lang = next;
|
|
if (opts.persist ?? true) {
|
|
try {
|
|
localStorage.setItem(LANG_KEY, next);
|
|
} catch {
|
|
// gilt dann nur für diese Sitzung
|
|
}
|
|
}
|
|
// Kopie, damit sich ein Hörer beim Melden abmelden darf.
|
|
if (changed) for (const fn of [...listeners]) fn(next);
|
|
}
|
|
|
|
/** Meldet jeden Sprachwechsel (nicht den bloßen Aufruf mit der aktuellen Sprache). Gibt die Abmeldung zurück. */
|
|
export function onLangChange(fn: (lang: Lang) => void): () => void {
|
|
listeners.add(fn);
|
|
return () => {
|
|
listeners.delete(fn);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Übersetzt einen deutschen Text in die Sprache der Seite und setzt die Platzhalter `{name}` aus `params` ein;
|
|
* ein Platzhalter ohne Wert bleibt stehen. Ohne Eintrag in EN bleibt der deutsche Text.
|
|
*/
|
|
export function t(text: string, params?: Readonly<Record<string, string | number>>): string {
|
|
const base = currentLang() === 'en' && Object.hasOwn(EN, text) ? EN[text] : text;
|
|
if (!params) return base;
|
|
return base.replace(/\{(\w+)\}/g, (placeholder, name: string) => (Object.hasOwn(params, name) ? String(params[name]) : placeholder));
|
|
}
|
|
|
|
/** Markiert einen deutschen Text, der vorab in einer Konstante steht; übersetzt wird erst beim Anzeigen mit `t()`. */
|
|
export function msg<T extends string>(text: T): T {
|
|
return text;
|
|
}
|