Examples here import from
@spree/dashboard, which re-exports the framework and the design system for host applications. A distributed plugin imports @spree/dashboard-core and @spree/dashboard-ui directly instead — it extends the shell rather than shipping it. See plugin overview.react-i18next bindings. All strings live in i18next’s default translation namespace, with every key under a top-level admin. prefix (admin.nav.orders, admin.fields.<resource>.<attribute>.label, …). You register your strings into the same namespace with the same prefix so they look and feel native.
Add translations
deep: true your keys merge into the existing tree instead of replacing it; overwrite: true lets your bundle win where a key genuinely collides (this is what the framework and the official plugins use). Note the admin wrapper is part of the bundle contents — the namespace argument stays 'translation'.
Repeat per locale:
defineDashboardPlugin, pass the same bundles as locales
instead and it registers them for you, before anything else in your config runs:
Name the types your gem registers
A backend gem that registers a promotion rule, price rule, calculator, delivery method rule, commission rule or seller requirement kind should ship the name merchants read for it, underadmin.types.<family>.<code>:
<code> is the type’s wire shorthand — what Spree::Base.api_type returns and
what the API reports as type. Families are promotion_rule,
promotion_action, calculator, price_rule, collection_rule,
order_routing_rule, delivery_method_rule, commission_rule,
seller_requirement, integration and permission.
Without these keys the dashboard falls back to the label your Ruby class
supplies through the API. That fallback is resolved in the store’s locale
rather than the admin’s interface language, so an admin working in one language
sees your type’s name in another. Shipping the keys is what keeps a page in one
language.
Use them
i18n.t resolves at the call site, so the label is a snapshot taken at registration time. Registry entries store plain strings — if the user switches language without a reload, registered labels keep their old language until re-registered. Components that call useTranslation() re-render on locale change; registry labels don’t. If that matters for your entry, re-register it on the i18n languageChanged event.
Field-key convention
Form labels, placeholders, and help text follow a two-level key convention:admin.fields.<resource>.<attribute>.<facet>— resource-specific, e.g.admin.fields.report.name.labeladmin.fields.<attribute>.<facet>— cross-resource defaults, e.g.admin.fields.name.label
t() with these keys explicitly — pick the resource-scoped form for labels that differ per resource, and reuse the shared admin.fields.<attribute>.<facet> keys the framework already ships for common attributes (name, email, created_at, storefront_visible, …).
In dev mode, missing keys log to the console (in production a missing key falls back to a humanized version of the attribute name).
Server-side error messages
ThemapSpreeErrorsToForm helper routes 422 responses onto form.formState.errors, but the messages themselves are the server’s strings, verbatim — { "name": ["can't be blank"] } becomes a field error reading “can’t be blank”. To localize validation messages, configure the locale on the backend (Rails i18n translates ActiveRecord error messages); the dashboard displays whatever the API returns.
Order of operations
Register translations before any code that callsi18n.t() at module-load time (registry labels, table titles, etc.). The framework’s own bundles are loaded before your code runs, so the pattern is simply — top of the file:
Reference
- i18next docs — full API
- The dashboard’s own bundle:
packages/dashboard/src/locales/en.json— copy keys for forms, status badges, validation messages, etc.

