Skip to main content
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.
The dashboard uses i18next with the 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

With 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:
If you already call 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, under admin.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

In a registry entry, pass the key directly:
Note: 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:
  1. admin.fields.<resource>.<attribute>.<facet> — resource-specific, e.g. admin.fields.report.name.label
  2. admin.fields.<attribute>.<facet> — cross-resource defaults, e.g. admin.fields.name.label
Components call 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

The mapSpreeErrorsToForm 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 calls i18n.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