Skip to content
Back to skills

Dev I18n

ASecurity

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.

  • 5 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added May 28, 2026
ai-agentsgobashreactvuenextjsnodegitapi

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 4, 2026

npx -y skills add christopherlouet/claude-base --skill dev-i18n --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dev I18n?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dev I18n
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/christopherlouet-dev-i18n/badge)](https://www.skillsdirectory.com/skills/christopherlouet-dev-i18n)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dev-i18n
description: 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.
---

# Internationalization (i18n)

## Choosing your lib

### Web

| Lib | Stack | Strength | Avoid |
|-----|-------|----------|-------|
| **next-intl** | Next.js 13+ App Router | Server Components first, type-safe, localized routes | Pages Router projects (use next-i18next) |
| **react-i18next** | React vanilla / SPA | Mature, large ecosystem, plugins | Heavy for SSR without effort |
| **formatjs (react-intl)** | React | ICU MessageFormat standard | More verbose boilerplate |
| **vue-i18n** | Vue 3 / Nuxt | Native, Composition API, lazy load | Vue-specific |
| **svelte-i18n** / **paraglide** | Svelte/SvelteKit | Lean, compile-time (paraglide) | Smaller ecosystem |

### Mobile

| Lib | Stack |
|-----|-------|
| **flutter_localizations + intl** | Flutter official, ARB files |
| **slang** | Flutter alternative, type-safe, code-generation |
| **react-native-localize + i18next** | React Native |

## next-intl (Next.js App Router)

### Setup

```bash
npm install next-intl
```

```
messages/
  fr.json
  en.json
app/
  [locale]/
    layout.tsx
    page.tsx
middleware.ts
i18n/
  request.ts
  routing.ts
```

### Config

```ts
// 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)
});
```

```ts
// 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

```tsx
// 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

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

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

### Plurals (ICU)

```json
{
  "notifications": "{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
}
```

```tsx
t("notifications", { count: 3 });  // "3 notifications"
```

### Date/number formatting

```tsx
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)

```bash
npm install react-i18next i18next i18next-browser-languagedetector
```

```ts
// 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 },
});
```

```tsx
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)

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

flutter:
  generate: true
```

```yaml
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_fr.arb
output-localization-file: app_localizations.dart
```

```json
// 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" } }
  }
}
```

```dart
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)

```bash
npm install vue-i18n@9
```

```ts
// 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 },
});
```

```vue
<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

```json
{
  "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

```ts
// 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:

| Stack | Tool |
|-------|------|
| next-intl | `@formatjs/cli` with extract |
| react-i18next | `i18next-parser` |
| Flutter | `flutter gen-l10n` |
| formatjs | `formatjs extract` |

```bash
# i18next-parser example
npx i18next-parser 'src/**/*.{ts,tsx}' --output 'public/locales/$LOCALE/$NAMESPACE.json'
```

## Common pitfalls

| Pitfall | Prevention |
|---------|------------|
| String concatenation | NEVER. Use placeholders: `t("hello", { name })` |
| Hardcoded strings in code | Automatic 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 order | Sentences change order between languages → interpolate, don't split |
| Hardcoded formats | Use `Intl.DateTimeFormat`, `Intl.NumberFormat`, not `date.toLocaleString()` without options |
| RTL forgotten | Test 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

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

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

## Typical workflow

### 1. Extract

```bash
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

```bash
# 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

```tsx
// 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`](https://github.com/formatjs/formatjs/tree/main/.agents/skills) (MIT per package, pin `bc0fd225`), indexed at its `.well-known/agent-skills`. For Flutter, the Flutter team's `flutter-setup-localization` ([`flutter/agent-plugins`](https://github.com/flutter/agent-plugins)). Library setup (next-intl, react-i18next, vue-i18n) stays here: no vendor ships a setup skill.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.