Install
$ agentstack add skill-christopherlouet-claude-base-dev-i18n ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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
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 {t("title")};
}
Client Component usage
"use client";
import { useTranslations } from "next-intl";
export function Greeting() {
const t = useTranslations("home");
return {t("welcome", { name: "Alice" })};
}
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 (
<>
{t("welcome")}
i18n.changeLanguage("en")}>EN
);
}
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 },
});
{{ t('welcome') }}
import { useI18n } from "vue-i18n";
const { t } = useI18n();
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.usersnotlabel1 - 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:
| Stack | Tool | |-------|------| | next-intl | @formatjs/cli with extract | | react-i18next | i18next-parser | | Flutter | flutter gen-l10n | | formatjs | formatjs extract |
# 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
/* 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 `` 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
- Structure: namespaces (not by screen), descriptive hierarchical keys
- Plurals in ICU MessageFormat (never manual conditions)
- Dates/numbers via Intl or wrapper lib (never hardcoded)
- Extractor configured (i18next-parser, formatjs, flutter gen-l10n)
- CI check: validate that all locales have the same keys
- 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.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: christopherlouet
- Source: christopherlouet/claude-base
- License: MIT
- Homepage: https://christopherlouet.github.io/claude-base/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.