国际化
Shopify 为超过 175 个国家提供商业服务,并原生支持 20 多种语言。作为应用开发者,国际化(i18n)不是锦上添花——它是触达全球商家群体的关键。本模块涵盖 Shopify Markets 配置、Translation API、多币种定价、税费和关税计算,以及构建区域感知的主题扩展。
Shopify Markets 配置
Shopify Markets 是管理国际销售的集中系统。你的应用必须支持 Markets 才能正确服务于进行国际销售的商家。
理解 Markets 架构
一个 Shopify 商店可以拥有多个 Market,每个代表一个地理区域,具有自己的:
- 货币 -- 展示价格的货币
- 语言 -- 店面语言
- 域名 -- 该区域的子域名或顶级域名
- 定价 -- 调整后的价格(百分比或固定调整)
- 关税和税费 -- 是否在结账时收取关税
通过 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 感知的应用逻辑
当你的应用处理订单或显示产品信息时,始终考虑 Market 上下文:
// 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 指令以确保价格、可用性和翻译反映正确的 Market 上下文。如果没有此指令,API 只返回主 Market 的数据。
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 包会自动为嵌入管理后台的应用处理此问题。
应用中的特定 Market 定价
// 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 语言中都能正确布局,无需单独的样式表。
国际化检查清单
| 领域 | 要求 | 备注 |
|---|---|---|
| Markets | 为所有定价查询 Market 上下文 | 使用 @inContext 指令 |
| 货币 | 使用 Intl.NumberFormat 进行格式化 | 永远不要硬编码货币符号 |
| 翻译 | 为主题扩展提供语言文件 | 至少包括:en、fr、de、es、pt、ja |
| 关税 | 在订单总额中考虑关税金额 | 不要手动计算 |
| RTL | 使用 CSS 逻辑属性 | 使用阿拉伯语语言环境测试 |
| 日期/时间 | 使用 Intl.DateTimeFormat | 尊重商家时区 |
| 数字 | 使用 Intl.NumberFormat | 小数分隔符因区域而异 |
构建真正国际化的 Shopify 应用需要关注每个面向用户的界面的细节。这项投资会带来显著回报——进行国际销售的商家是 Shopify 生态系统中增长最快的群体,支持他们需求的应用具有重大竞争优势。