Fix TTS module initialization and dependency issues. Update module IDs for consistency, improve circular dependency detection, and fix UI Controller event handling.
This commit is contained in:
+188
-183
@@ -1,214 +1,188 @@
|
||||
/**
|
||||
* Localization Module
|
||||
* Manages translations and locale settings for the application
|
||||
* Localization Module for AI Interactive Fiction
|
||||
* Handles translations and locale settings
|
||||
*/
|
||||
import { BaseModule } from './base-module.js';
|
||||
import { moduleRegistry } from './module-registry.js';
|
||||
|
||||
class LocalizationModule extends BaseModule {
|
||||
/**
|
||||
* Create a new localization module
|
||||
*/
|
||||
constructor() {
|
||||
super('localization', 'Localization');
|
||||
this.currentLocale = 'en-us'; // Default locale
|
||||
|
||||
// Current locale
|
||||
this.currentLocale = 'en-us';
|
||||
|
||||
// Available translations
|
||||
this.translations = {};
|
||||
this.observers = new Set(); // Modules that need to be notified of locale changes
|
||||
|
||||
// Language names mapping
|
||||
this.languageNames = {
|
||||
'en-us': 'English (US)',
|
||||
'en-gb': 'English (UK)',
|
||||
'de': 'Deutsch',
|
||||
'de-de': 'Deutsch (Deutschland)',
|
||||
'fr': 'Français',
|
||||
'fr-fr': 'Français (France)',
|
||||
'es': 'Español',
|
||||
'es-es': 'Español (España)',
|
||||
'it': 'Italiano',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'zh': 'Chinese (Simplified)',
|
||||
'zh-tw': 'Chinese (Traditional)',
|
||||
'ru': 'Russian',
|
||||
'pt': 'Portuguese',
|
||||
'pt-br': 'Portuguese (Brazil)'
|
||||
};
|
||||
|
||||
// Bind methods
|
||||
this.bindMethods([
|
||||
'setLocale',
|
||||
'getLocale',
|
||||
'translate',
|
||||
'getAvailableLocales',
|
||||
'getLanguageName',
|
||||
'getLanguage'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
* @returns {Promise<boolean>} - Resolves with success status
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
// Load translations
|
||||
this.loadTranslations();
|
||||
this.reportProgress(10, "Initializing localization");
|
||||
|
||||
// Set global locale for SmartyPants
|
||||
window.locale = this.currentLocale;
|
||||
// Load default translations
|
||||
await this.loadTranslations('en-us');
|
||||
|
||||
this.reportProgress(100, "Localization module ready");
|
||||
// Try to load browser locale if available
|
||||
const browserLocale = navigator.language.toLowerCase();
|
||||
if (browserLocale && browserLocale !== 'en-us') {
|
||||
try {
|
||||
this.reportProgress(50, `Loading browser locale: ${browserLocale}`);
|
||||
await this.loadTranslations(browserLocale);
|
||||
this.currentLocale = browserLocale;
|
||||
} catch (localeError) {
|
||||
console.warn(`Failed to load browser locale ${browserLocale}:`, localeError);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't check for persistence manager here to avoid circular dependency
|
||||
// The persistence manager will update our locale after it initializes if needed
|
||||
|
||||
this.reportProgress(100, "Localization ready");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error initializing localization module:", error);
|
||||
console.error("Error initializing localization:", error);
|
||||
this.reportProgress(100, "Localization failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all translations
|
||||
*/
|
||||
loadTranslations() {
|
||||
// Add English translations (default)
|
||||
this.addTranslations('en-us', {
|
||||
// UI elements
|
||||
'by': 'powered by Generative AI',
|
||||
'title': 'AI Interactive Fiction',
|
||||
'subtitle': 'An open-world text adventure',
|
||||
'speech': 'speech',
|
||||
'speed': 'speed',
|
||||
'restart': 'restart',
|
||||
'save': 'save',
|
||||
'load': 'load',
|
||||
'prompt': 'What do you want to do next?',
|
||||
'remark': '<i><sup>*</sup>click on page or press spacebar to fast forward text animation</i>',
|
||||
|
||||
// Tooltips
|
||||
'title_speech': 'Toggle text to speech',
|
||||
'title_speech_unavailable': 'Text-to-Speech not available',
|
||||
'title_restart': 'Restart story from beginning',
|
||||
'title_save': 'Save progress',
|
||||
'title_load': 'Reload from save point',
|
||||
|
||||
// Confirm dialogs
|
||||
'confirm_restart': 'Are you sure you want to restart the game? All progress will be lost.'
|
||||
});
|
||||
|
||||
// Add German translations
|
||||
this.addTranslations('de', {
|
||||
'by': 'unterstützt durch KI',
|
||||
'title': 'KI Interaktive Fiktion',
|
||||
'subtitle': 'Ein Textabenteuer in offener Welt',
|
||||
'speech': 'Sprache',
|
||||
'speed': 'Tempo',
|
||||
'restart': 'Neustart',
|
||||
'save': 'Speichern',
|
||||
'load': 'Laden',
|
||||
'prompt': 'Was möchtest du als nächstes tun?',
|
||||
'remark': '<i><sup>*</sup>Klicke auf die Seite oder drücke die Leertaste, um die Textanimation zu beschleunigen</i>',
|
||||
|
||||
'title_speech': 'Text-zu-Sprache umschalten',
|
||||
'title_speech_unavailable': 'Text-zu-Sprache nicht verfügbar',
|
||||
'title_restart': 'Geschichte von Anfang an neu starten',
|
||||
'title_save': 'Fortschritt speichern',
|
||||
'title_load': 'Von Speicherpunkt neu laden',
|
||||
|
||||
'confirm_restart': 'Bist du sicher, dass du das Spiel neu starten möchtest? Der gesamte Fortschritt geht verloren.'
|
||||
});
|
||||
|
||||
// Add French translations
|
||||
this.addTranslations('fr', {
|
||||
'by': 'propulsé par l\'IA',
|
||||
'title': 'Fiction Interactive IA',
|
||||
'subtitle': 'Une aventure textuelle en monde ouvert',
|
||||
'speech': 'parole',
|
||||
'speed': 'vitesse',
|
||||
'restart': 'recommencer',
|
||||
'save': 'sauver',
|
||||
'load': 'charger',
|
||||
'prompt': 'Que voulez-vous faire ensuite?',
|
||||
'remark': '<i><sup>*</sup>cliquez sur la page ou appuyez sur la barre d\'espace pour accélérer l\'animation du texte</i>',
|
||||
|
||||
'title_speech': 'Activer/désactiver la synthèse vocale',
|
||||
'title_speech_unavailable': 'Synthèse vocale non disponible',
|
||||
'title_restart': 'Redémarrer l\'histoire depuis le début',
|
||||
'title_save': 'Sauvegarder la progression',
|
||||
'title_load': 'Recharger depuis le point de sauvegarde',
|
||||
|
||||
'confirm_restart': 'Êtes-vous sûr de vouloir redémarrer le jeu? Tous les progrès seront perdus.'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add translations for a specific locale
|
||||
* @param {string} locale - Locale code
|
||||
* @param {Object} translations - Translation key-value pairs
|
||||
* Load translations for a locale
|
||||
* @param {string} locale - Locale to load
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
addTranslations(locale, translations) {
|
||||
if (!this.translations[locale]) {
|
||||
this.translations[locale] = {};
|
||||
async loadTranslations(locale) {
|
||||
if (this.translations[locale]) {
|
||||
return; // Already loaded
|
||||
}
|
||||
|
||||
Object.assign(this.translations[locale], translations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation for a key in current locale
|
||||
* @param {string} key - Translation key
|
||||
* @param {string} [defaultValue] - Default value if translation not found
|
||||
* @returns {string} - Translated text or default value
|
||||
*/
|
||||
translate(key, defaultValue = null) {
|
||||
const localeTranslations = this.translations[this.currentLocale];
|
||||
|
||||
if (localeTranslations && localeTranslations[key] !== undefined) {
|
||||
return localeTranslations[key];
|
||||
try {
|
||||
// Normalize locale
|
||||
const normalizedLocale = locale.toLowerCase();
|
||||
|
||||
// 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 {
|
||||
// Try to load the language part only
|
||||
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 {
|
||||
// Fallback to English
|
||||
if (normalizedLocale !== 'en-us' && normalizedLocale !== 'en') {
|
||||
await this.loadTranslations('en-us');
|
||||
this.translations[normalizedLocale] = this.translations['en-us'];
|
||||
} else {
|
||||
// If English is not found, create an empty translation set
|
||||
console.warn('English translations not found, using empty set');
|
||||
this.translations[normalizedLocale] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading translations for ${locale}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Fall back to English if translation not found
|
||||
if (this.currentLocale !== 'en-us' && this.translations['en-us'] && this.translations['en-us'][key]) {
|
||||
return this.translations['en-us'][key];
|
||||
}
|
||||
|
||||
// Return default value or key if no translation found
|
||||
return defaultValue || key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current locale
|
||||
* @param {string} locale - Locale code
|
||||
* @param {string} locale - Locale to set
|
||||
* @returns {Promise<boolean>} - Success status
|
||||
*/
|
||||
setLocale(locale) {
|
||||
if (this.translations[locale]) {
|
||||
this.currentLocale = locale;
|
||||
|
||||
// Update global locale for SmartyPants
|
||||
window.locale = locale;
|
||||
|
||||
// Notify observers of locale change
|
||||
this.notifyObservers();
|
||||
|
||||
console.log(`Localization: Locale set to ${locale}`);
|
||||
return true;
|
||||
}
|
||||
async setLocale(locale) {
|
||||
if (!locale) return false;
|
||||
|
||||
console.warn(`Localization: Locale ${locale} not available`);
|
||||
return false;
|
||||
try {
|
||||
// Normalize locale
|
||||
const normalizedLocale = locale.toLowerCase();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Dispatch locale change event
|
||||
this.dispatchEvent('locale-changed', {
|
||||
locale: normalizedLocale
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error setting locale to ${locale}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current locale
|
||||
* @returns {string} - Current locale code
|
||||
* @returns {string} - Current locale
|
||||
*/
|
||||
getLocale() {
|
||||
return this.currentLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module to be notified of locale changes
|
||||
* @param {Object} module - Module to register
|
||||
* @param {Function} updateMethod - Method to call on locale change
|
||||
* Get the language part of the current locale (e.g., 'en' from 'en-us')
|
||||
* @returns {string} - Language code
|
||||
*/
|
||||
registerObserver(module, updateMethod) {
|
||||
if (typeof updateMethod !== 'function') {
|
||||
console.error('Localization: Update method must be a function');
|
||||
return;
|
||||
}
|
||||
|
||||
this.observers.add({ module, updateMethod });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an observer module
|
||||
* @param {Object} module - Module to unregister
|
||||
*/
|
||||
unregisterObserver(module) {
|
||||
this.observers.forEach(observer => {
|
||||
if (observer.module === module) {
|
||||
this.observers.delete(observer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all observer modules of locale change
|
||||
*/
|
||||
notifyObservers() {
|
||||
this.observers.forEach(observer => {
|
||||
try {
|
||||
observer.updateMethod(this.currentLocale);
|
||||
} catch (error) {
|
||||
console.error(`Error notifying observer for locale change:`, error);
|
||||
}
|
||||
});
|
||||
getLanguage() {
|
||||
return this.currentLocale.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,34 +190,68 @@ class LocalizationModule extends BaseModule {
|
||||
* @returns {Array<string>} - Array of locale codes
|
||||
*/
|
||||
getAvailableLocales() {
|
||||
return Object.keys(this.translations);
|
||||
// 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 all translations for a specific locale
|
||||
* Get the language name for a locale
|
||||
* @param {string} locale - Locale code
|
||||
* @returns {Object} - Translations for the locale
|
||||
* @returns {string} - Language name
|
||||
*/
|
||||
getTranslationsForLocale(locale) {
|
||||
return this.translations[locale] || {};
|
||||
getLanguageName(locale) {
|
||||
if (!locale) return '';
|
||||
|
||||
// Normalize locale
|
||||
const normalizedLocale = locale.toLowerCase();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current locale's direction (ltr or rtl)
|
||||
* @returns {string} - Text direction ('ltr' or 'rtl')
|
||||
* Translate a key
|
||||
* @param {string} key - Translation key
|
||||
* @param {Object} params - Parameters for interpolation
|
||||
* @returns {string} - Translated text
|
||||
*/
|
||||
getTextDirection() {
|
||||
// List of RTL languages
|
||||
const rtlLocales = ['ar', 'he', 'fa', 'ur'];
|
||||
translate(key, params = {}) {
|
||||
if (!key) return '';
|
||||
|
||||
// Check if current locale starts with any RTL language code
|
||||
for (const rtl of rtlLocales) {
|
||||
if (this.currentLocale.startsWith(rtl)) {
|
||||
return 'rtl';
|
||||
// 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 'ltr';
|
||||
return translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up when module is disposed
|
||||
*/
|
||||
dispose() {
|
||||
// Clear translations
|
||||
this.translations = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +263,3 @@ moduleRegistry.register(Localization);
|
||||
|
||||
// Export the module
|
||||
export { Localization };
|
||||
|
||||
// Keep a reference in window for loader system
|
||||
window.Localization = Localization;
|
||||
|
||||
Reference in New Issue
Block a user