type { ResourceLanguage } from 'i18next';
export const translationRegistry: Record<string, ResourceLanguage> = {
en: {
dashboard: {
greeting: 'Welcome back, {{userName}}',
stats: {
active: 'Active sessions: {{count}}',
pending: 'Pending approvals: {{count}}'
}
},
navigation: {
home: 'Home',
settings: 'Configuration'
}
},
ja: {
dashboard: {
greeting: 'ใใใใใชใใใ{{userName}}ใใ',
stats: {
active: 'ใขใฏใใฃใใปใใทใงใณ: {{count}}',
pending: 'ไฟ็ไธญใฎๆฟ่ช: {{count}}'
}
},
navigation: {
home: 'ใใผใ ',
settings: '่จญๅฎ'
}
}
};
Semantic keys (`dashboard.greeting`) decouple translation content from component structure. When marketing updates copy, engineers only modify the registry, not the component tree.
### 2. Native Formatting via `Intl` APIs
Manual string concatenation for dates, numbers, and currencies fails across locales. The ECMAScript Internationalization API (`Intl`) provides locale-aware formatting without external dependencies.
```typescript
// utils/formatters.ts
export const formatTemporal = (
timestamp: number,
locale: string,
options?: Intl.DateTimeFormatOptions
): string => {
const defaults: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC'
};
return new Intl.DateTimeFormat(locale, { ...defaults, ...options }).format(
new Date(timestamp)
);
};
export const formatMonetary = (
amount: number,
locale: string,
currencyCode: string
): string => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: 2
}).format(amount);
};
Storing all temporal data in UTC at the persistence layer ensures consistency. Formatting occurs exclusively at the presentation boundary, preventing timezone drift during data transmission.
3. Directional Layout & CSS Logical Properties
Right-to-left (RTL) languages require structural mirroring, not simple text alignment overrides. Hardcoded directional styles (margin-left, text-align: right) break layout flow in Arabic, Hebrew, and Urdu.
/* base/layout.css */
.container {
display: flex;
flex-direction: row;
padding-inline-start: 1.5rem;
padding-inline-end: 1.5rem;
}
.icon-wrapper {
margin-inline-end: 0.75rem;
/* Automatically flips to margin-inline-start in RTL */
}
.text-block {
text-align: start;
/* Resolves to left in LTR, right in RTL */
}
Applying dir="rtl" to the root <html> element triggers browser-native layout mirroring. CSS logical properties (padding-inline-start, margin-inline-end, text-align: start) resolve directionally without JavaScript intervention.
4. CLDR Pluralization & Grammatical Rules
English pluralization (${count} item${count !== 1 ? 's' : ''}) fails in languages with complex grammatical number systems. Arabic utilizes six plural forms; Russian uses three; Slovenian uses four. The Unicode Common Locale Data Repository (CLDR) defines locale-specific plural rules.
// hooks/usePluralResolver.ts
import { useTranslation } from 'react-i18next';
export const usePluralResolver = () => {
const { t } = useTranslation();
const resolve = (key: string, count: number) => {
return t(key, { count, returnObjects: false });
};
return resolve;
};
Translation registries must declare CLDR-compliant keys:
{
"notifications_zero": "No notifications",
"notifications_one": "{{count}} notification",
"notifications_two": "{{count}} notifications",
"notifications_few": "{{count}} notifications",
"notifications_many": "{{count}} notifications",
"notifications_other": "{{count}} notifications"
}
The i18n runtime automatically selects the correct form based on the active locale's CLDR rules. Manual branching should never be implemented in component logic.
5. Timezone Normalization
Client-side new Date() relies on the user's system clock, which is unreliable for global applications. All temporal operations must normalize to UTC, then resolve to the user's preferred timezone at render time.
// utils/timezone.ts
import { toZonedTime, format } from 'date-fns-tz';
export const formatInUserTimezone = (
utcTimestamp: string,
userTimezone: string,
locale: string
): string => {
const zonedDate = toZonedTime(new Date(utcTimestamp), userTimezone);
return format(zonedDate, 'PPpp', { locale });
};
Persisting userTimezone in profile metadata or detecting it via Intl.DateTimeFormat().resolvedOptions().timeZone ensures consistent temporal rendering across devices.
Pitfall Guide
1. Semantic Key Collapse
Explanation: Using literal English text as translation keys ("Welcome back, John!") forces key regeneration whenever copy changes, breaking translation memory and increasing localization costs.
Fix: Enforce dot-notation semantic keys (auth.greeting.user) tied to component context, not content.
2. Client-Side Timezone Drift
Explanation: Relying on new Date().toLocaleString() without explicit timezone parameters causes inconsistent rendering when users travel or adjust system clocks.
Fix: Store all timestamps as ISO 8601 UTC strings. Resolve timezones explicitly using date-fns-tz or Luxon at the presentation layer.
3. Manual Plural Branching
Explanation: Implementing count === 1 ? 'item' : 'items' in JavaScript ignores CLDR plural categories, causing grammatical errors in Russian, Arabic, Polish, and others.
Fix: Delegate plural resolution to the i18n runtime. Declare all CLDR forms in translation files and pass count as an interpolation variable.
4. Flag-Based Locale Selectors
Explanation: Flags represent nations, not languages. Spanish is spoken in 20+ countries; Chinese has multiple written variants. Flag selectors create political friction and inaccurate mappings.
Fix: Use ISO 639-1 language codes (EN, JA, AR) or native language names (English, ๆฅๆฌ่ช, ุงูุนุฑุจูุฉ).
5. Ignoring Text Expansion Buffers
Explanation: UI components sized for English text overflow when rendering German, Finnish, or Dutch. Truncation breaks readability and breaks responsive layouts.
Fix: Design components with 30-40% expansion headroom. Use text-overflow: ellipsis only as a fallback, not a primary strategy. Implement dynamic height containers for variable-length content.
6. Synchronous Translation Loading
Explanation: Bundling all locale files into the initial JavaScript payload increases bundle size by 15-25%, degrading Time to Interactive (TTI) for users who only need one language.
Fix: Implement code-splitting for translation chunks. Load the default locale synchronously, then fetch additional locales on demand using i18next-http-backend or dynamic import().
7. Hardcoded Visual Semantics
Explanation: Icons, colors, and imagery carry cultural weight. White signifies mourning in parts of Asia; red indicates danger in Western contexts but prosperity in China.
Fix: Abstract visual assets into locale-specific resource directories. Provide alternative iconography for regions where default metaphors carry negative connotations.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single-region launch with future expansion | i18next + lazy-loaded backend | Minimal initial overhead; scales to multi-locale | Low upfront, moderate scaling |
| High-traffic global SaaS | @formatjs/intl + build-time extraction | Zero runtime interpolation overhead; optimized SSR | Higher initial dev time, lower runtime cost |
| Static marketing site | next-intl or astro-i18n | Static generation with locale routing; no client JS | Lowest runtime cost, fastest TTI |
| Legacy codebase retrofit | String extraction CLI + incremental key migration | Avoids full rewrite; isolates i18n layer gradually | High initial effort, prevents compounding debt |
Configuration Template
// i18n/config.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { translationRegistry } from './locales/registry';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'ja', 'ar', 'de'],
nonExplicitSupportedLngs: true,
resources: translationRegistry,
interpolation: {
escapeValue: false,
format: (value, format, locale) => {
if (format === 'currency') {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(value);
}
if (format === 'date') {
return new Intl.DateTimeFormat(locale, {
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(value));
}
return value;
}
},
backend: {
loadPath: '/locales/{{lng}}.json',
crossDomain: false
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
caches: ['localStorage'],
lookupQuerystring: 'lang'
}
});
export default i18n;
// components/LocaleProvider.tsx
import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n/config';
interface LocaleProviderProps {
children: React.ReactNode;
}
export const LocaleProvider: React.FC<LocaleProviderProps> = ({ children }) => {
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
};
Quick Start Guide
- Initialize the i18n layer: Install
i18next, react-i18next, and i18next-http-backend. Create the configuration file using the template above.
- Extract hardcoded strings: Run a static analysis scan across your component tree. Replace all literal text with semantic keys using the
t() function.
- Configure lazy loading: Set up a
/locales/ directory structure. Configure the HTTP backend to fetch locale files on demand. Verify network requests in browser dev tools.
- Implement formatting hooks: Create
useFormatDate, useFormatCurrency, and usePluralResolver hooks wrapping Intl APIs and i18next interpolation. Replace manual formatting in components.
- Test directional & temporal behavior: Toggle
dir="rtl" on the root element. Verify layout mirroring. Inject UTC timestamps and confirm timezone resolution matches user preferences.
Internationalization is not a translation workflow; it is a rendering architecture. When string resolution, temporal normalization, and directional layout are abstracted from component logic, localization becomes a content operation rather than an engineering constraint. Build the scaffolding first. Fill it with regional data later.