Skip to main content

國際化

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;
}
tip

在進行 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;
}
warning

透過 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"
info

某些貨幣如 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;
}
tip

當商家在 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);
}
warning

在您的佈景主題擴充功能中務必使用 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 生態系統中成長最快的群體,而支援其需求的應用程式擁有重大的競爭優勢。