PIM Storefront i18n
Custom React Context-based system with 4 languages, ~105 translation keys per language, localStorage persistence, and a useLanguage() hook.
╔══════════════════════════════════════════════════════════════╗║ ║║ 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 ║║ ║╚══════════════════════════════════════════════════════════════╝Phase 1 — @magiverse/i18n shared package aangemaakt op /mnt/data/magic_omniverse/packages/magiverse-i18n/
formatCurrency, formatDate, formatNumber, formatRelativeTime)Intl.PluralRulesLanguageProvider, useLanguage() hook, <T /> componenti18n:check (missing key checker) en i18n:types (TypeScript type generator)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 LanguageProvidernext.config.js bijgewerkt met transpilePackages: ["@magiverse/i18n"]brinxx_language → magiverse_language (met auto-migratie)Phase 3 — Alle 9 tenant storefronts bijgewerkt
overrides/ directory aangemaakt met lege JSON bestanden per taal (klaar voor tenant-specifieke teksten)magic_development (template) volledig bijgewerktNIEUW: /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)Wat valt WEL onder i18n (platform UI):
Wat valt NIET onder i18n (productdata):
Waarom geen productdata vertaling:
╔══════════════════════════════════════════════════════════════════════╗║ ║║ ██╗ ██╗ █████╗ ███╗ ██╗ ║║ ██║███║██╔══██╗████╗ ██║ ║║ ██║╚██║╚█████╔╝██╔██╗ ██║ ║║ ██║ ██║██╔══██╗██║╚██╗██║ ║║ ██║ ██║╚█████╔╝██║ ╚████║ ║║ ╚═╝ ╚═╝ ╚════╝ ╚═╝ ╚═══╝ ║║ ║║ 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 flagsKey characteristics:
nl) is the default and fallback language"header.language")brinxx_language)document.documentElement.lang attribute updated on change| Gap | Impact | Priority |
|---|---|---|
| No shared translation package | Each app reimplements i18n from scratch | High |
| Inconsistent storage keys | brinxx_language vs flowbuilder-lang | Medium |
| No translation management workflow | Devs manually edit JSON files | Medium |
| Tenant storefronts lack i18n | Hardcoded Dutch strings in commerce tenants | High |
| Portal has no i18n | Dashboard is Dutch-only | Medium |
| No Medusa-native i18n for product data | Product names/descriptions are single-language | |
| Agent templates use field suffixes | welcome_message_nl pattern doesn’t scale | Low |
| No pluralization support | Custom t() can’t handle plural forms | Low |
| No date/time localization | Only currency uses Intl API | Low |
┌─────────────────────────────────────────────────────────┐│ @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 │ └─────────┘ └─────────┘ └─────────┘ └─────────┘| Code | Language | Native Name | Flag | Status |
|---|---|---|---|---|
nl | Dutch | Nederlands | 🇳🇱 | Default / Fallback |
en | English | English | 🇬🇧 | Supported |
de | German | Deutsch | 🇩🇪 | Supported |
fr | French | Français | 🇫🇷 | Supported |
Goal: Extract the existing PIM storefront i18n into a reusable shared package.
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 outputDefine the core types
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>Implement the resolver with merge support
The resolver should support merging base translations with app-specific overrides:
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 }}Add formatting utilities
Wrap Intl APIs for consistent locale-aware formatting:
export function formatCurrency(amount: number, currency: string, locale: Language): stringexport function formatDate(date: Date, locale: Language, options?: Intl.DateTimeFormatOptions): stringexport function formatNumber(value: number, locale: Language, options?: Intl.NumberFormatOptions): stringexport function formatRelativeTime(date: Date, locale: Language): stringStandardize the React bindings
Unify the provider interface used across all React apps:
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}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.
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.
Migrate translation files
Move PIM-specific translations to use the merge pattern:
import { createI18n, baseTranslations } from '@magiverse/i18n'import nl from './translations/nl.json' // PIM-specific keysimport 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})Replace LanguageProvider and useLanguage
Swap the local context with the shared provider. The API stays the same, so component code changes are minimal.
Standardize localStorage key
Migrate from brinxx_language to magiverse_language. Include a one-time migration:
// Check for legacy key on first loadconst legacy = localStorage.getItem('brinxx_language')if (legacy && !localStorage.getItem('magiverse_language')) { localStorage.setItem('magiverse_language', legacy) localStorage.removeItem('brinxx_language')}Add formatting to existing money.ts
Replace the custom Intl.NumberFormat usage with formatCurrency() from the shared package.
Test all 4 languages end-to-end
Verify every page renders correctly in NL, EN, DE, FR. Check:
Goal: Roll out i18n to all 9 commerce tenant storefronts.
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.
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.jsonResolution order: Tenant override → Storefront base → @magiverse/i18n base
Distribute to all tenants
Use the existing rsync/distribution workflow to push i18n to:
Per-tenant language configuration
Not every tenant needs all 4 languages. Add a tenant config to control which languages are available:
// Per-tenant configexport const tenantLanguages: Language[] = ["nl", "de"] // Brinxx: NL + DE onlyAudit hardcoded strings
Systematically scan tenant storefront components for hardcoded Dutch strings and replace with t() calls.
Goal: Add i18n to Magic Portal (React + Vite) and Dashboard.
Install @magiverse/i18n in magic_portal ✅
LanguageProvider wraps the app in main.tsx. useLanguage() hook used across 17+ components.
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.
Add language selector to portal header ✅
Language selector integrated in portal header component with persistent magiverse_language storage key.
Migrate all hardcoded strings ✅
Complete audit and migration:
t() callst() callst() callst() callst() callst('common.noAccess')LOCALE_MAP[language] replaces hardcoded 'nl-NL'Goal: Align FlowBuilder’s i18n with the shared package.
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.
Migrate localStorage key
From flowbuilder-lang to magiverse_language (with legacy migration).
Add French support
FlowBuilder currently only supports NL, EN, DE. Add FR translations.
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) │└──────────────────────────────────────────────────────────┘| Convention | Example | Rule |
|---|---|---|
| Namespace | cart.empty | Group by feature/page |
| Nesting | product.status.inStock | Max 3 levels deep |
| Params | "Welkom, {name}" | Use {paramName} for interpolation |
| Plurals | items.count_one / items.count_other | Suffix with _one, _other |
| Actions | action.save | Prefix interactive elements |
| Labels | label.email | Prefix form labels |
Missing key checker (npm run i18n:check)
Script that compares all language files against the Dutch (default) file and reports missing keys:
# 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.titleUnused key detector (npm run i18n:unused)
Scans source files for t("...") calls and flags translation keys that are never referenced.
Type generation (npm run i18n:types)
Auto-generate TypeScript types from the Dutch translation file so t() calls are type-safe:
// Auto-generatedtype TranslationKey = | "common.save" | "common.cancel" | "cart.empty" | "product.addToCart" // ...| What | Path |
|---|---|
| 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 migrationPhase 3: Tenant Storefronts ██████████ ← Highest business impactPhase 4: Portal & Dashboard ██████ ← Internal tool, lower urgencyPhase 5: Product Data i18n ────────── ← BUITEN SCOPE (productdata niet vertalen)Phase 6: FlowBuilder ████ ← Already has partial i18n| Risk | Impact | Mitigation |
|---|---|---|
| Missing translations at runtime | Users see raw keys like cart.empty | Dutch fallback is always loaded; i18n:check script catches gaps before deploy |
| Tenant distribution breaks i18n | Tenant stores lose translations after sync | Include i18n files in distribution checklist; add post-sync verification |
| Buiten scope — productdata wordt niet vertaald | ||
| Bundle size increase | 4 language files loaded | Lazy-load non-default languages; only load the active language’s translations |
| Inconsistent translations across apps | Same word translated differently | Shared base translations enforce consistency for common terms |
t() — zero hardcoded stringsmagiverse_language key)npm run i18n:check passes with zero missing keys across all apps