mcpField
S
spree-i18n

spree-i18n

github.com/spree開発10224 回インストール4 週間前に更新
リポジトリ

spree-i18n について

spree-i18nは、Spreeのローカライズを解説する開発者向けガイドです。マーチャントコンテンツ、バックエンド文字列、Reactダッシュボードを扱い、ロケール解決、翻訳API、独自モデル、RTL対応を説明します。

主な機能

  1. 01Mobilityによるマーチャントコンテンツ翻訳
  2. 02ロケール選択とフォールバック動作
  3. 03Admin APIの翻訳取得と一括書き込み
  4. 04Rails I18n文字列とコミュニティ翻訳
  5. 05ダッシュボードのi18nextバンドル
  6. 06独自の翻訳可能モデルとRTL対応

活用シーン

  1. 01マーケット別の商品・カテゴリ情報のローカライズ
  2. 02Admin APIを使った翻訳同期
  3. 03独自モデルへの翻訳可能フィールド追加
  4. 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.tconfig/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 は次の処理を行います。

  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)。ストアフロントの言語切り替え 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::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 で翻訳されません。レコードごとに値は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_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 の管理シリアライザーに 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 を実行してください。

詳細情報

コメント