import { Injectable, signal, computed, effect } from '@angular/core'; const merge = require('lodash/merge'); const { getTranslations, loadTranslation: loadTranslationChunk } = require('../../core/translation-loader'); const { detectLanguageFromLocale } = require('../../core/detect-language'); /** * i18n service — Angular-native translation management. * * Uses function-valued translations (e.g. arrow functions that accept params), * which no standard i18n library supports. Translation storage and lazy loading * are provided by the standalone translation-loader module. * * Language changes are persisted to localStorage. */ @Injectable({ providedIn: 'root' }) export class I18nService { private static readonly STORAGE_KEY = 'p3xr-language'; private static readonly AUTO = 'auto'; /** * Whether language is in auto-detect mode (from browser/system locale). */ readonly isAuto = signal(this.detectIsAuto()); /** * Current language code signal. * Initialized from localStorage or browser detection, same as AngularJS boot.js. */ readonly currentLang = signal(this.detectInitialLanguage()); /** * Merged strings object: English fallback merged with current language. * Recomputes when currentLang changes. Supports function-valued translations. */ readonly strings = computed(() => { const translations = this.getTranslations(); const en = translations['en'] || {}; const current = translations[this.currentLang()] || {}; return merge({}, en, current); }); constructor() { // Persist language changes to localStorage and sync with AngularJS effect(() => { const lang = this.currentLang(); const auto = this.isAuto(); const storageValue = auto ? I18nService.AUTO : lang; this.setStorageItem(I18nService.STORAGE_KEY, storageValue); this.applyDocumentLanguage(lang); // Notify Electron shell so language persists across restarts try { if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'p3x-ui-storage-set', key: I18nService.STORAGE_KEY, value: storageValue }, '*'); } } catch { /* not in iframe */ } }); } /** * Switch the active language. 'auto' resolves from browser locale. * Lazily loads the translation chunk if not yet cached. */ setLanguage(choice: string): void { const auto = choice === I18nService.AUTO; const lang = auto ? this.resolveAutoLanguage() : (choice || 'en'); this.isAuto.set(auto); loadTranslationChunk(lang).then( () => this.currentLang.set(lang), () => this.currentLang.set(lang), ); } private resolveAutoLanguage(): string { try { return detectLanguageFromLocale(navigator.language); } catch { return 'en'; } } /** * Get available language codes. */ getAvailableLanguages(): string[] { return Object.keys(this.getTranslations()); } // --- Private helpers --- private getTranslations(): Record { return getTranslations(); } private detectIsAuto(): boolean { const stored = this.readStorageItem(I18nService.STORAGE_KEY); return !stored || stored === I18nService.AUTO; } private detectInitialLanguage(): string { const storedLang = this.readStorageItem(I18nService.STORAGE_KEY); if (storedLang && storedLang !== I18nService.AUTO) return storedLang; try { return detectLanguageFromLocale(navigator.language); } catch { return 'en'; } } private readStorageItem(name: string): string | null { try { return localStorage.getItem(name); } catch { return null; } } private setStorageItem(name: string, value: string): void { try { localStorage.setItem(name, value); } catch {} } private applyDocumentLanguage(lang: string): void { if (typeof document === 'undefined') { return; } document.documentElement.setAttribute('lang', lang === 'zn' ? 'zh' : lang); } }