Install
$ agentstack add skill-spree-agent-skills-spree-i18n ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Spree I18n + Translations
Two distinct translation surfaces, each with its own mechanism:
| What | Mechanism | Where it lives | |---|---|---| | UI strings (labels, buttons, errors, emails) | Standard Rails I18n + Spree.t | config/locales/.yml | | Data (product names, category names, descriptions) | Mobility gem translation tables | spree__translations tables |
You need both for a multilingual store. UI strings are about how the app speaks; data translations are about what merchant content the customer sees.
UI strings — Spree.t and the YAML files
Every Spree gem ships its own English locale file. Spree.t looks up a key scoped under spree.* in the active locale:
Spree.t(:save) # => "Save"
Spree.t('i18n.this_file_language') # => "English (US)"
Spree.t(:paid, scope: 'payment_states') # => "Paid"
Spree.t(:missing_key, default: 'Fallback') # => "Fallback"
In views / helpers, the shorthand is just Spree.t(...). In ERB templates, you can also use `` for lazy lookup based on the controller + action name (standard Rails).
Adding a new UI language
- Install the translations gem (community-maintained):
``ruby # Gemfile gem 'spree_i18n' # ships translations for 40+ locales ``
This adds config/locales/.yml files for every Spree gem in the bundle.
- Add the locale to your store's supported list:
``ruby # backend/config/initializers/spree.rb I18n.available_locales = %i[en es fr de it ja] I18n.default_locale = :en ``
- (Optional) Add the locale to the relevant market's supported_locales so the storefront language switcher offers it. Locales are configured per Market, not on the store:
``ruby store = Spree::Store.default store.default_market.update!(supported_locales: ['es', 'fr']) ``
supported_locales accepts an Array or a comma-separated string; default_locale also lives on the market. When a store has markets (the norm — stores created with a default_country_iso, including seeds and the admin flow, get a default market automatically), market values take precedence: Store#supported_locales_list comes entirely from the markets, and Store#default_locale returns the default market's locale — falling back to the store column only when the market's default_locale is blank. With no markets at all, the store-level columns are used directly.
- Customize keys by overriding in your app's
config/locales/.yml— Rails merges later-loaded locale files over earlier ones, and your app'sconfig/locales/is loaded last by default.
Adding a new key
If a string isn't in any locale yet:
# config/locales/en.yml in your app
en:
spree:
custom_feature:
title: "Loyalty rewards"
cta: "Join now"
Spree.t('custom_feature.title') # => "Loyalty rewards"
Then add the same key under es, fr, etc. in matching files.
Normalizing translation keys
Spree uses i18n-tasks to keep locale files clean. After adding keys:
bundle exec i18n-tasks normalize # sort + dedupe
bundle exec i18n-tasks missing # list missing keys
bundle exec i18n-tasks unused # list unused keys
bundle exec i18n-tasks health # all of the above
The Spree monorepo runs normalize on its YAML files; if you're modifying spree/admin/config/locales/en.yml (the Rails admin), always normalize after.
Default + fallback
# config/application.rb (or config/environments/production.rb)
config.i18n.default_locale = :en
config.i18n.fallbacks = [:en] # missing :es key falls back to :en
Rails only mixes I18n::Backend::Fallbacks into the backend when config.i18n.fallbacks is set — assigning I18n.fallbacks directly in an initializer does not enable fallback lookups. For Mobility data translations no setup is needed: Spree configures store-based fallbacks per request via Spree::Locales::SetFallbackLocaleForStore (each supported locale falls back to the store's default locale).
Spree::Current.locale is the per-request locale. The Store API resolves the per-request locale from the x-spree-locale header, then the ?locale= param (each honored only if in the store's supported locales), then Spree::Current.locale (market default → store default).
Data translations — Mobility
Spree uses Mobility for translatable model attributes. Each model declares which fields translate:
# spree/core/app/models/spree/product.rb (paraphrased)
class Spree::Product "Camiseta"
end
I18n.with_locale(:en) do
product.name # => "T-shirt"
end
If the translation for the current locale is missing, behavior depends on column_fallback:
column_fallback: true(default unlessSpree.always_use_translations?) — falls back to the model's own column (which holds the default-locale value).column_fallback: false— skips the base column entirely; reads always hit the translation table. Note this does not mean missing translations returnnilin practice: in request contexts (Store API and controllers), Spree configures Mobility's store-based fallbacks per request (Spree::Locales::SetFallbackLocaleForStore), mapping every supported locale to the store's default locale — so a missing translation returns the store-default-locale value. Reads returnnilonly outside that configuration (e.g. a bare console) or when bypassing fallbacks explicitly withproduct.name(fallback: false)— use the latter if you genuinely need to detect/hide missing translations.
Writing translations
Two patterns:
# Via locale block
I18n.with_locale(:es) do
product.update(name: 'Camiseta', description: 'Una camiseta cómoda')
end
# Via the translation association directly
product.translations.find_or_initialize_by(locale: 'es').update!(
name: 'Camiseta',
description: 'Una camiseta cómoda',
)
Which models translate
Out of the box (5.x+):
Spree::Product— name, description, slug, metadescription, metatitleSpree::Taxon(Category) — name, pretty_name, description, permalinkSpree::Taxonomy— nameSpree::OptionType— presentationSpree::OptionValue— presentationSpree::Store— name, metadescription, metakeywords, seotitle, customersupportemail, address, contactphoneSpree::Policy— name, body
The 5.4 plan covers translating MetafieldDefinition names + Metafield text values — see docs/plans/5.4-metafield-translations.md if you have the monorepo.
Locale availability
Spree.always_use_translations? is set per app:
# config/initializers/spree.rb
Spree::Config[:always_use_translations] = false # default — fallback to column for missing locale
Spree::Config[:always_use_translations] = true # never fallback — only use translation tables
true is the right choice for stores where the column value is meaningless (e.g. it's the merchant's internal admin-only string) and only translations are customer-facing. false is right for single-locale stores starting out.
RTL languages (Arabic, Hebrew, Persian)
For RTL support:
- Locale config:
``ruby I18n.available_locales = %i[en ar he] ``
- Storefront direction: storefronts are external (Next.js) apps, so RTL direction is the storefront's responsibility — set
dir="rtl"in its own layout based on the active locale. On the Ruby side,Spree::Locale.new(code: locale).rtl?/.directionis the source of truth (there's noi18n.dirlocale key). - Admin UI direction: the admin flips to RTL automatically — its layouts set
dir=""(viaSpree::Admin::RtlHelper#html_dir→Spree::Locale#direction) and the gem ships an RTL stylesheet (_rtl.css). RTL triggers for locales whose language code is inSpree::Locale::RTL_LANGUAGE_CODES(ar he fa ur yi); no extra setup needed. - Mobility data works the same — you store Arabic strings in
spree_product_translationswithlocale: 'ar'.
Storefront integration
The Store API responds in the locale specified by the X-Spree-Locale header (or per-request ?locale=es). Pass the exact locale code the store supports (e.g. es, not es-ES); unsupported values silently fall back to the store's default locale. Translated fields are returned in that locale; if the locale isn't available, fallback applies.
curl -H "X-Spree-Api-Key: pk_…" \
-H "X-Spree-Locale: es" \
https://my-spree.example.com/api/v3/store/products/cool-shirt
# => { "name": "Camiseta", ... }
The @spree/sdk exposes setLocale:
const client = createClient({ baseUrl, publishableKey, locale: 'es' })
// or
client.setLocale('es')
See the spree-typescript-sdk and spree-api-v3 skills for more.
Common problems
"I see translation missing: es.spree.…"
The key doesn't exist in the active locale. Either:
- Add the key to
config/locales/es.ymlin your app. - Install
spree_i18ngem if the missing key is a Spree-core string. - Add a fallback:
config.i18n.fallbacks = [:en]inconfig/application.rbor an environment file (the standard Rails production.rb already setsconfig.i18n.fallbacks = true).
"Product name shows English even after I set Spanish"
Walk this list:
I18n.localeis actually:es? Add aputs I18n.localein the controller to confirm.product.translations.find_by(locale: 'es')exists and hasnameset?column_fallback: truewould return the English column value. Check thetranslatesdeclaration on the model; if you want strict translations, override withcolumn_fallback: falsein a decorator.- Mobility caching — calls in the same request memoize. Reload the product (
product.reload) after writing translations in the same process.
"Adding a new translated field"
Two steps:
- Generate the migration to add columns to the per-model translation table:
``ruby class AddCustomFieldToSpreeProductTranslations < ActiveRecord::Migration[7.2] def change add_column :spree_product_translations, :custom_field, :text end end ``
- Declare it on the model (via decorator):
``ruby module Spree::ProductDecorator def self.prepended(base) fields = base::TRANSLATABLE_FIELDS + [:custom_field] base.send(:remove_const, :TRANSLATABLE_FIELDS) base.const_set(:TRANSLATABLE_FIELDS, fields.freeze) base.translates :custom_field, column_fallback: !Spree.always_use_translations? end Spree::Product.prepend self end ``
(TRANSLATABLE_FIELDS is frozen — mutating it with << raises FrozenError; redefine the constant instead.)
If the model also stores the field on the base table (for fallback), add a column there too.
"Storefront language switcher doesn't show my new locale"
Supported locales are aggregated from the store's markets — the legacy Store#supported_locales column is only used when a store has no markets (rare; a default market is auto-created). Add the locale to a market:
# Stores with markets (the default since Spree 5.4) derive locales from their markets:
store.default_market.update!(supported_locales: ['en', 'es', 'fr', 'de'])
# Stores without markets fall back to the legacy store-level column:
store.update!(supported_locales: 'en,es,fr,de')
Switchers should read store.supported_locales_list (markets' locales + the store default locale). Headless storefronts fetch it via GET /api/v3/store/locales (client.locales.list() in @spree/sdk). If your locale isn't on any market, it's hidden even when present in I18n.available_locales.
"Translations admin is missing for new content"
The Rails admin already ships a centralized Product Translations page: an overview grid with per-locale coverage stats at /admin/product_translations, plus bulk CSV export/import via Spree::Exports::ProductTranslations / Spree::Imports::ProductTranslations. Per-field editing for other translatable models (Spree.translatable_resources: OptionType, OptionValue, Product, Taxon, Taxonomy, Store, Policy) lives on each record's own translations page (/admin/translations/:resource_type/:id/edit). The plan in docs/plans/5.4-centralized-translations-admin.md is still marked Draft, but its core scope — the product overview grid + CSV bulk operations — has already landed; only extensions beyond products remain open.
Where to read further
- Mobility gem docs: https://github.com/shioyama/mobility — backends, fallbacks, dirty tracking.
- Spree docs:
node_modules/@spree/docs/dist/developer/core-concepts/translations.md(resource + UI translations);node_modules/@spree/docs/dist/developer/core-concepts/markets.mdfor locale/currency configuration per market. spree_i18ngem: https://github.com/spree-contrib/spree_i18n — community translations.- Plan files (monorepo):
docs/plans/5.4-centralized-translations-admin.md,docs/plans/5.4-metafield-translations.md.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: spree
- Source: spree/agent-skills
- License: MIT
- Homepage: https://spreecommerce.org/docs/developer/agentic/overview
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.