Aller au contenu principal

Skill: dev-i18n

Inline

Internationalization (i18n) and localization (l10n) for web and mobile applications. Libraries next-intl, react-i18next, vue-i18n, formatjs, flutter_localizations, ARB. Trigger when the user wants to add multiple languages, extract strings, handle plurals, date/number formats, or when translation files are detected.

Configuration​

PropertyValue
Contextinline
Pre-approved toolsNone declared: the project's permission settings apply
Triggerkeywords: dev, i18n, d'accord

Detailed description​

Internationalization (i18n)

Choosing your lib​

Web​

LibStackStrengthAvoid
next-intlNext.js 13+ App RouterServer Components first, type-safe, localized routesPages Router projects (use next-i18next)
react-i18nextReact vanilla / SPAMature, large ecosystem, pluginsHeavy for SSR without effort
formatjs (react-intl)ReactICU MessageFormat standardMore verbose boilerplate
vue-i18nVue 3 / NuxtNative, Composition API, lazy loadVue-specific
svelte-i18n / paraglideSvelte/SvelteKitLean, compile-time (paraglide)Smaller ecosystem

Mobile​

LibStack
flutter_localizations + intlFlutter official, ARB files
slangFlutter alternative, type-safe, code-generation
react-native-localize + i18nextReact Native

next-intl (Next.js App Router)​

Setup​

npm install next-intl
messages/
fr.json
en.json
app/
[locale]/
layout.tsx
page.tsx
middleware.ts
i18n/
request.ts
routing.ts

Config​

// i18n/routing.ts
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
locales: ["fr", "en"],
defaultLocale: "fr",
localePrefix: "as-needed", // /en/about, /about (default locale)
});
// middleware.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
matcher: ["/", "/(fr|en)/:path*"],
};

Server Component usage​

// app/[locale]/page.tsx
import { getTranslations } from "next-intl/server";

export default async function Page() {
const t = await getTranslations("home");
return <h1>{t("title")}</h1>;
}

Client Component usage​

"use client";
import { useTranslations } from "next-intl";

export function Greeting() {
const t = useTranslations("home");
return <p>{t("welcome", { name: "Alice" })}</p>;
}

Plurals (ICU)​

{
"notifications": "{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
}
t("notifications", { count: 3 }); // "3 notifications"

Date/number formatting​

import { useFormatter } from "next-intl";

const format = useFormatter();
format.dateTime(new Date(), { dateStyle: "long" }); // "November 4, 2026"
format.number(1234.5, { style: "currency", currency: "EUR" }); // "€1,234.50"
format.relativeTime(date, now); // "2 days ago"

react-i18next (SPA)​

npm install react-i18next i18next i18next-browser-languagedetector
// i18n/config.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

import fr from "./locales/fr.json";
import en from "./locales/en.json";

i18n.use(LanguageDetector).use(initReactI18next).init({
resources: { fr: { translation: fr }, en: { translation: en } },
fallbackLng: "fr",
interpolation: { escapeValue: false },
});
import { useTranslation } from "react-i18next";

function Welcome() {
const { t, i18n } = useTranslation();
return (
<>
<h1>{t("welcome")}</h1>
<button onClick={() => i18n.changeLanguage("en")}>EN</button>
</>
);
}

Flutter (flutter_localizations + intl)​

# pubspec.yaml
dependencies:
flutter_localizations:
sdk: flutter
intl: any

flutter:
generate: true
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_fr.arb
output-localization-file: app_localizations.dart
// lib/l10n/app_fr.arb
{
"@@locale": "fr",
"welcome": "Bienvenue",
"notifications": "{count, plural, =0{Aucune notification} one{{count} notification} other{{count} notifications}}",
"@notifications": {
"placeholders": { "count": { "type": "int" } }
}
}
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);

Text(AppLocalizations.of(context)!.welcome);
Text(AppLocalizations.of(context)!.notifications(count));

Vue 3 (vue-i18n)​

npm install vue-i18n@9
// i18n.ts
import { createI18n } from "vue-i18n";
import fr from "./locales/fr.json";
import en from "./locales/en.json";

export const i18n = createI18n({
legacy: false,
locale: "fr",
fallbackLocale: "en",
messages: { fr, en },
});
<template>
<h1>{{ t('welcome') }}</h1>
</template>

<script setup>
import { useI18n } from "vue-i18n";
const { t } = useI18n();
</script>

Best practices​

File structure​

Organize by namespace (not by screen):

messages/
fr/
common.json # Buttons, generic messages
errors.json # Error messages
auth.json # Auth screens (shared)
dashboard.json # Dashboard section
en/
...

Bad: 1 file per screen (duplication of shared messages).

Translation keys​

{
"dashboard": {
"header": {
"title": "Tableau de bord",
"subtitle": "Vue d'ensemble"
},
"metrics": {
"users": "Utilisateurs actifs",
"revenue": "Revenu"
}
}
}

Conventions:

  • kebab-case or camelCase depending on the lib (camelCase for JS)
  • Hierarchical: group by feature
  • Descriptive: dashboard.metrics.users not label1
  • Typed placeholders: {count, plural, ...}, {name}

ICU MessageFormat​

Universal standard for plurals, gender, select:

{count, plural,
=0 {No items}
one {One item}
other {# items}
}

{gender, select,
male {He}
female {She}
other {They}
}

Supported by: next-intl, formatjs, flutter intl.

Locale negotiation​

// Priority order
1. User preference (stored in DB or cookie)
2. URL path (/fr/..., /en/...)
3. Accept-Language header
4. Fallback locale

String extraction​

Tools to extract strings from code into translation files:

StackTool
next-intl@formatjs/cli with extract
react-i18nexti18next-parser
Flutterflutter gen-l10n
formatjsformatjs extract
# i18next-parser example
npx i18next-parser 'src/**/*.{ts,tsx}' --output 'public/locales/$LOCALE/$NAMESPACE.json'

Common pitfalls​

PitfallPrevention
String concatenationNEVER. Use placeholders: t("hello", { name })
Hardcoded strings in codeAutomatic extractor + lint rule (i18next/no-literal-string)
Plurals with manual conditions{count === 1 ? "item": "items"} doesn't work in all languages (Arabic, Russian: 6 forms) → ICU plural
Fixed word orderSentences change order between languages → interpolate, don't split
Hardcoded formatsUse Intl.DateTimeFormat, Intl.NumberFormat, not date.toLocaleString() without options
RTL forgottenTest with Arabic/Hebrew: dir="rtl", text-align: start instead of left
Variable length"OK" in English → "D'accord" in French (2x longer). Flexible layout.

RTL examples​

/* Instead of: */
.card { padding-left: 16px; text-align: left; }

/* Write: */
.card { padding-inline-start: 16px; text-align: start; }

Typical workflow​

1. Extract​

npx i18next-parser 'src/**/*.tsx' -o 'messages/$LOCALE.json'

2. Translate​

Hand off to translators via:

  • Lokalise, Crowdin, Phrase (SaaS, collaboration)
  • JSON/ARB files in git (small projects)
  • DeepL / LLM for draft, native human review mandatory

3. Validate​

# Check that all locales have the same keys
npx i18next-resources-for-ts --check

# Or custom script
node scripts/check-i18n.js

4. Integrate​

CI: fail if a key is missing in a locale.

Multi-language SEO​

// next-intl
export async function generateMetadata({ params: { locale } }) {
return {
alternates: {
canonical: `/${locale}`,
languages: { fr: "/fr", en: "/en" },
},
};
}

Add hreflang in <head> and sitemap.xml.

Complement with the foundation​

  • Rule .claude/rules/accessibility.md: lang="fr", dir="rtl" for a11y
  • Skill growth-localization: localization strategy (markets, pricing per country)

Expected output​

  1. Structure: namespaces (not by screen), descriptive hierarchical keys
  2. Plurals in ICU MessageFormat (never manual conditions)
  3. Dates/numbers via Intl or wrapper lib (never hardcoded)
  4. Extractor configured (i18next-parser, formatjs, flutter gen-l10n)
  5. CI check: validate that all locales have the same keys
  6. RTL tested if RTL language targeted (CSS logical properties)

Rules​

IMPORTANT: NEVER concatenate strings to build sentences. Use placeholders.

IMPORTANT: NEVER count === 1 ? "item": "items". Use ICU plurals.

IMPORTANT: NEVER hardcode formatted dates/numbers. Use Intl.DateTimeFormat or wrapper lib.

YOU MUST extract all user-visible strings (not "Error" in code).

YOU MUST add a CI check that validates translation completeness between locales.

NEVER commit LLM translations without native speaker human review (variable quality on nuances).

NEVER use padding-left / margin-right / text-align: left in an RTL-supported app. Use logical properties.

See also​

FormatJS publishes two skills of its own — localization-review (ICU message review) and translate — in formatjs/formatjs (MIT per package, pin bc0fd225), indexed at its .well-known/agent-skills. For Flutter, the Flutter team's flutter-setup-localization (flutter/agent-plugins). Library setup (next-intl, react-i18next, vue-i18n) stays here: no vendor ships a setup skill.

Automatic triggering​

This skill is automatically activated when:

  • The matching keywords are detected in the conversation
  • The task context matches the skill's domain

Triggering examples​

  • "I want to dev..."
  • "I want to i18n..."
  • "I want to d'accord..."

Context inline​

Inline means the skill runs in the conversation (the default, no context key):

  • Sees the full history
  • Its work is visible as it happens
  • Right for almost every skill

See also​