| name | i18n-manager |
| description | Expert in internationalization (i18n) and localization (l10n) with i18next, react-intl, multi-language support, RTL layouts, locale-specific formatting, translation management, and automated text extraction |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep, Task |
Internationalization Manager
Expert skill for building multi-language applications with complete i18n/l10n support. Specializes in i18next, react-intl, RTL layouts, locale formatting, translation workflows, and automated text extraction.
Core Capabilities
1. i18n Setup & Configuration
- i18next: Most popular i18n framework
- react-i18next: React integration
- react-intl: FormatJS implementation
- next-i18next: Next.js integration
- vue-i18n: Vue.js integration
- Language Detection: Browser, URL, cookie
- Namespace Organization: Modular translations
2. Translation Management
- JSON Format: Simple key-value translations
- Nested Keys: Hierarchical organization
- Pluralization: ICU message format
- Interpolation: Dynamic values in translations
- Context: Gender, formality variations
- Fallback: Missing translation handling
- Lazy Loading: Load translations on demand
3. RTL (Right-to-Left) Support
- Arabic: العربية
- Hebrew: עברית
- Persian: فارسی
- Urdu: اردو
- CSS Logical Properties: start/end vs left/right
- Mirroring: Icons, layouts, animations
- BiDi Text: Mixed LTR/RTL content
4. Locale-Specific Formatting
- Dates: Intl.DateTimeFormat
- Numbers: Intl.NumberFormat
- Currency: Format with symbols
- Relative Time: "2 hours ago"
- Lists: Conjunction formatting
- Phone Numbers: Country-specific formats
- Addresses: Locale-specific orders
5. Text Extraction & Automation
- Hardcoded Text Detection: Find non-i18n strings
- Key Generation: Auto-generate translation keys
- Missing Translations: Detect untranslated keys
- Unused Keys: Find and remove dead translations
- Translation Coverage: Report per-language coverage
- CI/CD Integration: Automated checks
6. Translation Workflow
- Developer Flow: Mark strings for translation
- Translator Flow: Provide context and examples
- Review Flow: Approval process
- Update Flow: Handle translation updates
- Version Control: Track translation changes
- Translation Services: Crowdin, Lokalise, POEditor
7. SEO & Performance
- URL Localization: /en/about, /es/acerca
- Meta Tags: Translated titles, descriptions
- hreflang: Search engine language hints
- Language Switcher: User language selection
- Code Splitting: Load only needed translations
- Caching: CDN and browser caching
Workflow
Phase 1: i18n Planning
Define Requirements
- Which languages? (start with 2-3)
- RTL support needed?
- Date/currency formats?
- Translation workflow?
Choose Solution
- i18next (flexible, popular)
- react-intl (FormatJS, enterprise)
- Custom solution
- Library integration
Plan Structure
- Translation file organization
- Namespace strategy
- Key naming convention
- Fallback language
Phase 2: Implementation
Setup i18n
- Install dependencies
- Configure i18n
- Create translation files
- Add language detector
Add Translations
- Extract hardcoded text
- Generate translation keys
- Create base language
- Add fallback translations
Implement RTL
- Add dir="rtl" support
- Convert CSS to logical properties
- Mirror icons/images
- Test layout
Add Formatting
- Date formatting
- Number formatting
- Currency formatting
- Relative time
Phase 3: Translation Workflow
Developer Tools
- Missing key warnings
- Translation extraction
- Key generation scripts
- Hot reload translations
Translator Tools
- Translation platform integration
- Context screenshots
- Translation memory
- Plural/gender rules
Quality Assurance
- Translation coverage reports
- Unused key detection
- Consistency checks
- Automated tests
Implementation Patterns
i18next Setup
// i18n.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import Backend from 'i18next-http-backend'
i18n
// Load translations from backend
.use(Backend)
// Detect user language
.use(LanguageDetector)
// Pass i18n instance to react-i18next
.use(initReactI18next)
// Initialize
.init({
// Fallback language
fallbackLng: 'en',
// Supported languages
supportedLngs: ['en', 'es', 'fr', 'de', 'ar', 'he'],
// Debug mode (development only)
debug: process.env.NODE_ENV === 'development',
// Interpolation options
interpolation: {
escapeValue: false, // React already escapes
},
// Backend options
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
// Detection options
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
caches: ['localStorage', 'cookie'],
},
// Namespaces
ns: ['common', 'auth', 'dashboard'],
defaultNS: 'common',
// React options
react: {
useSuspense: true,
},
})
export default i18n
Translation Files
// public/locales/en/common.json
{
"app": {
"title": "My Application",
"description": "Welcome to our amazing app"
},
"navigation": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm"
},
"messages": {
"success": "Operation completed successfully",
"error": "An error occurred",
"loading": "Loading..."
},
"validation": {
"required": "This field is required",
"email": "Please enter a valid email",
"minLength": "Must be at least {{count}} characters"
}
}
// public/locales/en/auth.json
{
"login": {
"title": "Log In",
"email": "Email Address",
"password": "Password",
"submit": "Log In",
"forgotPassword": "Forgot password?",
"noAccount": "Don't have an account?",
"signUp": "Sign up"
},
"register": {
"title": "Create Account",
"submit": "Sign Up",
"hasAccount": "Already have an account?",
"logIn": "Log in"
}
}
Using Translations in Components
// Component.tsx
import { useTranslation } from 'react-i18next'
export function Welcome() {
const { t, i18n } = useTranslation('common')
return (
<div>
{/* Simple translation */}
<h1>{t('app.title')}</h1>
<p>{t('app.description')}</p>
{/* Translation with interpolation */}
<p>{t('validation.minLength', { count: 8 })}</p>
{/* Current language */}
<p>Language: {i18n.language}</p>
{/* Change language */}
<button onClick={() => i18n.changeLanguage('es')}>
Español
</button>
</div>
)
}
Pluralization
// Pluralization in i18next (ICU format)
{
"items": "{{count}} item",
"items_one": "{{count}} item",
"items_other": "{{count}} items",
// Complex pluralization
"cart": {
"zero": "Your cart is empty",
"one": "You have {{count}} item in your cart",
"other": "You have {{count}} items in your cart"
}
}
// Usage
const { t } = useTranslation()
<p>{t('items', { count: 0 })}</p> // "0 items"
<p>{t('items', { count: 1 })}</p> // "1 item"
<p>{t('items', { count: 5 })}</p> // "5 items"
Context-Aware Translations
{
"friend": "Friend",
"friend_male": "Friend (male)",
"friend_female": "Friend (female)",
"welcome": "Welcome",
"welcome_formal": "Welcome (formal)",
"welcome_informal": "Hi there!"
}
// Usage with context
const { t } = useTranslation()
<p>{t('friend', { context: 'male' })}</p> // "Friend (male)"
<p>{t('welcome', { context: 'formal' })}</p> // "Welcome (formal)"
RTL Support
// RTLProvider.tsx
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
export function RTLProvider({ children }: { children: React.ReactNode }) {
const { i18n } = useTranslation()
useEffect(() => {
const isRTL = RTL_LANGUAGES.includes(i18n.language)
document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
document.documentElement.lang = i18n.language
}, [i18n.language])
return <>{children}</>
}
// App.tsx
import { RTLProvider } from './RTLProvider'
function App() {
return (
<RTLProvider>
<YourApp />
</RTLProvider>
)
}
RTL-Aware CSS
/* ❌ BAD - Hard-coded direction */
.sidebar {
margin-left: 20px;
text-align: left;
}
/* ✅ GOOD - Logical properties (auto RTL) */
.sidebar {
margin-inline-start: 20px;
text-align: start;
}
/* ✅ GOOD - RTL-specific rules */
[dir="rtl"] .sidebar {
margin-right: 20px;
margin-left: 0;
}
/* ✅ GOOD - CSS logical properties */
.container {
padding-inline: 20px; /* horizontal padding */
padding-block: 10px; /* vertical padding */
border-inline-start: 1px; /* left border (LTR), right border (RTL) */
border-inline-end: 1px; /* right border (LTR), left border (RTL) */
}
Date Formatting
// DateFormatter.tsx
import { useTranslation } from 'react-i18next'
export function DateFormatter({ date }: { date: Date }) {
const { i18n } = useTranslation()
// Automatic locale-based formatting
const formatted = new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date)
return <time dateTime={date.toISOString()}>{formatted}</time>
}
// Usage
<DateFormatter date={new Date('2024-01-15')} />
// en: "January 15, 2024"
// es: "15 de enero de 2024"
// fr: "15 janvier 2024"
// de: "15. Januar 2024"
Number & Currency Formatting
// NumberFormatter.tsx
import { useTranslation } from 'react-i18next'
export function CurrencyFormatter({
amount,
currency = 'USD',
}: {
amount: number
currency?: string
}) {
const { i18n } = useTranslation()
const formatted = new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency,
}).format(amount)
return <span>{formatted}</span>
}
// Usage
<CurrencyFormatter amount={1234.56} currency="USD" />
// en-US: "$1,234.56"
// de-DE: "1.234,56 $"
// fr-FR: "1 234,56 $US"
<CurrencyFormatter amount={1234.56} currency="EUR" />
// en-US: "€1,234.56"
// de-DE: "1.234,56 €"
// fr-FR: "1 234,56 €"
Language Switcher
// LanguageSwitcher.tsx
import { useTranslation } from 'react-i18next'
const LANGUAGES = [
{ code: 'en', name: 'English', flag: '🇬🇧' },
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
{ code: 'ar', name: 'العربية', flag: '🇸🇦', rtl: true },
{ code: 'he', name: 'עברית', flag: '🇮🇱', rtl: true },
]
export function LanguageSwitcher() {
const { i18n } = useTranslation()
const changeLanguage = async (lng: string) => {
await i18n.changeLanguage(lng)
// Update URL (optional)
const url = new URL(window.location.href)
url.searchParams.set('lng', lng)
window.history.pushState({}, '', url)
// Update dir attribute
const isRTL = LANGUAGES.find((l) => l.code === lng)?.rtl
document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
}
return (
<div className="language-switcher">
<select
value={i18n.language}
onChange={(e) => changeLanguage(e.target.value)}
aria-label="Select language"
>
{LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.name}
</option>
))}
</select>
</div>
)
}
Translation Extraction Script
// scripts/extract-translations.ts
import fs from 'fs'
import path from 'path'
import { glob } from 'glob'
interface Translation {
key: string
defaultValue: string
file: string
line: number
}
async function extractTranslations() {
const translations: Translation[] = []
// Find all source files
const files = await glob('src/**/*.{ts,tsx}')
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8')
const lines = content.split('\n')
// Find t('key') or t("key") calls
const tRegex = /t\(['"]([^'"]+)['"]/g
const matches = content.matchAll(tRegex)
for (const match of matches) {
const key = match[1]
const lineNumber = content.substring(0, match.index).split('\n').length
translations.push({
key,
defaultValue: '', // Extract from default value if present
file,
line: lineNumber,
})
}
}
// Group by namespace
const byNamespace: Record<string, Translation[]> = {}
for (const trans of translations) {
const [namespace] = trans.key.split('.')
if (!byNamespace[namespace]) {
byNamespace[namespace] = []
}
byNamespace[namespace].push(trans)
}
// Output JSON files
for (const [namespace, keys] of Object.entries(byNamespace)) {
const outputPath = path.join('public', 'locales', 'en', `${namespace}.json`)
// Load existing translations
let existing: Record<string, any> = {}
if (fs.existsSync(outputPath)) {
existing = JSON.parse(fs.readFileSync(outputPath, 'utf-8'))
}
// Add new keys
for (const { key } of keys) {
const keyPath = key.split('.')
let current = existing
for (let i = 1; i < keyPath.length; i++) {
const part = keyPath[i]
if (i === keyPath.length - 1) {
if (!current[part]) {
current[part] = `TODO: Translate ${key}`
}
} else {
if (!current[part]) {
current[part] = {}
}
current = current[part]
}
}
}
// Write back
fs.writeFileSync(outputPath, JSON.stringify(existing, null, 2))
}
console.log(`✅ Extracted ${translations.length} translation keys`)
}
extractTranslations()
Missing Translation Detection
// scripts/check-translations.ts
import fs from 'fs'
import path from 'path'
const SUPPORTED_LANGUAGES = ['en', 'es', 'fr', 'de', 'ar', 'he']
const NAMESPACES = ['common', 'auth', 'dashboard']
function loadTranslations(lang: string, namespace: string): Record<string, any> {
const filePath = path.join('public', 'locales', lang, `${namespace}.json`)
if (!fs.existsSync(filePath)) {
return {}
}
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
}
function getAllKeys(obj: Record<string, any>, prefix = ''): string[] {
let keys: string[] = []
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
if (typeof value === 'object' && value !== null) {
keys = keys.concat(getAllKeys(value, fullKey))
} else {
keys.push(fullKey)
}
}
return keys
}
function checkTranslations() {
const baseLanguage = 'en'
let hasErrors = false
for (const namespace of NAMESPACES) {
const baseTranslations = loadTranslations(baseLanguage, namespace)
const baseKeys = new Set(getAllKeys(baseTranslations))
console.log(`\n📦 Namespace: ${namespace}`)
console.log(` Base keys (${baseLanguage}): ${baseKeys.size}`)
for (const lang of SUPPORTED_LANGUAGES) {
if (lang === baseLanguage) continue
const translations = loadTranslations(lang, namespace)
const keys = new Set(getAllKeys(translations))
// Missing keys
const missing = Array.from(baseKeys).filter((key) => !keys.has(key))
// Extra keys (not in base)
const extra = Array.from(keys).filter((key) => !baseKeys.has(key))
if (missing.length > 0 || extra.length > 0) {
hasErrors = true
console.log(`\n ❌ ${lang}:`)
if (missing.length > 0) {
console.log(` Missing ${missing.length} keys:`)
missing.slice(0, 5).forEach((key) => console.log(` - ${key}`))
if (missing.length > 5) {
console.log(` ... and ${missing.length - 5} more`)
}
}
if (extra.length > 0) {
console.log(` Extra ${extra.length} keys:`)
extra.slice(0, 5).forEach((key) => console.log(` - ${key}`))
}
} else {
console.log(` ✅ ${lang}: Complete (${keys.size} keys)`)
}
}
}
if (hasErrors) {
console.log('\n❌ Translation check failed\n')
process.exit(1)
} else {
console.log('\n✅ All translations are complete!\n')
}
}
checkTranslations()
Best Practices
Translation Keys
// ✅ GOOD - Hierarchical, descriptive keys
t('auth.login.title')
t('dashboard.widgets.revenue.title')
t('errors.validation.required')
// ❌ BAD - Flat, unclear keys
t('loginTitle')
t('title1')
t('err1')
Context & Examples
{
"button": {
"save": "Save",
"_comment": "Button text for saving changes",
"_example": "Click 'Save' to save your changes"
}
}
Avoid HTML in Translations
// ❌ BAD
{
"message": "Click <a href='/help'>here</a> for help"
}
// ✅ GOOD
{
"message": "Click {{link}} for help",
"link": "here"
}
// Usage
<Trans i18nKey="message">
Click <a href="/help">{{link: t('link')}}</a> for help
</Trans>
Pluralization
{
"items_zero": "No items",
"items_one": "{{count}} item",
"items_other": "{{count}} items"
}
Gender & Formality
{
"welcome_male": "Welcome, Mr. {{name}}",
"welcome_female": "Welcome, Ms. {{name}}",
"welcome_formal": "Good day, {{name}}",
"welcome_informal": "Hey {{name}}!"
}
When to Use This Skill
Activate this skill when you need to:
- Add multi-language support to an application
- Setup i18next or react-intl
- Implement RTL layouts
- Extract hardcoded text for translation
- Create translation management workflows
- Add locale-specific formatting (dates, numbers, currency)
- Build language switchers
- Integrate with translation services (Crowdin, Lokalise)
- Detect missing translations
- Optimize i18n performance
Integration with Agents
// Agent Explore → Find all hardcoded strings
// i18n-manager → Extract and create translation keys
// Example workflow:
1. Agent scans codebase for hardcoded text
2. Agent finds: 150 strings not using t()
3. i18n-manager generates translation keys
4. i18n-manager creates JSON files for all languages
5. Agent verifies: All text now translatable
Output Format
When implementing i18n, provide:
- Complete i18n Setup: Configuration files
- Translation Files: JSON with all keys
- Component Integration: How to use translations
- RTL Support: CSS and layout changes
- Extraction Scripts: Automated text extraction
- Testing Guide: How to test translations
- Documentation: Translation workflow for team
Always build applications that are accessible to users worldwide in their native language.