import { IntlMessageFormat } from 'intl-messageformat'; /** * Shared locale and message formatting service for the web application. * * The app keeps existing `setLanguage` hooks on components, but all locale * detection, persistence, and ICU formatting now flows through this singleton. */ type Locale = 'en' | 'es' | 'pt'; type MessageTree = string | MessageTree[] | { [key: string]: MessageTree }; type MessageValues = Parameters[0]; /** * Locale source messages. * * Keys ending in `Html` intentionally contain trusted markup that is rendered * into static page containers. Other strings can be formatted through ICU. */ const source = { en: { locale: 'en', htmlLang: 'en', languageName: 'English', app: { title: 'MathJSLab', description: 'An interpreter with language syntax like MATLAB®/Octave. ISBN 978-65-00-82338-7.', }, page: { titleHtml: 'MathJSLab', subtitleHtml: 'An interpreter with language syntax like MATLAB®/Octave', abstractHtml: 'This is a demo application of the MathJSLab npm package (repository), an emulator of a subset of the MATLAB®/Octave language written completely in TypeScript. This application is intended for educational purposes. See the notes below for detailed information.', trademarkNoticeHtml: 'Important Notice: This software, the MathJSLab, is not affiliated, sponsored, or endorsed by The MathWorks, Inc. MATLAB® is a registered trademark of The MathWorks, Inc. For more information about MATLAB, visit www.mathworks.com.', readmeFile: 'README.md', examples: 'Examples', openFile: 'Open...', readme: 'More Info', githubRepository: 'GitHub Repository', curriculum: 'Curriculum', }, shell: { variables: 'Variables', evaluate: 'Evaluate', }, theme: { dark: 'dark', light: 'light', }, help: { unavailableOfflineHtml: 'help command unavailable offline.', tooManyInputs: 'help: function called with too many inputs', notFound: 'help: {topic} not found.', }, error: { loadTextNetwork: 'loadText: Network error.', }, }, es: { locale: 'es', htmlLang: 'es', languageName: 'Español', app: { title: 'MathJSLab', description: 'Un intérprete con sintaxis de lenguaje como MATLAB®/Octave. ISBN 978-65-00-82338-7.', }, page: { titleHtml: 'MathJSLab', subtitleHtml: 'Un intérprete con sintaxis de lenguaje como MATLAB®/Octave', abstractHtml: 'Esta es una aplicación de demostración del paquete npm MathJSLab (repositorio), un emulador de un subconjunto del lenguaje MATLAB®/Octave escrito completamente en TypeScript. Esta aplicación está destinada a fines educativos. Consulte las notas a continuación para obtener información detallada.', trademarkNoticeHtml: 'Aviso Importante: Este software, MathJSLab, no está afiliado, patrocinado ni respaldado por The MathWorks, Inc. MATLAB® es una marca registrada de The MathWorks, Inc. Para más información sobre MATLAB, visita www.mathworks.com.', readmeFile: 'LEAME.md', examples: 'Ejemplos', openFile: 'Abrir...', readme: 'Más información', githubRepository: 'Repositorio GitHub', curriculum: 'Currículum', }, shell: { variables: 'Variables', evaluate: 'Computar', }, theme: { dark: 'oscuro', light: 'claro', }, help: { unavailableOfflineHtml: 'comando help no disponible sin conexión.', tooManyInputs: 'help: función llamada con demasiadas entradas', notFound: 'help: {topic} no encontrado.', }, error: { loadTextNetwork: 'loadText: Error de red.', }, }, pt: { locale: 'pt', htmlLang: 'pt-BR', languageName: 'Português', app: { title: 'MathJSLab', description: 'Um interpretador com sintaxe de linguagem como MATLAB®/Octave. ISBN 978-65-00-82338-7.', }, page: { titleHtml: 'MathJSLab', subtitleHtml: 'Um interpretador com sintaxe de linguagem como o MATLAB®/Octave', abstractHtml: 'Este é um aplicativo de demonstração do pacote npm MathJSLab (repositório), um emulador de um subconjunto da linguagem MATLAB®/Octave escrito completamente em TypeScript. Este aplicativo é destinado a fins educacionais. Consulte as notas abaixo para obter informações detalhadas.', trademarkNoticeHtml: 'Aviso Importante: Este software, o MathJSLab, não é afiliado, patrocinado ou endossado por The MathWorks, Inc. MATLAB® é uma marca registrada de The MathWorks, Inc. Para mais informações sobre o MATLAB, visite www.mathworks.com.', readmeFile: 'LEIAME.md', examples: 'Exemplos', openFile: 'Abrir...', readme: 'Mais Informações', githubRepository: 'Repositório GitHub', curriculum: 'Currículo Lattes', }, shell: { variables: 'Variáveis', evaluate: 'Computar', }, theme: { dark: 'escuro', light: 'claro', }, help: { unavailableOfflineHtml: 'comando help indisponível offline.', tooManyInputs: 'help: função chamada com muitas entradas', notFound: 'help: {topic} não encontrado.', }, error: { loadTextNetwork: 'loadText: Erro de rede.', }, }, } as const; const locales = Object.keys(source) as Locale[]; /** * Normalize a browser, query-string, or stored language code to a supported * application locale. */ const normalizeLocale = (locale?: string | null): Locale => { const language = locale?.toLowerCase().split('-')[0] as Locale | undefined; return language && locales.includes(language) ? language : 'en'; }; /** * Prepare non-parameterized page messages for direct rendering. * * ICU messages with placeholders and rich HTML strings are left untouched so * callers can format them explicitly or assign them to HTML containers. */ const formatValue = (value: MessageTree, locale: Locale): any => { if (typeof value === 'string') { if (value.includes('{') || value.includes('<')) { return value; } return new IntlMessageFormat(value, locale).format(); } if (Array.isArray(value)) { return value.map((entry) => formatValue(entry, locale)); } return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, formatValue(entry, locale)])); }; const pages = Object.fromEntries(Object.entries(source).map(([locale, values]) => [locale, formatValue(values, locale as Locale)])) as Record; /** * Resolve a dotted message path against a locale message tree. */ const getByPath = (messages: MessageTree, path: string): MessageTree => { return path.split('.').reduce((current, key) => { if (typeof current !== 'object' || Array.isArray(current)) { return ''; } return current[key] ?? ''; }, messages); }; /** * Pick the startup locale from `?lang=`, then localStorage, then the browser. */ const getInitialLocale = (): Locale => { const params = new URLSearchParams(globalThis.location.search); return normalizeLocale(params.get('lang') || globalThis.localStorage.getItem('mathjslab-app:locale') || globalThis.navigator.language); }; /** * Locale coordinator used by the page, components, examples, help command, and * interpreter alias configuration. */ class I18n extends EventTarget { public readonly defaultLocale: Locale = 'en'; public readonly locales = locales; public readonly languageNames = Object.fromEntries(Object.entries(source).map(([locale, values]) => [locale, values.languageName])) as Record; public readonly pages = pages; private currentLocale: Locale = getInitialLocale(); public get locale(): Locale { return this.currentLocale; } public get page(): any { return this.pages[this.currentLocale]; } /** * Format an ICU message in the current locale. */ public format(path: string, values?: MessageValues): string { const message = getByPath(source[this.currentLocale], path); if (typeof message !== 'string') { return ''; } return String(new IntlMessageFormat(message, this.currentLocale).format(values)); } /** * Change and persist the current locale, then notify language-aware code. */ public setLocale(locale?: string | null): void { const nextLocale = normalizeLocale(locale); if (nextLocale === this.currentLocale) { return; } this.currentLocale = nextLocale; globalThis.localStorage.setItem('mathjslab-app:locale', nextLocale); this.applyDocumentLanguage(); this.dispatchEvent(new CustomEvent('languagechange', { detail: { locale: nextLocale } })); } /** * Apply localized document metadata used by browsers and link previews. */ public applyDocumentLanguage(): void { document.documentElement.lang = this.page.htmlLang; document.title = this.page.app.title; document.querySelector('meta[name="description"]')?.setAttribute('content', this.page.app.description); document.querySelector('meta[property="og:description"]')?.setAttribute('content', this.page.app.description); document.querySelector('meta[property="og:image:alt"]')?.setAttribute('content', this.page.app.description); document.querySelector('meta[name="twitter:description"]')?.setAttribute('content', this.page.app.description); } } const i18n = new I18n(); export { type Locale, i18n }; export default i18n;