web-i18n-next-intl-1172c9 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-i18n-next-intl-1172c9 (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.
Quick Guide:useTranslationsrenders messages,useFormatterrenders dates, numbers and lists, andcreateMiddlewaredetects the locale.setRequestLocale(locale)at the top of a page or layout is what keeps it statically renderable. v4.0+ registers types through theAppConfiginterface and sets the locale cookie only when the user switches away from their Accept-Language preference. Every pattern here is App Router — the Pages Router integration is a separate API and none of this transfers to it.
Detailed Resources:
useTranslations, plurals, formatting, static rendering, locale switching, typest.markup() for HTML strings: email bodies, feeds, sanitisationuseTranslations and useFormatter, withNextIntlClientProvider above any Client Component that calls them. Follow examples/core.md.
getTranslations({ locale, namespace }), which needs the locale passed explicitly. Also in examples/core.md.
t.markup() instead of t.rich(), and thesanitisation that goes with it. Follow examples/markup.md.
<critical_requirements>
Call `setRequestLocale(locale)` at the top of every page and layout, before any hook. It is what lets next-intl resolve the locale without a request, which is what keeps the route statically renderable.
Validate the locale with `hasLocale(routing.locales, locale)` before using it. An unvalidated segment reaches the message loader and fails there, well away from the route that produced it.
Wrap the tree in `NextIntlClientProvider`. Client Components read their messages from that context and render nothing without it.
</critical_requirements>
Auto-detection: next-intl, useTranslations, useFormatter, useLocale, getTranslations, setRequestLocale, NextIntlClientProvider, defineRouting, createNavigation, hasLocale, ICU message format
Applies to:
Handled elsewhere:
to a route, not how routes are defined
they came from is not this skill's concern
useLocale() and never mirroredDate is this skill's job; producing one is not<philosophy>
Translations are namespaced JSON, resolved per request on the server and handed to the client through context. Two decisions follow from that. Locale-aware rendering is a server concern by default, so the client tree carries only what interactivity needs. And because a request is what normally supplies the locale, static rendering needs it supplied another way — which is what setRequestLocale is for, and why it has to run before anything reads the locale.
</philosophy>
<patterns>
Four modules and a proxy: routing.ts declares the locales, request.ts resolves one per request, navigation.ts produces locale-aware navigation APIs, and the proxy detects the locale from URL, cookie and Accept-Language.
// i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["en", "de", "fr"],
defaultLocale: "en",
});
export type Locale = (typeof routing.locales)[number];The proxy file is proxy.ts from Next.js 16 onwards and middleware.ts before it; the export is createMiddleware(routing) either way.
Full code: examples/core.md
Validate the locale, set it, load the messages, and wrap the tree.
if (!hasLocale(routing.locales, locale)) notFound();
setRequestLocale(locale);
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={await getMessages()}>{children}</NextIntlClientProvider>
</body>
</html>
);From v4.0 the provider inherits messages from the server config, so the messages prop is optional.
Full code: examples/core.md
A namespace scopes the keys, and values are named placeholders.
const t = useTranslations("Profile");
t("greeting", { name: user.name }); // "Hello, Jane!"
t("unreadCount", { count: messages.length });Full code: examples/core.md
The plural form is chosen by the locale's own CLDR rules, and # renders the formatted count. =0 matches exactly zero, which is distinct from the zero CLDR category.
{
"itemCount": "{count, plural, =0 {No items} one {# item} other {# items}}"
}Ordinals use selectordinal; enum-valued messages use select.
Full code: examples/core.md, ordinals and nesting in examples/pluralization.md
Tags in the message are developer-defined and map to components, so the sentence stays whole for the translator.
t.rich("terms", {
link: (chunks) => <a href="/terms">{chunks}</a>,
bold: (chunks) => <strong>{chunks}</strong>,
});Full code: examples/core.md
One hook covers dates, numbers, lists and relative time, each backed by the matching Intl formatter.
const format = useFormatter();
format.dateTime(date, { year: "numeric", month: "long", day: "numeric" });
format.number(amount, { style: "currency", currency });
format.relativeTime(date, useNow({ updateInterval: 60_000 }));Full code: examples/core.md, auto-updating relative time and lists in examples/formatting.md
generateStaticParams enumerates the locale variants, and setRequestLocale makes each one renderable without a request.
export function generateStaticParams() {
return routing.locales.flatMap((locale) =>
slugs.map((slug) => ({ locale, slug })),
);
}Full code: examples/core.md
The navigation APIs from createNavigation swap the locale while preserving the current path.
const router = useRouter();
const pathname = usePathname();
router.replace(pathname, { locale: newLocale });Full code: examples/core.md
Register the message shape on the AppConfig interface (v4.0+) and a wrong key becomes a compile error.
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof en;
Formats: typeof formats;
}
}Set allowArbitraryExtensions: true in tsconfig.json to import the JSON. For inferred argument types, the plugin's experimental.createMessagesDeclaration generates them.
Full code: examples/core.md
getTranslations works where hooks cannot. Metadata runs outside the component tree, so the locale is passed rather than inferred.
const t = await getTranslations({ locale, namespace: "Metadata" });
return { title: t("title"), description: t("description") };Full code: examples/core.md
</patterns>
<red_flags>
Breaks at runtime:
params without awaiting it — it is a Promise from Next.js 15 onwardssetRequestLocale after a hook has already read the locale — the hook fails, and theerror names the hook rather than the ordering
NextIntlClientProvider — no messages reach themmiddleware.ts on Next.js 16 — it is not picked up, so no locale is detectedt() on a message containing markup — development reports FORMATTING_ERROR and rendersnamespace.key; in production a call without values renders the tags as literal text
useTranslations inside generateMetadata or a Server Action — it is a hook, and both run outsidethe component tree; getTranslations({ locale, namespace }) is what works there
Surprising behaviour:
setRequestLocale costs static rendering silently: the route still works, dynamically.generateStaticParams is the other half, and a route missing either one is rendered per request
t.rich() tag functions receive chunks as an array, not a single elementuseNow() only ticks on the client, so SSR shows the initial value until hydrationuseTranslations puts every key in one global space, where names collidefrom their Accept-Language preference — localeCookie in the routing config changes both
Anti-patterns with the code that fixes them: reference.md.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.