i18n
Locale-prefixed URLs and per-locale message catalogs via next-intl, without Shopify Markets.
How to use
Enable i18n (next-intl, no Markets)
Wire next-intl into the template so the storefront serves locale-prefixed URLs (/en-US/products/foo), loads per-locale message catalogs, and exposes a locale switcher. The template ships single-locale by default with clean URLs (/products/foo) — this skill restores the i18n machinery.
Use
enable-shopify-marketsinstead if you want region-aware pricing/inventory/payments. That skill builds on the same routing layer plus Markets-specific operations. If you only want URL prefixing and translated copy, this skill is the right one.
Source of truth: lib/i18n/index.ts
The locale list lives in lib/i18n/index.ts as locales and enabledLocales. Always read those at the start of the skill — don't hardcode a list. Adding new locales means editing that file plus the localeCurrency map; everything downstream (routing, sitemap, alternates, switcher) reads from it.
What this skill turns on
lib/i18n/routing.tsandlib/i18n/navigation.ts(next-intl)- Route segment
app/[locale]/containing every page proxy.tsmiddleware runningnext-intl/middlewarelib/params.tsgetLocale()reading fromnext/root-paramslib/i18n/request.tsloading messages by resolved locale- Locale-prefixed canonicals + hreflang alternates in
lib/seo.ts - Sitemap entries per locale
next.config.tsrewrites/redirects on/:locale/*sourcesapp/(unlocalized)/page.tsxfallback redirect to default localegenerateStaticParamson the root layout- (If
enable-shopify-menusalready ran) Re-enableLocaleCurrencySelectorin the megamenu
Cache Components compatibility — read this first
The template runs with cacheComponents: true (Next.js 16). That changes a few things this skill needs to handle correctly. Skipping any of these will produce build errors that look unrelated:
A. experimental.rootParams: true must stay on
next/root-params (used by getLocale() below) is gated behind a flag. The template ships with it enabled in next.config.ts:
If it's been removed, add it back. Without it, import { locale } from "next/root-params" resolves but returns undefined, and you'll see notFound() on every request.
B. There must be no app/layout.tsx above app/[locale]/
For [locale] to be recognized as a root param, the dynamic segment must be the root layout. After Step 2, the file at app/layout.tsx should be gone (moved into app/[locale]/layout.tsx). If both exist, rootParams.locale() returns undefined.
C. setRequestLocale is not used
next-intl docs sometimes show setRequestLocale(locale) calls in layouts/pages. Don't add them under cacheComponents. That helper writes to a request-scoped store and forces dynamic rendering — it defeats the cache. The rootParams + request-config pattern below makes it unnecessary because the resolved locale is already a cache key.
D. Don't swap next/link to next-intl's <Link>
The straightforward instinct is to replace every import Link from "next/link" with import { Link } from "@/lib/i18n/navigation". Don't. next-intl's Link reads request context (locale) on render; in a server-component tree under cacheComponents, that triggers:
or a generic "blocking route" prerender failure.
Do this instead: keep next/link and let proxy.ts middleware redirect unprefixed paths (/products/foo → /en-US/products/foo). Internal links work; there's a one-time middleware redirect on click for unprefixed hrefs. Trade a few redirects for a clean prerender.
If you must locale-prefix a programmatic URL (server actions, redirect(), permanentRedirect()), build the path yourself: `/${await getLocale()}/account/login`.
E. unstable_instant samples need locale in params
Any route that exports unstable_instant (currently: products [handle], collections [handle], search) needs locale added to every sample, or the build fails:
Fix:
F. unstable_instant samples need headers declarations if any layout-level server component reads headers()
This is easy to forget. If you (or a downstream skill) adds a server component to the layout that calls headers() — e.g. a "Shipping to " bar reading x-vercel-ip-postal-code — every unstable_instant sample in the app must declare the headers it might access:
null means "header may be absent." If you forget, the build error is explicit:
G. redirect() from next-intl doesn't return never
next-intl's redirect is typed to return void, so TypeScript doesn't treat it as control-flow-ending. Use next/navigation's redirect (which returns never) and prefix the locale yourself:
Step-by-step
Step 1: Routing config
Create lib/i18n/routing.ts:
Create lib/i18n/navigation.ts:
Per "Cache Components compatibility D" above,
Linkhere is mostly used by the locale switcher / programmatic routing in client components — not as a wholesale replacement fornext/link.
Step 2: Move routes under app/[locale]/
Move every route file from app/ into app/[locale]/:
app/layout.tsx→app/[locale]/layout.tsx(becomes the root layout for the locale segment). Delete the originalapp/layout.tsxafter the move — see compatibility B above; both files cannot coexist.app/page.tsx,app/error.tsx,app/not-found.tsx→app/[locale]/...app/about/,app/account/,app/cart/,app/collections/,app/products/,app/search/→app/[locale]/...
Stay at app/: api/, sitemap.ts, robots.ts, global-error.tsx, globals.css, favicon.ico.
In the moved layout, fix import "./globals.css" → import "../globals.css".
Update every PageProps<"/foo"> and LayoutProps<"/foo"> generic to include the locale segment: PageProps<"/[locale]/products/[handle]">, LayoutProps<"/[locale]">, etc.
Step 3: lib/params.ts reads from root params
Step 4: lib/i18n/request.ts loads messages by resolved locale
Step 5: proxy.ts middleware
The file is
proxy.ts(Next.js 16 convention), notmiddleware.ts.
Step 6: Internal hrefs — keep next/link
Per the cache-components note above, leave existing next/link imports alone. Middleware redirects unprefixed URLs to the active locale on click. The only places to use the next-intl-aware Link are inside client components that explicitly need to switch locales (e.g. a locale switcher) — and even then, usePathname() + useRouter().push() from next/navigation plus a manual segment swap is often cleaner under cacheComponents.
For programmatic redirects in server code, use next/navigation's redirect:
Step 7: lib/seo.ts — locale-aware canonicals + hreflang alternates
buildAlternates is now async — update every caller to await.
Step 8: Sitemap per-locale entries
Step 9: next.config.ts rewrites/redirects on /:locale/*
Existing markdown content-negotiation rewrites must move their source from /products/:handle to /:locale/products/:handle, etc. Destinations stay at /md/products/:handle, /md/collections/:handle, and /md/search — the handlers read locale from query params, not the URL path. Add the locale-prefixed redirect rules from the original config (/:locale/product* → /:locale/products*).
Step 10: app/(unlocalized)/page.tsx fallback
This is a defensive fallback; with localePrefix: "always" middleware should already redirect /.
Step 11: generateStaticParams on the locale layout
Step 12: Patch unstable_instant samples
Walk every route file that exports unstable_instant and add locale to each sample's params:
If any layout-level server component (e.g. a shipping/postal banner, geo-aware nav) reads headers(), also add a headers array to every sample:
(See "Cache Components compatibility E/F" at the top.)
Step 13: (Conditional) Re-enable LocaleCurrencySelector in the megamenu
Only if the enable-shopify-menus skill has already been run and components/nav/megamenu/index.tsx exists. The selector component lives at components/nav/locale-currency.tsx (with a fallback at locale-currency-fallback.tsx). Wire it into both MegamenuDesktop and MegamenuMobile per the original instructions.
Verifying
After applying:
Smoke-test checklist:
- Build passes
- Bare
/redirects to default locale - Each enabled locale serves 200 at its prefix
-
<html lang>matches the URL's locale segment - Sitemap emits one entry per locale per page
- Canonical + hreflang alternates appear in page metadata
- Unprefixed internal links from existing
next/linkcalls redirect (one extra hop, but correct)