Internationalization
Internationalization
Section titled “Internationalization”Problem and Rationale
Section titled “Problem and Rationale”Zelta Chat serves a global user base requiring:
- Support for 42+ languages
- Dynamic language switching without app restart
- Fallback to English for missing translations
- Right-to-left (RTL) language support
- User preference persistence
Why i18n-js:
- Lightweight and simple API
- Support for pluralization and interpolation
- Fallback mechanism for missing translations
- No native dependencies
- Compatible with JSON translation files
Design
Section titled “Design”Supported Languages
Section titled “Supported Languages”42 Languages:
- European: English (en), German (de), French (fr), Spanish (es), Italian (it), Portuguese (pt, pt_BR), Dutch (nl), Polish (pl), Swedish (sv), Norwegian (no), Danish (da), Finnish (fi), Czech (cs), Romanian (ro), Croatian (hr), Hungarian (hu), Slovenian (sl), Serbian (sr, sh), Albanian (sq), Catalan (ca), Greek (el)
- Asian: Chinese (zh, zh_CN, zh_TW), Japanese (ja), Korean (ko), Vietnamese (vi), Indonesian (id), Malay (ms), Tamil (ta), Malayalam (ml), Hindi (hi), Hebrew (he)
- Middle Eastern: Arabic (ar), Persian/Farsi (fa), Turkish (tr), Urdu (ur), Azerbaijani (az)
- Eastern European: Russian (ru), Ukrainian (uk)
Translation Structure
Section titled “Translation Structure”Translation Keys Organization:
{ "AUTH": { "LOGIN": "Login", "EMAIL": "Email", "PASSWORD": "Password" }, "CONVERSATIONS": { "TITLE": "Conversations", "EMPTY_STATE": "No conversations found" }, "ERRORS": { "COMMON_ERROR": "Something went wrong", "NETWORK_ERROR": "Network error" }}Locale Management
Section titled “Locale Management”Locale stored in Redux:
interface SettingsState { locale: string; // 'en', 'es', 'fr', etc. // ... other settings}Lifecycle and Data Flow
Section titled “Lifecycle and Data Flow”Language Initialization Flow
Section titled “Language Initialization Flow”App Launch ↓Redux Persist Rehydrates Settings ↓Get locale from settings.locale (default: 'es') ↓Set i18n.locale = locale ↓Load Translation Files ↓App Renders with Selected LanguageLanguage Switch Flow
Section titled “Language Switch Flow”User Changes Language in Settings ↓Dispatch: settingsActions.updateLocale(newLocale) ↓Update Redux: settings.locale = newLocale ↓Update i18n: i18n.locale = newLocale ↓Redux Persist Saves Setting ↓Components Re-render with New TranslationsTranslation Lookup Flow
Section titled “Translation Lookup Flow”Component Calls: I18n.t('CONVERSATIONS.TITLE') ↓i18n-js Checks Current Locale ↓Lookup Translation Key in Current Locale ↓├─ Found: Return Translation└─ Not Found: ↓ Check Fallback Locale (English) ↓ ├─ Found: Return Fallback └─ Not Found: Return KeyImplementation Notes
Section titled “Implementation Notes”I18n Setup
Section titled “I18n Setup”Configuration (src/i18n/index.js):
import i18n from 'i18n-js';
// Import all translationsimport en from './en.json';import es from './es.json';import fr from './fr.json';// ... more imports
// Set default localei18n.locale = 'es'; // Custom default
// Enable fallbacksi18n.fallbacks = true; // Falls back to English
// Register translationsi18n.translations = { en, es, fr, de, // ... all 42 languages};
export default i18n;Usage in Components
Section titled “Usage in Components”Basic Translation:
import I18n from '@/i18n';
const MyComponent = () => { return ( <View> <Text>{I18n.t('CONVERSATIONS.TITLE')}</Text> <Text>{I18n.t('CONVERSATIONS.EMPTY_STATE')}</Text> </View> );};Translation with Interpolation:
{ "MESSAGES": { "GREETING": "Hello, {{name}}!", "COUNT": "You have {{count}} new messages" }}const greeting = I18n.t('MESSAGES.GREETING', { name: user.name });const count = I18n.t('MESSAGES.COUNT', { count: unreadCount });Pluralization:
{ "MESSAGES": { "COUNT": { "one": "{{count}} message", "other": "{{count}} messages" } }}const message = I18n.t('MESSAGES.COUNT', { count: messageCount });Dynamic Locale Update
Section titled “Dynamic Locale Update”Update Locale (src/store/settings/settingsSlice.ts):
const settingsSlice = createSlice({ name: 'settings', initialState: { locale: 'es', // ... other settings }, reducers: { updateLocale: (state, action: PayloadAction<string>) => { state.locale = action.payload; }, },});Apply Locale in Navigation (src/navigation/index.tsx):
export const AppNavigationContainer = () => { const locale = useAppSelector(selectLocale);
// Update i18n locale when Redux state changes i18n.locale = locale;
return ( <NavigationContainer> <AppTabs /> </NavigationContainer> );};RTL Support
Section titled “RTL Support”For RTL languages (Arabic, Hebrew, Persian):
import { I18nManager } from 'react-native';import I18n from '@/i18n';
const rtlLanguages = ['ar', 'he', 'fa'];
export const setupRTL = (locale: string) => { const isRTL = rtlLanguages.includes(locale);
if (I18nManager.isRTL !== isRTL) { I18nManager.forceRTL(isRTL); // Requires app restart on Android if (Platform.OS === 'android') { Alert.alert( I18n.t('SETTINGS.RESTART_REQUIRED'), I18n.t('SETTINGS.RESTART_MESSAGE') ); } }};Patterns and Anti-Patterns
Section titled “Patterns and Anti-Patterns”✅ Good Patterns:
// Use constants for translation keysconst TRANSLATION_KEYS = { TITLE: 'CONVERSATIONS.TITLE', EMPTY: 'CONVERSATIONS.EMPTY_STATE',} as const;
const title = I18n.t(TRANSLATION_KEYS.TITLE);
// Provide fallback for missing translationsconst message = I18n.t('KEY.THAT.MIGHT.NOT.EXIST', { defaultValue: 'Fallback Message'});
// Group related translations{ "CONVERSATION_ACTIONS": { "ASSIGN": "Assign", "RESOLVE": "Resolve", "REOPEN": "Reopen" }}❌ Anti-Patterns:
// DON'T: Hardcode strings<Text>Conversations</Text> // ❌ Use I18n.t()
// DON'T: Split translation keys unnecessarilyI18n.t('CONVERSATIONS') + ': ' + I18n.t('TITLE') // ❌ Use single key
// DON'T: Store translated strings in Reduxdispatch(setTitle(I18n.t('TITLE'))); // ❌ Store key, translate in render
// DON'T: Use complex interpolation logicI18n.t('MESSAGE', { value: someComplexCalculation()}); // ❌ Calculate before translationTesting Strategy
Section titled “Testing Strategy”Translation Tests
Section titled “Translation Tests”import I18n from '@/i18n';
describe('Internationalization', () => { beforeEach(() => { I18n.locale = 'en'; });
it('should return English translation', () => { expect(I18n.t('CONVERSATIONS.TITLE')).toBe('Conversations'); });
it('should return Spanish translation', () => { I18n.locale = 'es'; expect(I18n.t('CONVERSATIONS.TITLE')).toBe('Conversaciones'); });
it('should fallback to English for missing translation', () => { I18n.locale = 'fr'; // If French translation missing, should return English const result = I18n.t('MISSING.KEY'); expect(result).toBeDefined(); });
it('should interpolate values', () => { const result = I18n.t('MESSAGES.GREETING', { name: 'John' }); expect(result).toContain('John'); });});Coverage Tests
Section titled “Coverage Tests”describe('Translation Coverage', () => { const baseTranslations = require('./en.json'); const languages = ['es', 'fr', 'de'];
languages.forEach(lang => { it(`should have all keys in ${lang}`, () => { const translations = require(`./${lang}.json`); const baseKeys = getAllKeys(baseTranslations); const langKeys = getAllKeys(translations);
expect(langKeys).toEqual(expect.arrayContaining(baseKeys)); }); });});Performance Considerations
Section titled “Performance Considerations”Lazy Load Translations
Section titled “Lazy Load Translations”For large translation files:
const translations: Record<string, any> = {};
export const loadTranslation = async (locale: string) => { if (!translations[locale]) { translations[locale] = await import(`./i18n/${locale}.json`); i18n.translations[locale] = translations[locale]; }};Memoize Translations
Section titled “Memoize Translations”For frequently used translations:
import { useMemo } from 'react';
const MyComponent = () => { const translations = useMemo(() => ({ title: I18n.t('CONVERSATIONS.TITLE'), empty: I18n.t('CONVERSATIONS.EMPTY_STATE'), // ... more translations }), []); // Re-compute only when locale changes
return <Text>{translations.title}</Text>;};Extending This Concept
Section titled “Extending This Concept”Adding a New Language
Section titled “Adding a New Language”- Create translation file:
cp src/i18n/en.json src/i18n/new-lang.json- Translate content:
{ "CONVERSATIONS": { "TITLE": "Translated Title", "EMPTY_STATE": "Translated Empty State" }}- Register in i18n:
import newLang from './new-lang.json';
i18n.translations = { // ... existing 'new-lang': newLang,};Adding Context-Aware Translations
Section titled “Adding Context-Aware Translations”{ "BUTTON": { "SUBMIT": { "LOGIN": "Sign In", "SIGNUP": "Create Account", "DEFAULT": "Submit" } }}const getSubmitText = (context: string) => { return I18n.t(`BUTTON.SUBMIT.${context.toUpperCase()}`, { defaultValue: I18n.t('BUTTON.SUBMIT.DEFAULT'), });};Translation Validation Script
Section titled “Translation Validation Script”import en from '../src/i18n/en.json';import es from '../src/i18n/es.json';
const getAllKeys = (obj: any, prefix = ''): string[] => { return Object.keys(obj).flatMap(key => { const value = obj[key]; const fullKey = prefix ? `${prefix}.${key}` : key;
return typeof value === 'object' ? getAllKeys(value, fullKey) : [fullKey]; });};
const enKeys = getAllKeys(en);const esKeys = getAllKeys(es);
const missingKeys = enKeys.filter(key => !esKeys.includes(key));
if (missingKeys.length > 0) { console.error('Missing translations in Spanish:', missingKeys); process.exit(1);}Further Reading
Section titled “Further Reading”- State Management - Locale storage
- UI Components - Translation usage
- i18n-js Documentation