/** * Localization Module for AI Interactive Fiction * Handles translations and locale settings */ import { BaseModule } from './base-module.js'; class LocalizationModule extends BaseModule { /** * Create a new localization module */ constructor() { super('localization', 'Localization'); // Current locale this.translations = {}; this.defaultLocale = 'en_US'; this.currentLocale = this.defaultLocale; this.dependencies = ['persistence-manager']; // Available translations this.languageNames = { 'en_US': 'English (US)', 'de_DE': 'Deutsch (Deutschland)' }; // Bind methods this.bindMethods([ 'setLocale', 'applyServerLocale', 'normalizeLocale', 'getLocale', 'translate', 'getAvailableLocales', 'getLanguageName', 'getLanguage' ]); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, "Initializing localization"); // Load default English locale await this.loadTranslations(this.defaultLocale); this.reportProgress(50, "Loaded default locale"); // Get stored locale from persistence manager if available const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { const storedLocale = persistenceManager.getPreference('app', 'locale'); if (storedLocale) { const normalizedLocale = this.normalizeLocale(storedLocale); console.log(`Localization: Found stored locale: ${normalizedLocale}`); await this.loadTranslations(normalizedLocale); this.currentLocale = normalizedLocale; this.reportProgress(80, `Loaded stored locale: ${normalizedLocale}`); } else { console.log(`Localization: No stored locale found, using default ${this.defaultLocale}`); this.currentLocale = this.defaultLocale; this.reportProgress(80, `Using default locale: ${this.defaultLocale}`); } } else { console.log(`Localization: Persistence manager not available, using default ${this.defaultLocale} locale`); this.reportProgress(80, `Using default locale: ${this.defaultLocale}`); } // Dispatch event to notify about loaded locale document.dispatchEvent(new CustomEvent('localization:languageChanged', { detail: { locale: this.currentLocale } })); document.documentElement.lang = this.currentLocale.replace('_', '-'); this.reportProgress(100, "Localization ready"); return true; } catch (error) { console.error("Error initializing localization:", error); this.reportProgress(100, "Localization failed"); return false; } } /** * Load translations for a locale * @param {string} locale - Locale to load * @returns {Promise} */ async loadTranslations(locale) { const normalizedLocale = this.normalizeLocale(locale); if (this.translations[normalizedLocale]) { return; // Already loaded } try { // Try to load the exact locale const response = await fetch(`/locales/${normalizedLocale}.json`); if (response.ok) { const translations = await response.json(); this.translations[normalizedLocale] = translations; } else { // If exact locale not found, try to load just the language part const langPart = normalizedLocale.split('_')[0]; if (langPart !== normalizedLocale) { const langResponse = await fetch(`/locales/${langPart}.json`); if (langResponse.ok) { const translations = await langResponse.json(); this.translations[normalizedLocale] = translations; } else { console.warn(`No translations found for ${locale} or ${langPart}`); } } } } catch (error) { console.error(`Error loading translations for ${locale}:`, error); throw error; } } /** * Set the current locale * @param {string} locale - Locale to set * @returns {Promise} - Success status */ async setLocale(locale, options = {}) { if (!locale) return false; try { const normalizedLocale = this.normalizeLocale(locale); const userInitiated = options.userInitiated !== false; // Load translations if not already loaded if (!this.translations[normalizedLocale]) { await this.loadTranslations(normalizedLocale); } // Set current locale this.currentLocale = normalizedLocale; // Update persistence const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('app', 'locale', normalizedLocale); persistenceManager.updatePreference('tts', 'language', normalizedLocale); if (userInitiated) { persistenceManager.updatePreference('app', 'localeUserOverride', true); } } document.documentElement.lang = normalizedLocale.replace('_', '-'); // Dispatch locale change event this.dispatchEvent('locale-changed', { locale: normalizedLocale }); document.dispatchEvent(new CustomEvent('locale:changed', { detail: { locale: normalizedLocale } })); document.dispatchEvent(new CustomEvent('localization:languageChanged', { detail: { locale: normalizedLocale } })); return true; } catch (error) { console.error(`Error setting locale to ${locale}:`, error); return false; } } async applyServerLocale(locale) { const persistenceManager = this.getModule('persistence-manager'); const userOverride = persistenceManager?.getPreference('app', 'localeUserOverride', false); if (userOverride) { return false; } return this.setLocale(locale, { userInitiated: false }); } normalizeLocale(locale) { const normalized = String(locale || this.defaultLocale).trim().replace('-', '_').toLowerCase(); if (normalized.startsWith('de')) return 'de_DE'; return 'en_US'; } /** * Get the current locale * @returns {string} - Current locale */ getLocale() { return this.currentLocale; } /** * Get the language part of the current locale (e.g., 'en' from 'en-us') * @returns {string} - Language code */ getLanguage() { return this.currentLocale.split('_')[0]; } /** * Get all available locales * @returns {Array} - Array of locale codes */ getAvailableLocales() { // Return the keys of the language names object // This is a simplification - in a real app, we would dynamically load available locales return Object.keys(this.languageNames); } /** * Get the language name for a locale * @param {string} locale - Locale code * @returns {string} - Language name */ getLanguageName(locale) { if (!locale) return ''; // Normalize locale const normalizedLocale = this.normalizeLocale(locale); // Try exact match if (this.languageNames[normalizedLocale]) { return this.languageNames[normalizedLocale]; } // Try language part only const langPart = normalizedLocale.split('_')[0]; if (this.languageNames[langPart]) { return this.languageNames[langPart]; } // Fallback: return the locale code itself return locale; } /** * Translate a key * @param {string} key - Translation key * @param {Object} params - Parameters for interpolation * @returns {string} - Translated text */ translate(key, params = {}) { if (!key) return ''; // Get translations for current locale const translations = this.translations[this.currentLocale] || {}; // Get translation or fallback to key let translation = translations[key] || key; // Interpolate parameters if (params && Object.keys(params).length > 0) { for (const [param, value] of Object.entries(params)) { translation = translation.replace(new RegExp(`\{\{${param}\}\}`, 'g'), value); } } return translation; } /** * Clean up when module is disposed */ dispose() { // Clear translations this.translations = {}; } } // Create the singleton instance const Localization = new LocalizationModule(); // Export the module export { Localization };