Zum Hauptinhalt springen

Mehrsprachigkeit (i18n)

Seit April 2026 ist das Portal vollständig in Englisch und Deutsch verfügbar. Endkunden wechseln die Sprache jederzeit über einen Flaggen-Dropdown rechts oben im Header — die Auswahl wird im LocalStorage gespeichert (licensio_locale).

Architektur

Das i18n-Paket lebt als eigener Workspace unter packages/i18n/:

packages/i18n/
├── src/
│ ├── index.ts → re-exports (server-safe)
│ ├── translate.ts → reine t()-Funktion, kein React
│ ├── LanguageContext.tsx → 'use client' Provider + useLanguage
│ ├── LanguageDropdown.tsx → 'use client' EN/DE-Dropdown
│ ├── server.ts → getServerLocale() Helper für Server Components
│ └── locales/
│ ├── en.ts → ~1.100 Keys, Single Source of Truth
│ └── de.ts → typed gegen en (TS-Error bei fehlenden Keys)
├── package.json
└── ...

Apps importieren nur aus @licensio/i18n:

import { useLanguage, LanguageProvider, LanguageDropdown, t, type Locale } from '@licensio/i18n'

Verwendung

Client Components

'use client'
import { useLanguage } from '@licensio/i18n'

function MyComponent() {
const { t, locale, setLocale } = useLanguage()
return <h1>{t('portal.dashboard.title')}</h1>
}

Mit Variablen-Interpolation

t('portal.products.heading', { count: '12' })
// EN: "Products (12)"
// DE: "Produkte (12)"

Server Components

t() aus @licensio/i18n ist React-frei und funktioniert auf dem Server, braucht aber den Locale explizit:

import { t } from '@licensio/i18n'
const subject = t('de', 'email.welcome.subject')

Für Server Components, die den Locale aus URL-Param bzw. Tenant-Default ableiten sollen, gibt es den Helper getServerLocale() aus @licensio/i18n (re-exportiert aus server.ts): Priorität ?lang= (URL) → Tenant-default_locale'en'. Synchron, framework-agnostisch.

import { getServerLocale, t } from '@licensio/i18n'
const locale = getServerLocale(searchParams, tenant.settings?.default_locale)
const title = t(locale, 'admin.something.title')

Hinweis Next.js 16: searchParams ist ein Promise — in der Page erst await searchParams, dann an getServerLocale übergeben.

Bei SSR-Pages mit vielen dynamischen Strings extrahieren wir alternativ die View-Schicht in eine Client-Komponente (Pattern: OverviewView.tsx, LicenseTabView.tsx, LegalPageView.tsx).

Außerdem liest der LanguageProvider (Client) beim ersten Paint den ?lang=-URL-Param (vor LocalStorage, vor Default-Prop) — so funktionieren Deep-Links aus der Marketing-Site (…?lang=en) auch im Portal/Admin.

Tenant-Settings

Pro Tenant konfigurierbar in tenants.settings:

  • default_locale: 'en' oder 'de' — Default-Sprache beim ersten Besuch (überschreibbar via Dropdown)
  • enabled_locales: ['en', 'de'] — schränkt verfügbare Sprachen ein. Wenn nur eine Sprache erlaubt ist, rendert der Dropdown nicht.

Backwards-Compat: Wenn default_locale nicht gesetzt ist, fällt der LanguageProvider auf 'de' zurück — schützt Live-Tenants ohne Migration.

Übersetzte Bereiche (Portal)

100% übersetzt:

  • Auth: Login, OTP-Flow, Passwort-Reset (Banner/Errors/Success)
  • Dashboard: Sidebar, Overview-KPIs, Downloads, Notifications (inkl. Time-Ago + Notification-Badges)
  • Produkte: List (Filter/Suche/Cards), Detail (Druckeinstellungen, Zubehör, Basisteile), Feed-Export (kompletter 738-Zeilen-Form)
  • Account: alle 4 Tabs (Mein Account, Lizenz & Vertrag, Drucker, CRM) mit allen Sub-Components (AccountEditForm mit 15 Ländern, ChangeEmailModal, CancelSection, IntervalSection, InvoicesSection, LegalSection, PackageUsageSection)
  • Support: FAQ, Ticket-Form, Tickets-Liste, Ticket-Detail (Sender-Labels, locale-aware Datums-Formatierung)
  • Drucker: PrinterList, PrinterForm (inkl. IP-Hilfe), Drucker-Detail (5 Sektionen + 4-Step-Setup-Wizard + Modals)
  • Join Flow: /join Übersicht + /join/[packageSlug] Registrierungsformular (4 Steps: Firma → OTP → Passwort → Zahlung) + /join/success
  • Register Flow: /register/[token] (Full Reg + Checkout-Only + Invalid-Link), /register/payment, /register/success
  • Public Landing-Page: Header (Login/Register), Packages-Section (Empfohlen-Badge, Feature-Listen), Footer (Legal-Spalte + Powered-by), 4 Demo-Views der Portal-Vorschau (Dashboard/Products/Assets/Account)
  • Komponenten: OnboardingTour (5 Steps), Lightbox, BackButton, LegalFooter, MobilePreview
  • Misc: Maintenance-Page, Unsubscribe-Page, Legal-Pages (Impressum/AGB/Datenschutz)

Datums- und Zahlenformatierung

Lokal-spezifisch via Locale-aware Helper:

  • DE: de-DE (Datum: 15.04.2026, Zahlen: 20,00)
  • EN: en-US (Datum: 04/15/2026, Zahlen: 20.00)

Beispiel:

const { locale } = useLanguage()
const dateStr = new Date().toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US')

Neue Keys hinzufügen

  1. Key in packages/i18n/src/locales/en.ts hinzufügen
  2. SAME Key in packages/i18n/src/locales/de.ts mit deutscher Übersetzung — TypeScript meldet sonst Compile-Error (Record<TranslationKey, string>)
  3. In Komponente verwenden: t('mein.neuer.key')

Übersetzte Bereiche (Admin)

Der Admin-Bereich wird seit Mai 2026 systematisch in Tiers migriert. Tier 1+2 (12 Komponenten) sind erledigt; ~22 kleine Files + 7 Sub-Components (Tier 3) stehen noch aus.

Bereits übersetzt:

  • Auth: Login (mit Floating-Dropdown oben rechts), Forgot-Password, Reset-Password (Supabase verwaltet die eigentliche Reset-Mail)
  • Chrome: Header (Dropdown links neben Email + LogoutButton), Sidebar (9 Nav-Items + Lock-Badges + Unread-Counter)
  • Dashboard: Welcome-Heading, KPIs (Produkte/Aktive Kunden/Downloads), Summary, Admin-Notifications + Open-Tickets
  • Customers: Liste (Filter-Pills + Status-Badges + Activate-Button + Toast-Messages), Kunden-Detail (customer/[id] — alle Felder, Pakete, Notizen, Aktivierung/Ablehnung, ~115 Keys), Einladen-Form (Paketauswahl + 31 Länder)
  • Products: Listen-Heading + Import/Export/Create-Buttons, Produkt-Editor (products/[id]/edit — Allgemein/Varianten/Druckeinstellungen/Zubehör, ~50 Keys)
  • Assets: AssetManager (Logos / Produktbilder / Zubehör / Basisteile + EmptyStates)
  • Settings: Erweiterte Einstellungen Hub mit 13 Kacheln, Language & Region-Page, Rechnungsstellung (BillingForm, ~50 Keys + 14 Länder), Branding (BrandingForm, ~40 Keys), Rechtstexte (LegalForm), Landing Page Builder, Stub-Pages + Section-Helpers, Danger Zone, E-Mail-Benachrichtigungen-Page
  • Support: Tickets-Liste + Ticket-Detail, Produkt-Vorschläge-Tab (SuggestionList + Detail-Panel)
  • FAQs: FaqManager (CRUD + Kategorien + Vorlagen), Zubehör-Bibliothek (AccessoriesManager)
  • Onboarding: OnboardingChecklist (6-Step Popup) + OnboardingWizard
  • Maintenance, Forgot, Reset: Public-facing Pages

Noch offen (Tier 3 / Phase 3): ~22 kleine Files, darunter NotificationsManager, NewsletterManager, PrintFarm-Stub, IntervalEditor, ImageUploader, sowie 7 Sub-Components (VariantEditor, AccessoryAutocomplete, ImageUploader, PrintSettingsEditor, CategorySelect, CompatibilitySelect, BasePartPicker) und 5 Server-Component-Headings. RegisterForm behält sein eigenes inline EN/DE-Dict (funktioniert bereits zweisprachig, teilt den LocalStorage-Key). DomainForm + StripeForm noch hardcoded deutsch.

Email-Übersetzung

Sieben transaktionale Email-Routen lesen heute aus dem zentralen Template-System (apps/{admin,portal}/lib/email-templates.ts — 2 synchron gehaltene Kopien). Locale wird aus tenant.settings.default_locale aufgelöst (Fallback 'de'). Siehe E-Mail-Templates für die Tenant-Sicht auf die 9 Template-Slots + Variablen.

Migrierte Routen (Stand 2026-05-28)

Admin:

  • /api/customers/[id]/activateactivation-Slot
  • /api/customers/[id]/cancelcancellation-Slot
  • /api/customers/[id]/change-emailemail_change_old + email_change_new (2 Sends)
  • /api/customers/[id]/rejectrejection-Slot
  • /api/registerwelcome-Slot

Portal:

  • /api/portal/cancel-subscriptioncancellation-Slot
  • /api/portal/cancel-subscription/withdrawcancellation_withdraw-Slot
  • /api/portal/change-intervalinterval_change-Slot

Helper-API

import { renderEmailTemplate, resolveEmailLocale } from '@/lib/email-templates'

const { subject, body } = renderEmailTemplate(
'activation',
{
tenant_name: tenant.name,
kunde_firma: customer.company,
kundennummer: customer.customer_number,
support_email: tenant.settings.support_email,
},
tenant.settings,
)

renderEmailTemplate(slot, vars, tenantSettings) macht: (1) Locale via resolveEmailLocale(tenantSettings?.default_locale), (2) Tenant-Custom-Template über Default-Template legen (pro Feld), (3) Variablen-Interpolation. Tenant-Custom-Template gewinnt pro Feld — fehlende Felder fallen auf Locale-Default zurück.

Nicht-migrierte Routen

  • /api/tickets/[id] (Ticket-Reply) — nutzt noch alten t(locale, key)-Pattern. Migration mit Stripe-Webhook-Sprint.
  • /api/portal/tickets (Ticket-Forward) — siehe oben.
  • Stripe-Webhook-Routen (Portal) — out-of-scope bis Stripe-Webhook-Sprint.

Supabase-managed

Reset-Password-Email: Locale wird über Supabase Studio Email-Templates konfiguriert — ausserhalb des Codes und ausserhalb des Template-Systems.

Bekannte Einschränkungen

  • Hydration-Flicker: Wenn LocalStorage-Locale vom Server-Default abweicht, sieht der Nutzer für ~30ms die Default-Sprache, dann switcht es. Akzeptabel — Marketing-Site hat denselben Flicker.
  • Tenant-User-Content (Produktnamen, FAQ-Texte, Notifications) wird nicht automatisch übersetzt — Tenants pflegen Inhalte selbst in der gewünschten Sprache.
  • Admin Tier 3 (offen): StripeForm, DomainForm, NotificationsManager, NewsletterManager, PrintFarm-Stub und 7 Form-Sub-Components (VariantEditor, AccessoryAutocomplete, ImageUploader, PrintSettingsEditor, CategorySelect, CompatibilitySelect, BasePartPicker) sowie 5 Server-Component-Headings sind noch hardcoded deutsch.

Stand

Stand 2026-05-29. Portal 100% übersetzt. Admin: Tier 1+2 abgeschlossen (12 Komponenten, ~480 Keys), Tier 3 grossteils erledigt (Cleanup-Sprint 2026-05-21–26: 4 von 6 Tier-3-Tickets voll durchgezogen, landing-editor nur teilerledigt — der LandingPageBuilder hat noch ~30 hardcoded DE-Chrome-Strings, Danger-Zone offen). Multi-Locale-Foundation-Sprint hat 7 Email-Routen auf zentralen renderEmailTemplate-Helper migriert + Sprach-Dropdown bei Tenant-Registrierung + default_locale in tenants.settings. Gesamt-Dictionary ~1.100 Keys im @licensio/i18n-Workspace.

Vollständige Inventur der verbleibenden hardcoded-DE-Strings: docs/i18n-audit.md (im Repo).