Aller au contenu principal

Skill: dev-i18n

Fork

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
Contextfork
Allowed toolsRead, Write, Edit, Bash, Glob, Grep
Keywordsdev, 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

  • Agent doc-i18n: helps with documentation translation
  • 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.

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 fork

Fork means the skill runs in an isolated context:

  • Does not pollute the main conversation
  • Results are returned cleanly
  • Ideal for autonomous tasks

See also