spree-i18n について
spree-i18nは、Spreeのローカライズを解説する開発者向けガイドです。マーチャントコンテンツ、バックエンド文字列、Reactダッシュボードを扱い、ロケール解決、翻訳API、独自モデル、RTL対応を説明します。
主な機能
- 01Mobilityによるマーチャントコンテンツ翻訳
- 02ロケール選択とフォールバック動作
- 03Admin APIの翻訳取得と一括書き込み
- 04Rails I18n文字列とコミュニティ翻訳
- 05ダッシュボードのi18nextバンドル
- 06独自の翻訳可能モデルとRTL対応
活用シーン
- 01マーケット別の商品・カテゴリ情報のローカライズ
- 02Admin APIを使った翻訳同期
- 03独自モデルへの翻訳可能フィールド追加
- 04ロケールのフォールバックとストアフロント言語設定の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-i18nインストール後、Claude Code で/spree-i18nを入力して読み込まれたか確認します。
詳細
Spree I18n + 翻訳
翻訳対象は3つに分かれています。
| 対象 | 仕組み | 保存場所 |
|---|---|---|
| マーチャントのコンテンツ(商品名、説明、スラッグ、カテゴリー名など) | 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 が含まれています。プラグインは独自のロケールを登録します |
ストアで提供するロケールは、マーケット(各マーケットの default_locale + supported_locales)で決まります。spree-data-model/マーケットのドキュメントを参照してください。
ロケールの解決(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)。ストアフロントの言語切り替え UI は 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 で翻訳されません。レコードごとに値は1つです。
読み込み時の解決方法
すべてのモデルで 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/ダッシュボードでの翻訳
マーチャントはダッシュボードで翻訳します(レコードごとのロケール切り替え機能と、翻訳状況を確認できる一元管理の「Translations」ページを利用できます)。同じ機能をインテグレーション(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 の管理シリアライザーにSpree::Api::V3::Admin::Translatableを含めます。ネストされたGET …/:id/translationsの読み取りエンドポイントには独自のルートが必要です(コアでは、コアリソースに:translatableルートのコンサーンをマウントして実装しています)。
コアモデルにフィールドを追加する場合:その翻訳テーブル(およびベーステーブル)にカラムを追加し、デコレーターで凍結された定数を再定義して、新しいフィールドに対して 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 を使用しません。文字列は admin. プレフィックスを付けて、i18next の translation 名前空間に配置します。プラグインはバンドルを同梱します:
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