core_I18n.js

import { CustomEvents } from "./CustomEvents.js";

/**
 * I18n - Modern internationalization utility for multi-language applications
 * 
 * Features:
 * - Translation management with namespace support
 * - Named and numbered parameter interpolation
 * - Plural forms support
 * - Locale-aware number and date formatting
 * - Browser language detection with fallback chains
 * - Async translation loading
 * - Event-driven language switching
 * 
 * @example
 * // Basic usage
 * const i18n = new I18n(app);
 * i18n.addTranslation('en', { 'hello': 'Hello', 'welcome': 'Welcome {name}!' });
 * i18n.addTranslation('es', { 'hello': 'Hola', 'welcome': '¡Bienvenido {name}!' });
 * 
 * console.log(i18n.t('hello')); // 'Hello'
 * console.log(i18n.t('welcome', { name: 'John' })); // 'Welcome John!'
 * 
 * @example
 * // Namespace support
 * i18n.addTranslation('en', { 
 *   'common.save': 'Save',
 *   'user.profile.title': 'User Profile'
 * });
 * 
 * @example
 * // Plural forms
 * i18n.addTranslation('en', {
 *   'items': {
 *     one: '{count} item',
 *     other: '{count} items'
 *   }
 * });
 * console.log(i18n.t('items', { count: 1 })); // '1 item'
 * console.log(i18n.t('items', { count: 5 })); // '5 items'
 */
class I18n {
    // Default language constant
    static DEFAULT_LANG = 'en';

    // Common ISO 639-1 language codes
    static LANGUAGE_CODES = [
        'ab', 'aa', 'af', 'ak', 'sq', 'am', 'ar', 'an', 'hy', 'as', 'av', 'ae', 'ay', 'az', 'bm', 'ba', 'eu', 'be', 'bn', 'bh', 'bi', 'bs', 'br', 'bg', 'my', 'ca', 'ch', 'ce', 'ny', 'zh', 'cv', 'kw', 'co', 'cr', 'hr', 'cs', 'da', 'dv', 'nl', 'dz', 'en', 'eo', 'et', 'ee', 'fo', 'fj', 'fi', 'fr', 'ff', 'gl', 'ka', 'de', 'el', 'gn', 'gu', 'ht', 'ha', 'he', 'hz', 'hi', 'ho', 'hu', 'ia', 'id', 'ie', 'ga', 'ig', 'ik', 'io', 'is', 'it', 'iu', 'ja', 'jv', 'kl', 'kn', 'kr', 'ks', 'kk', 'km', 'ki', 'rw', 'ky', 'kv', 'kg', 'ko', 'ku', 'kj', 'la', 'lb', 'lg', 'li', 'ln', 'lo', 'lt', 'lu', 'lv', 'gv', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi', 'mr', 'mh', 'mn', 'na', 'nv', 'nb', 'nd', 'ne', 'ng', 'nn', 'no', 'ii', 'nr', 'oc', 'oj', 'cu', 'om', 'or', 'os', 'pa', 'pi', 'fa', 'pl', 'ps', 'pt', 'qu', 'rm', 'rn', 'ro', 'ru', 'sa', 'sc', 'sd', 'se', 'sm', 'sg', 'sr', 'gd', 'sn', 'si', 'sk', 'sl', 'so', 'st', 'es', 'su', 'sw', 'ss', 'sv', 'ta', 'te', 'tg', 'th', 'ti', 'bo', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw', 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'cy', 'wo', 'fy', 'xh', 'yi', 'yo', 'za', 'zu'
    ];

    // Plural rules for different languages (simplified)
    static PLURAL_RULES = {
        // English, German, Dutch, etc. (1 = singular, other = plural)
        en: (n) => n === 1 ? 'one' : 'other',
        de: (n) => n === 1 ? 'one' : 'other',
        nl: (n) => n === 1 ? 'one' : 'other',
        
        // French (0-1 = singular, other = plural)
        fr: (n) => (n >= 0 && n <= 1) ? 'one' : 'other',
        
        // Russian, Ukrainian (complex rules)
        ru: (n) => {
            if (n % 10 === 1 && n % 100 !== 11) return 'one';
            if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'few';
            return 'many';
        },
        
        // Polish (complex rules)
        pl: (n) => {
            if (n === 1) return 'one';
            if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'few';
            return 'many';
        },
        
        // Arabic (complex rules)
        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';
        }
    };

    /**
     * Constructor
     * @param {App} appInstance - InfrontJS App reference
     * @param {Object} options - Configuration options
     */
    constructor(appInstance, options = {}) {
        this.app = appInstance;
        this.options = {
            defaultLanguage: I18n.DEFAULT_LANG,
            fallbackLanguage: I18n.DEFAULT_LANG,
            detectBrowserLanguage: true,
            globalExpose: false,
            globalPrefix: '_',
            interpolation: {
                prefix: '{',
                suffix: '}'
            },
            ...options
        };

        this.defaultLanguage = this.options.defaultLanguage;
        this.fallbackLanguage = this.options.fallbackLanguage;
        this.dictionary = new Map(); // lang -> translations map
        this.loadingPromises = new Map(); // track async loading
        this.formatters = new Map(); // cached formatters

        // Initialize current language
        let initialLang = this.defaultLanguage;
        if (this.options.detectBrowserLanguage) {
            const browserLang = this.detectBrowserLanguage();
            if (browserLang) {
                initialLang = browserLang;
            }
        }

        this.setCurrentLanguage(initialLang);

        // Expose globally if requested
        if (this.options.globalExpose) {
            this.expose();
        }
    }

    /**
     * Detect browser language with improved locale support
     * @returns {string|null} Detected language code or null
     */
    detectBrowserLanguage() {
        if (typeof window === "undefined" || !window.navigator) {
            return null;
        }

        const nav = window.navigator;
        const languages = [];

        // Collect all available languages
        if (Array.isArray(nav.languages)) {
            languages.push(...nav.languages);
        }

        // Fallback properties
        const fallbackProps = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'];
        for (const prop of fallbackProps) {
            if (nav[prop] && typeof nav[prop] === 'string') {
                languages.push(nav[prop]);
            }
        }

        // Find best match
        for (const lang of languages) {
            if (!lang) continue;

            // Try exact match first (e.g., 'en-US')
            if (this._isValidLanguageCode(lang)) {
                return lang.toLowerCase();
            }

            // Try language part only (e.g., 'en' from 'en-US')
            const langCode = lang.split('-')[0];
            if (this._isValidLanguageCode(langCode)) {
                return langCode.toLowerCase();
            }
        }

        return null;
    }


    /**
     * Set current language with validation and events
     * @param {string} langCode - Language code (e.g., 'en', 'en-US')
     * @throws {Error} Invalid language code
     */
    setCurrentLanguage(langCode) {
        if (!langCode || typeof langCode !== 'string') {
            throw new Error(`Invalid langCode: ${langCode}`);
        }

        const normalizedLang = this._normalizeLanguageCode(langCode);
        if (!normalizedLang) {
            throw new Error(`Invalid langCode: ${langCode}`);
        }

        // Fire before event
        if (this.app && this.currentLanguage !== normalizedLang) {
            this.app.dispatchEvent(new CustomEvent(CustomEvents.TYPE.BEFORE_LANGUAGE_SWITCH, {
                detail: {
                    currentLanguage: this.currentLanguage,
                    newLanguage: normalizedLang
                }
            }));
        }

        const oldLanguage = this.currentLanguage;
        this.currentLanguage = normalizedLang;

        // Update formatters
        this._updateFormatters();

        // Fire after event
        if (this.app && oldLanguage !== normalizedLang) {
            this.app.dispatchEvent(new CustomEvent(CustomEvents.TYPE.AFTER_LANGUAGE_SWITCH, {
                detail: {
                    oldLanguage: oldLanguage,
                    currentLanguage: this.currentLanguage
                }
            }));
        }
    }

    /**
     * Get current language
     * @returns {string} Current language code
     */
    getCurrentLanguage() {
        return this.currentLanguage;
    }

    /**
     * Set entire dictionary for a language
     * @param {string} langCode - Language code
     * @param {Object} translations - Translation object
     */
    setDictionary(langCode, translations) {
        const normalizedLang = this._normalizeLanguageCode(langCode);
        if (!normalizedLang) {
            throw new Error(`Invalid langCode: ${langCode}`);
        }

        if (!translations || typeof translations !== 'object') {
            throw new Error('Translations must be an object');
        }

        this.dictionary.set(normalizedLang, this._flattenTranslations(translations));
    }

    /**
     * Add translations for a language (merge with existing)
     * @param {string} langCode - Language code
     * @param {Object} translationObject - Translations to add
     * @param {string} namespace - Optional namespace prefix
     */
    addTranslation(langCode, translationObject, namespace = '') {
        const normalizedLang = this._normalizeLanguageCode(langCode);
        if (!normalizedLang) {
            throw new Error(`Invalid langCode: ${langCode}`);
        }

        if (!translationObject || typeof translationObject !== 'object') {
            throw new Error('Translation object must be an object');
        }

        let currentDict = this.dictionary.get(normalizedLang) || {};
        const flatTranslations = this._flattenTranslations(translationObject, namespace);

        // Merge translations
        currentDict = { ...currentDict, ...flatTranslations };
        this.dictionary.set(normalizedLang, currentDict);
    }

    /**
     * Load translations asynchronously
     * @param {string} langCode - Language code
     * @param {string|Function} source - URL string or function returning translations
     * @returns {Promise} Loading promise
     */
    async loadTranslations(langCode, source) {
        const normalizedLang = this._normalizeLanguageCode(langCode);
        if (!normalizedLang) {
            throw new Error(`Invalid langCode: ${langCode}`);
        }

        // Return existing promise if already loading
        const loadingKey = `${normalizedLang}-${typeof source === 'string' ? source : 'function'}`;
        if (this.loadingPromises.has(loadingKey)) {
            return this.loadingPromises.get(loadingKey);
        }

        const loadingPromise = this._loadTranslationsInternal(normalizedLang, source);
        this.loadingPromises.set(loadingKey, loadingPromise);

        try {
            const result = await loadingPromise;
            this.loadingPromises.delete(loadingKey);
            return result;
        } catch (error) {
            this.loadingPromises.delete(loadingKey);
            throw error;
        }
    }

    /**
     * Get translation with interpolation and pluralization
     * @param {string} key - Translation key (supports namespaces with dots)
     * @param {Object|Array} params - Parameters for interpolation
     * @returns {string} Translated text
     */
    t(key, params = {}) {
        if (!key || typeof key !== 'string') {
            return '';
        }

        // Get translation with fallback
        let translation = this._getTranslation(key, this.currentLanguage);
        
        if (!translation && this.currentLanguage !== this.fallbackLanguage) {
            translation = this._getTranslation(key, this.fallbackLanguage);
        }

        if (!translation) {
            // Return key as fallback
            translation = key;
        }

        // Handle pluralization
        if (typeof translation === 'object' && params.count !== undefined) {
            translation = this._selectPluralForm(translation, params.count, this.currentLanguage);
        }

        if (typeof translation !== 'string') {
            return key;
        }

        // Interpolate parameters
        return this._interpolate(translation, params);
    }


    /**
     * Get formatted number
     * @param {number} num - Number to format
     * @param {Object} options - Intl.NumberFormat options
     * @returns {string} Formatted number
     */
    n(num = 0, options = null) {
        try {
            if (options) {
                return new Intl.NumberFormat(this.currentLanguage, options).format(num);
            }
            
            if (!this.formatters.has('number')) {
                this.formatters.set('number', new Intl.NumberFormat(this.currentLanguage, {
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2
                }));
            }
            
            return this.formatters.get('number').format(num);
        } catch (error) {
            console.warn('Number formatting failed:', error);
            return String(num);
        }
    }


    /**
     * Get formatted date/time
     * @param {Date} date - Date to format
     * @param {Object} options - Intl.DateTimeFormat options
     * @returns {string} Formatted date
     */
    d(date, options = null) {
        try {
            if (!(date instanceof Date) || isNaN(date.getTime())) {
                throw new Error('Invalid date provided');
            }

            if (options) {
                return new Intl.DateTimeFormat(this.currentLanguage, options).format(date);
            }
            
            if (!this.formatters.has('date')) {
                this.formatters.set('date', new Intl.DateTimeFormat(this.currentLanguage));
            }
            
            return this.formatters.get('date').format(date);
        } catch (error) {
            console.warn('Date formatting failed:', error);
            return String(date);
        }
    }


    /**
     * Check if translation exists
     * @param {string} key - Translation key
     * @param {string} langCode - Language code (optional, uses current if not provided)
     * @returns {boolean} True if translation exists
     */
    exists(key, langCode = null) {
        const lang = langCode ? this._normalizeLanguageCode(langCode) : this.currentLanguage;
        return !!this._getTranslation(key, lang);
    }

    /**
     * Get all available languages
     * @returns {Array} Array of language codes
     */
    getAvailableLanguages() {
        return Array.from(this.dictionary.keys());
    }

    /**
     * Remove translations for a language
     * @param {string} langCode - Language code
     */
    removeLanguage(langCode) {
        const normalizedLang = this._normalizeLanguageCode(langCode);
        if (normalizedLang) {
            this.dictionary.delete(normalizedLang);
        }
    }

    /**
     * Clear all translations
     */
    clearAll() {
        this.dictionary.clear();
        this.formatters.clear();
    }

    /**
     * Expose functions to global scope (optional)
     */
    expose() {
        if (typeof window === "undefined") {
            return;
        }

        const prefix = this.options.globalPrefix;
        const functions = {
            [`${prefix}t`]: this.t.bind(this),
            [`${prefix}n`]: this.n.bind(this),
            [`${prefix}d`]: this.d.bind(this)
        };

        for (const [name, func] of Object.entries(functions)) {
            if (window[name]) {
                console.warn(`Global function ${name} already exists, skipping`);
                continue;
            }
            window[name] = func;
        }
    }

    // Private methods
    
    /**
     * Normalize language code
     * @private
     */
    _normalizeLanguageCode(langCode) {
        if (!langCode || typeof langCode !== 'string') {
            return null;
        }

        const normalized = langCode.toLowerCase().trim();
        
        // Check full locale first (e.g., 'en-us')
        if (this._isValidLanguageCode(normalized)) {
            return normalized;
        }

        // Check language part only (e.g., 'en' from 'en-us')
        const langPart = normalized.split('-')[0];
        if (this._isValidLanguageCode(langPart)) {
            return langPart;
        }

        return null;
    }

    /**
     * Check if language code is valid
     * @private
     */
    _isValidLanguageCode(langCode) {
        if (!langCode) return false;
        
        const code = langCode.toLowerCase();
        
        // Check if it's a basic language code
        if (I18n.LANGUAGE_CODES.includes(code)) {
            return true;
        }

        // Check if it's a locale (language-region)
        const parts = code.split('-');
        if (parts.length === 2) {
            return I18n.LANGUAGE_CODES.includes(parts[0]);
        }

        return false;
    }


    /**
     * Update formatters when language changes
     * @private
     */
    _updateFormatters() {
        this.formatters.clear();
    }

    /**
     * Flatten nested translation object
     * @private
     */
    _flattenTranslations(obj, prefix = '') {
        const flattened = {};
        
        for (const [key, value] of Object.entries(obj)) {
            const newKey = prefix ? `${prefix}.${key}` : key;
            
            if (value && typeof value === 'object' && !Array.isArray(value)) {
                // Check if it's a plural form object
                if (this._isPluralForm(value)) {
                    flattened[newKey] = value;
                } else {
                    // Recursively flatten
                    Object.assign(flattened, this._flattenTranslations(value, newKey));
                }
            } else {
                flattened[newKey] = value;
            }
        }
        
        return flattened;
    }

    /**
     * Check if object is a plural form
     * @private
     */
    _isPluralForm(obj) {
        const pluralKeys = ['zero', 'one', 'two', 'few', 'many', 'other'];
        const keys = Object.keys(obj);
        return keys.some(key => pluralKeys.includes(key));
    }

    /**
     * Get translation for specific language
     * @private
     */
    _getTranslation(key, langCode) {
        const translations = this.dictionary.get(langCode);
        return translations ? translations[key] : null;
    }

    /**
     * Select appropriate plural form
     * @private
     */
    _selectPluralForm(pluralObj, count, langCode) {
        const langBase = langCode.split('-')[0];
        const pluralRule = I18n.PLURAL_RULES[langBase] || I18n.PLURAL_RULES.en;
        const form = pluralRule(count);
        
        return pluralObj[form] || pluralObj.other || pluralObj.one || '';
    }

    /**
     * Interpolate parameters in translation
     * @private
     */
    _interpolate(text, params) {
        if (!params || (typeof params !== 'object' && !Array.isArray(params))) {
            return text;
        }

        const { prefix, suffix } = this.options.interpolation;
        
        return text.replace(new RegExp(`${this._escapeRegex(prefix)}([^${this._escapeRegex(suffix)}]+)${this._escapeRegex(suffix)}`, 'g'), (match, key) => {
            // Handle array parameters (numbered: {0}, {1}, etc.)
            if (Array.isArray(params)) {
                const index = parseInt(key, 10);
                return !isNaN(index) && params[index] !== undefined ? params[index] : match;
            }
            
            // Handle object parameters (named: {name}, {count}, etc.)
            return params.hasOwnProperty(key) ? params[key] : match;
        });
    }

    /**
     * Escape regex special characters
     * @private
     */
    _escapeRegex(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    /**
     * Internal translation loading
     * @private
     */
    async _loadTranslationsInternal(langCode, source) {
        let translations;

        if (typeof source === 'string') {
            // Load from URL
            const response = await fetch(source);
            if (!response.ok) {
                throw new Error(`Failed to load translations from ${source}: ${response.status}`);
            }
            translations = await response.json();
        } else if (typeof source === 'function') {
            // Load from function
            translations = await source();
        } else {
            throw new Error('Source must be a URL string or function');
        }

        this.setDictionary(langCode, translations);
        return translations;
    }
}

export { I18n };