From vercel-shop
Restores locale-prefixed URLs (e.g., /en-US/products/foo) and next-intl i18n routing in Next.js shop template codebases using [locale] segments, middleware, navigation, and params helpers.
npx claudepluginhub vercel/shop --plugin vercel-shopThis skill uses the workspace's default tool permissions.
Restore locale-prefixed URLs (e.g., `/en-US/products/foo`) to this codebase. This was removed in favor of clean URLs (`/products/foo`) with locale hardcoded to `en-US`.
Sets up Shopify Markets with next-intl multi-locale routing for Next.js storefronts. Supports sub-path (/en/products) and per-domain (en.store.com) strategies. Use for adding i18n, locale-prefixed URLs, or multi-locale support.
Provides i18n best practices for React Router apps using remix-i18next, including middleware setup, locale detection, resource routes, and language switching.
Implements Next.js App Router advanced routing: dynamic [slug] and catch-all [...slug] routes, route groups (name), parallel @slot, intercepting modals (.)path, private _prefix folders, Route Handlers APIs, search params, programmatic navigation.
Share bugs, ideas, or general feedback.
Restore locale-prefixed URLs (e.g., /en-US/products/foo) to this codebase. This was removed in favor of clean URLs (/products/foo) with locale hardcoded to en-US.
app/[locale]/All page routes lived under app/[locale]/ as a dynamic segment:
app/[locale]/layout.tsx
app/[locale]/page.tsx
app/[locale]/products/[handle]/page.tsx
app/[locale]/collections/[handle]/page.tsx
...
Pages used PageProps<"/[locale]/products/[handle]"> and LayoutProps<"/[locale]"> types. The root layout had generateStaticParams returning all locales:
export const generateStaticParams = async () => {
return locales.map((locale) => ({ locale }));
};
proxy.ts — Locale MiddlewareThe middleware used next-intl/middleware to handle locale detection, redirects, and rewrites:
import createMiddleware from "next-intl/middleware";
import { routing } from "@/lib/i18n/routing";
const handlei18n = createMiddleware(routing);
export default async function middleware(request: NextRequest) {
// Content negotiation for markdown (product pages)
// ...
// Normal flow: i18n handling and routing
let response = handlei18n(request);
if (response.ok) {
const url = new URL(response.headers.get("x-middleware-rewrite") || request.url);
const [, locale, ...rest] = url.pathname.split("/");
const rewriteUrl = new URL(`/${[locale, ...rest].filter(Boolean).join("/")}`, request.url);
rewriteUrl.search = url.search;
response = NextResponse.rewrite(rewriteUrl, { headers: response.headers });
}
return response;
}
lib/i18n/routing.tsimport { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["en-US", "en-GB", "de-DE", "fr-FR", "nl-NL", "es-ES"],
defaultLocale: "en-US",
localePrefix: "always",
});
lib/i18n/navigation.tsimport { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
Components imported Link from @/lib/i18n/navigation instead of next/link. The locale-aware Link automatically prefixed URLs with the current locale. useRouter supported router.replace(pathname, { locale }) for locale switching.
lib/params.ts — getLocale()Used next/root-params to extract locale from the [locale] dynamic segment:
import { locale } from "next/root-params";
export async function getLocale(): Promise<Locale> {
const currentLocale = await locale();
if (!currentLocale || !locales.includes(currentLocale)) notFound();
return currentLocale;
}
lib/i18n/request.tsLoaded messages based on the current locale from params:
const requested = await getLocale();
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
const language = locale.split("-")[0];
let messages;
try {
messages = (await import(`./messages/${locale}.json`)).default;
} catch {
messages = (await import(`./messages/${language}.json`)).default;
}
return { locale, messages };
lib/seo.ts — withLocalePath()function withLocalePath(locale: string, pathname: string): string {
const normalizedPath = normalizePath(pathname);
if (normalizedPath === "/") return `/${locale}`;
return `/${locale}${normalizedPath}`;
}
buildAlternates() generated per-locale canonical URLs and language alternates using this function.
Components used /${locale}/... template literals:
/${locale}/search?q=.../${locale}/products/${handle}/${locale}/collections/${handle}/${locale}/cartapp/sitemap.tsHad localizePath() function and generated per-locale URLs for every page (6 entries per page instead of 1).
next.config.ts RedirectsHad locale-prefixed redirect rules:
{ source: "/:locale/product", destination: "/:locale/products", permanent: true },
{ source: "/:locale/product/:path*", destination: "/:locale/products/:path*", permanent: true },
app/(unlocalized)/page.tsxFallback redirect for requests without locale prefix:
permanentRedirect(`/${locales[0]}`);
Create lib/i18n/routing.ts:
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["en-US", "en-GB", "de-DE", "fr-FR", "nl-NL", "es-ES"],
defaultLocale: "en-US",
localePrefix: "always",
});
Create lib/i18n/navigation.ts:
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
app/[locale]/Move all routes from app/ to app/[locale]/:
app/layout.tsx → app/[locale]/layout.tsxapp/page.tsx → app/[locale]/page.tsxapp/error.tsx → app/[locale]/error.tsxapp/not-found.tsx → app/[locale]/not-found.tsxapp/cart/ → app/[locale]/cart/Update all PageProps<"/..."> → PageProps<"/[locale]/...">.
lib/params.tsimport { notFound } from "next/navigation";
import { locale } from "next/root-params";
import { type Locale, locales } from "./i18n";
export async function getLocale(): Promise<Locale> {
const currentLocale = await locale();
if (!currentLocale || !locales.includes(currentLocale)) notFound();
return currentLocale;
}
lib/i18n/request.tsimport { hasLocale } from "next-intl";
import { getRequestConfig } from "next-intl/server";
import { getLocale } from "../params";
import { routing } from "./routing";
export default getRequestConfig(async () => {
const requested = await getLocale();
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
const language = locale.split("-")[0];
let messages;
try {
messages = (await import(`./messages/${locale}.json`)).default;
} catch {
messages = (await import(`./messages/${language}.json`)).default;
}
return { locale, messages };
});
proxy.tsBring back full next-intl middleware with locale routing. See original code in this document.
Replace all /products/..., /collections/..., /search?q=... patterns with /${locale}/... equivalents. Replace next/link imports with @/lib/i18n/navigation imports where locale-aware linking is needed.
Add back withLocalePath() to lib/seo.ts and update buildAlternates() to generate per-locale alternates.
Add back localizePath() and per-locale URL generation in app/sitemap.ts.
next.config.tsapp/(unlocalized)/page.tsx fallbackgenerateStaticParams to root layoutexport const generateStaticParams = async () => {
return locales.map((locale) => ({ locale }));
};
Prerequisite: This step requires the
enable-shopify-menusskill to have been run first. If the megamenu has not been added, skip this step.
The browse menu previously included a LocaleCurrencySelector that let users switch language and see their currency. It was removed when locale URL prefixes were removed (since there's only one locale without them). To restore it:
The component already exists at components/layout/nav/locale-currency.tsx (with a fallback at locale-currency-fallback.tsx).
Add it back to components/layout/nav/megamenu/index.tsx:
import { LocaleCurrencySelector } from "../locale-currency";
// In MegamenuContent, pass as children to both desktop and mobile:
<MegamenuDesktop locale={locale} items={data.items}>
<LocaleCurrencySelector locale={locale} />
</MegamenuDesktop>
<MegamenuMobile data={data} locale={locale}>
<ShippingAddress className="flex items-center" />
<LocaleCurrencySelector locale={locale} />
</MegamenuMobile>
The selector renders at the bottom of the megamenu nav column (in a border-t footer area) on both desktop and mobile. It uses syncCartLocaleAction to update the cart's country/currency when the user switches locale.
The account/currency-selector.tsx compound component also exists for use in account settings pages if needed.