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:
2025-04-04 19:15:28 +00:00
parent 02c7b9ef28
commit 49a5af252c
33 changed files with 7227 additions and 4060 deletions
+188 -183
View File
@@ -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;