Multi-Language (i18n)
Overview
Section titled “Overview”The Magic eVERSE platform supports 4 languages: Dutch (NL), English (EN), German (DE), and French (FR). Dutch is the default.
There are two layers of translation:
| Layer | What it translates | Source | Changes with language? |
|---|---|---|---|
| UI Labels | Buttons, tab names, form labels, navigation, static text | JSON translation files | Yes, instantly |
| Product Content | Product titles, descriptions, materials, colors, packaging | Medusa product metadata.translations | Yes, instantly |
Language Switching
Section titled “Language Switching”The language selector is in the top-right header bar of every storefront — a small button showing the current language code (nl/en/de) with a flag icon. Clicking it opens a dropdown with all 4 languages.
The selected language is stored in localStorage as magiverse_language and applies instantly — no page reload required. All UI labels and product content switch immediately via React state.
How It Works Technically
Section titled “How It Works Technically”User clicks "Deutsch" in language dropdown → LanguageProvider updates state: language = "de" → All components using useLanguage() re-render → t("product.color") now returns "Farbe" instead of "Kleur" → useProductTranslation(product) now reads metadata.translations.de → Product title changes from "Fleecedeken HANGOVER" to "Fleecedecke HANGOVER"Layer 1: UI Label Translations
Section titled “Layer 1: UI Label Translations”Static text (buttons, labels, navigation, tab names, etc.) is translated via JSON files.
Translation File Locations
Section titled “Translation File Locations”There are 3 levels of translation files, merged in priority order:
-
Tenant overrides (highest priority)
storefront/src/lib/i18n/overrides/{nl,en,de,fr}.jsonTenant-specific text like company name, address, custom labels.
-
Storefront base translations
storefront/src/lib/i18n/translations/{nl,en,de,fr}.jsonAll shared e-commerce labels: product specs, cart, checkout, navigation, etc.
-
@magiverse/i18n package (lowest priority)
packages/magiverse-i18n/src/translations/base/{nl,en,de,fr}.json57 universal keys shared across all apps (Portal, PIM, Storefronts).
Resolution: tenant override → storefront base → @magiverse/i18n base. First match wins.
Using Translations in Components
Section titled “Using Translations in Components”"use client"import { useLanguage } from "@lib/context/language-context"
const MyComponent = () => { const { language, t } = useLanguage()
return ( <div> <h1>{t("product.tabInfo")}</h1> {/* "Informatie" / "Information" / "Informationen" */} <p>{t("product.deliveryTimeValue")}</p> {/* "5-15 werkdagen" / "5-15 working days" / "5-15 Werktage" */} </div> )}Available Translation Keys (Product Section)
Section titled “Available Translation Keys (Product Section)”| Key | NL | EN | DE |
|---|---|---|---|
product.tabInfo | Informatie | Information | Informationen |
product.tabShipping | Verzending | Shipping | Versand |
product.tabPrices | Prijzen | Prices | Preise |
product.tabPrintPricing | Print Staffel | Print Pricing | Druckstaffel |
product.articleNumber | Artikelnummer | Article Number | Artikelnummer |
product.color | Kleur | Color | Farbe |
product.material | Materiaal | Material | Material |
product.packaging | Verpakking | Packaging | Verpackung |
product.weight | Gewicht (g) | Weight (g) | Gewicht (g) |
product.dimensions | Afmetingen (mm) | Dimensions (mm) | Abmessungen (mm) |
product.brand | Merk | Brand | Marke |
product.minOrderQuantity | Min. bestelhoeveelheid | Min. Order Quantity | Mindestbestellmenge |
product.pieces | stuks | pieces | Stück |
product.pricePerPiece | Prijs per stuk | Price per piece | Preis pro Stück |
product.deliveryTimeValue | 5-15 werkdagen | 5-15 working days | 5-15 Werktage |
product.freeShippingAbove | Gratis bij bestellingen boven €750 | Free for orders above €750 | Kostenlos ab €750 |
product.pricesExclVat | * Prijzen zijn exclusief BTW | * Prices exclude VAT | * Preise zzgl. MwSt. |
Adding New UI Translations
Section titled “Adding New UI Translations”To add a new translated label:
-
Add the key to all 4 JSON files in
storefront/src/lib/i18n/translations/:nl.json { "mySection": { "myLabel": "Mijn label" } }// en.json{ "mySection": { "myLabel": "My label" } }// de.json{ "mySection": { "myLabel": "Mein Label" } }// fr.json{ "mySection": { "myLabel": "Mon label" } } -
Use it in a component:
const { t } = useLanguage()return <span>{t("mySection.myLabel")}</span> -
Distribute the updated JSON files to all tenants (see Code Distribution).
Layer 2: Product Content Translations
Section titled “Layer 2: Product Content Translations”Product titles, descriptions, materials, colors, and packaging text are translated via Medusa product metadata.
Data Flow
Section titled “Data Flow”APLT Database (source of truth) ├── name_nl: "Fleecedeken HANGOVER" ├── name_en: "Fleece blanket HANGOVER" ├── name_de: "Fleecedecke HANGOVER" ├── description_nl: "De fleecedeken HANGOVER is niet alleen..." ├── description_en: "The HANGOVER fleece blanket is not only..." ├── description_de: "Die Fleecedecke HANGOVER ist nicht nur..." ├── color_nl: "zwart" ├── color_en: "black" ├── color_de: "schwarz" └── ... │ ▼ (medusa-sync)Medusa Product ├── title: "Fleecedeken HANGOVER" ← always NL (base) ├── description: "De fleecedeken HANGOVER..." ← always NL (base) └── metadata.translations: ├── nl: { title, description, material, color, packaging } ├── en: { title, description, material, color, packaging } ├── de: { title, description, material, color, packaging } └── fr: { title, description, material, color, packaging } │ ▼ (storefront)useProductTranslation(product) reads metadata.translations[language] → Falls back to product.title / product.description if no translation existsThe useProductTranslation Hook
Section titled “The useProductTranslation Hook”For client components (most product UI):
"use client"import { useProductTranslation } from "@lib/i18n/use-product-translation"
const ProductCard = ({ product }) => { const pt = useProductTranslation(product)
return ( <div> <h2>{pt.title}</h2> {/* Language-aware product title */} <p>{pt.description}</p> {/* Language-aware description */} <span>{pt.color}</span> {/* Language-aware color name */} <span>{pt.material}</span> {/* Language-aware material */} <span>{pt.packaging}</span> {/* Language-aware packaging text */} </div> )}For server components, use the non-hook version:
import { getProductTranslation } from "@lib/i18n/use-product-translation"
const ProductCard = ({ product, language }) => { const pt = getProductTranslation(product, language) return <h2>{pt.title}</h2>}Fallback Behavior
Section titled “Fallback Behavior”If a product has no translations in metadata (e.g., not yet synced, or APLT data incomplete):
pt.title→ falls back toproduct.title(NL)pt.description→ falls back toproduct.description(NL)pt.color→ falls back tometadata.color(NL)pt.material→ falls back toproduct.material(NL)
This means the storefront never shows blank content — worst case it stays Dutch.
How Translations Get Into Medusa
Section titled “How Translations Get Into Medusa”The medusa-sync process (triggered from Medusa admin → Connectors → Sync) reads all APLT products and pushes them to Medusa. During sync, it calls buildTranslations(product) to create the translations object from the APLT multi-language columns.
Trigger a sync:
# Get auth tokenTOKEN=$(curl -s -X POST http://localhost:4040/auth/user/emailpass \ -H 'Content-Type: application/json' \ -d '{"email":"admin@admin.com","password":"admin123"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
# Start synccurl -X POST "http://localhost:4040/admin/connectors/medusa-sync" \ -H "x-medusa-access-token: $TOKEN" \ -H "Authorization: Bearer $TOKEN"
# Check progresscurl "http://localhost:4040/admin/connectors/medusa-sync" \ -H "x-medusa-access-token: $TOKEN" \ -H "Authorization: Bearer $TOKEN"Where Does the Source Translation Data Come From?
Section titled “Where Does the Source Translation Data Come From?”The APLT database already stores multi-language product data in separate columns:
| Column | Language | Example |
|---|---|---|
name_nl | Dutch | Fleecedeken HANGOVER |
name_en | English | Fleece blanket HANGOVER |
name_de | German | Fleecedecke HANGOVER |
description_nl | Dutch | De fleecedeken HANGOVER is… |
description_en | English | The HANGOVER fleece blanket is… |
description_de | German | Die Fleecedecke HANGOVER ist… |
material_nl | Dutch | micro-PES geborstelde jersey fleece |
material_en | English | micro-PES brushed jersey fleece |
color_nl / color_en / color_de | All | zwart / black / schwarz |
packaging_nl / packaging_en / packaging_de | All | Various |
This data comes from the original product suppliers (Spranz, XD Connect, Langenberg, etc.) who provide product catalogs in multiple languages. The data is imported into the APLT tables via spreadsheet imports.
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
storefront/src/lib/context/language-context.tsx | Re-exports shared LanguageProvider |
storefront/src/lib/i18n/index.ts | Creates tenant i18n instance with merged translations |
storefront/src/lib/i18n/translations/{nl,en,de,fr}.json | Storefront UI translations |
storefront/src/lib/i18n/overrides/{nl,en,de,fr}.json | Tenant-specific overrides |
storefront/src/lib/i18n/use-product-translation.ts | Hook for reading translated product data |
packages/magiverse-i18n/ | Shared i18n package (core, React bindings, base translations) |
backend/src/api/admin/connectors/medusa-sync/route.ts | Sync route that writes translations to metadata |
React Context Architecture
Section titled “React Context Architecture”<LanguageProvider> ← from @magiverse/i18n (shared) │ ├── language: "de" ← current language (from localStorage) ├── setLanguage(lang) ← switch language ├── t(key) ← translate UI label ├── formatCurrency(amount) ← locale-aware currency formatting ├── formatDate(date) ← locale-aware date formatting └── formatNumber(number) ← locale-aware number formattingComponents use useLanguage() for UI labels and useProductTranslation(product) for product-specific content. Both react to the same language state — switching once updates everything.