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:
searchParamsist ein Promise — in der Page erstawait searchParams, dann angetServerLocaleü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
- Key in
packages/i18n/src/locales/en.tshinzufügen - SAME Key in
packages/i18n/src/locales/de.tsmit deutscher Übersetzung — TypeScript meldet sonst Compile-Error (Record<TranslationKey, string>) - 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]/activate—activation-Slot/api/customers/[id]/cancel—cancellation-Slot/api/customers/[id]/change-email—email_change_old+email_change_new(2 Sends)/api/customers/[id]/reject—rejection-Slot/api/register—welcome-Slot
Portal:
/api/portal/cancel-subscription—cancellation-Slot/api/portal/cancel-subscription/withdraw—cancellation_withdraw-Slot/api/portal/change-interval—interval_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 altent(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).