feat(i18n): Deutsch/Englisch, Sprachknopf und GitHub-Link in der Kopfzeile

- 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>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 18:33:15 +00:00
1 parent 9455013833
commit e1fb632cdd
21 files changed
+1321 -66

No files matched your search

+51 -10
View File
@@ -1,9 +1,11 @@
import '@fontsource-variable/nunito-sans';
import './styles.css';
import { mountAccountMenu } from './account/account-menu';
import { mountAccountMenu, type AccountMenu } from './account/account-menu';
import { client } from './account/appwrite';
import { getCurrentUser } from './account/auth';
import { DONATE_URL } from './config';
import { legacyRedirect, startRouter } from './pages/router';
import { currentLang, onLangChange, setLang } from './i18n/i18n';
import { legacyRedirect, rerender, startRouter } from './pages/router';
import { mountCardsPage } from './pages/cards-page';
import { mountCommunityPage } from './pages/community-page';
import { mountHomePage } from './pages/home-page';
@@ -12,12 +14,18 @@ import { mountEditor } from './editor/editor-page';
import { bindEditorToAuth } from './editor/auth-reset';
import { createStore, initialEditorState } from './editor/state';
import { mountDonateBanner } from './shell/donate-banner';
import { mountFooter } from './shell/footer';
import { mountHeader } from './shell/header';
import { initTheme } from './shell/theme';
// Hell/Dunkel: das Skript in index.html hat es vor dem ersten Paint gesetzt; hier wird nur die Browserleiste angeglichen.
initTheme();
// Sprache: das Skript in index.html hat <html lang> schon vor dem ersten Paint gesetzt; hier wird sie übernommen (ohne sie zu speichern:
// gespeichert wird nur eine Wahl per Sprachknopf) und dem Appwrite-Client mitgegeben, damit dessen Mails (Passwort) in ihr ankommen.
setLang(currentLang(), { persist: false });
client.setLocale(currentLang());
// Alte Editor-Links (`/?karte=<id>`) zeigen jetzt auf `/gestalten?karte=<id>`; der Verlauf bekommt keinen Eintrag dazu.
const legacyTarget = legacyRedirect(new URL(location.href));
if (legacyTarget) history.replaceState(null, '', legacyTarget);
@@ -27,24 +35,57 @@ const editorStore = createStore(initialEditorState());
// … und gehört zum angemeldeten Konto: beim Abmelden oder Kontowechsel wird er geleert bzw. entkoppelt.
bindEditorToAuth(editorStore);
// Spenden-Banner (ohne DONATE_URL bleibt der Platz leer) und Kopfzeile mit Navigation, Suche und Knöpfen.
// Der Rahmen der Seite: Spenden-Banner (ohne DONATE_URL bleibt der Platz leer), Kopfzeile mit Navigation, Suche und Knöpfen,
// Konto-Menü im Seitenkopf und Fußzeile. Alle tragen Texte der gewählten Sprache und werden bei einem Sprachwechsel neu aufgebaut.
const bannerSlot = document.getElementById('donate-banner');
if (bannerSlot) mountDonateBanner(bannerSlot, DONATE_URL);
const header = document.getElementById('app-header');
if (header) mountHeader(header, { donateUrl: DONATE_URL });
// Konto-Menü im Seitenkopf. Der Anmeldestatus wird einmal beim Start geholt; schlägt das fehl
// (außer 401, das getCurrentUser selbst als „abgemeldet“ meldet), gilt der Nutzer als abgemeldet.
const accountSlot = document.getElementById('account-slot');
const footer = document.getElementById('app-footer');
let unmountHeader: (() => void) | null = null;
let accountMenu: AccountMenu | null = null;
// Ob der Anmeldestatus schon geholt wurde: ein später neu gebautes Konto-Menü zeigt sich dann sofort (kein Aufblitzen von „Anmelden“).
let authKnown = false;
function mountShell(): void {
if (bannerSlot) mountDonateBanner(bannerSlot, DONATE_URL);
if (header) {
unmountHeader?.();
unmountHeader = mountHeader(header, { donateUrl: DONATE_URL });
}
if (accountSlot) {
accountMenu?.dispose();
accountMenu = mountAccountMenu(accountSlot);
if (authKnown) accountMenu.ready();
}
if (footer) mountFooter(footer);
}
mountShell();
// Der Anmeldestatus wird einmal beim Start geholt; schlägt das fehl (außer 401, das getCurrentUser selbst als „abgemeldet“ meldet),
// gilt der Nutzer als abgemeldet.
if (accountSlot) {
const menu = mountAccountMenu(accountSlot);
getCurrentUser()
.catch((error: unknown) => {
console.error('Anmeldestatus konnte nicht geprüft werden', error);
})
.finally(() => menu.ready());
.finally(() => {
authKnown = true;
accountMenu?.ready();
});
}
// Sprachwechsel ohne Neuladen: Der Appwrite-Client bekommt die neue Sprache, der Rahmen wird neu gebaut und die aktuelle Seite baut
// sich neu auf (der Router setzt auch den Titel neu). Der Editor-Store bleibt dasselbe Objekt, ein Entwurf geht also nicht verloren.
onLangChange((lang) => {
client.setLocale(lang);
// Die Kopfzeile wird ersetzt, der Sprachknopf darunter ebenso: wer ihn per Tastatur bedient hat, behält den Fokus auf dem neuen.
const hadFocus = document.activeElement?.matches('.lang-btn') ?? false;
mountShell();
if (hadFocus) header?.querySelector<HTMLElement>('.lang-btn')?.focus();
rerender();
});
// Der Router setzt `document.title` je Seite (titleFor) und ruft für jede Route die passende Mount-Funktion.
startRouter((route, root) => {
switch (route.id) {