Skip to content

Multi-Language (i18n)

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:

LayerWhat it translatesSourceChanges with language?
UI LabelsButtons, tab names, form labels, navigation, static textJSON translation filesYes, instantly
Product ContentProduct titles, descriptions, materials, colors, packagingMedusa product metadata.translationsYes, instantly

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.

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"

Static text (buttons, labels, navigation, tab names, etc.) is translated via JSON files.

There are 3 levels of translation files, merged in priority order:

  1. Tenant overrides (highest priority)

    storefront/src/lib/i18n/overrides/{nl,en,de,fr}.json

    Tenant-specific text like company name, address, custom labels.

  2. Storefront base translations

    storefront/src/lib/i18n/translations/{nl,en,de,fr}.json

    All shared e-commerce labels: product specs, cart, checkout, navigation, etc.

  3. @magiverse/i18n package (lowest priority)

    packages/magiverse-i18n/src/translations/base/{nl,en,de,fr}.json

    57 universal keys shared across all apps (Portal, PIM, Storefronts).

Resolution: tenant override → storefront base → @magiverse/i18n base. First match wins.

"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)”
KeyNLENDE
product.tabInfoInformatieInformationInformationen
product.tabShippingVerzendingShippingVersand
product.tabPricesPrijzenPricesPreise
product.tabPrintPricingPrint StaffelPrint PricingDruckstaffel
product.articleNumberArtikelnummerArticle NumberArtikelnummer
product.colorKleurColorFarbe
product.materialMateriaalMaterialMaterial
product.packagingVerpakkingPackagingVerpackung
product.weightGewicht (g)Weight (g)Gewicht (g)
product.dimensionsAfmetingen (mm)Dimensions (mm)Abmessungen (mm)
product.brandMerkBrandMarke
product.minOrderQuantityMin. bestelhoeveelheidMin. Order QuantityMindestbestellmenge
product.piecesstukspiecesStück
product.pricePerPiecePrijs per stukPrice per piecePreis pro Stück
product.deliveryTimeValue5-15 werkdagen5-15 working days5-15 Werktage
product.freeShippingAboveGratis bij bestellingen boven €750Free for orders above €750Kostenlos ab €750
product.pricesExclVat* Prijzen zijn exclusief BTW* Prices exclude VAT* Preise zzgl. MwSt.

To add a new translated label:

  1. 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" } }
  2. Use it in a component:

    const { t } = useLanguage()
    return <span>{t("mySection.myLabel")}</span>
  3. Distribute the updated JSON files to all tenants (see Code Distribution).

Product titles, descriptions, materials, colors, and packaging text are translated via Medusa product metadata.

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 exists

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>
}

If a product has no translations in metadata (e.g., not yet synced, or APLT data incomplete):

  • pt.title → falls back to product.title (NL)
  • pt.description → falls back to product.description (NL)
  • pt.color → falls back to metadata.color (NL)
  • pt.material → falls back to product.material (NL)

This means the storefront never shows blank content — worst case it stays Dutch.

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:

Terminal window
# Get auth token
TOKEN=$(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 sync
curl -X POST "http://localhost:4040/admin/connectors/medusa-sync" \
-H "x-medusa-access-token: $TOKEN" \
-H "Authorization: Bearer $TOKEN"
# Check progress
curl "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:

ColumnLanguageExample
name_nlDutchFleecedeken HANGOVER
name_enEnglishFleece blanket HANGOVER
name_deGermanFleecedecke HANGOVER
description_nlDutchDe fleecedeken HANGOVER is…
description_enEnglishThe HANGOVER fleece blanket is…
description_deGermanDie Fleecedecke HANGOVER ist…
material_nlDutchmicro-PES geborstelde jersey fleece
material_enEnglishmicro-PES brushed jersey fleece
color_nl / color_en / color_deAllzwart / black / schwarz
packaging_nl / packaging_en / packaging_deAllVarious

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.

FilePurpose
storefront/src/lib/context/language-context.tsxRe-exports shared LanguageProvider
storefront/src/lib/i18n/index.tsCreates tenant i18n instance with merged translations
storefront/src/lib/i18n/translations/{nl,en,de,fr}.jsonStorefront UI translations
storefront/src/lib/i18n/overrides/{nl,en,de,fr}.jsonTenant-specific overrides
storefront/src/lib/i18n/use-product-translation.tsHook for reading translated product data
packages/magiverse-i18n/Shared i18n package (core, React bindings, base translations)
backend/src/api/admin/connectors/medusa-sync/route.tsSync route that writes translations to metadata
<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 formatting

Components use useLanguage() for UI labels and useProductTranslation(product) for product-specific content. Both react to the same language state — switching once updates everything.