国際化
Shopify は175以上の国でコマースを支え、20以上の言語をネイティブにサポートしています。アプリ開発者として、国際化(i18n)はあれば良いというものではありません -- グローバルなマーチャントベースにリーチするために不可欠です。このモジュールでは、Shopify Markets 設定、Translation API、多通貨価格設定、税金と関税の計算、ロケール対応テーマエクステンションの構築をカバーします。
Shopify Markets 設定
Shopify Markets は国際販売を管理する集中管理システムです。国際的に販売するマーチャントに対してアプリが正しく動作するには、Markets 対応である必要があります。
Markets アーキテクチャの理解
Shopify ストアは複数の Markets を持つことができ、それぞれが地理的地域を表し、独自の設定を持ちます:
- 通貨 -- 価格の表示通貨
- 言語 -- ストアフロントの言語
- ドメイン -- 地域のサブドメインまたはトップレベルドメイン
- 価格設定 -- 調整された価格(パーセンテージまたは固定調整)
- 関税と税金 -- チェックアウト時に関税を徴収するかどうか
GraphQL による Markets のクエリ
query GetMarkets {
markets(first: 20) {
edges {
node {
id
name
enabled
primary
regions(first: 50) {
edges {
node {
... on MarketRegionCountry {
code
name
currency {
currencyCode
currencyName
}
}
}
}
}
webPresence {
domain {
host
}
defaultLocale
alternateLocales
rootUrls {
locale
url
}
}
}
}
}
}
Markets 対応アプリロジック
アプリが注文を処理したり商品情報を表示したりする際は、常にマーケットコンテキストを考慮してください:
// services/markets-service.js
export async function getMarketForRequest(client, request) {
const countryCode = request.headers['x-shopify-country'] || 'US';
const locale = request.headers['accept-language']?.split(',')[0] || 'en';
const response = await client.query({
data: {
query: `query MarketByGeography($countryCode: CountryCode!) {
marketByGeography(countryCode: $countryCode) {
id
name
currencySettings {
baseCurrency { currencyCode }
localCurrencies
}
}
}`,
variables: { countryCode },
},
});
return response.body.data.marketByGeography;
}
Storefront API クエリを行う際は、常に @inContext ディレクティブを使用して、価格、在庫状況、翻訳が正しいマーケットコンテキストを反映するようにしてください。このディレクティブがないと、API はプライマリマーケットのデータのみを返します。
query ProductInContext @inContext(country: CA, language: FR) {
product(handle: "winter-jacket") {
title
description
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
}
Translation API
Shopify の Translation API(TranslatableResource GraphQL タイプ経由)は、ストア内の翻訳可能なリソースの翻訳を読み書きすることを可能にします。
翻訳の読み取り
// services/translation-service.js
export async function getProductTranslations(client, productId, locale) {
const response = await client.query({
data: {
query: `query GetTranslations($resourceId: ID!, $locale: String!) {
translatableResource(resourceId: $resourceId) {
resourceId
translatableContent {
key
value
digest
locale
}
translations(locale: $locale) {
key
value
outdated
}
}
}`,
variables: {
resourceId: productId,
locale,
},
},
});
return response.body.data.translatableResource;
}
翻訳の書き込み
export async function createTranslation(client, resourceId, translations) {
const response = await client.query({
data: {
query: `mutation CreateTranslation($id: ID!, $translations: [TranslationInput!]!) {
translationsRegister(resourceId: $id, translations: $translations) {
userErrors {
field
message
}
translations {
key
value
locale
}
}
}`,
variables: {
id: resourceId,
translations: translations.map((t) => ({
key: t.key,
value: t.value,
locale: t.locale,
translatableContentDigest: t.digest,
})),
},
},
});
const result = response.body.data.translationsRegister;
if (result.userErrors.length > 0) {
throw new Error(
`Translation errors: ${result.userErrors.map((e) => e.message).join(', ')}`
);
}
return result.translations;
}
一括翻訳ワークフロー
翻訳サービスを提供するアプリの場合、効率のためにバルクオペレーションを使用してください:
// services/bulk-translator.js
export async function bulkTranslateProducts(client, locale, translations) {
// Group translations by resource to minimize API calls
const byResource = translations.reduce((acc, t) => {
if (!acc[t.resourceId]) acc[t.resourceId] = [];
acc[t.resourceId].push(t);
return acc;
}, {});
const results = [];
// Process in batches of 10 to stay within rate limits
const resourceIds = Object.keys(byResource);
for (let i = 0; i < resourceIds.length; i += 10) {
const batch = resourceIds.slice(i, i + 10);
const batchResults = await Promise.all(
batch.map((resourceId) =>
createTranslation(client, resourceId, byResource[resourceId])
)
);
results.push(...batchResults);
// Respect rate limits
if (i + 10 < resourceIds.length) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
return results;
}
API 経由で登録された翻訳は、マーチャントが Shopify 管理画面で手動編集すると上書きされます。アプリは既存の翻訳の outdated フィールドを確認し、マーチャントの変更を上書きする前にユーザーに警告する必要があります。
多通貨サポート
複数通貨を正しく扱うことは極めて重要です。Shopify は Storefront API を通じて表示価格を、Markets を通じて管理価格ルールを提供します。
正しい価格フォーマット
// utils/currency-formatter.js
export function formatPrice(amount, currencyCode, locale = 'en-US') {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}
// Examples:
// formatPrice(29.99, 'USD', 'en-US') → "$29.99"
// formatPrice(29.99, 'EUR', 'de-DE') → "29,99 €"
// formatPrice(3299, 'JPY', 'ja-JP') → "¥3,299"
JPY(日本円)や KRW(韓国ウォン)などの通貨は小数点を使用しません。通貨フォーマットルールをハードコードせず、常に Intl.NumberFormat を使用してください。Shopify の @shopify/react-i18n パッケージは管理画面埋め込みアプリに対してこれを自動的に処理します。
アプリでの市場固有の価格設定
// services/pricing-service.js
export async function getContextualPricing(client, productId, market) {
const response = await client.query({
data: {
query: `query ContextualPricing($productId: ID!, $marketId: ID!) {
product(id: $productId) {
variants(first: 100) {
edges {
node {
id
title
contextualPricing(context: { marketId: $marketId }) {
price {
amount
currencyCode
}
compareAtPrice {
amount
currencyCode
}
}
}
}
}
}
}`,
variables: { productId, marketId: market.id },
},
});
return response.body.data.product.variants.edges.map((edge) => ({
variantId: edge.node.id,
title: edge.node.title,
price: edge.node.contextualPricing.price,
compareAtPrice: edge.node.contextualPricing.compareAtPrice,
}));
}
税金と関税の計算
国際販売には正確な税金と関税の計算が必要です。Shopify はその関税・輸入税機能を通じてこれを処理しますが、アプリはこれが注文合計や商品価格にどのように影響するかを認識する必要があります。
関税情報のクエリ
query OrderDuties($orderId: ID!) {
order(id: $orderId) {
totalDutiesSet {
shopMoney {
amount
currencyCode
}
presentmentMoney {
amount
currencyCode
}
}
lineItems(first: 50) {
edges {
node {
title
duties {
id
price {
shopMoney { amount currencyCode }
presentmentMoney { amount currencyCode }
}
harmonizedSystemCode
countryCodeOfOrigin
}
}
}
}
}
}
HS コード管理
アプリが商品を管理する場合、正確な関税計算のために HS(統一商品分類制度)コードの設定をマーチャントに支援してください:
export async function updateHSCodes(client, variantId, hsCode, countryOfOrigin) {
const response = await client.query({
data: {
query: `mutation UpdateHSCode($input: InventoryItemUpdateInput!) {
inventoryItemUpdate(id: $input.id, input: $input) {
inventoryItem {
harmonizedSystemCode
countryCodeOfOrigin
}
userErrors {
field
message
}
}
}`,
variables: {
input: {
harmonizedSystemCode: hsCode,
countryCodeOfOrigin: countryOfOrigin,
},
},
},
});
return response.body.data.inventoryItemUpdate;
}
マーチャントが Markets 設定で「関税と輸入税」を有効にすると、Shopify はチェックアウト時に自動的に関税を計算します。アプリが手動で関税を計算する必要はありませんが、注文合計を表示したりレポートを生成したりする際に関税額を考慮する必要があります。
ロケール対応 Liquid テンプレート
アプリにテーマアプリエクステンションが含まれている場合や Liquid スニペットを注入する場合、ストアに設定された言語をサポートする必要があります。
Shopify の翻訳フィルターの使用
{% comment %}
Theme app extension: Product recommendation widget
This template automatically uses the store's active locale.
{% endcomment %}
<div class="app-recommendations" data-section-id="{{ section.id }}">
<h2>{{ 'apps.recommendations.heading' | t }}</h2>
{% for product in recommendations %}
<div class="recommendation-card">
<img
src="{{ product.featured_image | image_url: width: 300 }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
width="300"
height="300"
/>
<h3>{{ product.title }}</h3>
<p class="price">{{ product.price | money_with_currency }}</p>
<a href="{{ product.url }}" class="btn">
{{ 'apps.recommendations.view_product' | t }}
</a>
</div>
{% endfor %}
{% if recommendations.size == 0 %}
<p>{{ 'apps.recommendations.empty' | t }}</p>
{% endif %}
</div>
翻訳ファイルの提供
テーマアプリエクステンションにロケールファイルを含めてください:
// extensions/theme-block/locales/en.default.json
{
"apps": {
"recommendations": {
"heading": "You might also like",
"view_product": "View product",
"empty": "No recommendations available",
"add_to_cart": "Add to cart",
"out_of_stock": "Out of stock"
}
}
}
// extensions/theme-block/locales/fr.json
{
"apps": {
"recommendations": {
"heading": "Vous aimerez peut-etre aussi",
"view_product": "Voir le produit",
"empty": "Aucune recommandation disponible",
"add_to_cart": "Ajouter au panier",
"out_of_stock": "Rupture de stock"
}
}
}
// extensions/theme-block/locales/de.json
{
"apps": {
"recommendations": {
"heading": "Das konnte Ihnen auch gefallen",
"view_product": "Produkt ansehen",
"empty": "Keine Empfehlungen verfugbar",
"add_to_cart": "In den Warenkorb",
"out_of_stock": "Nicht auf Lager"
}
}
}
RTL 言語サポート
アラビア語やヘブライ語のような言語向けに、テーマエクステンションが右から左(RTL)レイアウトをサポートするようにしてください:
{% assign is_rtl = false %}
{% if request.locale.iso_code == 'ar' or request.locale.iso_code == 'he' %}
{% assign is_rtl = true %}
{% endif %}
<div class="app-widget" dir="{{ is_rtl | if: 'rtl', 'ltr' }}">
{% comment %} Widget content {% endcomment %}
</div>
/* Use logical properties for automatic RTL support */
.recommendation-card {
margin-inline-start: 1rem;
padding-inline-end: 0.5rem;
text-align: start;
border-inline-start: 3px solid var(--color-primary);
}
テーマエクステンションでは常に CSS 論理プロパティ(margin-left の代わりに margin-inline-start、padding-bottom の代わりに padding-block-end)を使用してください。これにより、別のスタイルシートなしで LTR と RTL の両方の言語で正しいレイアウトが確保されます。
国際化 Checklist
| エリア | 要件 | メモ |
|---|---|---|
| Markets | Query market context for all pricing | Use @inContext directive |
| Currency | Use Intl.NumberFormat for formatting | Never hardcode currency symbols |
| Translations | Provide locale files for theme extensions | At minimum: en, fr, de, es, pt, ja |
| Duties | Account for duty amounts in order totals | Do not calculate manually |
| RTL | Use CSS logical properties | Test with Arabic locale |
| Date/Time | Use Intl.DateTimeFormat | Respect merchant timezone |
| Numbers | Use Intl.NumberFormat | Decimal separators vary by locale |
真にグローバルな Shopify アプリを構築するには、すべてのユーザー向けの面で細部への注意が必要です。投資は大きなリターンをもたらします -- 国際的に販売するマーチャントは Shopify エコシステムで最も急速に成長しているセグメントであり、そのニーズをサポートするアプリは大きな競争優位性を持ちます。