关于 spree-i18n
spree-i18n 是介绍 Spree 本地化的开发指南,覆盖商家内容、后端字符串和 React 仪表板。指南说明语言解析、翻译存储与 API、自定义可翻译模型及 RTL 支持。
主要功能
- 01使用 Mobility 翻译商家内容
- 02语言选择与回退行为
- 03Admin API 翻译读取和批量写入
- 04Rails I18n 字符串与社区翻译
- 05仪表板 i18next 语言包
- 06自定义可翻译模型与 RTL 支持
适用场景
- 01按市场本地化商品和分类内容
- 02通过 Admin API 同步翻译
- 03为自定义模型添加可翻译字段
- 04排查语言回退和店面语言选项
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-i18n安装后在 Claude Code 中输入/spree-i18n确认已加载。
详细介绍
Spree I18n + 翻译
三个独立的翻译层:
| 内容 | 机制 | 所在位置 |
|---|---|---|
| 商家内容(商品名称、描述、别名、分类名称……) | Mobility 翻译表 | spree_<model>_translations 表;在仪表板中编辑,或通过 Admin API 编辑 |
| 后端字符串(邮件、验证错误、API 错误消息) | Rails I18n + Spree.t | config/locales/<locale>.yml;社区提供的 spree_i18n gem |
| 仪表板 UI(React 管理界面) | i18next JSON 包 | @spree/dashboard 内置 ar de en es fr pl zh-CN;插件注册自己的语言包 |
商店提供哪些语言由其 Markets 决定(每个市场都有 default_locale + supported_locales)——参见 spree-data-model / markets 文档。
语言区域解析(Store API)
每个请求都会由 Spree::Api::V3::LocaleAndCurrency 执行以下操作:
- 根据
X-Spree-Country解析市场。 - 语言区域 =
X-Spree-Locale请求头 →?locale=参数(仅当值位于current_store.supported_locales_list中时才会采用)→Spree::Current.locale(市场默认值 → 商店默认值)。不支持的值会静默回退。 - 设置
I18n.locale、Spree::Current.locale和Spree::Current.content_locale = current_store.default_locale。 - 通过
Spree::Locales::SetFallbackLocaleForStore配置 Mobility 回退——所有语言区域都会回退到商店的默认语言区域。
const client = createClient({ baseUrl, publishableKey, locale: 'fr' })
client.setLocale('de')
await client.products.get('sac-spree', {}, { locale: 'fr' }) // per-request override
传入商店确切支持的代码(例如 es;除非已配置,否则不要传 es-ES)。店面语言切换器会读取 GET /api/v3/store/locales。
缓存:访客的 Store API 响应会设置 Vary: Accept, x-spree-currency, x-spree-locale, x-spree-channel;已验证身份的响应为 private, no-store。Vary 列表不包含 x-spree-country,但国家/地区会选择市场(语言区域/货币回退、市场范围内的价格)——如果你的店面发送了该请求头,请自行将国家/地区加入 CDN 缓存键。手动实现的缓存(Next.js fetch 缓存、Redis)必须将语言区域、货币、渠道和国家/地区作为键。
数据翻译——Mobility
可翻译模型会声明 TRANSLATABLE_FIELDS 并包含 Spree::TranslatableResource:
| 模型 | 字段 |
|---|---|
Spree::Product | name、description、slug、meta_title、meta_description |
Spree::Category | name、pretty_name、description、permalink |
Spree::Collection | name、description、permalink |
Spree::ProductType | name |
Spree::OptionType、Spree::OptionValue | label |
Spree::Store | name、seo_title、meta_description、meta_keywords、支持联系字段 |
Spree::Policy | name、body |
Spree::Seller | name、about |
注册表为 Spree.translatable_resources(用于驱动 Admin API 翻译端点和仪表板翻译编辑器)。RICH_TEXT_TRANSLATABLE_FIELDS 用于标记 HTML 字段,以便仪表板显示富文本编辑器。
自定义字段值(Spree::CustomField)不使用 Mobility 翻译——每条记录只有一个值。
读取解析方式
每个模型都使用 translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback):
- 模型表上的基础列存储商店默认语言区域(
Spree::Current.content_locale)的内容。在该语言区域读取/写入时会访问基础列。 - 任何其他语言区域都会读取翻译表;缺失的值会通过上述商店回退设置回退到默认语言区域 → 基础列。部分翻译的商品目录仍可正常呈现。
Spree::Config[:always_use_translations] = true(环境变量SPREE_ALWAYS_USE_TRANSLATIONS)会禁用列回退:包括默认语言区域在内的所有语言区域都会通过翻译表处理。除非有特定原因,否则应保持为false。
Mobility.with_locale(:es) { product.name } # translated or fallback
product.name(locale: :es, fallback: false) # nil if no Spanish row — detect gaps
在请求之外(控制台、作业、rake)不会配置商店回退,且 content_locale 未设置。请先设置上下文:
Spree::Current.store = store
Spree::Current.content_locale = store.default_locale
Spree::Locales::SetFallbackLocaleForStore.new.call(store: store)
在 Ruby 中编写翻译
product.upsert_translations(
'es' => { 'name' => 'Camiseta', 'description' => '<p>Una camiseta</p>' },
'fr' => { 'name' => 'T-shirt' }
)
upsert_translations 的语义:缺少区域设置或字段 → 不做更改;"" → 空字符串(读取时回退);nil → 删除该单元格。记录存储的 supported_locales_list 中没有的区域设置会引发 ActiveRecord::RecordInvalid。也可以直接写入:Mobility.with_locale(:es) { product.update!(name: 'Camiseta') }。
已翻译的别名
Product 的 slug 和 Category/Collection 的 permalink 按区域设置区分——法语中为 /produits/sac-spree。别名在同一区域设置内必须唯一,但可以在不同区域设置间重复。Store API 会在请求区域设置中查找别名,并回退到默认区域设置的别名。旧别名不会自动解析(404)——请在店面中重定向。
通过 Admin API / 仪表板翻译
商家可在仪表板中翻译(每条记录都有区域设置切换器,另有显示覆盖率的集中式「翻译」页面)。集成也可使用相同接口(TMS 同步、AI 翻译任务):
| 端点 | SDK(@spree/admin-sdk) | 用途 |
|---|---|---|
GET /api/v3/admin/translatable_resources | client.translatableResources.list() | 注册表:[{ resource_type, fields: [{ key, type }] }] |
GET /api/v3/admin/locales | — | 商店支持的区域设置(code、name、default、rtl) |
GET /api/v3/admin/products/:id/translations | client.products.translations.get(id) | 矩阵:源值 + 各区域设置的翻译(+ 嵌套子项,例如选项值) |
GET /api/v3/admin/<resource>/:id?expand=translations | — | 在记录中内联显示翻译矩阵 |
POST /api/v3/admin/translations/batch | client.translations.batch(entries) | 写入接口——跨记录/类型原子执行 |
GET /api/v3/admin/translations?resource_type=product | client.translations.coverage('product') | 各区域设置的覆盖率网格 |
await admin.translations.batch([
{ resource_type: 'product', resource_id: 'prod_86Rf07xd4z',
values: { fr: { name: 'Sac Spree' }, de: { name: 'Spree Tasche' } } },
{ resource_type: 'option_value', resource_id: 'optval_…', values: { de: { label: 'Klein' } } },
])
resource_type 标记是带下划线的模型名称(product、option_type、option_value、category、collection、product_type、store、policy、seller)。要么所有条目都成功,要么一个也不成功。密钥需要对批次中的每种资源类型都具备 write_<resource> 权限。翻译不会通过 PATCH /products/:id 写入。
批量 CSV:Spree::Exports::ProductTranslations / Spree::Imports::ProductTranslations(仪表板导入/导出;参见 spree-reporting)。
让自己的模型支持翻译
-
模型:
module Spree class Brand < Spree.base_class include Spree::TranslatableResource TRANSLATABLE_FIELDS = %i[name description].freeze RICH_TEXT_TRANSLATABLE_FIELDS = %i[description].freeze translates(*TRANSLATABLE_FIELDS, column_fallback: Spree.mobility_column_fallback) end end -
为
spree_brand_translations创建迁移(包含spree_brand_id、locale、各字段和时间戳;在[spree_brand_id, locale]上建立唯一索引)。保留基础列——它们保存默认区域设置的内容。 -
注册模型(在核心配置加载后,通过初始化程序完成):
Rails.application.config.after_initialize do Spree.translatable_resources += [Spree::Brand] end这样即可通过
POST /translations/batch写入(resource_type: 'brand')。如需通过?expand=translations获取翻译,请在 Brand 的 Admin 序列化器中包含Spree::Api::V3::Admin::Translatable。嵌套的GET …/:id/translations读取端点需要单独的路由(核心资源通过:translatable路由 concern 挂载该端点)。
为核心模型添加字段:将列添加到其翻译表(和基础表),然后在装饰器中重新定义已冻结的常量,并对新字段调用 translates(切勿使用 TRANSLATABLE_FIELDS << :x——这会引发 FrozenError):
module Spree::ProductDecorator
def self.prepended(base)
fields = base::TRANSLATABLE_FIELDS + [:subtitle]
base.send(:remove_const, :TRANSLATABLE_FIELDS)
base.const_set(:TRANSLATABLE_FIELDS, fields.freeze)
base.translates :subtitle, column_fallback: Spree.mobility_column_fallback
end
Spree::Product.prepend self
end
按常规在序列化器和允许的属性中公开该字段。
后端字符串 — Spree.t 和 YAML
Spree.t(:save) # spree.save
Spree.t(:paid, scope: 'payment_states')
Spree.t('custom_feature.title', default: 'Rewards')
- 添加社区翻译:
spree bundle add spree_i18n(提供 40 多种语言,会自动加载)。 - 在应用的
config/locales/<locale>.yml中,通过<locale>.spree.*覆盖或添加键 — 应用的文件最后加载。 - 在
config/application.rb中设置config.i18n.fallbacks = [:en],启用 Rails 回退(在初始化程序中设置I18n.fallbacks不会启用后端模块)。通常,Production.rb 中会有config.i18n.fallbacks = true。 Spree::BaseMailer配置商店的 Mobility 回退,因此邮件中的已翻译数据会像 API 中一样解析。- 使用
i18n-tasks(normalize、missing、unused)保持 YAML 整洁。 - 你注册的作用域对应的权限目录标签位于
spree.permissions_catalog.resources.<name>下。
仪表板字符串 — i18next
React 仪表板不使用 Rails YAML。字符串位于 i18next 的 translation 命名空间中,并带有 admin. 前缀。插件会随附语言包:
import en from './locales/en.json'
import de from './locales/de.json'
export default defineDashboardPlugin({
locales: { en, de }, // registered before the rest of the config
nav: [{ key: 'brands', label: i18n.t('admin.brands.title'), path: '/brands' }],
})
约定:admin.fields.<resource>.<attribute>.label;后端 gem 注册的类型名称(促销规则、计算器、集成等)位于 admin.types.<family>.<code> 下,这样仪表板就不会回退到商店语言环境中的 Ruby 标签。参见 spree-dashboard-plugins。
从右向左书写(RTL)
Spree::Locale::RTL_LANGUAGE_CODES = ar he fa ur yi;Spree::Locale.new(code:).rtl? / .direction。GET /api/v3/admin/locales 会返回 rtl。店面负责设置自己的 dir="rtl"(在 Next.js 布局中根据当前语言环境设置)。仪表板随附阿拉伯语语言包。
常见问题
- “translation missing: es.spree.… ” — 在应用的
config/locales/es.yml中添加键、安装spree_i18n,或启用config.i18n.fallbacks。 - 西班牙语请求仍返回英语 —
es是否在某个市场的supported_locales中?(不支持的标头值会被忽略。)翻译行是否存在(product.name(locale: :es, fallback: false))?缓存键是否未包含语言环境? - 店面语言切换器中缺少语言环境 —
store.supported_locales_list汇总各市场的语言环境;添加它:market.update!(supported_locales: %w[en es fr])。仅设置I18n.available_locales不会让它显示出来。 - 批量写入返回 422“Unsupported locale(s)” — 该语言环境不在记录所属商店支持的语言环境中。
- 控制台为每种语言环境都显示默认语言环境的值 — 未设置商店回退或内容语言环境;参见“读取时的解析方式”。
- 同一进程中写入后仍显示旧值 — 执行
product.reload。
延伸阅读
node_modules/@spree/docs/dist/developer/core-concepts/translations.mdnode_modules/@spree/docs/dist/developer/core-concepts/slugs.mdnode_modules/@spree/docs/dist/developer/core-concepts/markets.mdnode_modules/@spree/docs/dist/developer/dashboard/customization/translations.md- Mobility:https://github.com/shioyama/mobility · spree_i18n:https://github.com/spree-contrib/spree_i18n