/** * CyberSim OS - Internationalization & Localization Engine (I18n) * * Offline-first, deterministic string translation with robust fallback policies. */ // Fallback embedded English catalog for instant initialization & environments without fetch const EMBEDDED_EN = { "login.title": "CyberSim OS - Enterprise Workstation Login", "login.subtitle": "Simulated Workplace Environment", "login.welcome": "Welcome", "login.instructions": "Enter your first name and select an available network to begin your scheduled shift.", "login.firstName": "First Name", "login.firstNamePlaceholder": "e.g. Chris", "login.firstNameRequired": "Please enter your first name.", "login.firstNameTooLong": "First name must be 50 characters or less.", "login.language": "Language", "login.availableNetworks": "Available Networks & Workplaces", "login.networkUnavailable": "Unavailable in selected language", "login.selectNetwork": "Select a workplace network to connect", "login.connect": "Connect to Workplace", "login.connecting": "Connecting...", "login.disclaimer": "CyberSim OS is a simulated training environment. Never enter real passwords or sensitive credentials.", "login.sessionInfo": "Simulated Endpoint \u2022 Zero-Trust Security Active", "desktop.start": "Start", "desktop.loading": "Loading scenario package...", "desktop.shiftResponsibilities": "Shift Responsibilities", "desktop.applications": "Applications", "desktop.finishShift": "Finish Shift & View Assessment", "desktop.networkProtected": "Network: Protected", "desktop.securityAndNotifications": "Security & Notifications", "apps.inlook": "Inlook Mail", "apps.navigator": "Navigator", "apps.files": "Files", "apps.securityCenter": "Security Center", "apps.docViewer": "Doc Viewer", "apps.verifyCert": "Verify Cert", "common.close": "Close", "common.reload": "Reload", "common.unknown": "Unknown" }; export class I18nService { constructor({ defaultLocale = 'en', enabledLocales = ['en', 'es'], basePath = 'locales' } = {}) { this.defaultLocale = defaultLocale; this.activeLocale = defaultLocale; this.enabledLocales = [...enabledLocales]; this.basePath = basePath; this.dictionaries = { en: { ...EMBEDDED_EN } }; this.listeners = new Set(); } /** * Register change listener for reactive UI updates */ onChange(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); } /** * Notify all listeners of a locale switch */ _notify() { this.listeners.forEach(fn => { try { fn(this.activeLocale); } catch (err) { console.error('I18n onChange listener error:', err); } }); } /** * Load locale bundle from JSON or dictionary object */ async loadLocale(locale) { if (this.dictionaries[locale] && Object.keys(this.dictionaries[locale]).length > 10) { return this.dictionaries[locale]; } try { if (typeof fetch !== 'undefined') { const res = await fetch(`${this.basePath}/${locale}.json`); if (res.ok) { const data = await res.json(); this.dictionaries[locale] = { ...(this.dictionaries[locale] || {}), ...data }; return this.dictionaries[locale]; } } } catch (e) { console.warn(`[I18n] Could not fetch ${this.basePath}/${locale}.json, checking local dictionary.`); } return this.dictionaries[locale] || null; } /** * Set custom dictionary directly */ setDictionary(locale, dictionary) { this.dictionaries[locale] = { ...(this.dictionaries[locale] || {}), ...dictionary }; } /** * Change current active locale and preload dictionary */ async setLocale(locale) { const target = this.enabledLocales.includes(locale) ? locale : this.defaultLocale; if (!this.dictionaries[target]) { await this.loadLocale(target); } this.activeLocale = target; this._notify(); return this.activeLocale; } getLocale() { return this.activeLocale; } getEnabledLocales() { return [...this.enabledLocales]; } /** * Core translation method with deterministic fallback policy: * 1. Active locale * 2. Deployment default locale * 3. English ('en') * 4. Visible missing key identifier `[missing: key]` */ t(key, params = {}) { if (!key || typeof key !== 'string') return ''; let text = null; // 1. Active locale if (this.dictionaries[this.activeLocale] && this.dictionaries[this.activeLocale][key] !== undefined) { text = this.dictionaries[this.activeLocale][key]; } // 2. Deployment default locale else if (this.dictionaries[this.defaultLocale] && this.dictionaries[this.defaultLocale][key] !== undefined) { text = this.dictionaries[this.defaultLocale][key]; } // 3. English fallback else if (this.dictionaries['en'] && this.dictionaries['en'][key] !== undefined) { text = this.dictionaries['en'][key]; } // 4. Missing key fallback if (text === null || text === undefined) { return `[missing: ${key}]`; } // Interpolate {param} or {{param}} if (params && typeof params === 'object') { return text.replace(/\{?\{([^{}]+)\}\}?/g, (match, paramName) => { const trimmed = paramName.trim(); if (params[trimmed] !== undefined && params[trimmed] !== null) { return String(params[trimmed]); } return match; }); } return text; } /** * Helper to format language display names */ getLocaleDisplayName(locale) { const names = { en: 'English', es: 'EspaƱol' }; return names[locale] || locale.toUpperCase(); } } // Singleton instance for global OS usage export const i18n = new I18nService();