UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

724 lines 22.2 kB
/** * @fileoverview OrdoJS Core - Internationalization Manager */ /** * Internationalization Manager for OrdoJS applications */ export class I18nManager { config; currentLocale; translations = new Map(); localeConfigs = new Map(); numberFormatters = new Map(); dateFormatters = new Map(); currencyFormatters = new Map(); constructor(config = {}) { this.config = { defaultLocale: 'en', supportedLocales: ['en'], fallbackLocale: 'en', enableRTL: true, enableNumberFormatting: true, enableDateFormatting: true, enableCurrencyFormatting: true, enablePluralization: true, enableGenderSupport: true, enableContextSupport: true, translationPath: '/translations', autoDetectLocale: true, persistLocale: true, ...config }; this.currentLocale = this.config.defaultLocale; this.initialize(); } /** * Initialize the i18n manager */ async initialize() { // Auto-detect locale if enabled if (this.config.autoDetectLocale) { this.currentLocale = this.detectLocale(); } // Load persisted locale if enabled if (this.config.persistLocale) { const persistedLocale = this.getPersistedLocale(); if (persistedLocale && this.config.supportedLocales.includes(persistedLocale)) { this.currentLocale = persistedLocale; } } // Load default locale translations await this.loadTranslations(this.currentLocale); // Setup locale configurations this.setupLocaleConfigs(); // Setup formatters this.setupFormatters(); // Setup RTL support if (this.config.enableRTL) { this.setupRTLSupport(); } } /** * Detect user's preferred locale */ detectLocale() { // Check browser language const browserLocale = navigator.language || navigator.languages?.[0]; if (browserLocale) { const baseLocale = browserLocale.split('-')[0]; if (this.config.supportedLocales.includes(baseLocale)) { return baseLocale; } } // Check for exact match if (browserLocale && this.config.supportedLocales.includes(browserLocale)) { return browserLocale; } return this.config.defaultLocale; } /** * Get persisted locale from storage */ getPersistedLocale() { try { return localStorage.getItem('ordojs-locale'); } catch { return null; } } /** * Set persisted locale in storage */ setPersistedLocale(locale) { try { localStorage.setItem('ordojs-locale', locale); } catch { // Ignore storage errors } } /** * Load translations for a locale */ async loadTranslations(locale) { try { const response = await fetch(`${this.config.translationPath}/${locale}.json`); if (response.ok) { const translations = await response.json(); this.translations.set(locale, translations); } else { // Load fallback translations if (locale !== this.config.fallbackLocale) { await this.loadTranslations(this.config.fallbackLocale); } } } catch (error) { console.warn(`Failed to load translations for locale: ${locale}`, error); // Load fallback translations if (locale !== this.config.fallbackLocale) { await this.loadTranslations(this.config.fallbackLocale); } } } /** * Setup locale configurations */ setupLocaleConfigs() { // English this.localeConfigs.set('en', { code: 'en', name: 'English', nativeName: 'English', rtl: false, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'USD' }, pluralRules: { one: 'one', other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); // Spanish this.localeConfigs.set('es', { code: 'es', name: 'Spanish', nativeName: 'Español', rtl: false, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'EUR' }, pluralRules: { one: 'one', other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); // Arabic this.localeConfigs.set('ar', { code: 'ar', name: 'Arabic', nativeName: 'العربية', rtl: true, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'SAR' }, pluralRules: { zero: 'zero', one: 'one', two: 'two', few: 'few', many: 'many', other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); // French this.localeConfigs.set('fr', { code: 'fr', name: 'French', nativeName: 'Français', rtl: false, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'EUR' }, pluralRules: { one: 'one', other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); // German this.localeConfigs.set('de', { code: 'de', name: 'German', nativeName: 'Deutsch', rtl: false, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'EUR' }, pluralRules: { one: 'one', other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); // Chinese (Simplified) this.localeConfigs.set('zh-CN', { code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', rtl: false, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'CNY' }, pluralRules: { other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); // Japanese this.localeConfigs.set('ja', { code: 'ja', name: 'Japanese', nativeName: '日本語', rtl: false, numberFormat: { style: 'decimal' }, dateFormat: { year: 'numeric', month: 'long', day: 'numeric' }, currencyFormat: { style: 'currency', currency: 'JPY' }, pluralRules: { other: 'other' }, genderRules: { male: 'male', female: 'female', neutral: 'neutral' } }); } /** * Setup formatters */ setupFormatters() { this.config.supportedLocales.forEach(locale => { const config = this.localeConfigs.get(locale); if (config) { // Number formatter this.numberFormatters.set(locale, new Intl.NumberFormat(locale, config.numberFormat)); // Date formatter this.dateFormatters.set(locale, new Intl.DateTimeFormat(locale, config.dateFormat)); // Currency formatter this.currencyFormatters.set(locale, new Intl.NumberFormat(locale, config.currencyFormat)); } }); } /** * Setup RTL support */ setupRTLSupport() { const config = this.localeConfigs.get(this.currentLocale); if (config?.rtl) { document.documentElement.setAttribute('dir', 'rtl'); document.documentElement.setAttribute('lang', this.currentLocale); } else { document.documentElement.setAttribute('dir', 'ltr'); document.documentElement.setAttribute('lang', this.currentLocale); } } /** * Translate a key with optional context */ translate(key, context = {}) { const translation = this.getTranslation(key); if (typeof translation === 'function') { return translation(context); } if (typeof translation === 'string') { return this.interpolate(translation, context); } // Return key if translation not found return key; } /** * Get translation for a key */ getTranslation(key) { const currentTranslations = this.translations.get(this.currentLocale); const fallbackTranslations = this.translations.get(this.config.fallbackLocale); if (currentTranslations) { const translation = this.getNestedTranslation(currentTranslations, key); if (translation !== undefined) { return translation; } } if (fallbackTranslations && this.currentLocale !== this.config.fallbackLocale) { const translation = this.getNestedTranslation(fallbackTranslations, key); if (translation !== undefined) { return translation; } } return undefined; } /** * Get nested translation from object */ getNestedTranslation(translations, key) { const keys = key.split('.'); let current = translations; for (const k of keys) { if (current && typeof current === 'object' && k in current) { current = current[k]; } else { return undefined; } } return current; } /** * Interpolate variables in translation string */ interpolate(text, context) { return text.replace(/\{\{(\w+)\}\}/g, (match, key) => { return context[key] !== undefined ? String(context[key]) : match; }); } /** * Format number according to current locale */ formatNumber(value, options) { const formatter = this.numberFormatters.get(this.currentLocale); if (formatter) { return formatter.format(value); } // Fallback to default formatting return new Intl.NumberFormat(this.currentLocale, options).format(value); } /** * Format date according to current locale */ formatDate(date, options) { const formatter = this.dateFormatters.get(this.currentLocale); if (formatter) { return formatter.format(date); } // Fallback to default formatting return new Intl.DateTimeFormat(this.currentLocale, options).format(date); } /** * Format currency according to current locale */ formatCurrency(value, currency, options) { const config = this.localeConfigs.get(this.currentLocale); const currencyCode = currency || config?.currencyFormat.currency || 'USD'; const formatter = this.currencyFormatters.get(this.currentLocale); if (formatter) { return formatter.format(value); } // Fallback to default formatting return new Intl.NumberFormat(this.currentLocale, { style: 'currency', currency: currencyCode, ...options }).format(value); } /** * Handle pluralization */ pluralize(key, count, context = {}) { if (!this.config.enablePluralization) { return this.translate(key, { ...context, count }); } const config = this.localeConfigs.get(this.currentLocale); const pluralRule = this.getPluralRule(count, config?.pluralRules); const pluralKey = `${key}.${pluralRule}`; const translation = this.translate(pluralKey, { ...context, count }); // If no plural translation found, fall back to base key if (translation === pluralKey) { return this.translate(key, { ...context, count }); } return translation; } /** * Get plural rule for count */ getPluralRule(count, rules) { if (!rules) { return count === 1 ? 'one' : 'other'; } // English-like rules if (rules.one && rules.other) { return count === 1 ? 'one' : 'other'; } // Arabic-like rules if (rules.zero && rules.one && rules.two && rules.few && rules.many && rules.other) { if (count === 0) return 'zero'; if (count === 1) return 'one'; if (count === 2) return 'two'; if (count >= 3 && count <= 10) return 'few'; if (count >= 11 && count <= 99) return 'many'; return 'other'; } // Chinese/Japanese-like rules if (rules.other) { return 'other'; } return 'other'; } /** * Handle gender-specific translations */ genderize(key, gender, context = {}) { if (!this.config.enableGenderSupport) { return this.translate(key, { ...context, gender }); } const genderKey = `${key}.${gender}`; const translation = this.translate(genderKey, { ...context, gender }); // If no gender-specific translation found, fall back to base key if (translation === genderKey) { return this.translate(key, { ...context, gender }); } return translation; } /** * Change current locale */ async setLocale(locale) { if (!this.config.supportedLocales.includes(locale)) { throw new Error(`Unsupported locale: ${locale}`); } this.currentLocale = locale; // Load translations if not already loaded if (!this.translations.has(locale)) { await this.loadTranslations(locale); } // Update RTL support if (this.config.enableRTL) { this.setupRTLSupport(); } // Persist locale if enabled if (this.config.persistLocale) { this.setPersistedLocale(locale); } // Dispatch locale change event window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale } })); } /** * Get current locale */ getCurrentLocale() { return this.currentLocale; } /** * Get supported locales */ getSupportedLocales() { return this.config.supportedLocales; } /** * Get locale configuration */ getLocaleConfig(locale) { return this.localeConfigs.get(locale); } /** * Check if locale is RTL */ isRTL(locale) { const targetLocale = locale || this.currentLocale; const config = this.localeConfigs.get(targetLocale); return config?.rtl || false; } /** * Get locale name */ getLocaleName(locale) { const config = this.localeConfigs.get(locale); return config?.name || locale; } /** * Get native locale name */ getNativeLocaleName(locale) { const config = this.localeConfigs.get(locale); return config?.nativeName || locale; } /** * Add custom locale configuration */ addLocaleConfig(locale, config) { this.localeConfigs.set(locale, config); this.setupFormatters(); } /** * Add custom translations */ addTranslations(locale, translations) { const existing = this.translations.get(locale) || {}; this.translations.set(locale, { ...existing, ...translations }); } /** * Create translation workflow */ createTranslationWorkflow() { return new TranslationWorkflow(this); } /** * Export translations for a locale */ exportTranslations(locale) { return this.translations.get(locale) || {}; } /** * Import translations for a locale */ importTranslations(locale, translations) { this.translations.set(locale, translations); } /** * Get missing translations */ getMissingTranslations(locale, baseLocale = this.config.defaultLocale) { const baseTranslations = this.translations.get(baseLocale) || {}; const targetTranslations = this.translations.get(locale) || {}; const missing = []; this.findMissingKeys(baseTranslations, targetTranslations, '', missing); return missing; } /** * Find missing translation keys */ findMissingKeys(base, target, prefix, missing) { for (const key in base) { const fullKey = prefix ? `${prefix}.${key}` : key; if (!(key in target)) { missing.push(fullKey); } else if (typeof base[key] === 'object' && typeof target[key] === 'object') { this.findMissingKeys(base[key], target[key], fullKey, missing); } } } } /** * Translation Workflow for managing translations */ export class TranslationWorkflow { i18n; constructor(i18n) { this.i18n = i18n; } /** * Extract translatable strings from code */ extractStrings(code) { const strings = []; const patterns = [ /translate\(['"`]([^'"`]+)['"`]/g, /t\(['"`]([^'"`]+)['"`]/g, /\{\{([^}]+)\}\}/g ]; patterns.forEach(pattern => { let match; while ((match = pattern.exec(code)) !== null) { strings.push(match[1]); } }); return [...new Set(strings)]; } /** * Generate translation template */ generateTemplate(locale, strings) { const template = {}; strings.forEach(str => { const keys = str.split('.'); let current = template; keys.forEach((key, index) => { if (index === keys.length - 1) { current[key] = str; } else { current[key] = current[key] || {}; current = current[key]; } }); }); return template; } /** * Validate translations */ validateTranslations(locale) { const errors = []; const translations = this.i18n.exportTranslations(locale); // Check for missing translations const missing = this.i18n.getMissingTranslations(locale); if (missing.length > 0) { errors.push(`Missing translations: ${missing.join(', ')}`); } // Check for unused translations const unused = this.findUnusedTranslations(locale); if (unused.length > 0) { errors.push(`Unused translations: ${unused.join(', ')}`); } return { valid: errors.length === 0, errors }; } /** * Find unused translations */ findUnusedTranslations(locale) { // This would require scanning the entire codebase // For now, return empty array return []; } /** * Merge translations */ mergeTranslations(locale, newTranslations) { const existing = this.i18n.exportTranslations(locale); const merged = this.deepMerge(existing, newTranslations); this.i18n.importTranslations(locale, merged); } /** * Deep merge objects */ deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = this.deepMerge(target[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } } //# sourceMappingURL=i18n-manager.js.map