Locale — multi-language ingest and resolution
ESPR (EU 2024/1781) and AGEC (FR Loi 2020-105) both require the consumer-facing DPP to be readable in the consumer's language. For brands operating across the EU + EEA + UK that's typically 6–10 locales per product, with care instructions and substance declarations needing exact translation control.
DPP Agent treats locale as a capability, not a security boundary. You decide which locales to ingest and store; the runtime picks the right one to render based on the consumer's browser.
Storage model
Every translatable field in dpp_data accepts two shapes:
// Flat string — legacy / single-locale brands
"espr.pid.product_name": "Pro Winter Jacket"
// Localised object — multi-locale brands
"espr.pid.product_name": {
"en-GB": "Pro Winter Jacket",
"sv-SE": "Vinterjacka Pro",
"de-DE": "Pro Winterjacke",
"no-NB": "Vinterjakke Pro",
"fi-FI": "Talvitakki Pro"
}
The renderer picks the right value at request time per the rules below. The admin UI displays both shapes in the same edit form: a single text input for the flat form, an expandable per-locale block for the localised form. Migration between shapes is non-destructive.
BCP 47 language tags throughout. sv-SE (Swedish-Sweden) is
distinct from sv-FI (Swedish-Finland) — your brand decides
whether to ship one or both.
Ingest
From Akeneo
Akeneo stores every translatable attribute per { locale, scope }.
The adapter has two related settings:
settings.locale— required, the fallback locale (e.g.en_GB). The locale Akeneo serves at this exact code is what flat-shape fields use when the brand chooses not to ingest multi-locale.settings.locales— optional, an array (e.g.["en_GB","sv_SE","de_DE","no_NB","fi_FI"]). When set, every translatable attribute is captured as a localised object, preserving every locale Akeneo serves for that scope.
Akeneo locale codes (sv_SE with underscore) are normalised to
BCP 47 (sv-SE with hyphen) at write time, so all storage is in the
canonical hyphenated form.
From Centra
Centra ships product copy in name, description, and friends
without explicit locale tagging — the data IS the brand's primary
locale and we treat it as such. Brands with Centra customised to
expose a translations block can override the default extractor
via the centra.translations_path setting.
From Delogue
Delogue's /v1/styles/{id}/translations endpoint returns per-locale
strings. The adapter's settings.locales array filters which ones
land in dpp_data. Same hyphenation normalisation as Akeneo.
From bulk XLSX
The import column can be labelled espr.pid.product_name (flat),
espr.pid.product_name:sv-SE (single locale), or
espr.pid.product_name@translations (in-cell JSON). All three end
up in the same canonical shape.
From manual create / AI Suggest
Manual create persists the operator's typed value. AI Suggest
proposes per-locale values when the source page exposes them
(JSON-LD inLanguage, <html lang>, og:locale); when only one
locale is detected, it proposes the flat shape.
Content negotiation at resolve time
When a DPP is requested, the resolver follows this precedence to pick the rendered locale:
- Explicit query parameter —
?locale=sv-SE. Operators use this to preview translations before publishing. Bypasses every other rule. - Accept-Language header — the browser's preference list.
Accept-Language: sv-SE,sv;q=0.9,en;q=0.8is matched against the locales available on the row, in priority order. - Tenant default —
tenants.default_locale, set during onboarding. Example Group defaults toen-GB; T&T tosv-SE. - First available — the first key in the localised object. Used only when none of the above match.
The picked locale is echoed in the response headers:
Content-Language: sv-SE
Vary: Accept-Language
Cache-Control: public, max-age=3600
Vary: Accept-Language ensures the CDN (Vercel Edge / CloudFlare)
caches one variant per language combination. A Swedish consumer and
a German consumer hitting the same URL get distinct cached
responses — no cross-language pollution.
Fallback semantics for missing locales
Translatable fields can be present in some locales and absent in others. A brand with care instructions translated to 5 EU locales but not to Finnish should still render acceptably to a Finnish consumer.
When a localised object lacks the requested locale, the resolver walks the fallback chain field-by-field:
- Exact requested locale (
fi-FI) - Language-only fallback (
fi) - Tenant default (
en-GB) - First available
The renderer logs every fallback in the structured response under
x-dpp-locale-fallback so analytics teams can see which fields are
under-translated.
Per-locale compliance coverage
AGEC mandates the declarations be readable on the French market in French. The admin UI's compliance dashboard tracks coverage per-locale:
Example Group — AGEC coverage by locale
────────────────────────────────────────
fr-FR 7 / 7 fields 100% ✓
sv-SE 7 / 7 fields 100% ✓
en-GB 6 / 7 fields 86% ⚠ tex.haz.summary missing for 142 SKUs
de-DE 5 / 7 fields 71% ⚠ tex.mat.natural_fibres_pct missing
fi-FI 3 / 7 fields 43% ⚠
no-NB 3 / 7 fields 43% ⚠
The dashboard surfaces missing-field-per-locale lists so the translation team can prioritise.
Implementation status
Locale handling ships in phases tied to onboarding cadence:
- Phase 1 (live) — flat-string ingest, single-locale render
- Phase 2 (live) — Accept-Language detection + tenant default
- Vary header
- Phase 3 (in design, ships alongside first multi-locale tenant) — localised object storage, multi-locale Akeneo ingest, per-locale AGEC coverage dashboard
Brands signing up today get Phase 1+2 immediately. Phase 3 ships inside the first sprint after a multi-locale tenant goes live.
Privacy
The renderer logs the requested Accept-Language for analytics purposes (counts per BCP 47 tag, broken down per tenant). No consumer IP is associated with the locale in our logs; the aggregation is many-hours-batched so individual scans cannot be reconstructed from the analytics layer.