diff --git a/components/mjs/core/config.json b/components/mjs/core/config.json index 87e95dc00..bdf45f334 100644 --- a/components/mjs/core/config.json +++ b/components/mjs/core/config.json @@ -18,6 +18,12 @@ "util/entities" ] }, + "copy": { + "to": "[bundle]/core", + "from": "[ts]/core", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] + }, "webpack": { "name": "core" } diff --git a/components/mjs/loader/loader.js b/components/mjs/loader/loader.js index f83c2b205..51f00bf9b 100644 --- a/components/mjs/loader/loader.js +++ b/components/mjs/loader/loader.js @@ -1,3 +1,4 @@ +import '../core/locale.js'; import './lib/loader.js'; import '../core/core.js'; diff --git a/components/mjs/startup/init.js b/components/mjs/startup/init.js index 4da1e1770..e51fbf1a4 100644 --- a/components/mjs/startup/init.js +++ b/components/mjs/startup/init.js @@ -1,4 +1,5 @@ import './hasown.js'; // Can be removed with ES2024 implementation of Object.hasown +import '../core/locale.js'; import './lib/startup.js'; import '../core/core.js'; diff --git a/package.json b/package.json index 01e89b577..9a87c4847 100644 --- a/package.json +++ b/package.json @@ -80,11 +80,13 @@ "clean:lib": "clean() { pnpm -s log:single \"Cleaning $1 component libs\"; pnpm rimraf -g components/$1'/**/lib'; }; clean", "clean:mod": "clean() { pnpm -s log:comp \"Cleaning $1 module\"; pnpm -s clean:dir $1 && pnpm -s clean:lib $1; }; clean", "=============================================================================== copy": "", - "copy:assets": "pnpm -s log:comp 'Copying assets'; copy() { pnpm -s copy:locales $1 && pnpm -s copy:mj2 $1 && pnpm -s copy:mml3 $1 && pnpm -s copy:html $1; }; copy", + "copy:assets": "pnpm -s log:comp 'Copying assets'; copy() { for name in locales mj2 mml3 html; do pnpm -s copy:$name ${1:-mjs}; done; }; copy", + "copy:bundle": "copy() { components/bin/makeAll --copy --terse components/mjs/${1:-}; }; copy", "copy:html": "copy() { pnpm -s log:single 'Copying sre auxiliary files'; pnpm copyfiles -u 1 'ts/a11y/sre/*.html' 'ts/a11y/sre/require.*' $1; }; copy", - "copy:locales": "copy() { pnpm -s copy:locales:menu $1; pnpm -s copy:locales:tex $1; }; copy ", + "copy:locales": "copy() { for name in core tex menu; do pnpm -s copy:locales:$name ${1:-mjs}; done; }; copy ", + "copy:locales:core": "pnpm -s log:single 'Copying core locales'; copy() { pnpm copyfiles -u 1 'ts/core/__locales__/*.json' $1; }; copy", "copy:locales:menu": "pnpm -s log:single 'Copying menu locales'; copy() { pnpm copyfiles -u 1 'ts/ui/menu/__locales__/*.json' $1; }; copy", - "copy:locales:tex": "pnpm -s log:single 'Copying TeX extension locales'; copy() { pnpm copyfiles -u 1 'ts/input/tex/__locales__/*.json' $1 && pnpm copyfiles -u 3 'ts/input/tex/*/__locales__/*.json' $1/input/tex/extensions; }; copy", + "copy:locales:tex": "pnpm -s log:single 'Copying TeX locales'; copy() { pnpm copyfiles -u 1 'ts/input/tex/__locales__/*.json' $1 && pnpm copyfiles -u 3 'ts/input/tex/*/__locales__/*.json' $1/input/tex/extensions; }; copy", "copy:mj2": "copy() { pnpm -s log:single 'Copying legacy code AsciiMath'; pnpm copyfiles -u 1 'ts/input/asciimath/legacy/**/*' $1; }; copy", "copy:mml3": "copy() { pnpm -s log:single 'Copying MathML3 extension json'; pnpm copyfiles -u 1 ts/input/mathml/mml3/mml3.sef.json $1; }; copy", "copy:pkg": "copy() { pnpm -s log:single \"Copying package.json to $1\"; pnpm copyfiles -u 2 components/bin/package.json $1; }; copy", diff --git a/ts/components/loader.ts b/ts/components/loader.ts index d29382859..a7b84664a 100644 --- a/ts/components/loader.ts +++ b/ts/components/loader.ts @@ -43,6 +43,8 @@ import { mjxRoot } from '#root/root.js'; import { context } from '../util/context.js'; import { Locale } from '../util/Locale.js'; +import { COMPONENT } from '../core/__locales__/Component.js'; + /** * Function used to determine path to a given package. */ @@ -187,7 +189,7 @@ export const Loader = { /** * Load the named packages and return a promise that is resolved when they are all loaded * - * @param {string[]} names The packages to load + * @param {string[]} names The packages to load * @returns {Promise} A promise that resolves when all the named packages are ready */ load(...names: string[]): Promise { @@ -224,9 +226,7 @@ export const Loader = { extension.isLoaded && !Loader.versions.has(Package.resolvePath(name)) ) { - console.warn( - `No version information available for component ${name}` - ); + Locale.warn(COMPONENT, 'NoVersionFor', name); } return extension.result; }) as Promise @@ -346,15 +346,13 @@ export const Loader = { * * @param {string} name The name of the extension being checked * @param {string} version The version of the extension to check - * @param {string} _type The type of extension (future code may use this to check ranges of versions) - * @returns {boolean} True if there was a mismatch, false otherwise + * @param {string} _type The type of extension (future code may use this to check ranges of versions) + * @returns {boolean} True if there was a mismatch, false otherwise */ checkVersion(name: string, version: string, _type?: string): boolean { this.saveVersion(name); if (CONFIG.versionWarnings && version !== VERSION) { - console.warn( - `Component ${name} uses ${version} of MathJax; version in use is ${VERSION}` - ); + Locale.warn(COMPONENT, 'WrongVersion', name, version, VERSION); return true; } return false; @@ -363,7 +361,7 @@ export const Loader = { /** * Set the version of an extension (used for combined components so they can be loaded) * - * @param {string} name The name of the extension being checked + * @param {string} name The name of the extension being checked */ saveVersion(name: string) { Loader.versions.set(Package.resolvePath(name), VERSION); @@ -407,7 +405,7 @@ if (typeof MathJax.loader === 'undefined') { load: [], ready: Loader.defaultReady.bind(Loader), failed: (error: PackageError) => - console.log(`MathJax(${error.package || '?'}): ${error.message}`), + console.warn(`MathJax(${error.package || '?'}): ${error.message}`), require: null, json: null, pathFilters: [], diff --git a/ts/components/package.ts b/ts/components/package.ts index c853c554c..0948fe599 100644 --- a/ts/components/package.ts +++ b/ts/components/package.ts @@ -24,6 +24,7 @@ import { CONFIG, Loader } from './loader.js'; import { context } from '../util/context.js'; +import { localize } from '../core/__locales__/Component.js'; /** * A map of package names to Package instances @@ -323,7 +324,7 @@ export class Package { .then((result) => (this.result = result)) .then(() => this.checkLoad()) .catch((err) => - this.failed('Can\'t load "' + url + '"\n' + err.message.trim()) + this.failed(localize('CantLoad', url, ':\n' + err.message.trim())) ); } else { this.result = result; @@ -344,7 +345,7 @@ export class Package { script.src = url; script.charset = 'UTF-8'; script.onload = (_event) => this.checkLoad(); - script.onerror = (_event) => this.failed('Can\'t load "' + url + '"'); + script.onerror = (_event) => this.failed(localize('CantLoad', url, '')); // FIXME: Should there be a timeout failure as well? context.document.head.appendChild(script); } diff --git a/ts/components/startup.ts b/ts/components/startup.ts index 00e7be451..24ffd0821 100644 --- a/ts/components/startup.ts +++ b/ts/components/startup.ts @@ -43,6 +43,8 @@ import { DOMAdaptor } from '../core/DOMAdaptor.js'; import { PrioritizedList } from '../util/PrioritizedList.js'; import { OptionList, OPTIONS } from '../util/Options.js'; import { context } from '../util/context.js'; +import { Locale } from '../util/Locale.js'; +import { COMPONENT } from '../core/__locales__/Component.js'; import { TeX } from '../input/tex.js'; @@ -527,9 +529,7 @@ export abstract class Startup { jax[name] = new inputClass(MathJax.config[name]); jax.push(jax[name]); } else { - throw Error( - 'Input Jax "' + name + '" is not defined (has it been loaded?)' - ); + Locale.throw(COMPONENT, 'InputJaxNotDefined', name); } } return jax; @@ -543,9 +543,7 @@ export abstract class Startup { if (!name) return null; const outputClass = Startup.constructors[name]; if (!outputClass) { - throw Error( - 'Output Jax "' + name + '" is not defined (has it been loaded?)' - ); + Locale.throw(COMPONENT, 'OutputJaxNotDefined', name); } return new outputClass(MathJax.config[name]); } @@ -559,9 +557,7 @@ export abstract class Startup { if (!name || name === 'none') return null; const adaptor = Startup.constructors[name]; if (!adaptor) { - throw Error( - 'DOMAdaptor "' + name + '" is not defined (has it been loaded?)' - ); + Locale.throw(COMPONENT, 'AdaptorNotDefined', name); } return adaptor(MathJax.config[name]); } @@ -574,9 +570,7 @@ export abstract class Startup { if (!name || name === 'none' || !Startup.adaptor) return null; const handlerClass = Startup.constructors[name]; if (!handlerClass) { - throw Error( - 'Handler "' + name + '" is not defined (has it been loaded?)' - ); + Locale.throw(COMPONENT, 'HandlerNotDefined', name); } let handler = new handlerClass(Startup.adaptor, 5); for (const extend of Startup.extensions) { diff --git a/ts/core/__locales__/Component.ts b/ts/core/__locales__/Component.ts new file mode 100644 index 000000000..030026917 --- /dev/null +++ b/ts/core/__locales__/Component.ts @@ -0,0 +1,39 @@ +/************************************************************* + * + * Copyright (c) 2026 The MathJax Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file Locale component registration for core component + * + * @author dpvc@mathjax.org (Davide P. Cervone) + */ + +import { Locale, namedData } from '../../util/Locale.js'; + +export const COMPONENT = 'core'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/core'); + +/** + * Get a localized message for this component + * + * @param {string} id The id of the message + * @param {(string|namedData)[]} args The replacement arguments for the message, if any + * @returns {string} The localized message + */ +export function localize(id: string, ...args: (string | namedData)[]): string { + return Locale.message(COMPONENT, id, ...args); +} diff --git a/ts/core/__locales__/de.json b/ts/core/__locales__/de.json new file mode 100644 index 000000000..067e6ea88 --- /dev/null +++ b/ts/core/__locales__/de.json @@ -0,0 +1,9 @@ +{ + "AdaptorNotDefined": "DOMAdaptor '%1' ist nicht definiert (wurde es geladen?)", + "CantLoad": "'%1' kann nicht geladen werden%2", + "HandlerNotDefined": "Handler '%1' ist nicht definiert (wurde es geladen?)", + "InputJaxNotDefined": "Input Jax '%1' ist nicht definiert (wurde es geladen?)", + "NoVersionFor": "Für die Komponente '%1' liegen keine Versionsinformationen vor", + "OutputJaxNotDefined": "Output Jax '%1' ist nicht definiert (wurde es geladen?)", + "WrongVersion": "Die Komponente %1 verwendet Version %2 von MathJax; die verwendete Version ist %3" +} diff --git a/ts/core/__locales__/en.json b/ts/core/__locales__/en.json new file mode 100644 index 000000000..2e186973a --- /dev/null +++ b/ts/core/__locales__/en.json @@ -0,0 +1,9 @@ +{ + "AdaptorNotDefined": "DOMAdaptor '%1' is not defined (has it been loaded?)", + "CantLoad": "Can't load '%1'%2", + "HandlerNotDefined": "Handler '%1' is not defined (has it been loaded?)", + "InputJaxNotDefined": "Input Jax '%1' is not defined (has it been loaded?)", + "NoVersionFor": "No version information available for component '%1'", + "OutputJaxNotDefined": "Output Jax '%1' is not defined (has it been loaded?)", + "WrongVersion": "Component %1 uses version %2 of MathJax; version in use is %3" +}