Skip to content

PLAN: i18n & Multi-Language Support

╔══════════════════════════════════════════════════════════════╗
║ ║
║ STATUS: PHASES 1-4 COMPLEET — PLAN AFGEROND ║
║ DATUM: 6 maart 2026 ║
║ DOOR: Claude Code (adminmichiel1) ║
║ ║
║ SCOPE: Alleen platform UI — geen productdata vertaling ║
║ ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ PHASE 1: Shared @magiverse/i18n Package ✅ COMPLEET ║
║ PHASE 2: PIM Storefront Migration ✅ COMPLEET ║
║ PHASE 3: Tenant Storefronts (9x) ✅ COMPLEET ║
║ PHASE 4: Portal & Dashboard ✅ COMPLEET ║
║ PHASE 5: Product Data i18n (Medusa) ✖ BUITEN SCOPE ║
║ PHASE 6: FlowBuilder Migration ✖ BUITEN SCOPE ║
║ ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ VERIFICATIE: ║
║ ║
║ @magiverse/i18n pakket TypeScript: OK | 57 base keys ║
║ Translation check (4 lang) ✅ nl, en, de, fr — COMPLEET ║
║ PIM Storefront build ✅ Next.js build GESLAAGD ║
║ Browser NL→EN test ✅ UI labels correct vertaald ║
║ Browser NL→DE test ✅ UI labels correct vertaald ║
║ ║
║ TENANT RESULTATEN: ║
║ ║
║ magic_development pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_brinxx pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_default pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_logohorloge pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_bovisales pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_demo pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_desluis pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_jodasign pkg:OK i18n:OK ctx:OK overrides:OK ║
║ magic_spranz pkg:OK i18n:OK ctx:OK overrides:OK ║
║ ║
╚══════════════════════════════════════════════════════════════╝
  1. Phase 1 — @magiverse/i18n shared package aangemaakt op /mnt/data/magic_omniverse/packages/magiverse-i18n/

    • Core types, config, resolver met dot-notation + fallback + parameter interpolatie
    • Formatting utilities (formatCurrency, formatDate, formatNumber, formatRelativeTime)
    • Pluralization support via Intl.PluralRules
    • React bindings: LanguageProvider, useLanguage() hook, <T /> component
    • 57 base translation keys in 4 talen (NL, EN, DE, FR) — allemaal geverifieerd
    • Tooling: i18n:check (missing key checker) en i18n:types (TypeScript type generator)
  2. Phase 2 — PIM Storefront gemigreerd

    • src/lib/i18n/index.ts herschreven: gebruikt nu createI18n() met mergeTranslations() (base + PIM-specifiek)
    • src/lib/context/language-context.tsx herschreven: re-exporteert shared LanguageProvider
    • next.config.js bijgewerkt met transpilePackages: ["@magiverse/i18n"]
    • localStorage key gemigreerd: brinxx_languagemagiverse_language (met auto-migratie)
    • Next.js build succesvol geverifieerd
  3. Phase 3 — Alle 9 tenant storefronts bijgewerkt

    • 3-laags translation systeem: tenant override → storefront base → @magiverse/i18n base
    • overrides/ directory aangemaakt met lege JSON bestanden per taal (klaar voor tenant-specifieke teksten)
    • magic_development (template) volledig bijgewerkt
    • Gedistribueerd naar alle 8 tenants via rsync
    • Jodasign en Spranz: permission issues opgelost (adminwayne3 ownership)
    • Alle 9 storefronts geverifieerd: package, i18n, context, overrides, next.config aanwezig
NIEUW: /mnt/data/magic_omniverse/packages/magiverse-i18n/
├── package.json
├── tsconfig.json
├── scripts/check-translations.ts
├── scripts/generate-types.ts
└── src/
├── index.ts, types.ts, config.ts, resolver.ts, format.ts, plural.ts
├── react/index.ts, provider.tsx, hook.ts, component.tsx
└── translations/base/nl.json, en.json, de.json, fr.json, index.ts
GEWIJZIGD: /mnt/data/magic_pim/storefront/
├── package.json (+@magiverse/i18n dependency)
├── next.config.js (+transpilePackages)
├── src/lib/i18n/index.ts (herschreven → shared package)
└── src/lib/context/language-context.tsx (herschreven → shared provider)
GEWIJZIGD: /mnt/data/magic_omniverse/magic_commerce/magic_*/storefront/ (×9)
├── package.json (+@magiverse/i18n dependency)
├── next.config.js (+transpilePackages)
├── packages/magiverse-i18n/ (shared package kopie)
├── src/lib/i18n/index.ts (herschreven → shared package)
├── src/lib/i18n/overrides/{nl,en,de,fr}.json (NIEUW: tenant overrides)
└── src/lib/context/language-context.tsx (herschreven → shared provider)

Scope: alleen platform UI, geen productdata

Section titled “Scope: alleen platform UI, geen productdata”

Wat valt WEL onder i18n (platform UI):

  • Navigatie: “Winkelwagen” → “Shopping Cart” → “Warenkorb”
  • Knoppen: “Toevoegen” → “Add to Cart” → “In den Warenkorb”
  • Labels: “Artikelnummer”, “Kleur”, “Materiaal” → vertaald per taal
  • Formulieren: placeholders, validatiemeldingen, foutmeldingen
  • Tabs en secties: “Informatie”, “Verzending”, “Specificaties”

Wat valt NIET onder i18n (productdata):

  • Producttitels (bijv. “Fleecedeken HANGOVER”) — blijft in leverancierstaal
  • Productbeschrijvingen — blijft in leverancierstaal
  • Materiaalnamen, kleurnamen — uit leveranciersdata
  • Verpakkingsteksten — uit leveranciersdata

Waarom geen productdata vertaling:

  • Leveranciers leveren productdata aan in hun eigen taal (meestal NL of DE)
  • Automatische vertaling (AI/DeepL) kan leiden tot onjuiste productnamen en specificaties
  • Handmatig vertalen van ~10.000+ producten is niet haalbaar
  • Productdata kwaliteit heeft prioriteit boven meertaligheid

╔══════════════════════════════════════════════════════════════════════╗
║ ║
║ ██╗ ██╗ █████╗ ███╗ ██╗ ║
║ ██║███║██╔══██╗████╗ ██║ ║
║ ██║╚██║╚█████╔╝██╔██╗ ██║ ║
║ ██║ ██║██╔══██╗██║╚██╗██║ ║
║ ██║ ██║╚█████╔╝██║ ╚████║ ║
║ ╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═══╝ ║
║ ║
║ M U L T I - L A N G U A G E I N T E G R A T I O N ║
║ ║
║ Magic e-VERSE Ecosystem ║
║ Internationalization & Localization Strategy ║
║ ║
╚══════════════════════════════════════════════════════════════════════╝

The Magic e-VERSE platform serves B2B/B2C customers across Europe, primarily in the Netherlands, Germany, France, and English-speaking markets. A custom i18n system already exists in the PIM storefront supporting 4 languages (NL, DE, FR, EN). This plan extends that foundation into a unified, ecosystem-wide i18n strategy covering all storefronts, the portal, AI agents, and shared packages.


PIM Storefront i18n

Custom React Context-based system with 4 languages, ~105 translation keys per language, localStorage persistence, and a useLanguage() hook.

FlowBuilder i18n

Separate custom t() function with 460+ keys for 3 languages (NL, EN, DE). Uses localStorage key flowbuilder-lang.

Currency Formatting

PIM storefront uses Intl.NumberFormat for locale-aware currency display.

Agent Multi-Language

AI agents use _nl, _en, _de suffixed fields for response templates — not a true i18n system.

src/lib/i18n/
├── index.ts # Core: Language type, getTranslation(), config
└── translations/
├── nl.json # Dutch (default) — 105 keys
├── de.json # German
├── fr.json # French
└── en.json # English
src/lib/context/
└── language-context.tsx # React Context + useLanguage() hook
src/modules/common/components/
└── language-selector/index.tsx # Dropdown UI with inline SVG flags

Key characteristics:

  • Dutch (nl) is the default and fallback language
  • Translations use nested JSON with dot-notation access (e.g. "header.language")
  • Language stored in localStorage (brinxx_language)
  • document.documentElement.lang attribute updated on change
  • No URL-based locale routing
  • No RTL support (not needed for current languages)
  • No server-side translation (client-only)
GapImpactPriority
No shared translation packageEach app reimplements i18n from scratchHigh
Inconsistent storage keysbrinxx_language vs flowbuilder-langMedium
No translation management workflowDevs manually edit JSON filesMedium
Tenant storefronts lack i18nHardcoded Dutch strings in commerce tenantsHigh
Portal has no i18nDashboard is Dutch-onlyMedium
No Medusa-native i18n for product dataProduct names/descriptions are single-languageHigh Buiten scope — producten blijven in leverancierstaal
Agent templates use field suffixeswelcome_message_nl pattern doesn’t scaleLow
No pluralization supportCustom t() can’t handle plural formsLow
No date/time localizationOnly currency uses Intl APILow

Approach: Shared Core + App-Specific Extensions

Section titled “Approach: Shared Core + App-Specific Extensions”
┌─────────────────────────────────────────────────────────┐
│ @magiverse/i18n (shared) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Core │ │ Translation │ │ Format Utilities │ │
│ │ Types & │ │ Loader & │ │ (dates, numbers, │ │
│ │ Config │ │ Resolver │ │ currency, plurals) │ │
│ └──────────┘ └──────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ React Bindings │ │ Shared Base Translations │ │
│ │ (Provider, Hook, │ │ (common keys used across │ │
│ │ HOC, Component) │ │ all apps: buttons, errors) │ │
│ └──────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ PIM │ │ Tenant │ │ Portal │ │ Flow │
│ Store- │ │ Store- │ │ │ │ Builder │
│ front │ │ fronts │ │ │ │ │
│ │ │ (×9) │ │ │ │ │
│ App- │ │ App- │ │ App- │ │ App- │
│ specific│ │ specific│ │ specific│ │ specific│
│ keys │ │ keys │ │ keys │ │ keys │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
CodeLanguageNative NameFlagStatus
nlDutchNederlands🇳🇱Default / Fallback
enEnglishEnglish🇬🇧Supported
deGermanDeutsch🇩🇪Supported
frFrenchFrançais🇫🇷Supported

Goal: Extract the existing PIM storefront i18n into a reusable shared package.

  1. Create @magiverse/i18n in packages/

    Create the shared package at /mnt/data/magic_omniverse/packages/magiverse-i18n/ with the following structure:

    packages/magiverse-i18n/
    ├── package.json
    ├── tsconfig.json
    ├── src/
    │ ├── index.ts # Public API exports
    │ ├── types.ts # Language, LanguageConfig, TranslationMap types
    │ ├── config.ts # Supported languages, default language
    │ ├── resolver.ts # getTranslation() with dot-notation & fallback
    │ ├── format.ts # Intl wrappers: formatCurrency, formatDate, formatNumber
    │ ├── plural.ts # Basic pluralization support
    │ ├── react/
    │ │ ├── provider.tsx # LanguageProvider (React Context)
    │ │ ├── hook.ts # useLanguage() + useTranslation()
    │ │ └── component.tsx # <T key="..." /> component for JSX usage
    │ └── translations/
    │ └── base/ # Shared base translations
    │ ├── nl.json
    │ ├── en.json
    │ ├── de.json
    │ └── fr.json
    └── dist/ # Built output
  2. Define the core types

    types.ts
    export type Language = "nl" | "de" | "fr" | "en"
    export interface LanguageConfig {
    code: Language
    name: string
    nativeName: string
    flag: string // Path to flag asset or inline SVG ID
    dir: "ltr" | "rtl" // Future-proofing
    }
    export type TranslationMap = Record<string, string | TranslationMap>
    export type FlatTranslationMap = Record<string, string>
  3. Implement the resolver with merge support

    The resolver should support merging base translations with app-specific overrides:

    resolver.ts
    export function createTranslator(
    translations: Record<Language, TranslationMap>,
    fallbackLang: Language = "nl"
    ) {
    return (lang: Language, key: string): string => {
    // 1. Try requested language
    // 2. Fall back to fallbackLang
    // 3. Return key as last resort
    }
    }
  4. Add formatting utilities

    Wrap Intl APIs for consistent locale-aware formatting:

    format.ts
    export function formatCurrency(amount: number, currency: string, locale: Language): string
    export function formatDate(date: Date, locale: Language, options?: Intl.DateTimeFormatOptions): string
    export function formatNumber(value: number, locale: Language, options?: Intl.NumberFormatOptions): string
    export function formatRelativeTime(date: Date, locale: Language): string
  5. Standardize the React bindings

    Unify the provider interface used across all React apps:

    react/hook.ts
    export function useLanguage(): {
    language: Language
    setLanguage: (lang: Language) => void
    t: (key: string, params?: Record<string, string | number>) => string
    formatCurrency: (amount: number, currency: string) => string
    formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string
    }
  6. Define shared base translations

    Extract keys that are common across all apps into translations/base/:

    {
    "common": {
    "save": "Opslaan",
    "cancel": "Annuleren",
    "delete": "Verwijderen",
    "edit": "Bewerken",
    "search": "Zoeken",
    "loading": "Laden...",
    "error": "Er is een fout opgetreden",
    "confirm": "Bevestigen",
    "back": "Terug",
    "next": "Volgende",
    "yes": "Ja",
    "no": "Nee",
    "close": "Sluiten"
    },
    "auth": {
    "login": "Inloggen",
    "logout": "Uitloggen",
    "email": "E-mail",
    "password": "Wachtwoord"
    },
    "validation": {
    "required": "Dit veld is verplicht",
    "invalidEmail": "Ongeldig e-mailadres",
    "minLength": "Minimaal {min} tekens"
    }
    }

Goal: Replace the PIM storefront’s custom i18n with @magiverse/i18n.

  1. Install the shared package

    Add @magiverse/i18n as a dependency in the PIM storefront package.json. Since this is a monorepo, use a workspace reference.

  2. Migrate translation files

    Move PIM-specific translations to use the merge pattern:

    storefront/src/lib/i18n/index.ts
    import { createI18n, baseTranslations } from '@magiverse/i18n'
    import nl from './translations/nl.json' // PIM-specific keys
    import en from './translations/en.json'
    import de from './translations/de.json'
    import fr from './translations/fr.json'
    export const i18n = createI18n({
    defaultLanguage: 'nl',
    translations: {
    nl: { ...baseTranslations.nl, ...nl },
    en: { ...baseTranslations.en, ...en },
    de: { ...baseTranslations.de, ...de },
    fr: { ...baseTranslations.fr, ...fr },
    },
    storageKey: 'magiverse_language', // Unified storage key
    })
  3. Replace LanguageProvider and useLanguage

    Swap the local context with the shared provider. The API stays the same, so component code changes are minimal.

  4. Standardize localStorage key

    Migrate from brinxx_language to magiverse_language. Include a one-time migration:

    // Check for legacy key on first load
    const legacy = localStorage.getItem('brinxx_language')
    if (legacy && !localStorage.getItem('magiverse_language')) {
    localStorage.setItem('magiverse_language', legacy)
    localStorage.removeItem('brinxx_language')
    }
  5. Add formatting to existing money.ts

    Replace the custom Intl.NumberFormat usage with formatCurrency() from the shared package.

  6. Test all 4 languages end-to-end

    Verify every page renders correctly in NL, EN, DE, FR. Check:

    • Language selector works
    • Persistence works across page reloads
    • All keys resolve (no raw keys visible)
    • Currency formatting is correct per locale

Goal: Roll out i18n to all 9 commerce tenant storefronts.

  1. Integrate in magic_development storefront

    This is the template/source for all tenants. Add i18n support here following the same pattern as the PIM storefront migration.

  2. Create a tenant translation override system

    Each tenant may need brand-specific language (e.g. different footer text, brand-specific terms):

    magic_development/storefront/src/i18n/
    ├── base/ # Shared storefront translations
    │ ├── nl.json
    │ ├── en.json
    │ ├── de.json
    │ └── fr.json
    └── overrides/ # Tenant-specific overrides (empty in template)
    ├── nl.json
    ├── en.json
    ├── de.json
    └── fr.json

    Resolution order: Tenant override → Storefront base → @magiverse/i18n base

  3. Distribute to all tenants

    Use the existing rsync/distribution workflow to push i18n to:

    • magic_brinxx
    • magic_default
    • magic_logohorloge
    • magic_bovisales
    • magic_demo
    • magic_desluis
    • magic_jodasign
    • magic_spranz
  4. Per-tenant language configuration

    Not every tenant needs all 4 languages. Add a tenant config to control which languages are available:

    // Per-tenant config
    export const tenantLanguages: Language[] = ["nl", "de"] // Brinxx: NL + DE only
  5. Audit hardcoded strings

    Systematically scan tenant storefront components for hardcoded Dutch strings and replace with t() calls.

Phase 4 — Portal & Dashboard ✅ COMPLEET

Section titled “Phase 4 — Portal & Dashboard ✅ COMPLEET”

Goal: Add i18n to Magic Portal (React + Vite) and Dashboard.

  1. Install @magiverse/i18n in magic_portal

    LanguageProvider wraps the app in main.tsx. useLanguage() hook used across 17+ components.

  2. Extract portal-specific translation keys

    4 complete translation files (NL, EN, DE, FR) with 561+ keys each in src/i18n/translations/. Keys cover: navigation, header, gatekeeper, ports, tenant, documentation, projects, dashboard, developer, leads, customer dashboard, admin, support, login, nginx, and common namespaces.

  3. Add language selector to portal header

    Language selector integrated in portal header component with persistent magiverse_language storage key.

  4. Migrate all hardcoded strings

    Complete audit and migration:

    • Navigation labels — all t() calls
    • Dashboard widget titles — all t() calls
    • Form labels and placeholders — all t() calls
    • Error messages and notifications — all t() calls
    • Table headers — all t() calls
    • NginxUrls page (~25 strings) — category names, stats, search, table headers
    • “Geen toegang” access denied strings (6 instances) — t('common.noAccess')
    • Date/time locale formatting (30 instances) — LOCALE_MAP[language] replaces hardcoded 'nl-NL'

Phase 5 — Product Data i18n (Medusa) — BUITEN SCOPE

Section titled “Phase 5 — Product Data i18n (Medusa) — BUITEN SCOPE”

Phase 6 — FlowBuilder Migration — BUITEN SCOPE

Section titled “Phase 6 — FlowBuilder Migration — BUITEN SCOPE”

Goal: Align FlowBuilder’s i18n with the shared package.

  1. Replace custom i18n.ts with @magiverse/i18n

    The FlowBuilder already has 460+ translation keys. Migrate the inline translation object to JSON files and use the shared resolver.

  2. Migrate localStorage key

    From flowbuilder-lang to magiverse_language (with legacy migration).

  3. Add French support

    FlowBuilder currently only supports NL, EN, DE. Add FR translations.

  4. Standardize agent template translations

    Replace the name_nl, name_en, name_de field suffix pattern with proper i18n:

    // Before
    { name_nl: "Begroeting", name_en: "Greeting", name_de: "Begrüßung" }
    // After
    { nameKey: "triggers.greeting" } // Resolved via t()

┌──────────────────────────────────────────────────────────┐
│ Developer Translation Workflow │
│ │
│ 1. Add key to translation file (Dutch first) │
│ └── nl.json: { "cart.empty": "Je winkelwagen..." } │
│ │
│ 2. Add same key to other language files │
│ ├── en.json: { "cart.empty": "Your cart is empty" } │
│ ├── de.json: { "cart.empty": "Ihr Warenkorb..." } │
│ └── fr.json: { "cart.empty": "Votre panier..." } │
│ │
│ 3. Use in component │
│ └── const { t } = useLanguage() │
│ return <p>{t("cart.empty")}</p> │
│ │
│ 4. Run translation check script │
│ └── npm run i18n:check (reports missing keys) │
└──────────────────────────────────────────────────────────┘
ConventionExampleRule
Namespacecart.emptyGroup by feature/page
Nestingproduct.status.inStockMax 3 levels deep
Params"Welkom, {name}"Use {paramName} for interpolation
Pluralsitems.count_one / items.count_otherSuffix with _one, _other
Actionsaction.savePrefix interactive elements
Labelslabel.emailPrefix form labels
  1. Missing key checker (npm run i18n:check)

    Script that compares all language files against the Dutch (default) file and reports missing keys:

    Terminal window
    # Output example:
    en.json missing 3 keys:
    - cart.shippingNote
    - product.customization.title
    - footer.newsletter.placeholder
    de.json all keys present
    fr.json missing 1 key:
    - product.customization.title
  2. Unused key detector (npm run i18n:unused)

    Scans source files for t("...") calls and flags translation keys that are never referenced.

  3. Type generation (npm run i18n:types)

    Auto-generate TypeScript types from the Dutch translation file so t() calls are type-safe:

    // Auto-generated
    type TranslationKey =
    | "common.save"
    | "common.cancel"
    | "cart.empty"
    | "product.addToCart"
    // ...

WhatPath
Shared package/packages/magiverse-i18n/
PIM translations/magic_pim/storefront/src/i18n/translations/
Development template i18n/magic_commerce/magic_development/storefront/src/i18n/
Portal translations/magic_portal/src/i18n/translations/
FlowBuilder translations/magic_agent/flowbuilder/src/i18n/translations/
Base shared translations/packages/magiverse-i18n/src/translations/base/
i18n check script/packages/magiverse-i18n/scripts/check-translations.ts

Phase 1: Shared Package ██████████ ← Foundation (do first)
Phase 2: PIM Storefront ████████ ← Already has i18n, easiest migration
Phase 3: Tenant Storefronts ██████████ ← Highest business impact
Phase 4: Portal & Dashboard ██████ ← Internal tool, lower urgency
Phase 5: Product Data i18n ────────── ← BUITEN SCOPE (productdata niet vertalen)
Phase 6: FlowBuilder ████ ← Already has partial i18n

RiskImpactMitigation
Missing translations at runtimeUsers see raw keys like cart.emptyDutch fallback is always loaded; i18n:check script catches gaps before deploy
Tenant distribution breaks i18nTenant stores lose translations after syncInclude i18n files in distribution checklist; add post-sync verification
Product data migration complexityIncorrect product content in wrong languageBuiten scope — productdata wordt niet vertaald
Bundle size increase4 language files loadedLazy-load non-default languages; only load the active language’s translations
Inconsistent translations across appsSame word translated differentlyShared base translations enforce consistency for common terms

  • All user-facing strings in PIM storefront use t() — zero hardcoded strings
  • All 9 tenant storefronts support at least NL + EN
  • Portal supports NL + EN (+ DE + FR) — Phase 4 voltooid 6 maart 2026
  • Language preference persists across all apps (unified magiverse_language key)
  • npm run i18n:check passes with zero missing keys across all apps
  • Product names and descriptions available in NL + EN in Medusa — Buiten scope: productdata blijft in leverancierstaal
  • Language selector visible and functional on all storefronts
  • Page load performance: no measurable regression (lazy loading works)