generated from Labyricorn/labyricorn-project-template
189 lines
5.7 KiB
JavaScript
189 lines
5.7 KiB
JavaScript
/**
|
|
* 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();
|