nextjs-locale-standalone — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited nextjs-locale-standalone (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Locale-prefixed routing for one Next.js App Router app, no shared package. Every page lives under /<locale>/…; a proxy (middleware) sends unprefixed requests to the right locale. Drop-in copy-paste files are in templates/.
Sibling skill: nextjs-locale-monorepo — the same behaviour factored into a shared workspace package for multi-app repos. Keep the two in sync.
/en-hk/about, /zh-hk/about. One locale isthe default.
/about (or /) with noknown locale prefix is 307-redirected to /<locale>/about, preserving the query string. (URL hashes are client-side only — they never reach the server, so the proxy can't and needn't carry them; the LocaleToggle, running in the browser, does preserve the hash.) The locale is resolved by this priority:
zh → zh-hk).NextResponse.next()), and the proxystamps the response: x-locale header and a (re)write of NEXT_LOCALE when it differs. It does not add Vary: Accept-Language, Cookie here — on a prefixed path the locale is fixed by the URL, so varying on those inputs would fragment the CDN cache for nothing. Only the negotiated redirect sets that Vary.
/<newLocale>/…; the proxy, seeing a prefixed path, writes NEXT_LOCALE — so the next time the visitor lands on /, step 1 sends them back to that choice. (The toggle doesn't set the cookie itself; the proxy is the single writer.)
| File | Role |
|---|---|
lib/i18n.ts | supportedLanguages, locales, defaultLocale, cookie name, and pure detection helpers (no Next imports) |
proxy.ts (Next 16) / middleware.ts (≤15) | the redirect + stamp logic |
app/[locale]/layout.tsx | validates the locale, generateStaticParams, renders <html lang>, wraps in LocaleProvider |
app/locale-provider.tsx | 'use client' context + useCurrentLocale / useIsLocale |
components/LocaleToggle.tsx | switches locale by rewriting the first path segment |
Copy them from templates/ and adjust supportedLanguages.
lib/i18n.ts ({ id, title, isDefault? }[]).topmost layout.tsx — is the one that must render <html> + <body>, and Next errors if it doesn't. So make app/[locale]/layout.tsx be that root layout and have no `app/layout.tsx` at all — valid as long as every page lives under [locale]. (app/globals.css, app/global-error.tsx, app/api/*, app/icon.png stay at app/.) Don't keep a pass-through app/layout.tsx that returns bare children: it would be the root layout, and Next rejects a root layout without <html>/<body>. You also can't read the [locale] param up there, so there's no reason to keep it — delete it and let [locale] be root.
proxy.ts vs middleware.tsNext 16 renamed the convention: the file is proxy.ts and the export is export function proxy(...). On Next ≤15 it's middleware.ts / export function middleware(...) — *the body of this template is identical, only the file and function names change. `proxy` is the go-forward direction — `middleware` is deprecated; Next 16 still runs a `middleware.ts` but logs a deprecation warning and is positioning it as the edge-only escape hatch (below), not the default. So on Next 16+ default to `proxy.ts`*; reach for middleware.ts only on Next ≤15 or when you specifically need the edge runtime. The template ships as proxy.ts. Migrate an existing file with npx @next/codemod middleware-to-proxy . (it also renames config flags like skipMiddlewareUrlNormalize → skipProxyUrlNormalize and types NextMiddleware → NextProxy).
The rename is not purely cosmetic — `proxy` is Node.js-only. proxy.ts defaults to the Node.js runtime and you cannot change it — setting the runtime config in a proxy file throws. (Middleware historically ran on the edge runtime; Node support went stable in 15.5, and 16 made Node the locked default.) For this locale logic that's a non-issue — redirects, rewrites, and cookie stamping don't need edge. But if you need the edge runtime, keep `middleware.ts` (Next will add edge guidance for proxy in a later release). Conceptually Next now frames this feature as a network boundary / gateway, to be used sparingly (redirects, rewrites, header/cookie stamping, light gating) — not a place for app logic. Don't trust it as the only auth gate: a matcher change can silently drop coverage (including Server Functions), so verify auth in the route/Server Function too.
The matcher excludes assets and API so they never redirect:
export const config = { matcher: ['/((?!_next(?:/|$)|api(?:/|$)|.*\\..*).*)'] };The (?:/|$) after _next and api anchors them to a path segment, so real pages like /apiary still get locale-prefixed (a bare api would skip them).
Also guard inside the function (shouldSkip) for /_next, /api, /.well-known, /favicon, and anything with a file extension — the matcher and the guard are belt-and-suspenders.
locale; it's not a secret. sameSite: 'lax', secure on HTTPS, 1-year maxAge.
does) so a CDN never serves one visitor's locale to another. Prefixed pass-through responses skip it — their locale is fixed by the URL, so the extra Vary would only fragment the cache.
zh-TW or zh shouldresolve to your zh-hk. The matcher in lib/i18n.ts tries exact, then the primary subtag.
extractLocaleFromPath returnsnull. Prefixed paths must pass through.
unsupported first segment (/xx/about) from a normal page path (/products/x) — both have a non-locale first segment — so it prefixes both: /xx/about → /en-hk/xx/about, which 404s because no such page route exists. So a bad locale still yields a 404, just under a /<default>/… URL rather than as a bare /xx/…. Don't expect the proxy to leave /xx/… untouched. The layout's notFound() (below) is the backstop for the cases the proxy doesn't intercept.
notFound() for an unknown [locale]as defense-in-depth — it catches an invalid locale that reaches [locale] directly (un-proxied render, an excluded matcher path) so it 404s instead of rendering with a bogus lang.
curl -sI localhost:3000/ → 307 to /<default>/ (no cookie, no Accept-Language).curl -sI -H 'Accept-Language: zh-HK' localhost:3000/about → 307 to /zh-hk/about.curl -sI --cookie 'NEXT_LOCALE=zh-hk' localhost:3000/ → 307 to /zh-hk/ even with an English Accept-Language (cookie wins)./zh-hk/x sets NEXT_LOCALE=zh-hk in the response Set-Cookie.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.