DPP Agent docs

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:

  1. Explicit query parameter?locale=sv-SE. Operators use this to preview translations before publishing. Bypasses every other rule.
  2. Accept-Language header — the browser's preference list. Accept-Language: sv-SE,sv;q=0.9,en;q=0.8 is matched against the locales available on the row, in priority order.
  3. Tenant defaulttenants.default_locale, set during onboarding. Example Group defaults to en-GB; T&T to sv-SE.
  4. 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:

  1. Exact requested locale (fi-FI)
  2. Language-only fallback (fi)
  3. Tenant default (en-GB)
  4. 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.