Internationalization
What I Do
I prepare applications for global audiences by designing systems that support multiple languages, date/time formats, number formats, and cultural conventions. I separate locale-specific content from code to enable efficient localization.
When to Use Me
- Preparing applications for international markets
- Implementing multiple language support
- Handling locale-specific formatting (dates, numbers, currencies)
- Managing right-to-left (RTL) language layouts
- Creating translation workflows and management systems
- Adapting content for cultural preferences
Core Concepts
- Locale Identification: Language tags (en-US, zh-Hans, ar-EG)
- Message Formatting: ICU MessageFormat with plurals, gender, select
- Date/Time Formatting: Locale-aware date and time display
- Number Formatting: Locale-specific decimal/thousand separators
- Pluralization Rules: Language-specific plural categories
- Text Expansion: Languages that require more space than English
- RTL Support: Right-to-left layout for Arabic, Hebrew, Persian
- Cultural Adaptations: Date formats, currencies, address formats
- Translation Management: Workflows for handling translations
- Pseudo-localization: Testing for i18n before actual translations
Code Examples
Internationalization Provider
class I18nProvider {
constructor(locale, translations) {
this.locale = locale;
this.translations = translations;
this.fallbackLocale = 'en';
}
t(key, params = {}) {
const keys = key.split('.');
let message = this.translations[this.locale];
for (const k of keys) {
message = message?.[k] || this.translations[this.fallbackLocale]?.[k];
}
if (!message) return key;
return this.interpolate(message, params);
}
interpolate(message, params) {
return message.replace(/\{(\w+)\}/g, (_, key) => {
return params[key] !== undefined ? params[key] : `{${key}}`;
});
}
formatDate(date, options = {}) {
const localeDate = new Date(date);
return new Intl.DateTimeFormat(this.locale, {
dateStyle: 'medium',
...options
}).format(localeDate);
}
formatNumber(number, options = {}) {
return new Intl.NumberFormat(this.locale, {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
...options
}).format(number);
}
formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat(this.locale, {
style: 'currency',
currency
}).format(amount);
}
}
ICU Message Formatter
class MessageFormatter {
constructor(locale) {
this.locale = locale;
this.plurals = {
en: (n) => n === 1 ? 'one' : 'other',
zh: () => 'other',
ar: (n) => {
if (n === 0) return 'zero';
if (n === 1) return 'one';
if (n === 2) return 'two';
if (n % 100 >= 3 && n % 100 <= 10) return 'few';
if (n % 100 >= 11) return 'many';
return 'other';
}
};
}
format(message, params) {
return message.replace(/\{(\w+)(,\s*\w+(?:,\s*[^}]+)?)?\}/g, (match, key, rest) => {
const value = params[key];
if (value === undefined) return match;
const pluralFn = this.plurals[this.locale] || this.plurals.en;
if (!rest) return value;
const [, type, options] = rest.match(/,(\w+)(?:,\s*([^}]+))?/) || [];
switch (type) {
case 'plural':
const category = pluralFn(value);
const optionMatch = options?.match(new RegExp(`${category}\\s*\\{([^}]+)\\}`));
return optionMatch ? optionMatch[1] : options.split(',').pop();
case 'select':
const selectMatch = options?.match(new RegExp(`${value}\\s*\\{([^}]+)\\}`));
return selectMatch ? selectMatch[1] : '';
default:
return value;
}
});
}
}
// Usage
const formatter = new MessageFormatter('en');
const message = formatter.format(
'{count, plural, one {# item} other {# items}}',
{ count: 5 }
);
RTL Layout Manager
class RTLManager {
constructor() {
this.supportedRTL = ['ar', 'he', 'fa', 'ur'];
this.currentDir = 'ltr';
}
detectLocale(locale) {
const baseLocale = locale.split('-')[0];
return this.supportedRTL.includes(baseLocale) ? 'rtl' : 'ltr';
}
applyDirection(locale) {
const dir = this.detectLocale(locale);
this.currentDir = dir;
document.documentElement.dir = dir;
document.documentElement.lang = locale;
}
flipStyles(styles) {
if (this.currentDir !== 'rtl') return styles;
const flipMap = {
'margin-left': 'margin-right',
'margin-right': 'margin-left',
'padding-left': 'padding-right',
'padding-right': 'padding-left',
'border-left': 'border-right',
'border-right': 'border-left',
'left': 'right',
'right': 'left',
'text-align-left': 'text-align-right',
'text-align-right': 'text-align-left'
};
const flipped = { ...styles };
Object.entries(styles).forEach(([key, value]) => {
if (flipMap[key]) {
flipped[flipMap[key]] = value;
delete flipped[key];
}
});
return flipped;
}
getStartEnd() {
return this.currentDir === 'rtl'
? { start: 'right', end: 'left' }
: { start: 'left', end: 'right' };
}
}
Best Practices
- Externalize all user-facing strings from the beginning
- Use locale-aware formatting libraries (Intl API, i18next, react-intl)
- Design for text expansion (languages can be 300% longer)
- Test with pseudo-localization before real translations
- Support RTL languages from the start, not as an afterthought
- Store translations in structured files (JSON, YAML, XLIFF)
- Use ICU MessageFormat for complex pluralization and gender
- Consider date, time, number, and currency formatting per locale
- Maintain separate translation memories for consistency
- Involve native speakers in translation review