| name | i18n-best-practices |
| description | Guide for internationalization with next-intl. Use when adding links, translations, or localized URLs. Always use Link from @i18n/routing. |
Internationalization (i18n) Best Practices Skill
Purpose
Enforce correct i18n patterns using next-intl to prevent broken navigation and SEO issues.
⚠️ CRITICAL: Always Use Link from @i18n/routing
For ALL internal navigation, use Link from @i18n/routing, NOT next/link.
ESLint will warn on next/link imports in components.
Why This Matters
Using next/link directly loses the locale prefix on navigation:
- User on
/es/barcelona clicks link
- With
next/link: Goes to /barcelona (loses Spanish locale)
- With
@i18n/routing Link: Goes to /es/barcelona (correct)
Correct Pattern
import { Link } from "@i18n/routing";
export function MyComponent() {
return <Link href="/barcelona">Barcelona</Link>;
}
Wrong Pattern
import Link from "next/link";
export function MyComponent() {
return <Link href="/barcelona">Barcelona</Link>;
}
Exceptions (Rare)
- Primitives with manual locale handling
- External-only links (use
<a> tag)
Project i18n Setup
| Aspect | Configuration |
|---|
| Library | next-intl ^4.6.1 |
| Locales | ca (default), es, en |
| Prefix Strategy | as-needed (no prefix for default ca) |
| Routing Config | i18n/routing.ts |
| Messages | messages/{locale}.json |
Navigation Exports from @i18n/routing
import {
Link,
redirect,
usePathname,
useRouter,
getPathname,
} from "@i18n/routing";
Server vs Client Components
Server Components
import { getTranslations, getLocale } from "next-intl/server";
export default async function ServerComponent() {
const locale = await getLocale();
const t = await getTranslations("common");
return <h1>{t("title")}</h1>;
}
Client Components
"use client";
import { useTranslations, useLocale } from "next-intl";
export function ClientComponent() {
const locale = useLocale();
const t = useTranslations("common");
return <button>{t("submit")}</button>;
}
JSON-LD URLs Must Be Localized
For structured data (JSON-LD), always use toLocalizedUrl:
import { toLocalizedUrl } from "@utils/i18n-seo";
const url = toLocalizedUrl("/barcelona/avui", locale);
const url = `https://example.com/barcelona/avui`;
Breadcrumbs in JSON-LD
Use the tested helper function:
import { generateBreadcrumbList } from "@components/partials/seo-meta";
const breadcrumbs = generateBreadcrumbList(items, locale);
Adding New Translations
-
Add keys to all locale files in messages/:
messages/ca.json
messages/es.json
messages/en.json
-
Prefer reusing existing keys before adding new ones
-
Use namespaced keys:
{
"common": {
"submit": "Enviar",
"cancel": "Cancel·lar"
},
"events": {
"title": "Esdeveniments"
}
}
Adding a New Locale
-
Update types/i18n.ts:
export type Locale = "ca" | "es" | "en" | "fr";
-
Create messages/fr.json with all translations
-
Update i18n/routing.ts:
export const routing = defineRouting({
locales: ["ca", "es", "en", "fr"],
defaultLocale: "ca",
});
-
Update loader map in i18n/request.ts
Checklist Before i18n Changes
Files to Reference