Shopify Markets
Add multi-locale and multi-currency support with Shopify Markets.
How to use
Enable Shopify Markets (Multi-Locale)
Description
Interactively set up Shopify Markets with multi-locale routing using next-intl. Supports both sub-path routing (/en/products/...) and per-domain routing (en.store.com/products/...). This skill supersedes add-locale-url-prefix.md as the recommended way to enable multi-locale support.
When to Use This Skill
- When the user wants to enable Shopify Markets / multi-locale support
- When the user wants to add internationalization (i18n) with locale-prefixed URLs
- When invoked via
/vercel-shop:enable-shopify-markets
Prerequisites
- The storefront is running in single-locale mode (default state)
next-intlis installed (it is by default)- Shopify Markets are configured in the Shopify admin for the desired locales
Step 1: Gather User Preferences
If the user hasn't already specified their preferences, ask them. Use two rounds of questions.
Round 1 — Strategy and Locales
Ask the user the following questions (use AskUserQuestion if available, otherwise ask directly):
If the user selects "Add other locales" or provides custom locales via free-form input, ask a follow-up question to get the exact locale codes they want. Any locale in BCP 47 format (e.g., ja-JP, pt-BR, zh-CN, it-IT, ko-KR, ar-SA) is supported — translation files and currency config will be generated for them.
Round 2 — Strategy-Specific Options
If sub-path routing was chosen, ask:
If per-domain routing was chosen, ask:
The user can provide a custom mapping via the "Other" option. Each domain should map to one default locale.
Step 2: Update Locale Config
File: lib/i18n.ts
Enable selected locales
Change enabledLocales to include the user's chosen locales:
Add custom locales (if any)
If the user chose locales not in the existing locales array, add them:
Also add entries to the localeCurrency map:
Use Intl.NumberFormat to look up the correct currency symbol if unsure.
Step 3: Create Routing Config
File: lib/i18n/routing.ts (create new)
Sub-path routing
If the user chose short prefixes, use the object form:
Per-domain routing
Step 4: Create Navigation Exports
File: lib/i18n/navigation.ts (create new)
Step 5: Move Routes Under app/[locale]/
Move all page routes from app/ into app/[locale]/. Keep api/, robots.ts, sitemap.ts, favicon.ico, globals.css, and global-error.tsx at the root level.
Update all PageProps and LayoutProps type parameters to include [locale]:
LayoutProps<"/">→LayoutProps<"/[locale]">PageProps<"/products/[handle]">→PageProps<"/[locale]/products/[handle]">PageProps<"/products/[handle]/[variantId]">→PageProps<"/[locale]/products/[handle]/[variantId]">PageProps<"/collections/[handle]">→PageProps<"/[locale]/collections/[handle]">PageProps<"/search">→PageProps<"/[locale]/search">PageProps<"/pages/[slug]">→PageProps<"/[locale]/pages/[slug]">- ... and so on for all page components.
The globals.css import in app/[locale]/layout.tsx should be updated to import "../globals.css" since the CSS file stays at the app/ root.
Step 6: Update Root Layout
File: app/[locale]/layout.tsx
Add generateStaticParams:
The rest of the layout stays the same — it already uses getLocale(), getMessages(), and NextIntlClientProvider.
The layout-level generateStaticParams provides locale values for all child routes — no per-page changes needed for the locale param.
Step 7: Update Locale Resolution
lib/params.ts
Replace the current hardcoded implementation:
lib/i18n/request.ts
Update to resolve locale dynamically:
Scope menu queries for markets
The base template keeps lib/shopify/operations/menu.ts unscoped so menus load before Shopify Markets is configured. When enabling markets, update getMenu to derive country and language from the active locale and query menu with @inContext(country: $country, language: $language). Without that change, quick links and footer menu stay pinned to the default market. If the enable-shopify-menus skill has been run, the megamenu will also need this scoping.
Step 8: Update Middleware
File: proxy.ts
Add a proxy.ts with next-intl middleware for locale routing and variant ID rewrites:
Note: The built-in content negotiation rewrite in
next.config.tshandles markdown negotiation automatically — no proxy.ts changes needed.
Step 9: Replace next/link with Locale-Aware Link
In all files that import from next/link, replace with the locale-aware Link from @/lib/i18n/navigation. The following files need updating:
Change import Link from "next/link" to import { Link } from "@/lib/i18n/navigation". The locale-aware Link automatically prefixes URLs with the current locale. Its API is the same as next/link — no other changes needed in the JSX.
Step 10: Wire Locale/Currency Selector into Megamenu
Prerequisite: This step requires the
enable-shopify-menusskill to have been run first. If the megamenu has not been added, skip this step.
File: components/layout/nav/megamenu/index.tsx
The LocaleCurrencySelector component already exists at components/layout/nav/locale-currency.tsx. Add it to the megamenu:
Also update locale-currency.tsx to use locale-aware routing for locale switching. Replace useRouter from next/navigation with useRouter from @/lib/i18n/navigation, and change the handleLocaleChange function to navigate to the same path in the new locale:
Step 11: Update SEO with Locale Alternates
File: lib/seo.ts
Add a helper to build locale-prefixed paths and update buildAlternates to include hreflang alternates:
Step 12: Update Sitemap with Per-Locale URLs
File: app/sitemap.ts
Add per-locale URL generation. For each page entry, generate an entry for each enabled locale with alternates.languages:
Step 13: Add Locale-Prefixed Redirects
File: next.config.ts
Add locale-aware redirect rules for common typos:
Step 14: Generate Translation Files for Custom Locales
For each custom locale not already in lib/i18n/messages/, create a translation file:
- Copy
en.jsonas the starting point - Translate all string values to the target language
- Keep the same JSON structure and key names
Existing translation files:
en.json(English — also used for en-GB)de-DE.json(German)fr-FR.json(French)nl-NL.json(Dutch)es-ES.json(Spanish)
For new locales like ja-JP, create lib/i18n/messages/ja-JP.json with translated content.
CRITICAL: Validate JSON after generating translation files
Translated strings must not contain unescaped ASCII double-quote characters (", U+0022) inside JSON string values. This is easy to hit when a language uses typographic quotation marks that look similar to ASCII ":
- German:
„(U+201E) opens,"(U+201C) closes — but LLMs sometimes emit a bare ASCII"for the closing mark, which terminates the JSON string early. - French:
«»(guillemets) are safe — they are not ASCII".
After writing each translation file, validate it is parseable JSON (e.g. node -e "require('./lib/i18n/messages/de-DE.json')" or equivalent). If validation fails, escape any rogue inner " as \" or replace typographic quotes with \"...\".
Step 15: Create Root Fallback (if localePrefix: "always")
Only needed if the user chose "always show locale prefix":
Create app/page.tsx (outside [locale]/) as a redirect fallback:
If localePrefix: "as-needed", skip this step — the middleware handles root requests automatically.
Verification
After completing all steps, verify the implementation:
- Build: Run
bun buildand confirm no TypeScript errors - Smoke test: Run
bun devand check:- Default locale URL works (e.g.,
http://localhost:3000/products/technest-smart-speaker-pro-jk0c) - Locale-prefixed URL works (e.g.,
http://localhost:3000/de-DE/products/technest-smart-speaker-pro-jk0c) - Product prices render in the correct currency for each locale
- Default locale URL works (e.g.,
- Locale selector: Confirm the selector appears in the megamenu and switching locales changes the URL + cart currency
- Middleware: Confirm
?variantId=rewrites still work - SEO: Check that page metadata includes
hreflangalternates for all enabled locales - Sitemap: Visit
/sitemap.xmland confirm per-locale entries