Claude Code Plugins

Community-maintained marketplace

Feedback

internationalization-i18n

@aj-geddes/useful-ai-prompts
4
0

Implement internationalization (i18n) and localization including message extraction, translation catalogs, pluralization rules, date/time/number formatting, RTL language support, and i18n libraries like i18next and gettext. Use for multi-language, translation, or localization needs.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name internationalization-i18n
description Implement internationalization (i18n) and localization including message extraction, translation catalogs, pluralization rules, date/time/number formatting, RTL language support, and i18n libraries like i18next and gettext. Use for multi-language, translation, or localization needs.

Internationalization (i18n) & Localization

Overview

Comprehensive guide to implementing internationalization and localization in applications. Covers message translation, pluralization, date/time/number formatting, RTL languages, and integration with popular i18n libraries.

When to Use

  • Building multi-language applications
  • Supporting international users
  • Implementing language switching
  • Formatting dates, times, and numbers for different locales
  • Supporting RTL (right-to-left) languages
  • Extracting and managing translation strings
  • Implementing pluralization rules
  • Setting up translation workflows

Instructions

1. i18next (JavaScript/TypeScript)

Basic Setup

// i18n.ts
import i18next from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

await i18next
  .use(Backend)
  .use(LanguageDetector)
  .init({
    fallbackLng: 'en',
    debug: process.env.NODE_ENV === 'development',

    interpolation: {
      escapeValue: false // React already escapes
    },

    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json'
    },

    detection: {
      order: ['querystring', 'cookie', 'localStorage', 'navigator'],
      caches: ['localStorage', 'cookie']
    }
  });

export default i18next;

Translation Files

// locales/en/translation.json
{
  "welcome": "Welcome to our app",
  "greeting": "Hello, {{name}}!",
  "itemCount": "You have {{count}} item",
  "itemCount_plural": "You have {{count}} items",
  "user": {
    "profile": "User Profile",
    "settings": "Settings",
    "logout": "Log out"
  },
  "validation": {
    "required": "This field is required",
    "email": "Please enter a valid email",
    "minLength": "Must be at least {{min}} characters"
  }
}

// locales/es/translation.json
{
  "welcome": "Bienvenido a nuestra aplicación",
  "greeting": "¡Hola, {{name}}!",
  "itemCount": "Tienes {{count}} artículo",
  "itemCount_plural": "Tienes {{count}} artículos",
  "user": {
    "profile": "Perfil de Usuario",
    "settings": "Configuración",
    "logout": "Cerrar sesión"
  },
  "validation": {
    "required": "Este campo es obligatorio",
    "email": "Por favor ingrese un correo válido",
    "minLength": "Debe tener al menos {{min}} caracteres"
  }
}

// locales/fr/translation.json
{
  "welcome": "Bienvenue dans notre application",
  "greeting": "Bonjour, {{name}} !",
  "itemCount": "Vous avez {{count}} article",
  "itemCount_plural": "Vous avez {{count}} articles",
  "user": {
    "profile": "Profil utilisateur",
    "settings": "Paramètres",
    "logout": "Se déconnecter"
  }
}

React Integration

// App.tsx
import { useTranslation } from 'react-i18next';
import './i18n';

export function App() {
  const { t, i18n } = useTranslation();

  const changeLanguage = (lng: string) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('greeting', { name: 'John' })}</p>
      <p>{t('itemCount', { count: 5 })}</p>

      {/* Language switcher */}
      <select
        value={i18n.language}
        onChange={(e) => changeLanguage(e.target.value)}
      >
        <option value="en">English</option>
        <option value="es">Español</option>
        <option value="fr">Français</option>
      </select>
    </div>
  );
}

// Component with namespace
export function UserProfile() {
  const { t } = useTranslation('user');

  return (
    <div>
      <h2>{t('profile')}</h2>
      <button>{t('logout')}</button>
    </div>
  );
}

Node.js/Express Backend

// i18n-middleware.ts
import i18next from 'i18next';
import Backend from 'i18next-fs-backend';
import middleware from 'i18next-http-middleware';

i18next
  .use(Backend)
  .use(middleware.LanguageDetector)
  .init({
    fallbackLng: 'en',
    preload: ['en', 'es', 'fr'],
    backend: {
      loadPath: './locales/{{lng}}/{{ns}}.json'
    }
  });

export const i18nMiddleware = middleware.handle(i18next);

// app.ts
import express from 'express';
import { i18nMiddleware } from './i18n-middleware';

const app = express();
app.use(i18nMiddleware);

app.get('/api/welcome', (req, res) => {
  res.json({
    message: req.t('welcome'),
    greeting: req.t('greeting', { name: 'User' })
  });
});

2. React-Intl (Format.js)

// IntlProvider setup
import { IntlProvider } from 'react-intl';
import messages_en from './translations/en.json';
import messages_es from './translations/es.json';

const messages = {
  en: messages_en,
  es: messages_es
};

export function App() {
  const [locale, setLocale] = useState('en');

  return (
    <IntlProvider locale={locale} messages={messages[locale]}>
      <YourApp />
    </IntlProvider>
  );
}

// Using translations
import { FormattedMessage, useIntl } from 'react-intl';

export function Welcome() {
  const intl = useIntl();

  return (
    <div>
      {/* Basic translation */}
      <h1>
        <FormattedMessage id="welcome" defaultMessage="Welcome" />
      </h1>

      {/* With variables */}
      <p>
        <FormattedMessage
          id="greeting"
          defaultMessage="Hello, {name}!"
          values={{ name: 'John' }}
        />
      </p>

      {/* Pluralization */}
      <p>
        <FormattedMessage
          id="itemCount"
          defaultMessage="{count, plural, =0 {No items} one {# item} other {# items}}"
          values={{ count: 5 }}
        />
      </p>

      {/* In code */}
      <button title={intl.formatMessage({ id: 'submit' })}>
        {intl.formatMessage({ id: 'submit' })}
      </button>
    </div>
  );
}

3. Python i18n (gettext)

# i18n.py
import gettext
import os

class I18n:
    def __init__(self, locale='en', domain='messages'):
        self.locale = locale
        self.domain = domain
        self._translator = None
        self._load_translations()

    def _load_translations(self):
        locale_dir = os.path.join(os.path.dirname(__file__), 'locales')
        try:
            self._translator = gettext.translation(
                self.domain,
                localedir=locale_dir,
                languages=[self.locale]
            )
        except FileNotFoundError:
            # Fall back to NullTranslations (no translation)
            self._translator = gettext.NullTranslations()

    def t(self, message, **kwargs):
        """Translate message with optional variable substitution"""
        translated = self._translator.gettext(message)
        if kwargs:
            return translated.format(**kwargs)
        return translated

    def tn(self, singular, plural, n, **kwargs):
        """Translate with pluralization"""
        translated = self._translator.ngettext(singular, plural, n)
        if kwargs:
            return translated.format(n=n, **kwargs)
        return translated

# Usage
i18n = I18n(locale='es')

print(i18n.t("Welcome to our app"))
print(i18n.t("Hello, {name}!", name="Juan"))
print(i18n.tn("You have {n} item", "You have {n} items", 5))
# Extracting messages for translation
# Install: pip install Babel

# babel.cfg
[python: **.py]

# Extract messages
# pybabel extract -F babel.cfg -o locales/messages.pot .

# Initialize new language
# pybabel init -i locales/messages.pot -d locales -l es

# Compile translations
# pybabel compile -d locales

4. Date and Time Formatting

JavaScript (Intl API)

// date-formatter.ts
export class DateFormatter {
  constructor(private locale: string) {}

  // Format date
  formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
    return new Intl.DateTimeFormat(this.locale, options).format(date);
  }

  // Predefined formats
  short(date: Date): string {
    return this.formatDate(date, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
  }

  long(date: Date): string {
    return this.formatDate(date, {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
      weekday: 'long'
    });
  }

  time(date: Date): string {
    return this.formatDate(date, {
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit'
    });
  }

  relative(date: Date): string {
    const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' });
    const diff = date.getTime() - Date.now();
    const days = Math.round(diff / (1000 * 60 * 60 * 24));

    if (Math.abs(days) < 1) {
      const hours = Math.round(diff / (1000 * 60 * 60));
      return rtf.format(hours, 'hour');
    }

    return rtf.format(days, 'day');
  }
}

// Usage
const enFormatter = new DateFormatter('en-US');
const esFormatter = new DateFormatter('es-ES');
const jaFormatter = new DateFormatter('ja-JP');

const date = new Date('2024-01-15');

console.log(enFormatter.short(date));  // Jan 15, 2024
console.log(esFormatter.short(date));  // 15 ene 2024
console.log(jaFormatter.short(date));  // 2024年1月15日

console.log(enFormatter.relative(new Date(Date.now() - 86400000)));  // yesterday

React-Intl Date Formatting

import { FormattedDate, FormattedTime, FormattedRelativeTime } from 'react-intl';

export function DateDisplay() {
  const date = new Date();

  return (
    <div>
      {/* Date */}
      <FormattedDate
        value={date}
        year="numeric"
        month="long"
        day="numeric"
      />

      {/* Time */}
      <FormattedTime value={date} />

      {/* Relative time */}
      <FormattedRelativeTime
        value={-1}
        unit="day"
        updateIntervalInSeconds={60}
      />
    </div>
  );
}

5. Number and Currency Formatting

// number-formatter.ts
export class NumberFormatter {
  constructor(private locale: string) {}

  // Format number
  formatNumber(value: number, options?: Intl.NumberFormatOptions): string {
    return new Intl.NumberFormat(this.locale, options).format(value);
  }

  // Currency
  currency(value: number, currency: string): string {
    return this.formatNumber(value, {
      style: 'currency',
      currency
    });
  }

  // Percentage
  percent(value: number): string {
    return this.formatNumber(value, {
      style: 'percent',
      minimumFractionDigits: 0,
      maximumFractionDigits: 2
    });
  }

  // Decimal
  decimal(value: number, decimals: number = 2): string {
    return this.formatNumber(value, {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals
    });
  }

  // Compact notation (1.2K, 1.5M)
  compact(value: number): string {
    return this.formatNumber(value, {
      notation: 'compact',
      compactDisplay: 'short'
    });
  }
}

// Usage
const enFormatter = new NumberFormatter('en-US');
const deFormatter = new NumberFormatter('de-DE');
const jaFormatter = new NumberFormatter('ja-JP');

console.log(enFormatter.currency(1234.56, 'USD'));  // $1,234.56
console.log(deFormatter.currency(1234.56, 'EUR'));  // 1.234,56 €
console.log(jaFormatter.currency(1234.56, 'JPY'));  // ¥1,235

console.log(enFormatter.percent(0.1234));  // 12.34%
console.log(enFormatter.compact(1234567));  // 1.2M

6. Pluralization Rules

// pluralization.ts
export class PluralRules {
  constructor(private locale: string) {}

  // Get plural category
  select(count: number): Intl.LDMLPluralRule {
    const pr = new Intl.PluralRules(this.locale);
    return pr.select(count);
  }

  // Format with pluralization
  format(count: number, forms: Record<Intl.LDMLPluralRule, string>): string {
    const rule = this.select(count);
    return forms[rule] || forms.other;
  }
}

// Usage
const enRules = new PluralRules('en');

console.log(enRules.format(0, {
  zero: 'No items',
  one: 'One item',
  other: '{{count}} items'
}));

console.log(enRules.format(1, {
  one: 'One item',
  other: '{{count}} items'
}));

// Different languages have different plural rules
const arRules = new PluralRules('ar'); // Arabic has 6 plural forms
const plRules = new PluralRules('pl'); // Polish has complex plural rules

ICU Message Format

// Using intl-messageformat
import IntlMessageFormat from 'intl-messageformat';

const message = new IntlMessageFormat(
  '{count, plural, =0 {No items} one {# item} other {# items}}',
  'en'
);

console.log(message.format({ count: 0 }));  // No items
console.log(message.format({ count: 1 }));  // 1 item
console.log(message.format({ count: 5 }));  // 5 items

// With gender
const genderMessage = new IntlMessageFormat(
  '{gender, select, male {He} female {She} other {They}} bought {count, plural, one {# item} other {# items}}',
  'en'
);

console.log(genderMessage.format({ gender: 'female', count: 2 }));
// She bought 2 items

7. RTL (Right-to-Left) Language Support

// rtl-utils.ts
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];

export function isRTL(locale: string): boolean {
  const lang = locale.split('-')[0];
  return RTL_LANGUAGES.includes(lang);
}

export function getDirection(locale: string): 'ltr' | 'rtl' {
  return isRTL(locale) ? 'rtl' : 'ltr';
}
/* styles/rtl.css */
:root {
  --text-align-start: left;
  --text-align-end: right;
  --margin-start: margin-left;
  --margin-end: margin-right;
  --padding-start: padding-left;
  --padding-end: padding-right;
}

[dir="rtl"] {
  --text-align-start: right;
  --text-align-end: left;
  --margin-start: margin-right;
  --margin-end: margin-left;
  --padding-start: padding-right;
  --padding-end: padding-left;
}

.container {
  text-align: var(--text-align-start);
  margin-left: var(--margin-start);
  padding-right: var(--padding-end);
}

/* Or use logical properties (modern approach) */
.modern-container {
  text-align: start;
  margin-inline-start: 1rem;
  padding-inline-end: 2rem;
}
// RTL React component
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { isRTL, getDirection } from './rtl-utils';

export function App() {
  const { i18n } = useTranslation();

  useEffect(() => {
    const direction = getDirection(i18n.language);
    document.documentElement.setAttribute('dir', direction);
    document.documentElement.setAttribute('lang', i18n.language);
  }, [i18n.language]);

  return (
    <div className="app">
      {/* Your app content */}
    </div>
  );
}

8. Translation Management

Message Extraction

// extract-messages.ts
import { sync as globSync } from 'glob';
import fs from 'fs';

const TRANSLATION_PATTERN = /t\(['"]([^'"]+)['"]\)/g;

export function extractMessages(pattern: string): Set<string> {
  const messages = new Set<string>();
  const files = globSync(pattern);

  for (const file of files) {
    const content = fs.readFileSync(file, 'utf8');
    let match;

    while ((match = TRANSLATION_PATTERN.exec(content)) !== null) {
      messages.add(match[1]);
    }
  }

  return messages;
}

// Generate translation template
export function generateTemplate(messages: Set<string>): object {
  const template: Record<string, string> = {};

  for (const message of messages) {
    template[message] = message; // Default to English
  }

  return template;
}

// Usage
const messages = extractMessages('src/**/*.{ts,tsx}');
const template = generateTemplate(messages);

fs.writeFileSync(
  'locales/en/translation.json',
  JSON.stringify(template, null, 2)
);

Translation Status

// check-translations.ts
export function checkTranslationStatus(
  baseLocale: object,
  targetLocale: object
): {
  missing: string[];
  extra: string[];
  coverage: number;
} {
  const baseKeys = new Set(Object.keys(baseLocale));
  const targetKeys = new Set(Object.keys(targetLocale));

  const missing = [...baseKeys].filter(key => !targetKeys.has(key));
  const extra = [...targetKeys].filter(key => !baseKeys.has(key));

  const coverage = (targetKeys.size / baseKeys.size) * 100;

  return { missing, extra, coverage };
}

// Usage
const enMessages = require('./locales/en/translation.json');
const esMessages = require('./locales/es/translation.json');

const status = checkTranslationStatus(enMessages, esMessages);
console.log(`Spanish translation coverage: ${status.coverage.toFixed(2)}%`);
console.log(`Missing keys: ${status.missing.join(', ')}`);

9. Locale Detection

// locale-detector.ts
export class LocaleDetector {
  // Detect from browser
  static fromBrowser(): string {
    return navigator.language || navigator.languages[0] || 'en';
  }

  // Detect from URL
  static fromURL(): string | null {
    const params = new URLSearchParams(window.location.search);
    return params.get('lang') || params.get('locale');
  }

  // Detect from cookie
  static fromCookie(name: string = 'locale'): string | null {
    const match = document.cookie.match(new RegExp(`${name}=([^;]+)`));
    return match ? match[1] : null;
  }

  // Detect from localStorage
  static fromStorage(key: string = 'locale'): string | null {
    return localStorage.getItem(key);
  }

  // Detect with priority
  static detect(defaultLocale: string = 'en'): string {
    return (
      this.fromURL() ||
      this.fromStorage() ||
      this.fromCookie() ||
      this.fromBrowser() ||
      defaultLocale
    );
  }

  // Save locale
  static save(locale: string): void {
    localStorage.setItem('locale', locale);
    document.cookie = `locale=${locale}; path=/; max-age=31536000`;
  }
}

10. Server-Side i18n

// Next.js i18n configuration
// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'es', 'fr', 'de', 'ja'],
    defaultLocale: 'en',
    localeDetection: true
  }
};

// pages/index.tsx
import { GetStaticProps } from 'next';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

export default function Home() {
  const { t } = useTranslation('common');

  return (
    <div>
      <h1>{t('welcome')}</h1>
    </div>
  );
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
  return {
    props: {
      ...(await serverSideTranslations(locale ?? 'en', ['common']))
    }
  };
};

Best Practices

✅ DO

  • Extract all user-facing strings to translation files
  • Use ICU message format for complex messages
  • Support pluralization correctly for each language
  • Use locale-aware date/time/number formatting
  • Implement RTL support for Arabic, Hebrew, etc.
  • Provide fallback language (usually English)
  • Use namespaces to organize translations
  • Test with pseudo-localization (ääçćëńţś)
  • Store locale preference (cookie, localStorage)
  • Use professional translators for production
  • Implement translation management workflow
  • Support dynamic locale switching
  • Use translation memory tools

❌ DON'T

  • Hardcode user-facing strings in code
  • Concatenate translated strings
  • Assume English grammar rules apply to all languages
  • Use generic plural forms (one/many) for all languages
  • Forget about text expansion (German is ~30% longer)
  • Store dates/times in locale-specific formats
  • Use flags to represent languages (flag ≠ language)
  • Translate technical terms without context
  • Mix translation keys with UI strings
  • Forget to translate alt text, titles, placeholders
  • Assume left-to-right layout

Common Patterns

Pattern 1: Translation Hook

export function useLocale() {
  const { i18n } = useTranslation();

  return {
    locale: i18n.language,
    changeLocale: (lng: string) => i18n.changeLanguage(lng),
    t: i18n.t,
    formatDate: (date: Date) => new DateFormatter(i18n.language).short(date),
    formatNumber: (num: number) => new NumberFormatter(i18n.language).formatNumber(num),
    formatCurrency: (amount: number, currency: string) =>
      new NumberFormatter(i18n.language).currency(amount, currency)
  };
}

Pattern 2: Language Switcher Component

export function LanguageSwitcher() {
  const { locale, changeLocale } = useLocale();

  const languages = [
    { code: 'en', name: 'English', nativeName: 'English' },
    { code: 'es', name: 'Spanish', nativeName: 'Español' },
    { code: 'fr', name: 'French', nativeName: 'Français' },
    { code: 'de', name: 'German', nativeName: 'Deutsch' }
  ];

  return (
    <select value={locale} onChange={(e) => changeLocale(e.target.value)}>
      {languages.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.nativeName}
        </option>
      ))}
    </select>
  );
}

Tools & Resources

  • i18next: Comprehensive i18n framework
  • react-intl (Format.js): React i18n library
  • LinguiJS: Developer-friendly i18n
  • vue-i18n: Vue.js i18n plugin
  • Crowdin: Translation management platform
  • Lokalise: Localization management
  • Phrase: Localization platform
  • POEditor: Translation management
  • BabelEdit: Translation editor
  • Pseudolocalization: Testing tool