Internationalization
Shopify powers commerce in over 175 countries and supports 20+ languages natively. As an app developer, internationalization (i18n) is not a nice-to-have -- it is essential for reaching the global merchant base. This module covers Shopify Markets configuration, the Translation API, multi-currency pricing, tax and duty calculations, and building locale-aware theme extensions.
Shopify Markets Configuration
Shopify Markets is the centralized system for managing international selling. Your app must be Markets-aware to work correctly for merchants who sell internationally.
Understanding Markets Architecture
A Shopify store can have multiple Markets, each representing a geographic region with its own:
- Currency -- The presentment currency for prices
- Language -- The storefront language
- Domain -- Subdomain or top-level domain for the region
- Pricing -- Adjusted prices (percentage or fixed adjustments)
- Duties and taxes -- Whether duties are collected at checkout
Querying Markets via GraphQL
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-Aware App Logic
When your app processes orders or displays product information, always consider the market context:
// 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;
}
Always use the @inContext directive when making Storefront API queries to ensure prices, availability, and translations reflect the correct market context. Without this directive, the API returns data for the primary market only.
query ProductInContext @inContext(country: CA, language: FR) {
product(handle: "winter-jacket") {
title
description
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
}
Translation API
Shopify's Translation API (via the TranslatableResource GraphQL type) allows your app to read and write translations for any translatable resource in a store.
Reading Translations
// 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;
}
Writing Translations
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;
}
Bulk Translation Workflow
For apps that provide translation services, use bulk operations for efficiency:
// 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;
}
Translations registered via the API will be overwritten if a merchant manually edits them in the Shopify admin. Your app should check the outdated field on existing translations and warn users before overwriting merchant changes.
Multi-Currency Support
Handling multiple currencies correctly is critical. Shopify provides presentment prices through the Storefront API and admin price rules through Markets.
Formatting Prices Correctly
// 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"
Some currencies like JPY (Japanese Yen) and KRW (Korean Won) do not use decimal places. Always use Intl.NumberFormat rather than hardcoding currency formatting rules. Shopify's @shopify/react-i18n package handles this automatically for admin-embedded apps.
Market-Specific Pricing in Your App
// 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,
}));
}
Tax and Duty Calculation
International sales require accurate tax and duty calculations. Shopify handles this through its Duties and Import Tax features, but your app needs to be aware of how this affects order totals and product pricing.
Querying Duty Information
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 Code Management
If your app manages products, help merchants set Harmonized System (HS) codes for accurate duty calculation:
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;
}
Shopify automatically calculates duties at checkout when the merchant enables "Duties and import taxes" in their Markets settings. Your app does not need to calculate duties manually -- but you do need to account for duty amounts when displaying order totals or generating reports.
Locale-Aware Liquid Templates
If your app includes theme app extensions or injects Liquid snippets, they must support the store's configured languages.
Using Shopify's Translation Filters
{% 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>
Providing Translation Files
Include locale files in your theme app extension:
// 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 Language Support
For languages like Arabic and Hebrew, ensure your theme extensions support right-to-left layouts:
{% 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);
}
Always use CSS logical properties (margin-inline-start instead of margin-left, padding-block-end instead of padding-bottom) in your theme extensions. This ensures correct layout in both LTR and RTL languages without separate stylesheets.
Internationalization Checklist
| Area | Requirement | Notes |
|---|---|---|
| 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 |
Building a truly international Shopify app requires attention to detail across every user-facing surface. The investment pays off significantly -- merchants who sell internationally represent the fastest-growing segment of the Shopify ecosystem, and apps that support their needs have a major competitive advantage.