mcpField
S
spree-i18n

spree-i18n

github.com/spree开发10224 次安装4 周前更新
代码仓库

关于 spree-i18n

spree-i18n 是介绍 Spree 本地化的开发指南,覆盖商家内容、后端字符串和 React 仪表板。指南说明语言解析、翻译存储与 API、自定义可翻译模型及 RTL 支持。

主要功能

  1. 01使用 Mobility 翻译商家内容
  2. 02语言选择与回退行为
  3. 03Admin API 翻译读取和批量写入
  4. 04Rails I18n 字符串与社区翻译
  5. 05仪表板 i18next 语言包
  6. 06自定义可翻译模型与 RTL 支持

适用场景

  1. 01按市场本地化商品和分类内容
  2. 02通过 Admin API 同步翻译
  3. 03为自定义模型添加可翻译字段
  4. 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.tconfig/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 执行以下操作:

  1. 根据 X-Spree-Country 解析市场。
  2. 语言区域 = X-Spree-Locale 请求头 → ?locale= 参数(仅当值位于 current_store.supported_locales_list 中时才会采用)→ Spree::Current.locale(市场默认值 → 商店默认值)。不支持的值会静默回退。
  3. 设置 I18n.locale、Spree::Current.locale 和 Spree::Current.content_locale = current_store.default_locale。
  4. 通过 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::Productname、description、slug、meta_title、meta_description
Spree::Categoryname、pretty_name、description、permalink
Spree::Collectionname、description、permalink
Spree::ProductTypename
Spree::OptionType、Spree::OptionValuelabel
Spree::Storename、seo_title、meta_description、meta_keywords、支持联系字段
Spree::Policyname、body
Spree::Sellername、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_resourcesclient.translatableResources.list()注册表:[{ resource_type, fields: [{ key, type }] }]
GET /api/v3/admin/locales—商店支持的区域设置(code、name、default、rtl)
GET /api/v3/admin/products/:id/translationsclient.products.translations.get(id)矩阵:源值 + 各区域设置的翻译(+ 嵌套子项,例如选项值)
GET /api/v3/admin/<resource>/:id?expand=translations—在记录中内联显示翻译矩阵
POST /api/v3/admin/translations/batchclient.translations.batch(entries)写入接口——跨记录/类型原子执行
GET /api/v3/admin/translations?resource_type=productclient.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)。

让自己的模型支持翻译

  1. 模型:

    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
    
  2. 为 spree_brand_translations 创建迁移(包含 spree_brand_id、locale、各字段和时间戳;在 [spree_brand_id, locale] 上建立唯一索引)。保留基础列——它们保存默认区域设置的内容。

  3. 注册模型(在核心配置加载后,通过初始化程序完成):

    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。

延伸阅读

评论

支持 Markdown