name: enable-shopify-markets description: > Enable Shopify Markets with multi-locale routing using next-intl. Use when the user wants to add internationalization, multi-locale support, locale-prefixed URLs, or Shopify Markets. Supports sub-path and per-domain routing strategies. argument-hint: "[sub-path|per-domain]"
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 is the Markets-aware path; if you only want next-intl routing/messages without Markets, use enable-i18n instead.
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):
{
"questions": [
{
"question": "Which routing strategy do you want for multi-locale URLs?",
"options": [
"Sub-path routing (/en-US/products/..., /de-DE/products/...)",
"Per-domain routing (en.store.com/products/..., de.store.com/products/...)"
]
},
{
"question": "Which locales should be enabled? (en-US is always included as the default). You can pick from the pre-configured locales below, or specify any additional locales — translation files will be generated automatically.",
"multiSelect": true,
"options": [
"en-GB (English - United Kingdom, GBP)",
"de-DE (German - Germany, EUR)",
"fr-FR (French - France, EUR)",
"nl-NL (Dutch - Netherlands, EUR)",
"es-ES (Spanish - Spain, EUR)",
"Add other locales (e.g., ja-JP, pt-BR, zh-CN, it-IT, ko-KR)"
]
}
]
}
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:
{
"questions": [
{
"question": "Should the default locale (en-US) have a URL prefix?",
"options": [
"No — clean URLs for default locale, prefixes for others (recommended)",
"Yes — always show the locale prefix, including for en-US"
]
},
{
"question": "What format should locale URL prefixes use?",
"options": [
"Full locale codes (/en-US/, /de-DE/, /fr-FR/)",
"Short language codes (/en/, /de/, /fr/)"
]
}
]
}
If per-domain routing was chosen, ask:
{
"questions": [
{
"question": "How should domains map to locales? Provide your domain mapping or pick a starting pattern.",
"options": [
"Use subdomains (e.g., en.mystore.com, de.mystore.com)",
"Use country TLDs (e.g., mystore.com, mystore.de, mystore.fr)"
]
}
]
}
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:
export const enabledLocales: readonly Locale[] = ["en-US", "de-DE", "fr-FR"]; // user's selection
Add custom locales (if any)
If the user chose locales not in the existing locales array, add them:
export const locales = [
"en-US",
"en-GB",
"de-DE",
"fr-FR",
"nl-NL",
"es-ES",
"ja-JP", // new custom locale
] as const;
Also add entries to the localeCurrency map:
const localeCurrency: Record<Locale, { currency: string; symbol: string }> = {
// ... existing entries ...
"ja-JP": { currency: "JPY", symbol: "¥" },
};
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
import { defineRouting } from "next-intl/routing";
import { enabledLocales, defaultLocale } from "../i18n";
export const routing = defineRouting({
locales: enabledLocales,
defaultLocale,
localePrefix: "as-needed", // or "always" based on user choice
});
If the user chose short prefixes, use the object form:
export const routing = defineRouting({
locales: enabledLocales,
defaultLocale,
localePrefix: {
mode: "as-needed", // or "always"
prefixes: {
"en-US": "/en",
"de-DE": "/de",
"fr-FR": "/fr",
// ... map each enabled locale to its short prefix
},
},
});
Per-domain routing
import { defineRouting } from "next-intl/routing";
import { enabledLocales, defaultLocale } from "../i18n";
export const routing = defineRouting({
locales: enabledLocales,
defaultLocale,
localePrefix: "as-needed",
domains: [
{
domain: "store.com", // from user's mapping
defaultLocale: "en-US",
locales: ["en-US"],
},
{
domain: "de.store.com", // from user's mapping
defaultLocale: "de-DE",
locales: ["de-DE"],
},
// ... one entry per domain
],
});
Step 4: Create Navigation Exports
File: lib/i18n/navigation.ts (create new)
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
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.
app/layout.tsx → app/[locale]/layout.tsx
app/page.tsx → app/[locale]/page.tsx
app/error.tsx → app/[locale]/error.tsx
app/not-found.tsx → app/[locale]/not-found.tsx
app/cart/ → app/[locale]/cart/
app/collections/ → app/[locale]/collections/
app/products/ → app/[locale]/products/
app/search/ → app/[locale]/search/
app/pages/ → app/[locale]/pages/
app/account/ → app/[locale]/account/
app/login/ → app/[locale]/login/
Update all PageProps and LayoutProps type parameters to include [locale]:
LayoutProps<"/">→LayoutProps<"/[locale]">PageProps<"/products/[handle]">→PageProps<"/[locale]/products/[handle]">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:
import { enabledLocales } from "@/lib/i18n";
export const generateStaticParams = async () => {
return enabledLocales.map((locale) => ({ locale }));
};
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:
import { notFound } from "next/navigation";
import { locale } from "next/root-params";
import { type Locale, isEnabledLocale } from "./i18n";
export async function getLocale(): Promise<Locale> {
const currentLocale = await locale();
if (!currentLocale || !isEnabledLocale(currentLocale)) notFound();
return currentLocale as Locale;
}
lib/i18n/request.ts
Update to resolve locale dynamically:
import { hasLocale } from "next-intl";
import { getRequestConfig } from "next-intl/server";
import { getLocale } from "../params";
import { routing } from "./routing";
import type enMessages from "./messages/en.json";
export default getRequestConfig(async () => {
const requested = await getLocale();
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
const language = locale.split("-")[0];
let messages: typeof enMessages;
try {
messages = (await import(`./messages/${locale}.json`)).default;
} catch {
messages = (await import(`./messages/${language}.json`)).default;
}
return { locale, messages };
});
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:
export const config = {
matcher: [
"/((?!.well-known|api|sitemaps|webhooks|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};
import type { NextRequest } from "next/server";
import createMiddleware from "next-intl/middleware";
import { routing } from "@/lib/i18n/routing";
const handlei18n = createMiddleware(routing);
export function proxy(request: NextRequest) {
return handlei18n(request);
}
Note: Product variant selection stays on Shopify's standard
?variant=query parameter. The built-in content negotiation rewrite innext.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:
components/ui/filter-sidebar.tsx
components/product/breadcrumb.tsx
components/prefetch-link.tsx
components/orders/order-detail.tsx
components/predictive-search-results.tsx
components/nav/quick-links.tsx
components/nav/account-client.tsx
components/nav/account.tsx
components/nav/current-page-link.tsx
components/nav/index.tsx
components/footer.tsx
components/collections/pagination.tsx
components/error-boundary-content.tsx
components/collections/collection-page.tsx
components/cart/overlay-content.tsx
components/cart/overlay-item.tsx
components/cart/empty-cart.tsx
components/account/sidebar.tsx
components/account/mobile-tabs.tsx
app/search/page.tsx (now app/[locale]/search/page.tsx)
app/not-found.tsx (now app/[locale]/not-found.tsx)
app/collections/page.tsx (now app/[locale]/collections/page.tsx)
app/account/orders/page.tsx
app/account/orders/[id]/page.tsx
components/agent/registry.tsx
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/nav/megamenu/index.tsx
The LocaleCurrencySelector component already exists at components/nav/locale-currency.tsx. Add it to the megamenu:
import { LocaleCurrencySelector } from "../locale-currency";
// In MegamenuContent, pass locale to both desktop and mobile:
<MegamenuDesktop items={data.items} locale={locale}>
<LocaleCurrencySelector locale={locale} />
</MegamenuDesktop>
<MegamenuMobile data={data} locale={locale}>
<LocaleCurrencySelector locale={locale} />
</MegamenuMobile>
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:
import { useRouter, usePathname } from "@/lib/i18n/navigation";
const handleLocaleChange = (locale: Locale) => {
if (locale === currentLocale) return;
setOpen(false);
startTransition(async () => {
const result = await syncCartLocaleAction(locale);
if (!result.success) {
console.error("Failed to sync cart locale:", result.error);
}
router.replace(pathname, { 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:
import { enabledLocales, defaultLocale, localeSwitchingEnabled } from "./i18n";
function withLocalePath(locale: string, pathname: string): string {
const normalizedPath = normalizePath(pathname);
if (normalizedPath === "/") return `/${locale}`;
return `/${locale}${normalizedPath}`;
}
export function buildAlternates({
pathname,
searchParams,
}: {
pathname: string;
searchParams?: SearchParamsInput;
}): Metadata["alternates"] {
const canonical = buildCanonicalPath(pathname, searchParams);
if (!localeSwitchingEnabled) {
return { canonical };
}
const languages: Record<string, string> = {};
for (const locale of enabledLocales) {
languages[locale] = withLocalePath(locale, buildCanonicalPath(pathname, searchParams));
}
languages["x-default"] = withLocalePath(
defaultLocale,
buildCanonicalPath(pathname, searchParams),
);
return { canonical, languages };
}
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:
import { enabledLocales, localeSwitchingEnabled } from "@/lib/i18n";
function localizePath(locale: string, path: string): string {
return path === "/" ? `/${locale}` : `/${locale}${path}`;
}
// When building sitemap entries, if localeSwitchingEnabled:
// For each path, create entries for all enabled locales
// and add alternates.languages pointing to all locale variants
Step 13: Add Locale-Prefixed Redirects
File: next.config.ts
Add locale-aware redirect rules for common typos:
redirects: async () => [
// existing redirects...
{ source: "/:locale/product", destination: "/:locale/products", permanent: true },
{ source: "/:locale/product/:path*", destination: "/:locale/products/:path*", permanent: true },
],
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:
import { permanentRedirect } from "next/navigation";
import { defaultLocale } from "@/lib/i18n";
export default function RootPage() {
permanentRedirect(`/${defaultLocale}`);
}
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
- Variants: Confirm product variant links preserve
?variant=across locale-prefixed URLs - SEO: Check that page metadata includes
hreflangalternates for all enabled locales - Sitemap: Visit
/sitemap.xmland confirm per-locale entries