nextjs-locale-monorepo — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited nextjs-locale-monorepo (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.
The same locale-routing behaviour as nextjs-locale-standalone, but the engine lives in one workspace package (i18n-routing) that every app imports, so detection, the cookie name, and the provider/hooks stay identical across apps. Read that sibling skill for the behaviour spec; this skill is about the package + wiring. Copy-paste files are in templates/.
Unprefixed requests 307-redirect to /<locale>/…, locale chosen by priority: `NEXT_LOCALE` cookie (toggle's last choice) → `Accept-Language` (browser) → default. The redirect carries Vary: Accept-Language, Cookie (its locale was negotiated from them); prefixed paths pass through with x-locale + the NEXT_LOCALE cookie stamped but no such Vary (their locale is fixed by the URL, so it'd only fragment the cache). The LocaleToggle navigates to /<newLocale>/…; the middleware is the single writer of the cookie, so the choice persists. Full explanation + a toggle template: see nextjs-locale-standalone.
packages/
i18n-routing/ # the engine — detection, middleware factory, provider/hooks
src/{config,utils,middleware,provider,hooks,client,index}.ts(x)
package.json # exports: "." (server) + "./client" (provider/hooks)
tsup.config.ts # dual CJS/ESM, dts, preserves "use client"
configs/ # shared supportedLanguages (one source of truth)
apps/
web/ marketing/ … # each: a thin middleware.ts + app/[locale]/layout.tsxTwo packages, deliberately:
is server-safe (config + utils + middleware factory); i18n-routing/client is the 'use client' provider + hooks. Keeping them separate stops the 'use client' boundary from poisoning middleware/server imports.
supportedLanguages({ id, title, isDefault? }[]). Apps and the middleware read locales from here, so adding a language is a one-line change in one place.
Use the templates/package/ files as-is. What matters:
index, client), format: ['cjs','esm'],dts: true, external: ['react','react-dom','next'], and crucially `treeshake: false` — tree-shaking strips the 'use client' directive and the provider breaks at runtime. (templates/package/tsup.config.ts.)
. and ./client, each pointing at types + import(esm) + require (cjs). (templates/package/package.json.)
next, react, react-dom — never bundle them.createI18nMiddleware(config) is pure logic — itreturns NextResponse | undefined and imports nothing app-specific, so it is unit-testable without a running app.
package.json:"i18n-routing": "workspace:^", "configs": "workspace:^".
templates/app-middleware.ts): build the config fromthe shared languages and delegate. This is also where you compose app-specific middleware (auth, an anon_id cookie, feature-flag gating) around the i18n redirect.
import { supportedLanguages } from 'configs/locale';
import { createI18nMiddleware, i18nConfig } from 'i18n-routing';
const i18n = createI18nMiddleware(i18nConfig(supportedLanguages));
export function middleware(req: NextRequest) {
return i18n(req) ?? NextResponse.next();
}
export const config = { matcher: ['/((?!_next(?:/|$)|api(?:/|$)|.*\\..*).*)'] };Next 16: name this thin app file proxy.ts and the export proxy (export function proxy(req)) — the body and the shared createI18nMiddleware factory are unchanged; only the app-level file/function names follow the new convention (npx @next/codemod middleware-to-proxy . automates it). One real caveat: `proxy` is Node.js-only (the runtime config throws), whereas middleware could run on the edge — locale redirects don't need edge, but if any app's request layer does, keep middleware.ts there. The shared factory is runtime-agnostic, so apps can mix conventions.
templates/app-locale-layout.tsx): validate thelocale, generateStaticParams from supportedLanguages, and wrap children in LocaleProvider from `i18n-routing/client` (not the root import). This is the root layout — render <html lang={locale}>/<body> here and keep no app/layout.tsx (every page lives under [locale]).
turbo.json so the lib is built first:
"your-app#build": { "dependsOn": ["i18n-routing#build", "configs#build"], "outputs": [".next/**"] },
"your-app#dev": { "dependsOn": ["i18n-routing#build", "configs#build"], "persistent": true }Run the package in watch mode (tsup --watch) during development so app HMR picks up engine edits.
next than the package's peer range, NextRequest types can mismatch and TS complains at the i18n(req) call. The pragmatic fix used in production is a localized cast — i18n(req as any) — with a comment; it's a types-only skew, the runtime shape is identical. Prefer aligning next versions when you can.
provider via the default entry drags a client boundary into server/middleware graphs. Always import hooks/provider from i18n-routing/client.
Add one { id, title } to supportedLanguages in configs. Every app's middleware, generateStaticParams, toggle, and detection pick it up — no per-app change. That single source of truth is the whole reason to use a package over copy-paste.
curl -sI localhost:3000/ → 307 to /<default>/.curl -sI -H 'Accept-Language: zh-HK' localhost:3000/x → 307 to /zh-hk/x.curl -sI --cookie 'NEXT_LOCALE=zh-hk' localhost:3000/ → 307 to /zh-hk/ (cookie beats Accept-Language).pnpm --filter i18n-routing build succeeds and dist/client.* keeps its "use client" banner.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.