Internationalization Implementation Stack
1. React-intl i18next Setup Configuration
Comprehensive i18n solutions for React apps with translation management, formatting, and pluralization support.
| Library | Core Features | Best For | Bundle Size |
|---|---|---|---|
| react-intl (FormatJS) | ICU message format, date/number formatting, React hooks | Enterprise apps, complex formatting | ~45KB |
| react-i18next | Plugin ecosystem, lazy loading, backend integration | Large projects, dynamic translations | ~30KB + i18next core |
| next-intl | Next.js optimized, SSR/SSG support, type-safe | Next.js apps | ~15KB |
| LinguiJS | CLI extraction, compile-time optimization, minimal runtime | Performance-critical apps | ~5KB runtime |
Example: react-i18next setup with TypeScript
// i18n.ts - Configuration
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
i18n
.use(Backend) // Load translations from backend
.use(LanguageDetector) // Detect user language
.use(initReactI18next) // Pass i18n to react-i18next
.init({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false // React already escapes
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
},
ns: ['common', 'auth', 'dashboard'],
defaultNS: 'common',
react: {
useSuspense: true
}
});
export default i18n;
// locales/en/common.json
{
"welcome": "Welcome, {{name}}!",
"itemCount": "You have {{count}} item",
"itemCount_plural": "You have {{count}} items",
"updated": "Last updated: {{date, datetime}}"
}
// locales/es/common.json
{
"welcome": "¡Bienvenido, {{name}}!",
"itemCount": "Tienes {{count}} artículo",
"itemCount_plural": "Tienes {{count}} artículos",
"updated": "Última actualización: {{date, datetime}}"
}
// App.tsx
import './i18n';
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback="Loading...">
<MainApp />
</Suspense>
);
}
// Component usage
import { useTranslation } from 'react-i18next';
function Dashboard() {
const { t, i18n } = useTranslation('common');
return (
<div>
<h1>{t('welcome', { name: 'John' })}</h1>
<p>{t('itemCount', { count: 5 })}</p>
<p>{t('updated', { date: new Date() })}</p>
<button onClick={() => i18n.changeLanguage('es')}>
Español
</button>
<button onClick={() => i18n.changeLanguage('en')}>
English
</button>
</div>
);
}
Example: react-intl (FormatJS) implementation
// App.tsx
import { IntlProvider } from 'react-intl';
import { useState } from 'react';
import enMessages from './locales/en.json';
import esMessages from './locales/es.json';
const messages = {
en: enMessages,
es: esMessages
};
function App() {
const [locale, setLocale] = useState('en');
return (
<IntlProvider
messages={messages[locale]}
locale={locale}
defaultLocale="en"
>
<Dashboard onLocaleChange={setLocale} />
</IntlProvider>
);
}
// Component with react-intl hooks
import {
useIntl,
FormattedMessage,
FormattedNumber,
FormattedDate
} from 'react-intl';
function Dashboard({ onLocaleChange }) {
const intl = useIntl();
return (
<div>
<h1>
<FormattedMessage
id="welcome"
defaultMessage="Welcome, {name}!"
values={{ name: 'John' }}
/>
</h1>
<p>
<FormattedNumber
value={1234.56}
style="currency"
currency="USD"
/>
</p>
<p>
<FormattedDate
value={new Date()}
year="numeric"
month="long"
day="numeric"
/>
</p>
{/* Imperative usage */}
<input
placeholder={intl.formatMessage({
id: 'search.placeholder',
defaultMessage: 'Search...'
})}
/>
</div>
);
}
Comparison: Use react-i18next for flexibility and ecosystem.
Use react-intl for standardized ICU formatting. Use LinguiJS
for smallest bundle and compile-time extraction.
2. Dynamic Locale Loading Lazy i18n
Load translation files on-demand to reduce initial bundle size and improve performance for multi-language apps.
| Strategy | Implementation | Benefits | Trade-offs |
|---|---|---|---|
| Code Splitting | Dynamic import() per locale | Smaller initial bundle | Network request on language change |
| Namespace Splitting | Split by feature/page | Load only needed translations | More HTTP requests |
| Backend Loading | Fetch from API/CDN | No rebuild for translation updates | Runtime dependency |
| Preloading | Prefetch likely locales | Instant switching | Additional bandwidth usage |
Example: Dynamic locale loading with react-i18next
// i18n.ts - Lazy loading configuration
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Custom backend for dynamic imports
const loadResources = async (lng: string, ns: string) => {
try {
const resources = await import(`./locales/${lng}/${ns}.json`);
return resources.default;
} catch (error) {
console.error(`Failed to load ${lng}/${ns}`, error);
return {};
}
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.use({
type: 'backend',
read(language, namespace, callback) {
loadResources(language, namespace)
.then(resources => callback(null, resources))
.catch(error => callback(error, null));
}
})
.init({
fallbackLng: 'en',
ns: ['common', 'auth', 'dashboard', 'settings'],
defaultNS: 'common',
react: {
useSuspense: true
},
// Preload common namespaces
preload: ['en'],
load: 'languageOnly' // 'en-US' -> 'en'
});
export default i18n;
// Hook for namespace loading
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
function useLazyTranslation(namespace: string) {
const { t, i18n, ready } = useTranslation(namespace, { useSuspense: false });
useEffect(() => {
if (!i18n.hasResourceBundle(i18n.language, namespace)) {
i18n.loadNamespaces(namespace);
}
}, [i18n, namespace]);
return { t, ready };
}
// Component usage
function SettingsPage() {
const { t, ready } = useLazyTranslation('settings');
if (!ready) return <div>Loading translations...</div>
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
Example: Webpack/Vite magic comments for chunk naming
// loadLocale.ts
export async function loadLocale(locale: string) {
// Webpack magic comments for chunk naming
const messages = await import(
/* webpackChunkName: "locale-[request]" */
/* webpackMode: "lazy" */
`./locales/${locale}/messages.json`
);
return messages.default;
}
// Vite dynamic import
export async function loadLocaleVite(locale: string) {
const modules = import.meta.glob('./locales/*/messages.json');
const path = `./locales/${locale}/messages.json`;
if (modules[path]) {
const messages = await modules[path]();
return messages.default;
}
throw new Error(`Locale ${locale} not found`);
}
// App.tsx - Progressive enhancement
import { Suspense, lazy } from 'react';
const LocaleProvider = lazy(() =>
import(/* webpackChunkName: "i18n-provider" */ './LocaleProvider')
);
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<LocaleProvider>
<Routes />
</LocaleProvider>
</Suspense>
);
}
Example: Preloading strategy for better UX
// useLocalePreload.ts
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
const COMMON_LOCALES = ['en', 'es', 'fr', 'de'];
export function useLocalePreload() {
const { i18n } = useTranslation();
useEffect(() => {
// Preload on idle
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
COMMON_LOCALES.forEach(locale => {
if (locale !== i18n.language) {
// Prefetch but don't block
import(`./locales/${locale}/common.json`).catch(() => {});
}
});
});
}
}, [i18n.language]);
}
// Link preload in HTML head
function injectPreloadLinks(locales: string[]) {
locales.forEach(locale => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = 'fetch';
link.href = `/locales/${locale}/common.json`;
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
});
}
// Next.js implementation
import Head from 'next/head';
function LocalePreload({ locales }: { locales: string[] }) {
return (
<Head>
{locales.map(locale => (
<link
key={locale}
rel="prefetch"
as="fetch"
href={`/locales/${locale}/common.json`}
crossOrigin="anonymous"
/>
))}
</Head>
);
}
Best Practice: Load
common namespace eagerly, lazy-load feature-specific
namespaces. Preload user's preferred alternate languages during idle time. Cache translations in localStorage.
3. Pluralization ICU Message Format
Handle complex pluralization rules across different languages using ICU MessageFormat standard.
| Feature | Syntax | Use Case | Example |
|---|---|---|---|
| Simple Plural | {count, plural, one{#} other{#}} |
Item counts | 1 item / 5 items |
| Select | {gender, select, male{} female{}} |
Gender-based text | He/She variations |
| SelectOrdinal | {num, selectordinal, one{#st}} |
Ordinal numbers | 1st, 2nd, 3rd |
| Nested | Combine plural + select | Complex messages | Gender + count variations |
Example: ICU MessageFormat pluralization rules
// English pluralization (2 forms)
{
"itemCount": "{count, plural, one {# item} other {# items}}"
}
// Usage: 0 items, 1 item, 2 items, 100 items
// Polish pluralization (3 forms)
{
"itemCount": "{count, plural, one {# przedmiot} few {# przedmioty} many {# przedmiotów} other {# przedmiotu}}"
}
// Arabic pluralization (6 forms!)
{
"itemCount": "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}}"
}
// Complex example with select + plural
{
"taskStatus": "{taskCount, plural, =0 {No tasks} one {# task} other {# tasks}} {status, select, pending {pending} completed {completed} failed {failed} other {unknown}}"
}
// Nested gender + plural
{
"friendRequest": "{gender, select, male {He has} female {She has} other {They have}} {count, plural, one {# friend request} other {# friend requests}}"
}
Example: react-intl with ICU MessageFormat
import { FormattedMessage, useIntl } from 'react-intl';
// Translation file (en.json)
{
"cart.items": "You have {itemCount, plural, =0 {no items} one {# item} other {# items}} in your cart",
"user.greeting": "{name} {gender, select, male {is online. Say hi to him!} female {is online. Say hi to her!} other {is online. Say hi!}}",
"finish.position": "You finished in {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place"
}
// Component usage
function ShoppingCart({ itemCount }: { itemCount: number }) {
return (
<div>
<FormattedMessage
id="cart.items"
values={{ itemCount }}
/>
</div>
);
}
function UserStatus({ name, gender }: { name: string; gender: 'male' | 'female' | 'other' }) {
return (
<FormattedMessage
id="user.greeting"
values={{ name, gender }}
/>
);
}
function RaceResult({ position }: { position: number }) {
const intl = useIntl();
return (
<div>
{intl.formatMessage(
{ id: 'finish.position' },
{ position }
)}
</div>
);
}
Example: i18next with ICU plugin
// i18n.ts - Enable ICU format
import i18n from 'i18next';
import ICU from 'i18next-icu';
import { initReactI18next } from 'react-i18next';
i18n
.use(ICU) // Add ICU plugin
.use(initReactI18next)
.init({
fallbackLng: 'en',
resources: {
en: {
translation: {
"notifications": "You have {count, plural, =0 {no notifications} one {# notification} other {# notifications}}",
"fileSize": "{size, number} {unit, select, KB {kilobytes} MB {megabytes} GB {gigabytes} other {bytes}}"
}
}
}
});
// Component usage
import { useTranslation } from 'react-i18next';
function Notifications({ count }: { count: number }) {
const { t } = useTranslation();
return <div>{t('notifications', { count })}</div>;
}
function FileInfo({ size, unit }: { size: number; unit: string }) {
const { t } = useTranslation();
return <div>{t('fileSize', { size, unit })}</div>;
}
Performance Note: ICU MessageFormat parsing has runtime cost. Use compile-time extraction
(LinguiJS) for production apps or cache parsed messages. Simple plural/other is sufficient for many use cases.
4. RTL LTR CSS Logical Properties
Support right-to-left (RTL) languages like Arabic and Hebrew with CSS logical properties for automatic layout mirroring.
| Physical Property | Logical Property | LTR Value | RTL Value |
|---|---|---|---|
| margin-left | margin-inline-start | Left margin | Right margin |
| margin-right | margin-inline-end | Right margin | Left margin |
| padding-left | padding-inline-start | Left padding | Right padding |
| border-left | border-inline-start | Left border | Right border |
| text-align: left | text-align: start | Align left | Align right |
| float: left | float: inline-start | Float left | Float right |
Example: CSS logical properties for RTL support
/* Traditional approach (manual RTL) */
.card {
margin-left: 16px;
padding-right: 24px;
border-left: 2px solid blue;
}
[dir="rtl"] .card {
margin-left: 0;
margin-right: 16px;
padding-right: 0;
padding-left: 24px;
border-left: none;
border-right: 2px solid blue;
}
/* Modern approach (automatic RTL) */
.card {
margin-inline-start: 16px;
padding-inline-end: 24px;
border-inline-start: 2px solid blue;
}
/* No RTL override needed! */
/* Complete example */
.sidebar {
/* Inline = horizontal (left/right) */
padding-inline-start: 20px;
padding-inline-end: 10px;
margin-inline: 8px; /* shorthand for start + end */
/* Block = vertical (top/bottom) - no change in RTL */
padding-block-start: 16px;
padding-block-end: 16px;
margin-block: 12px;
/* Positioning */
inset-inline-start: 0; /* left in LTR, right in RTL */
/* Text alignment */
text-align: start; /* left in LTR, right in RTL */
}
.icon {
margin-inline-end: 8px; /* Space after icon */
}
.arrow {
/* Transform for RTL */
transform: scaleX(1);
}
[dir="rtl"] .arrow {
transform: scaleX(-1); /* Flip horizontally */
}
Example: React RTL implementation with context
// DirectionProvider.tsx
import { createContext, useContext, useEffect } from 'react';
type Direction = 'ltr' | 'rtl';
const DirectionContext = createContext<Direction>('ltr');
export function DirectionProvider({
children,
direction
}: {
children: React.ReactNode;
direction: Direction
}) {
useEffect(() => {
document.documentElement.setAttribute('dir', direction);
document.documentElement.setAttribute('lang', direction === 'rtl' ? 'ar' : 'en');
}, [direction]);
return (
<DirectionContext.Provider value={direction}>
{children}
</DirectionContext.Provider>
);
}
export const useDirection = () => useContext(DirectionContext);
// App.tsx
import { useTranslation } from 'react-i18next';
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];
function App() {
const { i18n } = useTranslation();
const direction = RTL_LANGUAGES.includes(i18n.language) ? 'rtl' : 'ltr';
return (
<DirectionProvider direction={direction}>
<MainApp />
</DirectionProvider>
);
}
// Component with direction-aware styles
import styled from 'styled-components';
const Card = styled.div`
padding-inline-start: 20px;
padding-inline-end: 10px;
border-inline-start: 3px solid var(--primary-color);
.icon {
margin-inline-end: 8px;
}
`;
function MyCard() {
const direction = useDirection();
return (
<Card>
<span className="icon">→</span>
{direction === 'rtl' ? 'النص العربي' : 'English text'}
</Card>
);
}
Example: Tailwind CSS with RTL plugin
// tailwind.config.js
module.exports = {
plugins: [require('tailwindcss-rtl')],
};
// Component with RTL-aware utilities
function Navbar() {
return (
<nav className="flex items-center">
<img
src="/logo.png"
className="ms-4 me-2" // ms = margin-inline-start, me = margin-inline-end
alt="Logo"
/>
<ul className="flex gap-4">
<li className="ps-4">Home</li> {/* ps = padding-inline-start */}
<li className="ps-4">About</li>
</ul>
<button className="ms-auto">Login</button> {/* Push to end */}
</nav>
);
}
// Custom RTL utilities
<div className="ltr:text-left rtl:text-right">
Directional text
</div>
Browser Support: Excellent - All modern browsers support
CSS logical properties. Use
dir="rtl" on HTML element. Test with Arabic/Hebrew content thoroughly.
5. Date-fns Timezone Locale Formatting
Format dates, times, and numbers according to user's locale and timezone with proper internationalization support.
| Library | Features | Use Case | Bundle Size |
|---|---|---|---|
| date-fns | Modular, tree-shakeable, 80+ locales, immutable | Modern apps, small bundles | ~2KB per function |
| date-fns-tz | Timezone support addon for date-fns | Multi-timezone apps | ~10KB + date-fns |
| Luxon | Modern API, Intl wrapper, timezone native | Complex date logic | ~70KB |
| Day.js | Moment.js alternative, plugins, small | Simple date needs | ~7KB |
| Intl API (Native) | Browser built-in, no dependencies | Basic formatting | 0KB |
Example: date-fns with locale formatting
import { format, formatDistance, formatRelative } from 'date-fns';
import { enUS, es, ar, ja, de } from 'date-fns/locale';
// Locale mapping
const locales = { en: enUS, es, ar, ja, de };
function formatDate(date: Date, locale: string) {
return format(date, 'PPpp', { locale: locales[locale] });
}
// Usage examples
const date = new Date(2025, 0, 15, 14, 30);
// English: "Jan 15, 2025, 2:30 PM"
format(date, 'PPpp', { locale: enUS });
// Spanish: "15 ene 2025, 14:30"
format(date, 'PPpp', { locale: es });
// Arabic: "١٥ يناير ٢٠٢٥، ١٤:٣٠"
format(date, 'PPpp', { locale: ar });
// Relative time
formatDistance(date, new Date(), {
addSuffix: true,
locale: es
}); // "hace 3 días"
// Custom formats
format(date, 'EEEE, MMMM do yyyy', { locale: de });
// "Mittwoch, Januar 15. 2025"
// React hook for localized dates
import { useTranslation } from 'react-i18next';
function useLocalizedDate() {
const { i18n } = useTranslation();
const locale = locales[i18n.language] || enUS;
const formatLocalizedDate = (date: Date, formatStr: string) => {
return format(date, formatStr, { locale });
};
const formatRelativeTime = (date: Date) => {
return formatDistance(date, new Date(), {
addSuffix: true,
locale
});
};
return { formatLocalizedDate, formatRelativeTime };
}
Example: Timezone handling with date-fns-tz
import { format } from 'date-fns';
import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz';
import { enUS } from 'date-fns/locale';
// Display date in user's timezone
function formatUserTimezone(date: Date, userTimezone: string) {
return formatInTimeZone(
date,
userTimezone,
'yyyy-MM-dd HH:mm:ss zzz',
{ locale: enUS }
);
}
// Examples
const utcDate = new Date('2025-01-15T14:30:00Z');
formatUserTimezone(utcDate, 'America/New_York');
// "2025-01-15 09:30:00 EST"
formatUserTimezone(utcDate, 'Asia/Tokyo');
// "2025-01-15 23:30:00 JST"
formatUserTimezone(utcDate, 'Europe/London');
// "2025-01-15 14:30:00 GMT"
// Convert between timezones
const tokyoTime = toZonedTime(utcDate, 'Asia/Tokyo');
const nyTime = fromZonedTime(tokyoTime, 'America/New_York');
// React component with timezone display
function EventTime({ eventDate, timezone }: {
eventDate: Date;
timezone: string;
}) {
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return (
<div>
<p>Event time: {formatInTimeZone(eventDate, timezone, 'PPpp')}</p>
<p>Your time: {formatInTimeZone(eventDate, userTimezone, 'PPpp')}</p>
</div>
);
}
Example: Native Intl API for formatting
// Date formatting with Intl.DateTimeFormat
const date = new Date('2025-01-15T14:30:00');
// English (US)
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'short'
}).format(date);
// "Wednesday, January 15, 2025 at 2:30 PM"
// Spanish (Spain)
new Intl.DateTimeFormat('es-ES', {
dateStyle: 'full',
timeStyle: 'short'
}).format(date);
// "miércoles, 15 de enero de 2025, 14:30"
// Number formatting with Intl.NumberFormat
const number = 1234567.89;
// Currency
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(number);
// "$1,234,567.89"
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(number);
// "¥1,234,568"
// Percentage
new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2
}).format(0.1234);
// "12.34%"
// Relative time with Intl.RelativeTimeFormat
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(2, 'week'); // "in 2 weeks"
const rtfEs = new Intl.RelativeTimeFormat('es', { numeric: 'auto' });
rtfEs.format(-1, 'day'); // "ayer"
rtfEs.format(2, 'week'); // "dentro de 2 semanas"
// React hook for Intl formatting
function useIntlFormatting(locale: string) {
const dateFormatter = new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'short'
});
const currencyFormatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
});
const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, {
numeric: 'auto'
});
return {
formatDate: (date: Date) => dateFormatter.format(date),
formatCurrency: (amount: number) => currencyFormatter.format(amount),
formatRelativeTime: (value: number, unit: Intl.RelativeTimeFormatUnit) =>
relativeTimeFormatter.format(value, unit)
};
}
Timezone Pitfall: Always store dates in UTC on backend. Convert to user's timezone only for
display. Use
Date.prototype.toISOString() for API communication. Avoid
new Date(string)
parsing across timezones.
6. Translation Keys TypeScript Validation
Enforce type safety for translation keys to catch missing translations at compile-time instead of runtime.
| Approach | Tool | Benefits | Setup Complexity |
|---|---|---|---|
| Type Generation | i18next + typesafe-i18n | Auto-complete, type errors for invalid keys | Medium |
| CLI Extraction | LinguiJS, react-intl CLI | Extract keys from code, detect unused | High |
| Const Assertion | TypeScript as const | Simple, no build step | Low |
| Schema Validation | Zod, Yup | Runtime + compile validation | Medium |
Example: TypeScript with i18next typed translations
// locales/en/translation.json
{
"common": {
"welcome": "Welcome",
"logout": "Logout"
},
"auth": {
"login": {
"title": "Sign In",
"email": "Email Address",
"password": "Password"
}
},
"errors": {
"required": "This field is required",
"invalid_email": "Invalid email format"
}
}
// i18next.d.ts - Type augmentation
import 'i18next';
import type translation from './locales/en/translation.json';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: typeof translation;
};
}
}
// Now TypeScript knows all translation keys!
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t } = useTranslation();
// ✅ Valid - autocomplete works!
t('common.welcome');
t('auth.login.title');
// ❌ TypeScript error - key doesn't exist
t('common.invalid'); // Error: Property 'invalid' does not exist
// ✅ Nested object access with type safety
t('auth.login.email');
return <div>{t('common.welcome')}</div>;
}
Example: Generate types from translation files
// scripts/generate-i18n-types.ts
import fs from 'fs';
import path from 'path';
type TranslationKeys<T, Prefix extends string = ''> = {
[K in keyof T]: T[K] extends object
? TranslationKeys<T[K], `${Prefix}${K & string}.`>
: `${Prefix}${K & string}`;
}[keyof T];
function generateTypes() {
const enTranslations = JSON.parse(
fs.readFileSync('./locales/en/translation.json', 'utf-8')
);
const types = `
// Auto-generated - do not edit
import type en from './locales/en/translation.json';
export type TranslationKey = TranslationKeys<typeof en>;
export type TranslationKeys<T, Prefix extends string = ''> = {
[K in keyof T]: T[K] extends object
? TranslationKeys<T[K], \`\${Prefix}\${K & string}.\`>
: \`\${Prefix}\${K & string}\`;
}[keyof T];
`;
fs.writeFileSync('./src/types/i18n.ts', types);
}
generateTypes();
// Usage with custom hook
import type { TranslationKey } from './types/i18n';
import { useTranslation as useI18next } from 'react-i18next';
export function useTranslation() {
const { t, ...rest } = useI18next();
const typedT = (key: TranslationKey, options?: any) => {
return t(key, options);
};
return { t: typedT, ...rest };
}
Example: typesafe-i18n library (zero-dependency, full type safety)
// Installation: npm install typesafe-i18n
// locales/en.json
{
"HI": "Hi {name:string}!",
"ITEMS": "You have {count:number} {count:plural(item|items)}",
"PRICE": "Price: {amount:number|currency(USD)}"
}
// Generated types (automatic)
type Translation = {
HI: (params: { name: string }) => string;
ITEMS: (params: { count: number }) => string;
PRICE: (params: { amount: number }) => string;
}
// Usage
import { useI18nContext } from './i18nContext';
function MyComponent() {
const { LL } = useI18nContext(); // LL = Localized Language
// ✅ Type-safe with parameter validation
LL.HI({ name: 'John' }); // "Hi John!"
// ❌ TypeScript error - missing required parameter
LL.HI({ }); // Error: Property 'name' is missing
// ❌ TypeScript error - wrong type
LL.HI({ name: 123 }); // Error: Type 'number' is not assignable to 'string'
// ✅ Pluralization
LL.ITEMS({ count: 1 }); // "You have 1 item"
LL.ITEMS({ count: 5 }); // "You have 5 items"
// ✅ Formatted values
LL.PRICE({ amount: 99.99 }); // "Price: $99.99"
return <div>{LL.HI({ name: 'World' })}</div>;
}
Example: Validation in CI/CD pipeline
// scripts/validate-translations.ts
import fs from 'fs';
import path from 'path';
interface ValidationResult {
valid: boolean;
errors: string[];
}
function getTranslationKeys(obj: any, prefix = ''): string[] {
return Object.keys(obj).flatMap(key => {
const value = obj[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
return getTranslationKeys(value, fullKey);
}
return [fullKey];
});
}
function validateTranslations(): ValidationResult {
const errors: string[] = [];
const localesDir = path.join(__dirname, '../locales');
const locales = fs.readdirSync(localesDir);
// Load base locale (English)
const baseLocale = 'en';
const baseTranslations = JSON.parse(
fs.readFileSync(path.join(localesDir, baseLocale, 'translation.json'), 'utf-8')
);
const baseKeys = new Set(getTranslationKeys(baseTranslations));
// Check each locale
locales.forEach(locale => {
if (locale === baseLocale) return;
const translations = JSON.parse(
fs.readFileSync(path.join(localesDir, locale, 'translation.json'), 'utf-8')
);
const keys = new Set(getTranslationKeys(translations));
// Check for missing keys
baseKeys.forEach(key => {
if (!keys.has(key)) {
errors.push(`[${locale}] Missing translation key: ${key}`);
}
});
// Check for extra keys
keys.forEach(key => {
if (!baseKeys.has(key)) {
errors.push(`[${locale}] Extra translation key: ${key}`);
}
});
});
return {
valid: errors.length === 0,
errors
};
}
// Run validation
const result = validateTranslations();
if (!result.valid) {
console.error('Translation validation failed:');
result.errors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
console.log('✅ All translations are valid!');
// package.json script
{
"scripts": {
"validate:i18n": "ts-node scripts/validate-translations.ts",
"precommit": "npm run validate:i18n && npm run type-check"
}
}