configuration drives the entire application behavior.
// locale.config.ts
export interface LocaleConfig {
code: string;
name: string;
direction: 'ltr' | 'rtl';
currency: string;
numberFormat: Intl.NumberFormatOptions;
dateLocale: string;
businessRules: BusinessRuleSet;
uiConstraints: UIConstraints;
}
export interface BusinessRuleSet {
taxTerminology: Record<string, string>;
accountCategories: Record<string, string>;
maxDecimalPlaces: number;
}
export interface UIConstraints {
maxButtonLength: number;
stressTestLocale: string;
}
export const LOCALES: Record<string, LocaleConfig> = {
en: {
code: 'en',
name: 'English',
direction: 'ltr',
currency: 'USD',
numberFormat: { style: 'decimal', minimumFractionDigits: 2 },
dateLocale: 'en-US',
businessRules: { /* ... */ },
uiConstraints: { maxButtonLength: 20, stressTestLocale: 'ru' }
},
ru: {
code: 'ru',
name: 'Русский',
direction: 'ltr',
currency: 'UZS',
numberFormat: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
dateLocale: 'ru-RU',
businessRules: {
taxTerminology: { vat: 'НДС', incomeTax: 'Налог на доход' },
accountCategories: { assets: 'Активы', liabilities: 'Обязательства' },
maxDecimalPlaces: 2
},
uiConstraints: { maxButtonLength: 30, stressTestLocale: 'ru' }
},
// uz, cn configurations...
};
Rationale: Centralizing configuration allows the application to adapt behavior dynamically. The stressTestLocale flag identifies which locale imposes the most layout pressure, guiding design decisions. Separating businessRules ensures that financial terminology and formatting are handled independently of UI strings.
2. In-Memory Locale Switching
Avoid full page reloads when switching languages. Reloading destroys form state, disrupts user flow, and increases perceived latency. Implement in-memory switching with silent URL updates to maintain state while reflecting the change in the address bar.
// hooks/useLocaleSwitch.ts
import { useState, useCallback } from 'react';
import { useRouter } from 'next/router';
import { LOCALES } from '../config/locale.config';
export function useLocaleSwitch() {
const router = useRouter();
const [currentLocale, setCurrentLocale] = useState(
LOCALES[router.locale || 'en']
);
const switchLocale = useCallback((localeCode: string) => {
const newLocale = LOCALES[localeCode];
if (!newLocale) return;
// Update in-memory state
setCurrentLocale(newLocale);
// Update document direction
document.documentElement.dir = newLocale.direction;
document.documentElement.lang = newLocale.code;
// Silent URL update without reload
const currentPath = router.asPath;
const newUrl = currentPath.replace(
new RegExp(`^/(${Object.keys(LOCALES).join('|')})`),
`/${localeCode}`
);
router.replace(newUrl, undefined, { shallow: true });
}, [router]);
return { currentLocale, switchLocale };
}
Rationale: shallow: true in the router replacement prevents re-rendering of the page component, preserving form data and scroll position. Updating document.documentElement ensures accessibility attributes are correct. This approach provides a native app feel while maintaining SEO-friendly URLs.
3. Business Logic Formatters
Financial data requires locale-aware formatting that goes beyond standard libraries. Create custom formatters that apply business rules, such as specific tax terminology and currency precision.
// utils/formatters.ts
import { LocaleConfig } from '../config/locale.config';
export function formatBusinessAmount(
amount: number,
locale: LocaleConfig
): string {
const formatter = new Intl.NumberFormat(locale.dateLocale, {
style: 'currency',
currency: locale.currency,
minimumFractionDigits: 2,
maximumFractionDigits: locale.businessRules.maxDecimalPlaces
});
return formatter.format(amount);
}
export function getLocalizedTaxTerm(
termKey: string,
locale: LocaleConfig
): string {
return locale.businessRules.taxTerminology[termKey] || termKey;
}
Rationale: Standard Intl.NumberFormat may not handle region-specific nuances like UZS large number formatting. By wrapping formatters in utility functions, you can inject custom logic, such as adjusting decimal places or applying locale-specific rounding rules. This ensures consistency across the application and reduces the risk of calculation errors.
4. Translation Management System (TMS) Integration
Spreadsheets do not scale. Integrate a TMS early to manage translations, context, and workflows. Use a library that supports pluralization, gender, and interpolation to handle complex linguistic structures.
// i18n/translator.ts
import { createI18n } from 'some-i18n-lib';
import { LOCALES } from '../config/locale.config';
export const i18n = createI18n({
locales: Object.keys(LOCALES),
defaultLocale: 'en',
messages: {
en: { /* ... */ },
ru: { /* ... */ },
// ...
},
fallbackLocale: 'en',
pluralRules: {
ru: (choice, options) => {
// Custom pluralization logic for Russian
if (choice % 10 === 1 && choice % 100 !== 11) return 'one';
if ([2, 3, 4].includes(choice % 10) && ![12, 13, 14].includes(choice % 100)) return 'few';
return 'many';
}
}
});
Rationale: A TMS provides version control, collaboration tools for translators, and context injection. Custom plural rules handle languages with complex morphology. Fallback locales ensure the app remains usable even if a translation is missing.
Pitfall Guide
Avoid these common mistakes to ensure a robust multilingual implementation.
-
The Spreadsheet Trap
- Explanation: Managing translations in spreadsheets leads to version conflicts, lost context, and scalability issues. Translators lack visibility into where strings appear in the UI.
- Fix: Use a dedicated TMS. Provide screenshots and context for each string. Automate sync between code and TMS.
-
Layout Fragility from Text Expansion
- Explanation: Designing UI based on English string lengths causes breakage in locales like Russian or Chinese. Buttons overflow, modals truncate, and tables misalign.
- Fix: Design with the longest locale as the stress test. Use flexible layouts, ellipsis truncation, and dynamic sizing. Validate UI against all locales during QA.
-
State Loss on Language Switch
- Explanation: Reloading the page to switch languages discards form inputs, scroll position, and application state. Users lose progress and become frustrated.
- Fix: Implement in-memory locale switching. Update the URL silently. Preserve all application state during the transition.
-
Ignoring Business Logic Localization
- Explanation: Assuming financial formatting and terminology are universal leads to errors. Currency symbols, decimal separators, and tax terms vary by region.
- Fix: Create a locale layer for business logic. Use custom formatters for currency and numbers. Localize tax terminology and account categories.
-
Cultural Translation Errors
- Explanation: Literal translation misses cultural nuances. Business concepts may require different explanations in different regions. Tone and formality levels vary.
- Fix: Involve native speakers in UX review. Provide context to translators. Adapt terminology to local business practices. Define formality levels in locale config.
-
Grammatical Gender and Address Issues
- Explanation: Languages like Russian and Uzbek have grammatical gender and formal/informal address. Ignoring these results in awkward or incorrect UI copy.
- Fix: Use structured messages with interpolation. Support gender-neutral phrasing where possible. Define politeness levels in locale configuration.
-
Late i18n Implementation
- Explanation: Adding i18n after the app is built requires extensive refactoring. Hardcoded strings and layout assumptions make localization difficult.
- Fix: Integrate i18n from day one. Use translation functions for all user-facing text. Design layouts with flexibility in mind.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High form density | In-memory switching | Preserves form state and user progress. | Low dev cost; high UX benefit. |
| SEO critical | URL prefix + Server-side rendering | Ensures search engines index localized content. | Higher infra cost; better organic reach. |
| Complex tax region | Custom business rules layer | Ensures compliance and accurate reporting. | High dev cost; reduces legal risk. |
| Rapid expansion | TMS with automated workflows | Scales translation management efficiently. | Moderate TMS cost; saves engineering time. |
| Limited resources | Focus on core locales first | Prioritizes high-impact markets. | Lower initial cost; phased rollout. |
Configuration Template
Use this template to bootstrap your locale configuration.
// config/locales.ts
export const LOCALES = {
en: {
code: 'en',
name: 'English',
direction: 'ltr',
currency: 'USD',
numberFormat: { style: 'decimal', minimumFractionDigits: 2 },
dateLocale: 'en-US',
businessRules: {
taxTerminology: { vat: 'VAT', incomeTax: 'Income Tax' },
accountCategories: { assets: 'Assets', liabilities: 'Liabilities' },
maxDecimalPlaces: 2
},
uiConstraints: { maxButtonLength: 20, stressTestLocale: 'ru' }
},
ru: {
code: 'ru',
name: 'Русский',
direction: 'ltr',
currency: 'UZS',
numberFormat: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
dateLocale: 'ru-RU',
businessRules: {
taxTerminology: { vat: 'НДС', incomeTax: 'Налог на доход' },
accountCategories: { assets: 'Активы', liabilities: 'Обязательства' },
maxDecimalPlaces: 2
},
uiConstraints: { maxButtonLength: 30, stressTestLocale: 'ru' }
},
uz: {
code: 'uz',
name: "O'zbek",
direction: 'ltr',
currency: 'UZS',
numberFormat: { style: 'decimal', minimumFractionDigits: 2 },
dateLocale: 'uz-UZ',
businessRules: {
taxTerminology: { vat: 'QQS', incomeTax: 'Daromad soli' },
accountCategories: { assets: 'Aktivlar', liabilities: 'Majburiyatlar' },
maxDecimalPlaces: 2
},
uiConstraints: { maxButtonLength: 25, stressTestLocale: 'ru' }
},
cn: {
code: 'cn',
name: '中文',
direction: 'ltr',
currency: 'CNY',
numberFormat: { style: 'decimal', minimumFractionDigits: 2 },
dateLocale: 'zh-CN',
businessRules: {
taxTerminology: { vat: '增值税', incomeTax: '所得税' },
accountCategories: { assets: '资产', liabilities: '负债' },
maxDecimalPlaces: 2
},
uiConstraints: { maxButtonLength: 15, stressTestLocale: 'ru' }
}
};
Quick Start Guide
- Initialize i18n Library: Install a robust i18n library (e.g.,
next-intl, react-i18next). Configure it with your locale matrix and fallback settings.
- Wrap Application: Implement a
LocaleProvider at the root of your app. This provider should manage locale state, apply direction attributes, and expose translation functions.
- Add Switcher Component: Create a language switcher that uses in-memory switching. Ensure it updates the URL silently and preserves application state.
- Localize Content: Replace all hardcoded strings with translation function calls. Use custom formatters for financial data and business terminology.
- Test and Validate: Run QA against all locales. Check layout stability, business logic accuracy, and cultural appropriateness. Involve native speakers for review.