國際化
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 語言支援
對於阿拉伯語和希伯來語等語言,確保您的佈景主題擴充功能支援從右到左的版面:
{% 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-inline-start 而非 margin-left,使用 padding-block-end 而非 padding-bottom)。這確保了 LTR 和 RTL 語言中正確的版面,而無需單獨的樣式表。
國際化 Checklist
| 領域 | 要求 | 注意事項 |
|---|---|---|
| Markets | 查詢所有定價的市場情境 | 使用 @inContext 指令 |
| 貨幣 | 使用 Intl.NumberFormat 進行格式化 | 絕不硬編碼貨幣符號 |
| 翻譯 | 為佈景主題擴充功能提供語言環境檔案 | 至少包含:en、fr、de、es、pt、ja |
| 關稅 | 在訂單總額中計入關稅金額 | 不要手動計算 |
| RTL | 使用 CSS 邏輯屬性 | 使用阿拉伯語語言環境進行測試 |
| 日期/時間 | 使用 Intl.DateTimeFormat | 尊重商家的時區 |
| 數字 | 使用 Intl.NumberFormat | 小數分隔符因語言環境而異 |
建構真正國際化的 Shopify 應用程式需要在每個面向使用者的介面上注意細節。這項投資的回報非常顯著——從事國際銷售的商家代表了 Shopify 生態系統中成長最快的群體,而支援其需求的應用程式擁有重大的競爭優勢。