diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c8d6a72e..f65357899 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,12 @@ jobs: - name: Compile MathJax run: | pnpm -s mjs:compile - components/bin/makeAll --mjs --terse --build components/mjs + pnpm -s copy:assets mjs + components/bin/makeAll --mjs --terse --build --copy components/mjs pnpm -s cjs:compile pnpm -s cjs:components:src:build - components/bin/makeAll --cjs --terse --build components/cjs + pnpm -s copy:assets cjs + components/bin/makeAll --cjs --terse --build --copy components/cjs pnpm -s copy:pkg cjs - name: Build tests diff --git a/components/bin/copy b/components/bin/copy index a5e28a686..78abd3d3f 100755 --- a/components/bin/copy +++ b/components/bin/copy @@ -70,19 +70,21 @@ const bundleDir = path.resolve(parent, bundle); /** * Copy a file or directory tree * - * @param {string} from The directory to copy from - * @param {string} to The directory to copy to - * @param {string} name The name of the file or directory to copy - * @param {string} space The indentation for output + * @param {string} from The directory to copy from + * @param {string} to The directory to copy to + * @param {string} name The name of the file or directory to copy + * @param {string[]} excludes The files to exclude + * @param {string} space The indentation for output */ -function copyFile(from, to, name, space) { +function copyFile(from, to, name, excludes, space = '') { !fs.existsSync(to) && fs.mkdirSync(to, {recursive: true}); const copy = path.resolve(from, name); const dest = path.resolve(to, name); + if (excludes.includes(copy)) return; if (fs.lstatSync(copy).isDirectory()) { console.info(space + name + '/'); for (const file of fs.readdirSync(copy)) { - copyFile(copy, dest, file, space + INDENT); + copyFile(copy, dest, file, excludes, space + INDENT); } } else { console.info(space + name); @@ -112,8 +114,9 @@ function resolvePaths(name) { function processConfig(config) { const to = resolvePaths(config.to); const from = resolvePaths(config.from); + const excludes = config.excludes?.map((file) => path.resolve(from, file)) || []; for (const name of config.copy) { - copyFile(from, to, name, ''); + copyFile(from, to, name, excludes); } } diff --git a/components/json.cjs b/components/json.cjs new file mode 100644 index 000000000..d1ebf1f4c --- /dev/null +++ b/components/json.cjs @@ -0,0 +1,2 @@ +module.exports.json = async function (file) {return require(file)}; +module.exports.require = require; diff --git a/components/mjs/core/core.js b/components/mjs/core/core.js index 82dcdbd23..d583d9355 100644 --- a/components/mjs/core/core.js +++ b/components/mjs/core/core.js @@ -1,7 +1,9 @@ +import './locale.js'; import './lib/core.js'; import {HTMLHandler} from '#js/handlers/html/HTMLHandler.js'; import {browserAdaptor} from '#js/adaptors/browserAdaptor.js'; +import {Package} from '#js/components/package.js'; if (MathJax.startup) { MathJax.startup.registerConstructor('HTMLHandler', HTMLHandler); @@ -11,9 +13,17 @@ if (MathJax.startup) { } if (MathJax.loader) { const config = MathJax.config.loader; - MathJax._.mathjax.mathjax.asyncLoad = ( - (name) => name.substring(0, 5) === 'node:' + const {mathjax} = MathJax._.mathjax; + mathjax.asyncLoad = (name) => { + if (name.match(/\.json$/)) { + name = Package.resolvePath(name); + return (config.json || mathjax.json)(name).then((data) => data.default ?? data); + } + return name.substring(0, 5) === 'node:' ? config.require(name) - : MathJax.loader.load(name).then(result => result[0]) - ); + : MathJax.loader.load(name).then(result => result[0]); + }; + mathjax.json = mathjax.context.window + ? (file) => fetch(file).then((data) => data.json()) + : (file) => import( /* webpackIgnore: true */ file, {with: {type: 'json'}}); } diff --git a/components/mjs/core/locale.js b/components/mjs/core/locale.js new file mode 100644 index 000000000..6009400c7 --- /dev/null +++ b/components/mjs/core/locale.js @@ -0,0 +1,4 @@ +import {Locale} from '#js/util/Locale.js'; + +Locale.isComponent = true; + diff --git a/components/mjs/dependencies.js b/components/mjs/dependencies.js index 55a80c100..7efddddb1 100644 --- a/components/mjs/dependencies.js +++ b/components/mjs/dependencies.js @@ -72,7 +72,8 @@ export const paths = { }; export const provides = { - 'startup': ['loader'], + 'startup': ['loader', 'core'], + 'loader': ['core'], 'input/tex': [ 'input/tex-base', '[tex]/ams', diff --git a/components/mjs/input/tex/config.json b/components/mjs/input/tex/config.json index 135d306c3..70c9f09b5 100644 --- a/components/mjs/input/tex/config.json +++ b/components/mjs/input/tex/config.json @@ -15,6 +15,19 @@ ], "excludeSubdirs": true }, + "copy": [ + { + "to": "[bundle]/input/tex", + "from": "[ts]/input/tex", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] + },{ + "to": "[bundle]/input/tex/extensions/base", + "from": "[ts]/input/tex/base", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] + } + ], "webpack": { "name": "input/tex", "libs": [ diff --git a/components/mjs/input/tex/extensions/ams/config.json b/components/mjs/input/tex/extensions/ams/config.json index a236d0358..644793064 100644 --- a/components/mjs/input/tex/extensions/ams/config.json +++ b/components/mjs/input/tex/extensions/ams/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/ams", "component": "input/tex/extensions/ams", - "targets": ["input/tex/ams"] + "targets": [ + "input/tex/ams" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/ams", + "from": "[ts]/input/tex/ams", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/ams", + "from": "[ts]/input/tex/ams", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/ams", diff --git a/components/mjs/input/tex/extensions/bbox/config.json b/components/mjs/input/tex/extensions/bbox/config.json index 3c233eb2b..a83fdd54b 100644 --- a/components/mjs/input/tex/extensions/bbox/config.json +++ b/components/mjs/input/tex/extensions/bbox/config.json @@ -1,9 +1,16 @@ + { "build": { "id": "[tex]/bbox", "component": "input/tex/extensions/bbox", "targets": ["input/tex/bbox"] }, + "copy": { + "to": "[bundle]/input/tex/extensions/bbox", + "from": "[ts]/input/tex/bbox", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] + }, "webpack": { "name": "input/tex/extensions/bbox", "libs": [ diff --git a/components/mjs/input/tex/extensions/begingroup/config.json b/components/mjs/input/tex/extensions/begingroup/config.json index 8452effd2..6efc7f4f0 100644 --- a/components/mjs/input/tex/extensions/begingroup/config.json +++ b/components/mjs/input/tex/extensions/begingroup/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/begingroup", "component": "input/tex/extensions/begingroup", - "targets": ["input/tex/begingroup"] + "targets": [ + "input/tex/begingroup" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/begingroup", + "from": "[ts]/input/tex/begingroup", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/begingroup", + "from": "[ts]/input/tex/begingroup", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/begingroup", diff --git a/components/mjs/input/tex/extensions/braket/config.json b/components/mjs/input/tex/extensions/braket/config.json index 12cf84f1f..b548fd447 100644 --- a/components/mjs/input/tex/extensions/braket/config.json +++ b/components/mjs/input/tex/extensions/braket/config.json @@ -2,7 +2,9 @@ "build": { "id": "[tex]/braket", "component": "input/tex/extensions/braket", - "targets": ["input/tex/braket"] + "targets": [ + "input/tex/braket" + ] }, "webpack": { "name": "input/tex/extensions/braket", diff --git a/components/mjs/input/tex/extensions/bussproofs/config.json b/components/mjs/input/tex/extensions/bussproofs/config.json index dcfee11bf..b03251767 100644 --- a/components/mjs/input/tex/extensions/bussproofs/config.json +++ b/components/mjs/input/tex/extensions/bussproofs/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/bussproofs", "component": "input/tex/extensions/bussproofs", - "targets": ["input/tex/bussproofs"] + "targets": [ + "input/tex/bussproofs" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/bussproofs", + "from": "[ts]/input/tex/bussproofs", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/bussproofs", + "from": "[ts]/input/tex/bussproofs", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/bussproofs", diff --git a/components/mjs/input/tex/extensions/cases/config.json b/components/mjs/input/tex/extensions/cases/config.json index 49d7c48e4..8fbba386c 100644 --- a/components/mjs/input/tex/extensions/cases/config.json +++ b/components/mjs/input/tex/extensions/cases/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/cases", "component": "input/tex/extensions/cases", - "targets": ["input/tex/cases"] + "targets": [ + "input/tex/cases" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/cases", + "from": "[ts]/input/tex/cases", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/cases", + "from": "[ts]/input/tex/cases", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/cases", diff --git a/components/mjs/input/tex/extensions/color/config.json b/components/mjs/input/tex/extensions/color/config.json index 84041ca4d..19e470eb1 100644 --- a/components/mjs/input/tex/extensions/color/config.json +++ b/components/mjs/input/tex/extensions/color/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/color", "component": "input/tex/extensions/color", - "targets": ["input/tex/color"] + "targets": [ + "input/tex/color" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/color", + "from": "[ts]/input/tex/color", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/color", + "from": "[ts]/input/tex/color", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/color", diff --git a/components/mjs/input/tex/extensions/colortbl/config.json b/components/mjs/input/tex/extensions/colortbl/config.json index 13a313e50..97a680916 100644 --- a/components/mjs/input/tex/extensions/colortbl/config.json +++ b/components/mjs/input/tex/extensions/colortbl/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/colortbl", "component": "input/tex/extensions/colortbl", - "targets": ["input/tex/colortbl"] + "targets": [ + "input/tex/colortbl" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/colortbl", + "from": "[ts]/input/tex/colortbl", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/colortbl", + "from": "[ts]/input/tex/colortbl", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/colortbl", diff --git a/components/mjs/input/tex/extensions/empheq/config.json b/components/mjs/input/tex/extensions/empheq/config.json index 5231b5d62..d85f396f4 100644 --- a/components/mjs/input/tex/extensions/empheq/config.json +++ b/components/mjs/input/tex/extensions/empheq/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/empheq", "component": "input/tex/extensions/empheq", - "targets": ["input/tex/empheq"] + "targets": [ + "input/tex/empheq" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/empheq", + "from": "[ts]/input/tex/empheq", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/empheq", + "from": "[ts]/input/tex/empheq", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/empheq", diff --git a/components/mjs/input/tex/extensions/extpfeil/config.json b/components/mjs/input/tex/extensions/extpfeil/config.json index 493771252..31b77edf2 100644 --- a/components/mjs/input/tex/extensions/extpfeil/config.json +++ b/components/mjs/input/tex/extensions/extpfeil/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/extpfeil", "component": "input/tex/extensions/extpfeil", - "targets": ["input/tex/extpfeil"] + "targets": [ + "input/tex/extpfeil" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/extpfeil", + "from": "[ts]/input/tex/extpfeil", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/extpfeil", + "from": "[ts]/input/tex/extpfeil", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/extpfeil", diff --git a/components/mjs/input/tex/extensions/html/config.json b/components/mjs/input/tex/extensions/html/config.json index ee870d405..025a556d8 100644 --- a/components/mjs/input/tex/extensions/html/config.json +++ b/components/mjs/input/tex/extensions/html/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/html", "component": "input/tex/extensions/html", - "targets": ["input/tex/html"] + "targets": [ + "input/tex/html" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/html", + "from": "[ts]/input/tex/html", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/html", + "from": "[ts]/input/tex/html", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/html", diff --git a/components/mjs/input/tex/extensions/mathtools/config.json b/components/mjs/input/tex/extensions/mathtools/config.json index 0c57ebc10..91dd989af 100644 --- a/components/mjs/input/tex/extensions/mathtools/config.json +++ b/components/mjs/input/tex/extensions/mathtools/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/mathtools", "component": "input/tex/extensions/mathtools", - "targets": ["input/tex/mathtools"] + "targets": [ + "input/tex/mathtools" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/mathtools", + "from": "[ts]/input/tex/mathtools", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/mathtools", + "from": "[ts]/input/tex/mathtools", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/mathtools", diff --git a/components/mjs/input/tex/extensions/mhchem/config.json b/components/mjs/input/tex/extensions/mhchem/config.json index 120c6b886..5e911a4ca 100644 --- a/components/mjs/input/tex/extensions/mhchem/config.json +++ b/components/mjs/input/tex/extensions/mhchem/config.json @@ -5,6 +5,12 @@ "targets": ["input/tex/mhchem"], "excludeSubdirs": true }, + "copy": { + "to": "[bundle]/input/tex/extensions/mhchem", + "from": "[ts]/input/tex/mhchem", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] + }, "webpack": { "name": "input/tex/extensions/mhchem", "libs": [ diff --git a/components/mjs/input/tex/extensions/newcommand/config.json b/components/mjs/input/tex/extensions/newcommand/config.json index 0ef05e0a3..85a42b9f7 100644 --- a/components/mjs/input/tex/extensions/newcommand/config.json +++ b/components/mjs/input/tex/extensions/newcommand/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/newcommand", "component": "input/tex/extensions/newcommand", - "targets": ["input/tex/newcommand"] + "targets": [ + "input/tex/newcommand" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/newcommand", + "from": "[ts]/input/tex/newcommand", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/newcommand", + "from": "[ts]/input/tex/newcommand", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/newcommand", diff --git a/components/mjs/input/tex/extensions/physics/config.json b/components/mjs/input/tex/extensions/physics/config.json index c1e020b82..7a66526c3 100644 --- a/components/mjs/input/tex/extensions/physics/config.json +++ b/components/mjs/input/tex/extensions/physics/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/physics", "component": "input/tex/extensions/physics", - "targets": ["input/tex/physics"] + "targets": [ + "input/tex/physics" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/physics", + "from": "[ts]/input/tex/physics", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/physics", + "from": "[ts]/input/tex/physics", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/physics", diff --git a/components/mjs/input/tex/extensions/require/config.json b/components/mjs/input/tex/extensions/require/config.json index 6ba57a337..d347934e3 100644 --- a/components/mjs/input/tex/extensions/require/config.json +++ b/components/mjs/input/tex/extensions/require/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/require", "component": "input/tex/extensions/require", - "targets": ["input/tex/require"] + "targets": [ + "input/tex/require" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/require", + "from": "[ts]/input/tex/require", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/require", + "from": "[ts]/input/tex/require", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/require", diff --git a/components/mjs/input/tex/extensions/setoptions/config.json b/components/mjs/input/tex/extensions/setoptions/config.json index f329b6f34..2837201ca 100644 --- a/components/mjs/input/tex/extensions/setoptions/config.json +++ b/components/mjs/input/tex/extensions/setoptions/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/setoptions", "component": "input/tex/extensions/setoptions", - "targets": ["input/tex/setoptions"] + "targets": [ + "input/tex/setoptions" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/setoptions", + "from": "[ts]/input/tex/setoptions", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/setoptions", + "from": "[ts]/input/tex/setoptions", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/setoptions", diff --git a/components/mjs/input/tex/extensions/texhtml/config.json b/components/mjs/input/tex/extensions/texhtml/config.json index a47940bae..ec464a094 100644 --- a/components/mjs/input/tex/extensions/texhtml/config.json +++ b/components/mjs/input/tex/extensions/texhtml/config.json @@ -2,7 +2,9 @@ "build": { "id": "[tex]/texhtml", "component": "input/tex/extensions/texhtml", - "targets": ["input/tex/texhtml"] + "targets": [ + "input/tex/texhtml" + ] }, "webpack": { "name": "input/tex/extensions/texhtml", diff --git a/components/mjs/input/tex/extensions/textmacros/config.json b/components/mjs/input/tex/extensions/textmacros/config.json index bd05f220d..1b4cdcf04 100644 --- a/components/mjs/input/tex/extensions/textmacros/config.json +++ b/components/mjs/input/tex/extensions/textmacros/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/textmacros", "component": "input/tex/extensions/textmacros", - "targets": ["input/tex/textmacros"] + "targets": [ + "input/tex/textmacros" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/textmacros", + "from": "[ts]/input/tex/textmacros", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/textmacros", + "from": "[ts]/input/tex/textmacros", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/textmacros", diff --git a/components/mjs/input/tex/extensions/unicode/config.json b/components/mjs/input/tex/extensions/unicode/config.json index 4d228828e..3194a4fee 100644 --- a/components/mjs/input/tex/extensions/unicode/config.json +++ b/components/mjs/input/tex/extensions/unicode/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/unicode", "component": "input/tex/extensions/unicode", - "targets": ["input/tex/unicode"] + "targets": [ + "input/tex/unicode" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/unicode", + "from": "[ts]/input/tex/unicode", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/unicode", + "from": "[ts]/input/tex/unicode", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/unicode", diff --git a/components/mjs/input/tex/extensions/verb/config.json b/components/mjs/input/tex/extensions/verb/config.json index 4632c17a6..0efff86e0 100644 --- a/components/mjs/input/tex/extensions/verb/config.json +++ b/components/mjs/input/tex/extensions/verb/config.json @@ -2,7 +2,22 @@ "build": { "id": "[tex]/verb", "component": "input/tex/extensions/verb", - "targets": ["input/tex/verb"] + "targets": [ + "input/tex/verb" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/verb", + "from": "[ts]/input/tex/verb", + "copy": [ + "__locales__" + ] + }, + "copy": { + "to": "[bundle]/input/tex/extensions/verb", + "from": "[ts]/input/tex/verb", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] }, "webpack": { "name": "input/tex/extensions/verb", diff --git a/components/mjs/loader/loader.js b/components/mjs/loader/loader.js index de1d47cd4..f83c2b205 100644 --- a/components/mjs/loader/loader.js +++ b/components/mjs/loader/loader.js @@ -1,13 +1,20 @@ import './lib/loader.js'; +import '../core/core.js'; import {Loader, CONFIG} from '#js/components/loader.js'; import {combineDefaults} from '#js/components/global.js'; import {dependencies, paths, provides} from '../dependencies.js'; +import {Locale} from '#js/util/Locale.js'; + +Loader.preLoaded('loader', 'core'); combineDefaults(MathJax.config.loader, 'dependencies', dependencies); combineDefaults(MathJax.config.loader, 'paths', paths); combineDefaults(MathJax.config.loader, 'provides', provides); -Loader.load(...CONFIG.load) - .then(() => CONFIG.ready()) - .catch((message, name) => CONFIG.failed(message, name)); +let locale = MathJax.config.locale ?? Locale.current; +try { locale = localStorage.getitem('MathJax-locale') ?? locale; } catch (_err) {} +Locale.setLocale(locale) + .then(() => Loader.load(...CONFIG.load)) + .then(() => CONFIG.ready()) + .catch((message, name) => CONFIG.failed(message, name)); diff --git a/components/mjs/node-main/node-main.js b/components/mjs/node-main/node-main.js index f49b70e18..bbb67b9ab 100644 --- a/components/mjs/node-main/node-main.js +++ b/components/mjs/node-main/node-main.js @@ -21,16 +21,18 @@ import '../startup/init.js'; import {Loader, CONFIG} from '#js/components/loader.js'; -import {Package} from '#js/components/package.js'; -import {combineDefaults, combineConfig} from '#js/components/global.js'; +import {MathJax, combineDefaults, combineConfig} from '#js/components/global.js'; +import {resolvePath} from '#js/util/AsyncLoad.js'; import {context} from '#js/util/context.js'; import '../core/core.js'; import '../adaptors/liteDOM/liteDOM.js'; import {source} from '../source.js'; +import {mathjax} from '#js/mathjax.js'; +import {Locale} from '#js/util/Locale.js'; +import {Package} from '#js/components/package.js'; -const MathJax = global.MathJax; - -const path = eval('require("path")'); // get path from node, not webpack +const REQUIRE = eval('require'); // get require from node, not webpack +const path = REQUIRE("path"); const dir = context.path(MathJax.config.__dirname); // set up by node-main.mjs or node-main.cjs /* @@ -48,24 +50,31 @@ combineDefaults(MathJax.config, 'output', {font: 'mathjax-newcm'}); */ Loader.preLoaded('loader', 'startup', 'core', 'adaptors/liteDOM'); +/* + * Set the paths. + */ if (path.basename(dir) === 'node-main') { CONFIG.paths.esm = CONFIG.paths.mathjax; CONFIG.paths.sre = '[esm]/sre'; - CONFIG.paths.mathjax = path.dirname(dir); + CONFIG.paths.mathjax = path.resolve(dir, '..', '..', '..', 'bundle'); combineDefaults(CONFIG, 'source', source); } else { CONFIG.paths.mathjax = dir; } -// -// Set the asynchronous loader to use the js directory, so we can load -// other files like entity definitions -// -const ROOT = path.resolve(dir, '..', '..', '..', path.basename(path.dirname(dir))); -const REQUIRE = MathJax.config.loader.require; -MathJax._.mathjax.mathjax.asyncLoad = function (name) { - return REQUIRE(name.charAt(0) === '.' ? path.resolve(ROOT, name) : - name.charAt(0) === '[' ? Package.resolvePath(name) : name); + +/* + * Set the asynchronous loader to handle json files + */ +mathjax.asyncLoad = function (name) { + return REQUIRE( + resolvePath( + name, + (name) => path.resolve(CONFIG.paths.mathjax, name), + (name) => Package.resolvePath(name) + ) + ); }; +mathjax.asyncIsSynchronous = true; /* * The initialization function. Use as: @@ -83,7 +92,8 @@ MathJax._.mathjax.mathjax.asyncLoad = function (name) { */ const init = MathJax.init = (config = {}) => { combineConfig(MathJax.config, config); - return Loader.load(...CONFIG.load) + return Locale.setLocale(MathJax.config.locale ?? Locale.current) + .then(() => Loader.load(...CONFIG.load)) .then(() => CONFIG.ready()) .then(() => MathJax.startup.promise) // Wait for MathJax to finish starting up .then(() => MathJax) // Pass MathJax global as argument to subsequent .then() calls diff --git a/components/mjs/require/config.json b/components/mjs/require/config.json index 7e23a1374..bbe1ea8c0 100644 --- a/components/mjs/require/config.json +++ b/components/mjs/require/config.json @@ -3,7 +3,8 @@ "to": "[bundle]", "from": "../..", "copy": [ - "require.mjs" + "require.mjs", + "json.cjs" ] } } diff --git a/components/mjs/startup/init.js b/components/mjs/startup/init.js index 558f1af4c..4da1e1770 100644 --- a/components/mjs/startup/init.js +++ b/components/mjs/startup/init.js @@ -1,11 +1,13 @@ import './hasown.js'; // Can be removed with ES2024 implementation of Object.hasown import './lib/startup.js'; +import '../core/core.js'; import {combineDefaults} from '#js/components/global.js'; import {dependencies, paths, provides, compatibility} from '../dependencies.js'; import {Loader, CONFIG} from '#js/components/loader.js'; +import {Locale} from '#js/util/Locale.js'; -Loader.preLoaded('loader', 'startup'); +Loader.preLoaded('loader', 'startup', 'core'); combineDefaults(MathJax.config.loader, 'dependencies', dependencies); combineDefaults(MathJax.config.loader, 'paths', paths); @@ -13,7 +15,10 @@ combineDefaults(MathJax.config.loader, 'provides', provides); combineDefaults(MathJax.config.loader, 'source', compatibility); export function startup(ready) { - return Loader.load(...CONFIG.load) + let locale = MathJax.config.locale ?? Locale.current; + try { locale = localStorage.getItem('MathJax-locale') ?? locale; } catch (_err) {} + return Locale.setLocale(locale) + .then(() => Loader.load(...CONFIG.load)) .then(() => (ready || function () {})()) .then(() => CONFIG.ready()) .catch(error => CONFIG.failed(error)); diff --git a/components/mjs/ui/menu/config.json b/components/mjs/ui/menu/config.json index 4fffd9231..6c49de3d7 100644 --- a/components/mjs/ui/menu/config.json +++ b/components/mjs/ui/menu/config.json @@ -4,6 +4,12 @@ "targets": ["ui/menu", "a11y/speech/SpeechMenu.ts"], "excludeSubdirs": true }, + "copy": { + "to": "[bundle]/ui/menu", + "from": "[ts]/ui/menu", + "copy": ["__locales__"], + "excludes": ["__locales__/Component.ts"] + }, "webpack": { "name": "ui/menu", "libs": [ diff --git a/package.json b/package.json index 1c39dd754..01e89b577 100644 --- a/package.json +++ b/package.json @@ -80,10 +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:mj2 $1 && pnpm -s copy:mml3 $1 && pnpm -s copy:html $1; }; 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: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: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: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 legacy code MathML3'; pnpm copyfiles -u 1 ts/input/mathml/mml3/mml3.sef.json $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", "=============================================================================== log": "", "log:comp": "log() { echo \u001b[32m$1\u001b[0m; }; log", diff --git a/testsuite/lib/AsyncLoad.child.json b/testsuite/lib/AsyncLoad.child.json new file mode 100644 index 000000000..e5bb1c70c --- /dev/null +++ b/testsuite/lib/AsyncLoad.child.json @@ -0,0 +1,3 @@ +{ + "json": true +} diff --git a/testsuite/lib/component/__locales__/en.json b/testsuite/lib/component/__locales__/en.json new file mode 100644 index 000000000..1a72bb940 --- /dev/null +++ b/testsuite/lib/component/__locales__/en.json @@ -0,0 +1,3 @@ +{ + "Id1": "Test of %1 in %2" +} diff --git a/testsuite/lib/component/__locales__/test.json b/testsuite/lib/component/__locales__/test.json new file mode 100644 index 000000000..42743ede7 --- /dev/null +++ b/testsuite/lib/component/__locales__/test.json @@ -0,0 +1,8 @@ +{ + "test1": "Has %% percent", + "test2": "Has %1 one", + "test3": "Order %2 %1 reversed", + "test4": "Skip %1 %3", + "test5": "Named %{hello} %world", + "error": "Error in %1" +} diff --git a/testsuite/src/node-main.d.ts b/testsuite/src/node-main.d.ts new file mode 100644 index 000000000..5fadad0d7 --- /dev/null +++ b/testsuite/src/node-main.d.ts @@ -0,0 +1,3 @@ +declare module '#source/node-main/node-main.mjs' { + export function init(config: any): any; +} diff --git a/testsuite/src/setupTex.ts b/testsuite/src/setupTex.ts index 6f65ce8b0..e65ed2bf5 100644 --- a/testsuite/src/setupTex.ts +++ b/testsuite/src/setupTex.ts @@ -24,6 +24,7 @@ import { expect } from '@jest/globals'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { source } from '#source/source.js'; +import { Locale } from '#js/util/Locale.js'; declare const MathJax: any; type MATHITEM = MathItem; @@ -196,6 +197,7 @@ export function setupTex( const html = new HTMLDocument('', adaptor, { InputJax: tex }); convert = (expr: string, display: boolean) => toMathML(html.convert(expr, { display: display, end: STATE.CONVERT })); + return Locale.setLocale(); } /** @@ -223,6 +225,7 @@ export function setupTexRender( html.findMath().compile(); return toMathML((Array.from(html.math)[0] as MATHITEM).root); }; + return Locale.setLocale(); } /** @@ -307,6 +310,7 @@ export function setupTexWithOutput( const toMathML = (node: MmlNode) => visitor.visitTree(node); convert = (expr: string, display: boolean) => toMathML(html.convert(expr, { display: display, end: STATE.CONVERT })); + return Locale.setLocale(); } /*********************************************************************/ diff --git a/testsuite/src/source.d.ts b/testsuite/src/source.d.ts new file mode 100644 index 000000000..8d8744e5f --- /dev/null +++ b/testsuite/src/source.d.ts @@ -0,0 +1,3 @@ +declare module '#source/source.js' { + export const source: { [name: string]: string }; +} diff --git a/testsuite/tests/input/tex/Base.test.ts b/testsuite/tests/input/tex/Base.test.ts index 6e06df5d7..ac6aa539c 100644 --- a/testsuite/tests/input/tex/Base.test.ts +++ b/testsuite/tests/input/tex/Base.test.ts @@ -255,7 +255,7 @@ describe('Error', () => { }); it('Ampersand-error', () => { - expectTexError('&').toBe('Misplaced &'); + expectTexError('&').toBe("Misplaced '&'"); }); it('Argument-error', () => { @@ -389,7 +389,7 @@ describe('Error', () => { }); it('Misplaced Cr', () => { - expectTexError('a\\cr b').toBe('Misplaced \\cr'); + expectTexError('a\\cr b').toBe("Misplaced '\\cr'"); }); it('Dimension Error', () => { @@ -461,7 +461,7 @@ describe('Error', () => { }); it('Misplaced hline', () => { - expectTexError('\\hline').toBe('Misplaced \\hline'); + expectTexError('\\hline').toBe("Misplaced '\\hline'"); }); it('UnsupportedHFill', () => { diff --git a/testsuite/tests/input/tex/Bbox.test.ts b/testsuite/tests/input/tex/Bbox.test.ts index 90f8d072b..2e92a23e2 100644 --- a/testsuite/tests/input/tex/Bbox.test.ts +++ b/testsuite/tests/input/tex/Bbox.test.ts @@ -53,7 +53,7 @@ describe('Bbox', () => { it('Bbox-General-Error', () => { expectTexError('\\bbox[22-11=color]{a}').toBe( - `"22-11=color" doesn't look like a color, a padding dimension, or a style` + `'22-11=color' doesn't look like a color, a padding dimension, or a style` ); }); }); diff --git a/testsuite/tests/input/tex/Mathtools.test.ts b/testsuite/tests/input/tex/Mathtools.test.ts index 9119cdb2a..5c950609f 100644 --- a/testsuite/tests/input/tex/Mathtools.test.ts +++ b/testsuite/tests/input/tex/Mathtools.test.ts @@ -918,7 +918,7 @@ describe('Mathtools More Environments', () => { test('ArrowBetweenLines error', () => { expectTexError('\\ArrowBetweenLines').toBe( - '\\ArrowBetweenLines can only be used in aligment environments' + '\\ArrowBetweenLines can only be used in alignment environments' ); }); @@ -1040,7 +1040,7 @@ describe('Mathtools Boxed Equations', () => { test('Aboxed error', () => { expectTexError('\\Aboxed{ a & = b}').toBe( - '\\Aboxed can only be used in aligment environments' + '\\Aboxed can only be used in alignment environments' ); }); diff --git a/testsuite/tests/input/tex/Mhchem.test.ts b/testsuite/tests/input/tex/Mhchem.test.ts index 46571c44d..562d9bdcb 100644 --- a/testsuite/tests/input/tex/Mhchem.test.ts +++ b/testsuite/tests/input/tex/Mhchem.test.ts @@ -486,10 +486,14 @@ describe('Mhchem-Ams', () => { ).toMatchSnapshot(); }); - it('Mhchem Error', () => { + it('Mhchem Bond Error', () => { expectTexError('\\ce{A\\bond{x}B}').toBe('mhchem bug T. Please report.'); }); + it('Mhchem Brace Error', () => { + expectTexError('\\ce{{x^${x}}}').toBe('Extra close brace or missing open brace'); + }); + it('Mhchem stretchy <-', () => { expect(tex2mml('\\ce{A <-[text] B}')).toMatchSnapshot(); }); diff --git a/testsuite/tests/input/tex/Newcommand.test.ts b/testsuite/tests/input/tex/Newcommand.test.ts index 9347b0ac2..d482ff370 100644 --- a/testsuite/tests/input/tex/Newcommand.test.ts +++ b/testsuite/tests/input/tex/Newcommand.test.ts @@ -438,7 +438,7 @@ describe('NewcommandError', () => { it('Recursive Macro', () => { expectTexError('\\def\\x{\\x} \\x').toBe( - 'MathJax maximum macro substitution count exceeded; is here a recursive macro call?' + 'MathJax maximum macro substitution count exceeded; is there a recursive macro call?' ); }); @@ -646,4 +646,4 @@ describe('Nested Environments', () => { /**********************************************************************************/ - afterAll(() => getTokens('newcommand')); +afterAll(() => getTokens('newcommand')); diff --git a/testsuite/tests/input/tex/Physics.test.ts b/testsuite/tests/input/tex/Physics.test.ts index f95ea9cc5..7f0987202 100644 --- a/testsuite/tests/input/tex/Physics.test.ts +++ b/testsuite/tests/input/tex/Physics.test.ts @@ -2472,6 +2472,18 @@ describe('Physics Errors', () => { it('InvalidNumber XMatrix m+', () => { expectTexError('\\smqty(\\xmatrix{a}{2}{2.0})').toBe('Invalid number'); }); + + it('Missing Closing Delimiter', () => { + expect( + tex2mml('\\sin(1\\over2') + ).toMatchSnapshot(); + }); + + it('Extra Open Delimiter', () => { + expect( + tex2mml('\\sin((1\\over2)') + ).toMatchSnapshot(); + }); }); /**********************************************************************************/ diff --git a/testsuite/tests/input/tex/Require.test.ts b/testsuite/tests/input/tex/Require.test.ts index ec383f255..4cedb40ac 100644 --- a/testsuite/tests/input/tex/Require.test.ts +++ b/testsuite/tests/input/tex/Require.test.ts @@ -17,7 +17,7 @@ setupComponents({ loader: { load: ['input/tex-base', '[tex]/require'], source: { - '[tex]/error': '../../testsuite/lib/error.js', + '[tex]/error': '../testsuite/lib/error.js', }, dependencies: { '[tex]/upgreek': ['input/tex-base', '[tex]/error'], diff --git a/testsuite/tests/input/tex/Tex.test.ts b/testsuite/tests/input/tex/Tex.test.ts index 01c139fae..a085b0bdf 100644 --- a/testsuite/tests/input/tex/Tex.test.ts +++ b/testsuite/tests/input/tex/Tex.test.ts @@ -24,6 +24,7 @@ import { HandlerType, ConfigurationType } from '#js/input/tex/HandlerTypes.js'; import { CommandMap } from '#js/input/tex/TokenMap.js'; import { Token } from '#js/input/tex/Token.js'; import { TagsFactory } from '#js/input/tex/Tags.js'; +import { texError } from '#js/input/tex/TexError.js'; import TexError from '#js/input/tex/TexError.js'; import { ParseUtil, @@ -147,7 +148,7 @@ describe('TexError', () => { expect(err.message).toBe('Msg: OK, Number: 2'); }); - test('Plural', () => { + test.skip('Plural', () => { const err = new TexError('test', '%{plural:%1|abc}', 'apple'); expect(err.message).toBe('%{plural:%1|abc}'); }); @@ -158,6 +159,23 @@ describe('TexError', () => { }); }); +describe('texError', () => { + test('Number argument', () => { + expect(() => texError(null, 'test', 'Number: %1', '1')) + .toThrow('Number: 1'); + }); + + test('Braced insertion', () => { + expect(() => texError(null, 'test', 'Msg: %{1}, Number: %{2}', 'OK', '2')) + .toThrow('Msg: OK, Number: 2'); + }); + + test('Percent', () => { + expect(() => texError(null, 'test', '10%%')) + .toThrow('10%'); + }); +}); + /**********************************************************************************/ setupComponents({ diff --git a/testsuite/tests/input/tex/Verb.test.ts b/testsuite/tests/input/tex/Verb.test.ts index e1e00a168..1380689fc 100644 --- a/testsuite/tests/input/tex/Verb.test.ts +++ b/testsuite/tests/input/tex/Verb.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeEach, describe, expect, it } from '@jest/globals'; import { getTokens, setupTex, tex2mml, expectTexError } from '#helpers'; import '#js/input/tex/verb/VerbConfiguration'; -beforeEach(() => setupTex(['base', 'verb'])); +beforeEach(async () => setupTex(['base', 'verb'])); /**********************************************************************************/ diff --git a/testsuite/tests/input/tex/__snapshots__/Base.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/Base.test.ts.snap index bc59e9aa3..66b32fe9e 100644 --- a/testsuite/tests/input/tex/__snapshots__/Base.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/Base.test.ts.snap @@ -3495,8 +3495,8 @@ exports[`Environments math 1`] = ` exports[`Error merror node 1`] = ` " - - Misplaced & + + Misplaced '&' " `; diff --git a/testsuite/tests/input/tex/__snapshots__/Noerrors.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/Noerrors.test.ts.snap index 78b214274..2cf6cc091 100644 --- a/testsuite/tests/input/tex/__snapshots__/Noerrors.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/Noerrors.test.ts.snap @@ -2,7 +2,7 @@ exports[`NoError Ampersand-error 1`] = ` " - + & " @@ -210,7 +210,7 @@ exports[`NoError Middle with Right 1`] = ` exports[`NoError Misplaced Cr 1`] = ` " - + a\\cr b " @@ -226,7 +226,7 @@ exports[`NoError Misplaced Move Root 1`] = ` exports[`NoError Misplaced hline 1`] = ` " - + \\hline " diff --git a/testsuite/tests/input/tex/__snapshots__/Physics.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/Physics.test.ts.snap index 218039580..e644e868f 100644 --- a/testsuite/tests/input/tex/__snapshots__/Physics.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/Physics.test.ts.snap @@ -1154,6 +1154,22 @@ exports[`Options Italicdif true 1`] = ` " `; +exports[`Physics Errors Extra Open Delimiter 1`] = ` +" + + Extra open or missing close delimiter + +" +`; + +exports[`Physics Errors Missing Closing Delimiter 1`] = ` +" + + Extra open or missing close delimiter + +" +`; + exports[`Physics1_0 Quantities_Quantities_0 1`] = ` " diff --git a/testsuite/tests/util/AsyncLoad.test.ts b/testsuite/tests/util/AsyncLoad.test.ts index 93c1395fc..e9937663c 100644 --- a/testsuite/tests/util/AsyncLoad.test.ts +++ b/testsuite/tests/util/AsyncLoad.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from '@jest/globals'; -import { asyncLoad } from '#js/util/AsyncLoad.js'; +import { asyncLoad, resolvePath } from '#js/util/AsyncLoad.js'; import { mathjax } from '#js/mathjax.js'; describe('asyncLoad()', () => { @@ -43,4 +43,41 @@ describe('asyncLoad()', () => { }); await expect(asyncLoad('x.js')).rejects.toBe('fail'); }); + + test('resolvePath', () => { + // + // Test resolvePath woth Pacakge path resolution + // + (global as any).MathJax = { + _: { + components: { + package: { + Package: { + resolvePath: (file: string) => 'test:' + file, + }, + }, + }, + }, + }; + expect(resolvePath('[x]/y.js', (file) => file)).toBe('test:[x]/y.js'); + + // + // Remove MathJax._ and test relative and absolute paths + // + (global as any).MathJax = {}; + expect( + resolvePath( + './x.js', + (file) => `rel:${file.substring(2)}`, + (file) => `abs:${file}` + ) + ).toBe('rel:x.js'); + expect( + resolvePath( + 'x.js', + (file) => `rel:${file.substring(2)}`, + (file) => `abs:${file}` + ) + ).toBe('abs:x.js'); + }); }); diff --git a/testsuite/tests/util/Locale.test.ts b/testsuite/tests/util/Locale.test.ts new file mode 100644 index 000000000..aaa1299e4 --- /dev/null +++ b/testsuite/tests/util/Locale.test.ts @@ -0,0 +1,126 @@ +import { describe, test, expect } from '@jest/globals'; +import { Locale } from '#js/util/Locale.js'; +import '#js/util/asyncLoad/esm.js'; + +/**********************************************************************************/ +/**********************************************************************************/ + +describe('Locale', () => { + /********************************************************************************/ + + test('Set locale', async () => { + expect(Locale.current).toBe('en'); + await Locale.setLocale(); + expect(Locale.current).toBe('en'); + await Locale.setLocale('de'); + expect(Locale.current).toBe('de'); + await Locale.setLocale('en'); + expect(Locale.current).toBe('en'); + }); + + /********************************************************************************/ + + test('Register a component', async () => { + const locale = Locale as any; + Locale.registerLocaleFiles('component', '../testsuite/lib/component'); + expect(locale.locations.component).toEqual([ + '../testsuite/lib/component/__locales__', + new Set(), + ]); + const error = console.error; + console.error = (message) => { + throw message; + }; + await expect(Locale.setLocale('xy')).rejects.toContain( + "MathJax(component): Can't load 'xy.json': ENOENT: no such file or directory" + ); + await expect(Locale.setLocale('de')).rejects.toContain( + "MathJax(component): 'de.json' kann nicht geladen werden: ENOENT: no such file or directory" + ); + console.error = error; + await Locale.setLocale('en'); + expect(locale.data.component).toEqual({ en: { Id1: 'Test of %1 in %2' } }); + expect(Locale.message('component', 'Id1', 'message', 'Locale')).toBe( + 'Test of message in Locale' + ); + }); + + /********************************************************************************/ + + test('Messages', async () => { + Locale.registerLocaleFiles('component', '../testsuite/lib/component'); + await Locale.setLocale('en'); // load English backups + await Locale.setLocale('test'); + expect(Locale.message('component', 'test1')).toBe('Has % percent'); + expect(Locale.message('component', 'test2', 'x')).toBe('Has x one'); + expect(Locale.message('component', 'test3', 'a', 'b')).toBe( + 'Order b a reversed' + ); + expect(Locale.message('component', 'test4', 'a', 'b', 'c')).toBe( + 'Skip a c' + ); + expect(Locale.message('component', 'test4')).toBe('Skip '); + expect( + Locale.message('component', 'test5', { hello: 'HELLO', world: 'WORLD' }) + ).toBe('Named HELLO WORLD'); + expect(Locale.message('component', 'Id1', 'a', 'b')).toBe('Test of a in b'); + expect(Locale.message('component', 'Id2')).toBe( + "MathJax(Locale): No localized or default version for message with id 'Id2' from 'component'" + ); + expect(Locale.message('undefined', 'Id1')).toBe( + "MathJax(Locale): No localized or default version for message with id 'Id1' from 'undefined'" + ); + expect(() => Locale.error('component', 'error', 'x')).toThrow('Error in x'); + Locale.current = 'de'; + expect(Locale.message('undefined', 'Id1')).toBe( + "MathJax(Locale): Keine lokalisierte oder Standardversion für die Meldung mit der ID 'Id1' aus 'undefined'" + ); + Locale.current = 'xy'; + Locale.default = 'xy'; + expect(Locale.message('undefined', 'Id1')).toBe(''); + Locale.current = 'en'; + Locale.default = 'en'; + }); + + /********************************************************************************/ + + test('isComponent', async () => { + Locale.isComponent = true; + Locale.registerLocaleFiles('../testsuite/lib/component', 'notfound'); + await Locale.setLocale('test'); + expect(Locale.message('component', 'test1')).toBe('Has % percent'); + Locale.isComponent = false; + }); + + /********************************************************************************/ + + test('Message with empty component', () => { + expect(Locale.message('', 'any')).toBe(''); + expect(Locale.message('', 'any', {})).toBe(''); + expect(Locale.message('', 'any', 'raw text')).toBe('raw text'); + expect(Locale.message('', 'any', '%1 + %2', 'a', 'b')).toBe('a + b'); + }); + + /********************************************************************************/ + + test('Locale error falls back to default locale', async () => { + const locale = Locale as any; + Locale.registerLocaleFiles('fallback', '../testsuite/lib/component'); + + const errors: string[] = []; + const origError = console.error; + console.error = (msg: string) => errors.push(msg); + + await locale.localeError('fallback', 'xy', new Error('xy.json not found')); + + console.error = origError; + + expect(errors[0]).toContain("MathJax(fallback): Can't load 'xy.json'"); + expect(locale.data.fallback?.en).toEqual({ Id1: 'Test of %1 in %2' }); + }); + + /********************************************************************************/ +}); + +/**********************************************************************************/ +/**********************************************************************************/ diff --git a/testsuite/tests/util/Styles.test.ts b/testsuite/tests/util/Styles.test.ts index 85b8094d6..ff1df632a 100644 --- a/testsuite/tests/util/Styles.test.ts +++ b/testsuite/tests/util/Styles.test.ts @@ -204,7 +204,7 @@ describe('CssStyles object', () => { 'margin: 0;' ); cssTest('margin:', {}, ''); - }) + }); test('border', () => { cssTest('border: 3px solid red', { diff --git a/testsuite/tests/util/asyncLoad/esm.test.ts b/testsuite/tests/util/asyncLoad/esm.test.ts index 7a1c36616..2dea7c86e 100644 --- a/testsuite/tests/util/asyncLoad/esm.test.ts +++ b/testsuite/tests/util/asyncLoad/esm.test.ts @@ -11,40 +11,57 @@ describe('asyncLoad() for esm', () => { test('asyncLoad()', async () => { const cjsFile = path.join('..', 'testsuite', 'lib', 'AsyncLoad.child.cjs'); const mjsFile = path.join('..', 'testsuite', 'lib', 'AsyncLoad.child.mjs'); + const jsonFile = path.join( + '..', + 'testsuite', + 'lib', + 'AsyncLoad.child.json' + ); const relUnknown = path.join( '..', 'testsuite', 'lib', 'AsyncLoad.unknown.cjs' ); + const jsonUnknown = path.join( + '..', + 'testsuite', + 'lib', + 'AsyncLoad.unknown.json' + ); const absFile = path.join(root, cjsFile); + const absJson = path.join(root, jsonFile); const absUnknown = path.join(root, relUnknown); - await expect(asyncLoad(cjsFile)).resolves.toEqual({ loaded: true }); // relative file found - await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found - await expect(asyncLoad(absFile)).resolves.toEqual({ loaded: true }); // absolute file found - await expect(asyncLoad(absUnknown).catch(() => true)).resolves.toBe(true); // absolute file not found + await expect(asyncLoad(cjsFile)).resolves.toEqual({ loaded: true }); // relative file found + await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found + await expect(asyncLoad(absFile)).resolves.toEqual({ loaded: true }); // absolute file found + await expect(asyncLoad(absUnknown).catch(() => true)).resolves.toBe(true); // absolute file not found + + await expect(asyncLoad(jsonFile)).resolves.toEqual({ json: true }); // relative json file found + await expect(asyncLoad(absJson)).resolves.toEqual({ json: true }); // absolute json file found + await expect(asyncLoad(jsonUnknown).catch(() => true)).resolves.toBe(true); // unknown file not found await expect( - asyncLoad('#js/components/version.js') // load using package exports + asyncLoad('#js/components/version.js') // load using package exports .then((result: any) => result.VERSION) ).resolves.toBe(mathjax.version); await expect( - asyncLoad('@mathjax/src/js/components/version.js') // load from module + asyncLoad('@mathjax/src/js/components/version.js') // load from module .then((result: any) => result.VERSION) ).resolves.toBe(mathjax.version); await expect( - asyncLoad(mjsFile).then((result: any) => result.loaded) - ).resolves.toBe(true); // mjs file loads - expect(mathjax.asyncIsSynchronous).toBe(false); // esm.js is asynchronous + asyncLoad(mjsFile).then((result: any) => result.loaded) // mjs file loads + ).resolves.toBe(true); + expect(mathjax.asyncIsSynchronous).toBe(false); // esm.js is asynchronous }); test('setBaseURL() for esm', async () => { setBaseURL(lib); const relFile = './AsyncLoad.child.cjs'; const relUnknown = './AsyncLoad.unknown.cjs'; - await expect(asyncLoad(relFile)).resolves.toEqual({ loaded: true }); // relative file found - await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found + await expect(asyncLoad(relFile)).resolves.toEqual({ loaded: true }); // relative file found + await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found }); }); diff --git a/testsuite/tests/util/asyncLoad/node.test.ts b/testsuite/tests/util/asyncLoad/node.test.ts index 08d995929..b28ccb56b 100644 --- a/testsuite/tests/util/asyncLoad/node.test.ts +++ b/testsuite/tests/util/asyncLoad/node.test.ts @@ -13,38 +13,64 @@ describe('asyncLoad() for node', () => { test('asyncLoad()', async () => { const cjsFile = path.join('..', 'testsuite', 'lib', 'AsyncLoad.child.cjs'); const mjsFile = path.join('..', 'testsuite', 'lib', 'AsyncLoad.child.mjs'); + const jsonFile = path.join( + '..', + 'testsuite', + 'lib', + 'AsyncLoad.child.json' + ); const relUnknown = path.join( '..', 'testsuite', 'lib', 'AsyncLoad.unknown.cjs' ); + const jsonUnknown = path.join( + '..', + 'testsuite', + 'lib', + 'AsyncLoad.unknown.json' + ); const absFile = path.join(root, cjsFile); + const absJson = path.join(root, jsonFile); const absUnknown = path.join(root, relUnknown); - await expect(asyncLoad(cjsFile)).resolves.toEqual({ loaded: true }); // relative file found - await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found - await expect(asyncLoad(absFile)).resolves.toEqual({ loaded: true }); // absolute file found - await expect(asyncLoad(absUnknown).catch(() => true)).resolves.toBe(true); // absolute file not found + await expect(asyncLoad(cjsFile)).resolves.toEqual({ loaded: true }); // relative file found + await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found + await expect(asyncLoad(absFile)).resolves.toEqual({ loaded: true }); // absolute file found + await expect(asyncLoad(absUnknown).catch(() => true)).resolves.toBe(true); // absolute file not found + + await expect(asyncLoad(jsonFile)).resolves.toEqual({ json: true }); // relative json file found + await expect(asyncLoad(absJson)).resolves.toEqual({ json: true }); // absolute json file found + await expect(asyncLoad(jsonUnknown).catch(() => true)).resolves.toBe(true); // unknown file not found await expect( - asyncLoad('#js/../cjs/components/version.js') // load using package exports + asyncLoad('#js/../cjs/components/version.js') // load using package exports .then((result: any) => result.VERSION) ).resolves.toBe(mathjax.version); await expect( - asyncLoad('@mathjax/src/js/components/version.js') // load from module + asyncLoad('@mathjax/src/js/components/version.js') // load from module .then((result: any) => result.VERSION) ).resolves.toBe(mathjax.version); - await expect(asyncLoad(mjsFile).catch(() => true)).resolves.toBe(true); // mjs file fails - expect(mathjax.asyncIsSynchronous).toBe(true); // node.js is synchronous + await expect(asyncLoad(mjsFile).catch(() => true)).resolves.toBe(true); // mjs file fails + expect(mathjax.asyncIsSynchronous).toBe(true); // node.js is synchronous + + // + // Test mathjax.json separately, as asyncLoad doesn't call it. + // + await expect(mathjax.json(jsonFile)).resolves.toEqual({ json: true }); + await expect(mathjax.json(absJson)).resolves.toEqual({ json: true }); + await expect(mathjax.json(jsonUnknown).catch(() => true)).resolves.toBe( + true + ); }); test('setBaseURL() for node', async () => { setBaseURL(lib); const relFile = './AsyncLoad.child.cjs'; const relUnknown = './AsyncLoad.unknown.cjs'; - await expect(asyncLoad(relFile)).resolves.toEqual({ loaded: true }); // relative file found - await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found + await expect(asyncLoad(relFile)).resolves.toEqual({ loaded: true }); // relative file found + await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found }); }); diff --git a/testsuite/tests/util/asyncLoad/system.test.ts b/testsuite/tests/util/asyncLoad/system.test.ts index 160b876a5..2e906fe0f 100644 --- a/testsuite/tests/util/asyncLoad/system.test.ts +++ b/testsuite/tests/util/asyncLoad/system.test.ts @@ -22,31 +22,31 @@ describe('asyncLoad() for node', () => { const absFile = path.join(root, cjsFile); const absUnknown = path.join(root, relUnknown); - await expect(asyncLoad(cjsFile)).resolves.toEqual({ loaded: true }); // relative file found - await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found - await expect(asyncLoad(absFile)).resolves.toEqual({ loaded: true }); // absolute file found - await expect(asyncLoad(absUnknown).catch(() => true)).resolves.toBe(true); // absolute file not found + await expect(asyncLoad(cjsFile)).resolves.toEqual({ loaded: true }); // relative file found + await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found + await expect(asyncLoad(absFile)).resolves.toEqual({ loaded: true }); // absolute file found + await expect(asyncLoad(absUnknown).catch(() => true)).resolves.toBe(true); // absolute file not found await expect( - asyncLoad('#js/components/version.js') // can't load using package exports + asyncLoad('#js/components/version.js') // can't load using package exports .catch(() => true) ).resolves.toBe(true); await expect( - asyncLoad('mathjax-full/js/components/version.js') // can't load from module + asyncLoad('mathjax-full/js/components/version.js') // can't load from module .catch(() => true) ).resolves.toBe(true); await expect( - asyncLoad(mjsFile).then((result: any) => result.loaded) - ).resolves.toBe(true); // mjs file loads - expect(mathjax.asyncIsSynchronous).toBe(false); // system.js is asynchronous + asyncLoad(mjsFile).then((result: any) => result.loaded) // mjs file loads + ).resolves.toBe(true); + expect(mathjax.asyncIsSynchronous).toBe(false); // system.js is asynchronous }); test('setBaseURL() for node', async () => { setBaseURL(lib); const relFile = './AsyncLoad.child.cjs'; const relUnknown = './AsyncLoad.unknown.cjs'; - await expect(asyncLoad(relFile)).resolves.toEqual({ loaded: true }); // relative file found - await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found + await expect(asyncLoad(relFile)).resolves.toEqual({ loaded: true }); // relative file found + await expect(asyncLoad(relUnknown).catch(() => true)).resolves.toBe(true); // relative file not found }); }); diff --git a/ts/a11y/speech/SpeechMenu.ts b/ts/a11y/speech/SpeechMenu.ts index 1f3598f9d..5a19f96c7 100644 --- a/ts/a11y/speech/SpeechMenu.ts +++ b/ts/a11y/speech/SpeechMenu.ts @@ -29,6 +29,7 @@ import { SelectionGrid, } from '../../ui/dialog/SelectionDialog.js'; import { SubMenu, Submenu } from '../../ui/menu/mj-context-menu.js'; +import { localize } from '../../ui/menu/__locales__/Component.js'; import * as Sre from '../sre.js'; /** @@ -127,7 +128,7 @@ function csSelectionBox(menu: MJContextMenu, locale: string): object { }); } const sb = new SelectionDialog( - 'Clearspeak Preferences', + localize('ClearspeakTitle'), '', items, SelectionOrder.ALPHABETICAL, @@ -137,7 +138,7 @@ function csSelectionBox(menu: MJContextMenu, locale: string): object { return { type: 'command', id: 'ClearspeakPreferences', - content: 'Select Preferences', + content: localize('SelectPrefs'), action: () => sb.post(), }; } @@ -159,13 +160,13 @@ function basePreferences(previous: string): object[] { const items = [ { type: 'radio', - content: 'No Preferences', + content: localize('NoPrefs'), id: 'clearspeak-default', variable: 'speechRules', }, { type: 'radio', - content: 'Current Preferences', + content: localize('CurrentPrefs'), id: 'clearspeak-' + previous, variable: 'speechRules', }, @@ -191,7 +192,7 @@ function smartPreferences( ): object[] { const loc = localePreferences.get(locale); const items = [ - { type: 'label', content: 'Preferences for ' + smart }, + { type: 'label', content: localize('PrefsFor', smart) }, { type: 'rule' }, ]; return items.concat( diff --git a/ts/components/cjs/json.ts b/ts/components/cjs/json.ts new file mode 100644 index 000000000..ae4a23afd --- /dev/null +++ b/ts/components/cjs/json.ts @@ -0,0 +1,30 @@ +/************************************************************* + * + * 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 ES5 shim for loading json files + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +declare const require: (file: string) => any; + +import { context } from '../../util/context.js'; + +export const json = context.window + ? (file: string) => fetch(file).then((data) => data.json()) + : (file: string) => require(file); diff --git a/ts/components/loader.ts b/ts/components/loader.ts index 713b3947c..d29382859 100644 --- a/ts/components/loader.ts +++ b/ts/components/loader.ts @@ -41,15 +41,17 @@ import { import { FunctionList } from '../util/FunctionList.js'; import { mjxRoot } from '#root/root.js'; import { context } from '../util/context.js'; +import { Locale } from '../util/Locale.js'; /** * Function used to determine path to a given package. */ -export type PathFilterFunction = (data: { +export type PathFilterData = { name: string; original: string; addExtension: boolean; -}) => boolean; +}; +export type PathFilterFunction = (data: PathFilterData) => boolean; export type PathFilterList = ( | PathFilterFunction | [PathFilterFunction, number] @@ -99,11 +101,8 @@ export interface MathJaxObject extends MJObject { * Functions used to filter the path to a package */ export const PathFilters: { [name: string]: PathFilterFunction } = { - /** + /* * Look up the path in the configuration's source list - * - * @param {PathFilterFunction} data The data object containing the filter functions - * @returns {boolean} True */ source: (data) => { if (Object.hasOwn(CONFIG.source, data.name)) { @@ -112,11 +111,8 @@ export const PathFilters: { [name: string]: PathFilterFunction } = { return true; }, - /** + /* * Add [mathjax] before any relative path - * - * @param {PathFilterFunction} data The data object containing the filter functions - * @returns {boolean} True */ normalize: (data) => { const name = data.name; @@ -126,11 +122,8 @@ export const PathFilters: { [name: string]: PathFilterFunction } = { return true; }, - /** + /* * Recursively replace path prefixes (e.g., [mathjax], [tex], etc.) - * - * @param {PathFilterFunction} data The data object containing the filter functions - * @returns {boolean} True */ prefix: (data) => { let match; @@ -141,11 +134,8 @@ export const PathFilters: { [name: string]: PathFilterFunction } = { return true; }, - /** + /* * Add .js, if missing - * - * @param {PathFilterFunction} data The data object containing the filter functions - * @returns {boolean} True */ addExtension: (data) => { if (data.addExtension && !data.name.match(/\.[^/]+$/)) { @@ -212,61 +202,69 @@ export const Loader = { // // Create a promise for this load() call // - const promise = Promise.resolve().then(async () => { - // - // Collect the promises for all the named packages, - // creating the package if needed, and add checks - // for the version numbers used in the components. - // - const promises = []; - for (const name of names) { - let extension = Package.packages.get(name); - if (!extension) { - extension = new Package(name); - extension.provides(CONFIG.provides[name]); + const promise = Promise.resolve() + .then(async () => { + // + // Collect the promises for all the named packages, + // creating the package if needed, and add checks + // for the version numbers used in the components. + // + const promises = []; + for (const name of names) { + let extension = Package.packages.get(name); + if (!extension) { + extension = new Package(name); + extension.provides(CONFIG.provides[name]); + } + extension.checkNoLoad(); + promises.push( + extension.promise.then(() => { + if ( + CONFIG.versionWarnings && + extension.isLoaded && + !Loader.versions.has(Package.resolvePath(name)) + ) { + console.warn( + `No version information available for component ${name}` + ); + } + return extension.result; + }) as Promise + ); } - extension.checkNoLoad(); - promises.push( - extension.promise.then(() => { - if ( - CONFIG.versionWarnings && - extension.isLoaded && - !Loader.versions.has(Package.resolvePath(name)) - ) { - console.warn( - `No version information available for component ${name}` - ); - } - return extension.result; - }) as Promise - ); - } - // - // Load everything that was requested and wait for - // them to be loaded. - // - Package.loadAll(); - const result = await Promise.all(promises); - // - // If any other loads occurred while we were waiting, - // Wait for those promises, and clear the list so that - // if even MORE loads occur while waiting for those, - // we can wait for them, too. Keep doing that until - // no additional loads occurred, in which case we are - // now done. - // - while (nested.length) { - const promise = Promise.all(nested); - nested = this.nestedLoads[this.nestedLoads.indexOf(nested)] = []; - await promise; - } - // - // Remove the (empty) list from the nested list, - // and return the result. - // - this.nestedLoads.splice(this.nestedLoads.indexOf(nested), 1); - return result; - }); + // + // Load everything that was requested and wait for + // them to be loaded. + // + Package.loadAll(); + const result = await Promise.all(promises); + // + // If any other loads occurred while we were waiting, + // Wait for those promises, and clear the list so that + // if even MORE loads occur while waiting for those, + // we can wait for them, too. Keep doing that until + // no additional loads occurred, in which case we are + // now done. + // + while (nested.length) { + const promise = Promise.all(nested); + nested = this.nestedLoads[this.nestedLoads.indexOf(nested)] = []; + await promise; + } + // + // Remove the (empty) list from the nested list, + // and return the result. + // + this.nestedLoads.splice(this.nestedLoads.indexOf(nested), 1); + return result; + }) + .then(async (result) => { + // + // If any of the components registered localization files, load them. + // + await Locale.setLocale(); + return result; + }); // // Add this load promise to the lists for any parent load() call that are // pending when this load() was performed, then return the load promise. @@ -411,6 +409,7 @@ if (typeof MathJax.loader === 'undefined') { failed: (error: PackageError) => console.log(`MathJax(${error.package || '?'}): ${error.message}`), require: null, + json: null, pathFilters: [], versionWarnings: true, }); diff --git a/ts/components/mjs/json.ts b/ts/components/mjs/json.ts new file mode 100644 index 000000000..f23bba2d0 --- /dev/null +++ b/ts/components/mjs/json.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 ES6 shim for loading json files + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +import { context } from '../../util/context.js'; + +export const json = context.window + ? (file: string) => fetch(file).then((data) => data.json()) + : (file: string) => import(file, { with: { type: 'json' } }); diff --git a/ts/components/startup.ts b/ts/components/startup.ts index b3656b0d0..00e7be451 100644 --- a/ts/components/startup.ts +++ b/ts/components/startup.ts @@ -117,6 +117,7 @@ export interface MathJaxObject extends MJObject { defaultReady(): void; defaultPageReady(): Promise; defaultOptionError(message: string, key: string): void; + setLocale(): Promise; getComponents(): void; makeMethods(): void; makeTypesetMethods(): void; diff --git a/ts/input/tex/ColumnParser.ts b/ts/input/tex/ColumnParser.ts index a0beec563..ffa6ed05c 100644 --- a/ts/input/tex/ColumnParser.ts +++ b/ts/input/tex/ColumnParser.ts @@ -23,11 +23,13 @@ import { ArrayItem } from './base/BaseItems.js'; import TexParser from './TexParser.js'; -import TexError from './TexError.js'; +import { texError } from './TexError.js'; import { lookup } from '../../util/Options.js'; import { ParseUtil } from './ParseUtil.js'; import { UnitUtil } from './UnitUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + /***********************************************************************/ /** @@ -133,16 +135,13 @@ export class ColumnParser { let n = 0; while (state.i < state.template.length) { if (n++ > this.MAXCOLUMNS) { - throw new TexError( - 'MaxColumns', - 'Too many column specifiers (perhaps looping column definitions?)' - ); + texError(COMPONENT, 'MaxColumns'); } const code = state.template.codePointAt(state.i); const c = (state.c = String.fromCodePoint(code)); state.i += c.length; if (!Object.hasOwn(this.columnHandler, c)) { - throw new TexError('BadPreamToken', 'Illegal pream-token (%1)', c); + texError(COMPONENT, 'BadPreamToken', c); } this.columnHandler[c](state); } @@ -264,11 +263,7 @@ export class ColumnParser { public getDimen(state: ColumnState): string { const dim = this.getBraces(state); if (!UnitUtil.matchDimen(dim)[0]) { - throw new TexError( - 'MissingColumnDimOrUnits', - 'Missing dimension or its units for %1 column declaration', - state.c - ); + texError(COMPONENT, 'MissingColumnDimOrUnits', state.c); } return dim; } @@ -299,11 +294,7 @@ export class ColumnParser { public getBraces(state: ColumnState): string { while (state.template[state.i] === ' ') state.i++; if (state.i >= state.template.length) { - throw new TexError( - 'MissingArgForColumn', - 'Missing argument for %1 column declaration', - state.c - ); + texError(COMPONENT, 'MissingArgForColumn', state.c); } if (state.template[state.i] !== '{') { return state.template[state.i++]; @@ -325,7 +316,7 @@ export class ColumnParser { break; } } - throw new TexError('MissingCloseBrace', 'Missing close brace'); + texError(COMPONENT, 'MissingCloseBrace'); } /** @@ -410,11 +401,7 @@ export class ColumnParser { const cols = this.getBraces(state); const n = parseInt(num); if (String(n) !== num) { - throw new TexError( - 'ColArgNotNum', - 'First argument to %1 column specifier must be a number', - '*' - ); + texError(COMPONENT, 'ColArgNotNum', '*'); } state.template = new Array(n).fill(cols).join('') + state.template.substring(state.i); diff --git a/ts/input/tex/Configuration.ts b/ts/input/tex/Configuration.ts index 6306dafe2..50fc8c862 100644 --- a/ts/input/tex/Configuration.ts +++ b/ts/input/tex/Configuration.ts @@ -31,6 +31,7 @@ import { FunctionList } from '../../util/FunctionList.js'; import { TeX } from '../tex.js'; import { PrioritizedList } from '../../util/PrioritizedList.js'; import { TagsFactory } from './Tags.js'; +export { COMPONENT } from './__locales__/Component.js'; export type StackItemConfig = { [kind: string]: StackItemClass }; export type TagsConfig = { [kind: string]: TagsClass }; diff --git a/ts/input/tex/ParseUtil.ts b/ts/input/tex/ParseUtil.ts index 191254e9b..807c22b7b 100644 --- a/ts/input/tex/ParseUtil.ts +++ b/ts/input/tex/ParseUtil.ts @@ -27,11 +27,13 @@ import { ArrayItem } from './base/BaseItems.js'; import ParseOptions from './ParseOptions.js'; import NodeUtil from './NodeUtil.js'; import TexParser from './TexParser.js'; -import TexError from './TexError.js'; +import { texError } from './TexError.js'; import { entities } from '../../util/Entities.js'; import { MmlMunderover } from '../../core/MmlTree/MmlNodes/munderover.js'; import { UnitUtil } from './UnitUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * The data needed for checking the value of a key-value pair. */ @@ -185,10 +187,8 @@ function readValue( // Closing braces. case '}': if (!braces) { - throw new TexError( - 'ExtraCloseMissingOpen', - 'Extra close brace or missing open brace' - ); + // Closing braces. + texError(COMPONENT, 'ExtraCloseMissingOpen'); } braces--; countBraces = false; // Stop counting start left braces. @@ -211,10 +211,7 @@ function readValue( value += c; } if (braces) { - throw new TexError( - 'ExtraOpenMissingClose', - 'Extra open brace or missing close brace' - ); + texError(COMPONENT, 'ExtraOpenMissingClose'); } return dropBrace && start ? ['', '', removeBraces(value, 1)] @@ -546,11 +543,7 @@ export const ParseUtil = { .substring(i) .match(/^\s*(?:([0-9A-F])|\{\s*([0-9A-F]+)\s*\})/); if (!arg) { - throw new TexError( - 'BadRawUnicode', - 'Argument to %1 must a hexadecimal number with 1 to 6 digits', - '\\U' - ); + texError(COMPONENT, 'BadRawUnicode', '\\U'); } // Replace \U{...} with specified character const c = String.fromCodePoint(parseInt(arg[1] || arg[2], 16)); @@ -565,10 +558,7 @@ export const ParseUtil = { } if (match !== '') { // @test Internal Math Error - throw new TexError( - 'MathNotTerminated', - 'Math mode is not properly terminated' - ); + texError(COMPONENT, 'MathNotTerminated'); } } if (k < text.length) { @@ -733,10 +723,7 @@ export const ParseUtil = { text += c; } else { if (!c.match(/[1-9]/) || parseInt(c, 10) > args.length) { - throw new TexError( - 'IllegalMacroParam', - 'Illegal macro parameter reference' - ); + texError(COMPONENT, 'IllegalMacroParam'); } newstring = ParseUtil.addArgs( parser, @@ -767,11 +754,7 @@ export const ParseUtil = { s1 += ' '; } if (s1.length + s2.length > parser.configuration.options['maxBuffer']) { - throw new TexError( - 'MaxBufferSize', - 'MathJax internal buffer size exceeded; is there a' + - ' recursive macro call?' - ); + texError(COMPONENT, 'MaxBufferSize'); } return s1 + s2; }, @@ -787,17 +770,9 @@ export const ParseUtil = { return; } if (isMacro) { - throw new TexError( - 'MaxMacroSub1', - 'MathJax maximum macro substitution count exceeded; ' + - 'is here a recursive macro call?' - ); + texError(COMPONENT, 'MaxMacroSub1'); } else { - throw new TexError( - 'MaxMacroSub2', - 'MathJax maximum substitution count exceeded; ' + - 'is there a recursive latex environment?' - ); + texError(COMPONENT, 'MaxMacroSub2'); } }, @@ -820,10 +795,7 @@ export const ParseUtil = { return; } if (!top.isKind('start') || first) { - throw new TexError( - 'ErroneousNestingEq', - 'Erroneous nesting of equation structures' - ); + texError(COMPONENT, 'ErroneousNestingEq'); } }, @@ -902,17 +874,13 @@ export const ParseUtil = { const type = allowed[key] as KeyValueDef; const value = String(def[key]); if (!type.verify(value)) { - throw new TexError( - 'InvalidValue', - "Value for key '%1' is not of the expected type", - key - ); + texError(COMPONENT, 'InvalidValue', key); } def[key] = type.convert(value); } } else { if (error) { - throw new TexError('InvalidOption', 'Invalid option: %1', key); + texError(COMPONENT, 'InvalidOption', key); } delete def[key]; } diff --git a/ts/input/tex/StackItem.ts b/ts/input/tex/StackItem.ts index 95b84ccba..e86b23839 100644 --- a/ts/input/tex/StackItem.ts +++ b/ts/input/tex/StackItem.ts @@ -23,9 +23,10 @@ import { MmlNode } from '../../core/MmlTree/MmlNode.js'; import { FactoryNodeClass } from '../../core/Tree/Factory.js'; -import TexError from './TexError.js'; +import { texError } from './TexError.js'; import StackItemFactory from './StackItemFactory.js'; import { TexConstant } from './TexConstants.js'; +import { COMPONENT } from './__locales__/Component.js'; // Union types for abbreviation. export type EnvProp = string | number | boolean; @@ -388,16 +389,16 @@ export abstract class BaseItem extends MmlStack implements StackItem { /** * A list of basic errors. * - * @type {{[key: string]: string[]}} + * @type {{[key: string]: [string, string]}} */ - protected static errors: { [key: string]: string[] } = { + protected static errors: { [key: string]: [string, string] } = { // @test ExtraOpenMissingClose - end: ['MissingBeginExtraEnd', 'Missing \\begin{%1} or extra \\end{%1}'], + end: [COMPONENT, 'MissingBeginExtraEnd'], // @test ExtraCloseMissingOpen - close: ['ExtraCloseMissingOpen', 'Extra close brace or missing open brace'], + close: [COMPONENT, 'ExtraCloseMissingOpen'], // @test MissingLeftExtraRight - right: ['MissingLeftExtraRight', 'Missing \\left or extra \\right'], - middle: ['ExtraMiddle', 'Extra \\middle'], + right: [COMPONENT, 'MissingLeftExtraRight'], + middle: [COMPONENT, 'ExtraMiddle'], }; /** @@ -514,13 +515,12 @@ export abstract class BaseItem extends MmlStack implements StackItem { return BaseItem.fail; } // @test Ampersand-error - throw new TexError('Misplaced', 'Misplaced %1', item.getName()); + texError(COMPONENT, 'Misplaced', item.getName()); } - if (item.isClose && this.getErrors(item.kind)) { + if (item.isClose && this.getError(item.kind)) { // @test ExtraOpenMissingClose, ExtraCloseMissingOpen, // MissingLeftExtraRight, MissingBeginExtraEnd - const [id, message] = this.getErrors(item.kind); - throw new TexError(id, message, item.getName()); + texError(...this.getError(item.kind), item.getName()); } if (!item.isFinal) { return BaseItem.success; @@ -566,11 +566,11 @@ export abstract class BaseItem extends MmlStack implements StackItem { * subclasses. * * @param {string} kind The stack item type. - * @returns {string[]} The list of arguments for the TeXError. + * @returns {[string, string]} The component and id of the error message. */ - public getErrors(kind: string): string[] { + public getError(kind: string): [string, string] { const CLASS = this.constructor as typeof BaseItem; - return CLASS.errors[kind] || BaseItem.errors[kind]; + return CLASS.errors?.[kind] ?? BaseItem.errors[kind]; } /** diff --git a/ts/input/tex/TexError.ts b/ts/input/tex/TexError.ts index 793d69f6c..e5b75e973 100644 --- a/ts/input/tex/TexError.ts +++ b/ts/input/tex/TexError.ts @@ -21,75 +21,35 @@ * @author v.sorge@mathjax.org (Volker Sorge) */ -export default class TexError { - private static pattern = - /%(\d+|\{\d+\}|\{[a-z]+:%\d+(?:\|(?:%\{\d+\}|%.|[^}])*)+\}|.)/g; +import { Locale } from '../../util/Locale.js'; - /** - * Default error message. - * - * @type {string} - */ +export default class TexError { public message: string; /** - * The old MathJax processing function. - * - * @param {string} str The basic error message. - * @param {string[]} args The arguments to be replaced in the error message. - * @returns {string} The processed error string. - */ - private static processString(str: string, args: string[]): string { - const parts = str.split(TexError.pattern); - for (let i = 1, m = parts.length; i < m; i += 2) { - let c = parts[i].charAt(0); // first char will be { or \d or a char to be - // kept literally - if (c >= '0' && c <= '9') { - // %n - parts[i] = args[parseInt(parts[i], 10) - 1]; - if (typeof parts[i] === 'number') { - parts[i] = parts[i].toString(); - } - } else if (c === '{') { - // %{n} or %{plural:%n|...} - c = parts[i].substring(1); - if (c >= '0' && c <= '9') { - // %{n} - parts[i] = - args[ - parseInt( - // parts[i] = %{n} - parts[i].substring(1, parts[i].length - 1), - 10 - ) - 1 - ]; - if (typeof parts[i] === 'number') { - parts[i] = parts[i].toString(); - } - } else { - // %{plural:%n|...} - const match = parts[i].match(/^\{([a-z]+):%(\d+)\|(.*)\}$/); - if (match) { - // Removed plural here. - parts[i] = '%' + parts[i]; - } - } - } - } - return parts.join(''); - } - - /** - * @class - * @param {string} id message id (for localization) - * @param {string} message text of English message - * @param {string[]=} rest any substitution arguments + * @param {string} id message id + * @param {string} message text of English message + * @param {string[]} args substitution arguments */ constructor( public id: string, message: string, - ...rest: string[] + ...args: string[] ) { - this.message = TexError.processString(message, rest); + this.message = Locale.processMessage(message, args[0], ...args.slice(1)); } } + +/** + * @param {string} component locale component (e.g. '[tex]/base') + * @param {string} id message id + * @param {string[]} args substitution arguments + */ +export function texError( + component: string, + id: string, + ...args: string[] +): never { + const message = Locale.message(component, id, ...args); + throw new TexError(id, message, ...args); +} diff --git a/ts/input/tex/TexParser.ts b/ts/input/tex/TexParser.ts index 8e4b1603b..ba11b419a 100644 --- a/ts/input/tex/TexParser.ts +++ b/ts/input/tex/TexParser.ts @@ -27,7 +27,7 @@ import { UnitUtil } from './UnitUtil.js'; import Stack from './Stack.js'; import StackItemFactory from './StackItemFactory.js'; import { Tags } from './Tags.js'; -import TexError from './TexError.js'; +import { texError } from './TexError.js'; import { MmlNode, AbstractMmlNode } from '../../core/MmlTree/MmlNode.js'; import { ParseInput, ParseResult } from './Types.js'; import ParseOptions from './ParseOptions.js'; @@ -35,6 +35,7 @@ import { BaseItem, StackItem, EnvList } from './StackItem.js'; import { Token } from './Token.js'; import { OptionList } from '../../util/Options.js'; import { TexConstant } from './TexConstants.js'; +import { COMPONENT } from './__locales__/Component.js'; /** * The main Tex Parser class. @@ -330,20 +331,13 @@ export default class TexParser { case '': if (!noneOK) { // @test MissingArgFor - throw new TexError( - 'MissingArgFor', - 'Missing argument for %1', - this.currentCS - ); + texError(COMPONENT, 'MissingArgFor', this.currentCS); } return null; case '}': if (!noneOK) { // @test ExtraCloseMissingOpen - throw new TexError( - 'ExtraCloseMissingOpen', - 'Extra close brace or missing open brace' - ); + texError(COMPONENT, 'ExtraCloseMissingOpen'); } return null; case '\\': @@ -368,7 +362,7 @@ export default class TexParser { } } // @test MissingCloseBrace - throw new TexError('MissingCloseBrace', 'Missing close brace'); + texError(COMPONENT, 'MissingCloseBrace'); } } const c = this.getCodePoint(); @@ -406,11 +400,7 @@ export default class TexParser { case '}': if (braces-- <= 0) { // @test ExtraCloseLooking1 - throw new TexError( - 'ExtraCloseLooking', - 'Extra close brace while looking for %1', - "']'" - ); + texError(COMPONENT, 'ExtraCloseLooking', "']'"); } break; case '[': @@ -427,11 +417,7 @@ export default class TexParser { } } // @test MissingCloseBracket - throw new TexError( - 'MissingCloseBracket', - "Could not find closing ']' for argument to %1", - this.currentCS - ); + texError(COMPONENT, 'MissingCloseBracket', this.currentCS); } /** @@ -456,11 +442,7 @@ export default class TexParser { } } // @test MissingOrUnrecognizedDelim1, MissingOrUnrecognizedDelim2 - throw new TexError( - 'MissingOrUnrecognizedDelim', - 'Missing or unrecognized delimiter for %1', - this.currentCS - ); + texError(COMPONENT, 'MissingOrUnrecognizedDelim', this.currentCS); } /** @@ -487,11 +469,7 @@ export default class TexParser { } } // @test MissingDimOrUnits - throw new TexError( - 'MissingDimOrUnits', - 'Missing dimension or its units for %1', - this.currentCS - ); + texError(COMPONENT, 'MissingDimOrUnits', this.currentCS); } /** @@ -521,11 +499,7 @@ export default class TexParser { case '}': if (braces === 0) { // @test ExtraCloseLooking2 - throw new TexError( - 'ExtraCloseLooking', - 'Extra close brace while looking for %1', - token - ); + texError(COMPONENT, 'ExtraCloseLooking', token); } braces--; break; @@ -535,12 +509,7 @@ export default class TexParser { } } // @test TokenNotFoundForCommand - throw new TexError( - 'TokenNotFoundForCommand', - 'Could not find %1 for %2', - token, - this.currentCS - ); + texError(COMPONENT, 'TokenNotFoundForCommand', token, this.currentCS); } /** @@ -587,11 +556,7 @@ export default class TexParser { return c; } // @test MissingOrUnrecognizedDelim - throw new TexError( - 'MissingOrUnrecognizedDelim', - 'Missing or unrecognized delimiter for %1', - this.currentCS - ); + texError(COMPONENT, 'MissingOrUnrecognizedDelim', this.currentCS); } /** diff --git a/ts/input/tex/__locales__/Component.ts b/ts/input/tex/__locales__/Component.ts new file mode 100644 index 000000000..214ebf114 --- /dev/null +++ b/ts/input/tex/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex] + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../util/Locale.js'; + +export const COMPONENT = 'input/tex'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex'); diff --git a/ts/input/tex/__locales__/de.json b/ts/input/tex/__locales__/de.json new file mode 100644 index 000000000..ff5024281 --- /dev/null +++ b/ts/input/tex/__locales__/de.json @@ -0,0 +1,31 @@ +{ + "BadPreamToken": "Ungültiges Pream-Token (%1)", + "BadRawUnicode": "Das Argument für %1 muss eine hexadezimale Zahl mit 1 bis 6 Ziffern sein", + "ColArgNotNum": "Das erste Argument für den Spaltenbezeichner %1 muss eine Zahl sein", + "ErroneousNestingEq": "Fehlerhafte Verschachtelung von Gleichungsstrukturen", + "ExtraCloseLooking": "Überflüssige schließende Klammer bei der Suche nach %1", + "ExtraCloseMissingOpen": "Überflüssige schließende Klammer oder fehlende öffnende Klammer", + "ExtraMiddle": "Zusätzliches \\middle", + "ExtraOpenMissingClose": "Zusätzliche öffnende Klammer oder fehlende schließende Klammer", + "IllegalMacroParam": "Ungültiger Makroparameterverweis", + "InvalidOption": "Ungültige Option: %1", + "InvalidValue": "Der Wert für den Schlüssel '%1' hat nicht den erwarteten Typ", + "MathNotTerminated": "Der Mathematikmodus wurde nicht ordnungsgemäß beendet", + "MaxBufferSize": "Die interne Puffergröße von MathJax wurde überschritten; liegt ein rekursiver Makroaufruf vor?", + "MaxColumns": "Zu viele Spaltenangaben (möglicherweise sich wiederholende Spaltendefinitionen?)", + "MaxMacroSub1": "Maximale Anzahl an Makrosubstitutionen in MathJax überschritten; liegt ein rekursiver Makroaufruf vor?", + "MaxMacroSub2": "Maximale Anzahl an Ersetzungen in MathJax überschritten; liegt eine rekursive LaTeX-Umgebung vor?", + "Misplaced": "'%1' falsch platziert", + "MissingArgFor": "Fehlendes Argument für %1", + "MissingArgForColumn": "Fehlendes Argument für die Spaltendeklaration %1", + "MissingBeginExtraEnd": "Fehlendes \\begin{%1} oder zusätzliches \\end{%1}", + "MissingCloseBrace": "Fehlende schließende Klammer", + "MissingCloseBracket": "Schließendes ']' für Argument zu %1 nicht gefunden", + "MissingColumnDimOrUnits": "Missing dimension or its units for %1 column declaration", + "MissingDimOrUnits": "Missing dimension or its units for %1", + "MissingLeftExtraRight": "Missing \\left or extra \\right", + "MissingOrUnrecognizedDelim": "Missing or unrecognized delimiter for %1", + "MissingScript": "Fehlendes Argument für Hoch- oder Tiefstellung", + "TokenNotFoundForCommand": "Could not find %1 for %2", + "UnknownTag": "Unbekannte Bezeichner Klasse" +} diff --git a/ts/input/tex/__locales__/en.json b/ts/input/tex/__locales__/en.json new file mode 100644 index 000000000..c822ef075 --- /dev/null +++ b/ts/input/tex/__locales__/en.json @@ -0,0 +1,31 @@ +{ + "BadPreamToken": "Illegal pream-token (%1)", + "BadRawUnicode": "Argument to %1 must a hexadecimal number with 1 to 6 digits", + "ColArgNotNum": "First argument to %1 column specifier must be a number", + "ErroneousNestingEq": "Erroneous nesting of equation structures", + "ExtraCloseLooking": "Extra close brace while looking for %1", + "ExtraCloseMissingOpen": "Extra close brace or missing open brace", + "ExtraMiddle": "Extra \\middle", + "ExtraOpenMissingClose": "Extra open brace or missing close brace", + "IllegalMacroParam": "Illegal macro parameter reference", + "InvalidOption": "Invalid option: %1", + "InvalidValue": "Value for key '%1' is not of the expected type", + "MathNotTerminated": "Math mode is not properly terminated", + "MaxBufferSize": "MathJax internal buffer size exceeded; is there a recursive macro call?", + "MaxColumns": "Too many column specifiers (perhaps looping column definitions?)", + "MaxMacroSub1": "MathJax maximum macro substitution count exceeded; is there a recursive macro call?", + "MaxMacroSub2": "MathJax maximum substitution count exceeded; is there a recursive latex environment?", + "Misplaced": "Misplaced '%1'", + "MissingArgFor": "Missing argument for %1", + "MissingArgForColumn": "Missing argument for %1 column declaration", + "MissingBeginExtraEnd": "Missing \\begin{%1} or extra \\end{%1}", + "MissingCloseBrace": "Missing close brace", + "MissingCloseBracket": "Could not find closing ']' for argument to %1", + "MissingColumnDimOrUnits": "Missing dimension or its units for %1 column declaration", + "MissingDimOrUnits": "Missing dimension or its units for %1", + "MissingLeftExtraRight": "Missing \\left or extra \\right", + "MissingOrUnrecognizedDelim": "Missing or unrecognized delimiter for %1", + "MissingScript": "Missing superscript or subscript argument", + "TokenNotFoundForCommand": "Could not find %1 for %2", + "UnknownTag": "Unknown tags class" +} diff --git a/ts/input/tex/ams/AmsConfiguration.ts b/ts/input/tex/ams/AmsConfiguration.ts index 519c1435c..832f61105 100644 --- a/ts/input/tex/ams/AmsConfiguration.ts +++ b/ts/input/tex/ams/AmsConfiguration.ts @@ -27,6 +27,7 @@ import { MultlineItem, FlalignItem } from './AmsItems.js'; import { AbstractTags } from '../Tags.js'; import './AmsMappings.js'; import { NewcommandConfig } from '../newcommand/NewcommandConfiguration.js'; +export { COMPONENT } from './__locales__/Component.js'; /** * Standard AMS style tagging. diff --git a/ts/input/tex/ams/AmsItems.ts b/ts/input/tex/ams/AmsItems.ts index 6368b47d2..c4f122a93 100644 --- a/ts/input/tex/ams/AmsItems.ts +++ b/ts/input/tex/ams/AmsItems.ts @@ -24,11 +24,13 @@ import { ArrayItem, EqnArrayItem } from '../base/BaseItems.js'; import { ParseUtil } from '../ParseUtil.js'; import NodeUtil from '../NodeUtil.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { TexConstant } from '../TexConstants.js'; import StackItemFactory from '../StackItemFactory.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Item dealing with multiline environments as a special case of arrays. Note, * that all other AMS equation environments (e.g., align, split) can be handled @@ -77,11 +79,7 @@ export class MultlineItem extends ArrayItem { public EndRow() { if (this.row.length !== 1) { // @test MultlineRowsOneCol - throw new TexError( - 'MultlineRowsOneCol', - 'The rows within the %1 environment must have exactly one column', - 'multline' - ); + texError(COMPONENT, 'MultlineRowsOneCol', 'multline'); } const row = this.create('node', 'mtr', this.row); this.table.push(row); @@ -173,12 +171,7 @@ export class FlalignItem extends EqnArrayItem { const n = this.getProperty('xalignat') as number; if (!n) return; if (this.row.length > n) { - throw new TexError( - 'XalignOverflow', - 'Extra %1 in row of %2', - '&', - this.name - ); + texError(COMPONENT, 'XalignOverflow', '&', this.name); } } diff --git a/ts/input/tex/ams/AmsMethods.ts b/ts/input/tex/ams/AmsMethods.ts index 976a0ed45..9b0254645 100644 --- a/ts/input/tex/ams/AmsMethods.ts +++ b/ts/input/tex/ams/AmsMethods.ts @@ -29,7 +29,7 @@ import ParseMethods from '../ParseMethods.js'; import NodeUtil from '../NodeUtil.js'; import { TexConstant } from '../TexConstants.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { ArrayItem } from '../base/BaseItems.js'; import { FlalignItem } from './AmsItems.js'; import BaseMethods from '../base/BaseMethods.js'; @@ -42,6 +42,8 @@ import { } from '../../../core/MmlTree/MmlNode.js'; import { NewcommandUtil } from '../newcommand/NewcommandUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Utility for breaking the \sideset scripts from any other material. * @@ -147,11 +149,7 @@ export const AmsMethods: { [key: string]: ParseMethod } = { const n = parser.GetArgument('\\begin{' + name + '}'); if (n.match(/[^0-9]/)) { // @test PositiveIntegerArg - throw new TexError( - 'PositiveIntegerArg', - 'Argument to %1 must be a positive integer', - '\\begin{' + name + '}' - ); + texError(COMPONENT, 'PositiveIntegerArg', '\\begin{' + name + '}'); } let count = parseInt(n, 10); while (count > 0) { @@ -240,9 +238,9 @@ export const AmsMethods: { [key: string]: ParseMethod } = { ): ParseResult { const n = parser.GetArgument('\\begin{' + begin.getName() + '}'); if (n.match(/[^0-9]/)) { - throw new TexError( + texError( + COMPONENT, 'PositiveIntegerArg', - 'Argument to %1 must be a positive integer', '\\begin{' + begin.getName() + '}' ); } @@ -619,20 +617,16 @@ export const AmsMethods: { [key: string]: ParseMethod } = { // @test Shove (Left|Right) (Top|Middle|Bottom) if (top.kind !== 'multline') { // @test Shove Error Environment - throw new TexError( + texError( + COMPONENT, 'CommandOnlyAllowedInEnv', - '%1 only allowed in %2 environment', parser.currentCS, 'multline' ); } if (top.Size()) { // @test Shove Error (Top|Middle|Bottom) - throw new TexError( - 'CommandAtTheBeginingOfLine', - '%1 must come at the beginning of the line', - parser.currentCS - ); + texError(COMPONENT, 'CommandAtTheBeginingOfLine', parser.currentCS); } top.setProperty('shove', shove); }, @@ -666,11 +660,7 @@ export const AmsMethods: { [key: string]: ParseMethod } = { lr = lrMap[lr]; if (lr == null) { // @test Center Fraction Error - throw new TexError( - 'IllegalAlign', - 'Illegal alignment specified in %1', - parser.currentCS - ); + texError(COMPONENT, 'IllegalAlign', parser.currentCS); } if (lr) { // @test Right Fraction, Left Fraction @@ -731,11 +721,7 @@ export const AmsMethods: { [key: string]: ParseMethod } = { const styleAlpha = ['D', 'T', 'S', 'SS'][styleDigit]; if (styleAlpha == null) { // @test Genfrac Error - throw new TexError( - 'BadMathStyleFor', - 'Bad math style for %1', - parser.currentCS - ); + texError(COMPONENT, 'BadMathStyleFor', parser.currentCS); } frac = parser.create('node', 'mstyle', [frac]); if (styleAlpha === 'D') { @@ -764,16 +750,16 @@ export const AmsMethods: { [key: string]: ParseMethod } = { HandleTag(parser: TexParser, name: string) { if (!parser.tags.currentTag.taggable && parser.tags.env) { // @test Illegal Tag Error - throw new TexError( + texError( + COMPONENT, 'CommandNotAllowedInEnv', - '%1 not allowed in %2 environment', parser.currentCS, parser.tags.env ); } if (parser.tags.currentTag.tag) { // @test Double Tag Error - throw new TexError('MultipleCommand', 'Multiple %1', parser.currentCS); + texError(COMPONENT, 'MultipleCommand', parser.currentCS); } const star = parser.GetStar(); const tagId = UnitUtil.trimSpaces(parser.GetArgument(name)); diff --git a/ts/input/tex/ams/__locales__/Component.ts b/ts/input/tex/ams/__locales__/Component.ts new file mode 100644 index 000000000..b547dcf77 --- /dev/null +++ b/ts/input/tex/ams/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/ams + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/ams'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/ams'); diff --git a/ts/input/tex/ams/__locales__/de.json b/ts/input/tex/ams/__locales__/de.json new file mode 100644 index 000000000..f89ba889f --- /dev/null +++ b/ts/input/tex/ams/__locales__/de.json @@ -0,0 +1,11 @@ +{ + "BadMathStyleFor": "Falscher mathematischer Stil für %1", + "CommandAtTheBeginingOfLine": "%1 muss am Zeilenanfang stehen", + "CommandNotAllowedInEnv": "%1 ist in der Umgebung %2 nicht zulässig", + "CommandOnlyAllowedInEnv": "%1 ist nur in der Umgebung %2 zulässig", + "IllegalAlign": "Ungültige Ausrichtung in %1 angegeben", + "MultipleCommand": "Mehrere %1", + "MultlineRowsOneCol": "Die Zeilen innerhalb der %1-Umgebung müssen genau eine Spalte haben", + "PositiveIntegerArg": "Das Argument für %1 muss eine positive ganze Zahl sein", + "XalignOverflow": "Zusätzliches %1 in Zeile %2" +} diff --git a/ts/input/tex/ams/__locales__/en.json b/ts/input/tex/ams/__locales__/en.json new file mode 100644 index 000000000..a17d518ae --- /dev/null +++ b/ts/input/tex/ams/__locales__/en.json @@ -0,0 +1,11 @@ +{ + "BadMathStyleFor": "Bad math style for %1", + "CommandAtTheBeginingOfLine": "%1 must come at the beginning of the line", + "CommandNotAllowedInEnv": "%1 not allowed in %2 environment", + "CommandOnlyAllowedInEnv": "%1 only allowed in %2 environment", + "IllegalAlign": "Illegal alignment specified in %1", + "MultipleCommand": "Multiple %1", + "MultlineRowsOneCol": "The rows within the %1 environment must have exactly one column", + "PositiveIntegerArg": "Argument to %1 must be a positive integer", + "XalignOverflow": "Extra %1 in row of %2" +} diff --git a/ts/input/tex/base/BaseConfiguration.ts b/ts/input/tex/base/BaseConfiguration.ts index 30eab1ed0..fb3ef2b68 100644 --- a/ts/input/tex/base/BaseConfiguration.ts +++ b/ts/input/tex/base/BaseConfiguration.ts @@ -24,7 +24,7 @@ import { HandlerType, ConfigurationType } from '../HandlerTypes.js'; import { Configuration } from '../Configuration.js'; import { MapHandler } from '../MapHandler.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import NodeUtil from '../NodeUtil.js'; import TexParser from '../TexParser.js'; import { CharacterMap, RegExpMap } from '../TokenMap.js'; @@ -37,6 +37,8 @@ import ParseMethods from '../ParseMethods.js'; import { ParseUtil } from '../ParseUtil.js'; import { TexConstant } from '../TexConstants.js'; import { context } from '../../../util/context.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; const MATHVARIANT = TexConstant.Variant; @@ -89,11 +91,7 @@ export function Other(parser: TexParser, char: string) { */ function csUndefined(_parser: TexParser, name: string) { // @test Undefined-CS - throw new TexError( - 'UndefinedControlSequence', - 'Undefined control sequence %1', - '\\' + name - ); + texError(COMPONENT, 'UndefinedControlSequence', '\\' + name); } /** @@ -104,7 +102,7 @@ function csUndefined(_parser: TexParser, name: string) { */ function envUndefined(_parser: TexParser, env: string) { // @test Undefined-Env - throw new TexError('UnknownEnv', "Unknown environment '%1'", env); + texError(COMPONENT, 'UnknownEnv', env); } /** diff --git a/ts/input/tex/base/BaseItems.ts b/ts/input/tex/base/BaseItems.ts index 4704a6b57..133450956 100644 --- a/ts/input/tex/base/BaseItems.ts +++ b/ts/input/tex/base/BaseItems.ts @@ -29,7 +29,7 @@ import { MmlMo } from '../../../core/MmlTree/MmlNodes/mo.js'; import { MmlMsubsup } from '../../../core/MmlTree/MmlNodes/msubsup.js'; import { MmlMunderover } from '../../../core/MmlTree/MmlNodes/munderover.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { ParseUtil } from '../ParseUtil.js'; import { UnitUtil } from '../UnitUtil.js'; import NodeUtil from '../NodeUtil.js'; @@ -39,6 +39,9 @@ import { CheckType, BaseItem, StackItem, EnvList } from '../StackItem.js'; import { TRBL } from '../../../util/Styles.js'; import { TexConstant } from '../TexConstants.js'; +import { COMPONENT as TEX_COMPONENT } from '../__locales__/Component.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Initial item on the stack. It's pushed when parsing begins. */ @@ -111,7 +114,7 @@ export class OpenItem extends BaseItem { */ protected static errors = Object.assign(Object.create(BaseItem.errors), { // @test ExtraOpenMissingClose - stop: ['ExtraOpenMissingClose', 'Extra open brace or missing close brace'], + stop: [TEX_COMPONENT, 'ExtraOpenMissingClose'], }); /** @@ -222,11 +225,11 @@ export class SubsupItem extends BaseItem { */ protected static errors = Object.assign(Object.create(BaseItem.errors), { // @test MissingScript Sub, MissingScript Sup - stop: ['MissingScript', 'Missing superscript or subscript argument'], + stop: [COMPONENT, 'MissingScript'], // @test MissingOpenForSup - sup: ['MissingOpenForSup', 'Missing open brace for superscript'], + sup: [COMPONENT, 'MissingOpenForSup'], // @test MissingOpenForSub - sub: ['MissingOpenForSub', 'Missing open brace for subscript'], + sub: [COMPONENT, 'MissingOpenForSub'], }); /** @@ -276,10 +279,12 @@ export class SubsupItem extends BaseItem { const result = this.factory.create('mml', top); return [[result], true]; } - super.checkItem(item); - // @test Brace Superscript Error, MissingOpenForSup, MissingOpenForSub - const error = this.getErrors(['', 'sub', 'sup'][position]); - throw new TexError(error[0], error[1], ...error.splice(2)); + if (super.checkItem(item)[1]) { + // @test Brace Superscript Error, MissingOpenForSup, MissingOpenForSub + const error = this.getError(['', 'sub', 'sup'][position]); + texError(...error); + } + return null; } } @@ -315,11 +320,7 @@ export class OverItem extends BaseItem { public checkItem(item: StackItem): CheckType { if (item.isKind('over')) { // @test Double Over - throw new TexError( - 'AmbiguousUseOf', - 'Ambiguous use of %1', - item.getName() - ); + texError(COMPONENT, 'AmbiguousUseOf', item.getName()); } if (item.isClose) { // @test Over @@ -373,7 +374,7 @@ export class LeftItem extends BaseItem { */ protected static errors = Object.assign(Object.create(BaseItem.errors), { // @test ExtraLeftMissingRight - stop: ['ExtraLeftMissingRight', 'Extra \\left or missing \\right'], + stop: [COMPONENT, 'ExtraLeftMissingRight'], }); /** @@ -586,12 +587,7 @@ export class BeginItem extends BaseItem { if (item.isKind('end')) { if (item.getName() !== this.getName()) { // @test EnvBadEnd - throw new TexError( - 'EnvBadEnd', - '\\begin{%1} ended with \\end{%2}', - this.getName(), - item.getName() - ); + texError(COMPONENT, 'EnvBadEnd', this.getName(), item.getName()); } // @test Hfill const node = this.toMml(); @@ -600,7 +596,7 @@ export class BeginItem extends BaseItem { } if (item.isKind('stop')) { // @test EnvMissingEnd Array - throw new TexError('EnvMissingEnd', 'Missing \\end{%1}', this.getName()); + texError(COMPONENT, 'EnvMissingEnd', this.getName()); } return super.checkItem(item); } @@ -673,7 +669,7 @@ export class PositionItem extends BaseItem { public checkItem(item: StackItem): CheckType { if (item.isClose) { // @test MissingBoxFor - throw new TexError('MissingBoxFor', 'Missing box for %1', this.getName()); + texError(COMPONENT, 'MissingBoxFor', this.getName()); } if (item.isFinal) { let mml = item.toMml(); @@ -1083,7 +1079,7 @@ export class ArrayItem extends BaseItem { return [[newItem], true]; } // @test MissingCloseBrace2 - throw new TexError('MissingCloseBrace', 'Missing close brace'); + texError(TEX_COMPONENT, 'MissingCloseBrace'); } return [[newItem, item], true]; } @@ -1225,11 +1221,7 @@ export class ArrayItem extends BaseItem { ++this.templateSubs > parser.configuration.options.maxTemplateSubtitutions ) { - throw new TexError( - 'MaxTemplateSubs', - 'Maximum template substitutions exceeded; ' + - 'is there an invalid use of \\\\ in the template?' - ); + texError(COMPONENT, 'MaxTemplateSubs'); } } } @@ -1648,7 +1640,7 @@ export class EquationItem extends BaseItem { } if (item.isKind('stop')) { // @test EnvMissingEnd Equation - throw new TexError('EnvMissingEnd', 'Missing \\end{%1}', this.getName()); + texError(COMPONENT, 'EnvMissingEnd', this.getName()); } return super.checkItem(item); } diff --git a/ts/input/tex/base/BaseMethods.ts b/ts/input/tex/base/BaseMethods.ts index dc371a34d..3621f8f04 100644 --- a/ts/input/tex/base/BaseMethods.ts +++ b/ts/input/tex/base/BaseMethods.ts @@ -27,7 +27,7 @@ import { StackItem, EnvList } from '../StackItem.js'; import { Macro } from '../Token.js'; import { ParseResult, ParseMethod } from '../Types.js'; import NodeUtil from '../NodeUtil.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import TexParser from '../TexParser.js'; import { TexConstant } from '../TexConstants.js'; import { ParseUtil } from '../ParseUtil.js'; @@ -44,6 +44,9 @@ import { lookup } from '../../../util/Options.js'; import { ColumnState } from '../ColumnParser.js'; import { replaceUnicode } from '../../../util/string.js'; +import { COMPONENT as TEX_COMPONENT } from '../__locales__/Component.js'; +import { COMPONENT } from './__locales__/Component.js'; + const P_HEIGHT = 1.2 / 0.85; // cmex10 height plus depth over .85 const MmlTokenAllow: { [key: string]: number } = { fontfamily: 1, @@ -70,20 +73,12 @@ export function splitAlignArray(align: string, n: number = Infinity): string { .map((s: string) => { const name = { t: 'top', b: 'bottom', m: 'middle', c: 'center' }[s]; if (!name) { - throw new TexError( - 'BadBreakAlign', - 'Invalid alignment character: %1', - s - ); + texError(COMPONENT, 'BadBreakAlign', s); } return name; }); if (list.length > n) { - throw new TexError( - 'TooManyAligns', - 'Too many alignment characters: %1', - align - ); + texError(COMPONENT, 'TooManyAligns', align); } return n === 1 ? list[0] : list.join(' '); } @@ -224,10 +219,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { !NodeUtil.getProperty(base, 'subsupOK')) ) { // @test Double-super-error, Double-over-error - throw new TexError( - 'DoubleExponent', - 'Double exponent: use braces to clarify' - ); + texError(COMPONENT, 'DoubleExponent'); } if (!NodeUtil.isType(base, 'msubsup') || NodeUtil.isType(base, 'msup')) { if (movesupsub) { @@ -299,10 +291,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { !NodeUtil.getProperty(base, 'subsupOK')) ) { // @test Double-sub-error, Double-under-error - throw new TexError( - 'DoubleSubscripts', - 'Double subscripts: use braces to clarify' - ); + texError(COMPONENT, 'DoubleSubscripts'); } if (!NodeUtil.isType(base, 'msubsup') || NodeUtil.isType(base, 'msup')) { if (movesupsub) { @@ -356,10 +345,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { !NodeUtil.getProperty(base, 'subsupOK')) ) { // @test Double Prime Error - throw new TexError( - 'DoubleExponentPrime', - 'Prime causes double exponent: use braces to clarify' - ); + texError(COMPONENT, 'DoubleExponentPrime'); } let sup = ''; parser.i--; @@ -397,10 +383,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { */ Hash(_parser: TexParser, _c: string) { // @test Hash Error - throw new TexError( - 'CantUseHash1', - "You can't use 'macro parameter character #' in math mode" - ); + texError(COMPONENT, 'CantUseHash1'); }, /** @@ -633,11 +616,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { NodeUtil.getProperty(op, 'movesupsub') == null) ) { // @test Limits Error - throw new TexError( - 'MisplacedLimits', - '%1 is allowed only on operators', - parser.currentCS - ); + texError(COMPONENT, 'MisplacedLimits', parser.currentCS); } const top = parser.stack.Top(); let node; @@ -760,28 +739,16 @@ const BaseMethods: { [key: string]: ParseMethod } = { // @test Tweaked Root if (!parser.stack.env['inRoot']) { // @test Misplaced Move Root - throw new TexError( - 'MisplacedMoveRoot', - '%1 can appear only within a root', - parser.currentCS - ); + texError(COMPONENT, 'MisplacedMoveRoot', parser.currentCS); } if (parser.stack.global[id]) { // @test Multiple Move Root - throw new TexError( - 'MultipleMoveRoot', - 'Multiple use of %1', - parser.currentCS - ); + texError(COMPONENT, 'MultipleMoveRoot', parser.currentCS); } let n = parser.GetArgument(name); if (!n.match(/-?[0-9]+/)) { // @test Incorrect Move Root - throw new TexError( - 'IntegerArg', - 'The argument to %1 must be an integer', - parser.currentCS - ); + texError(COMPONENT, 'IntegerArg', parser.currentCS); } n = parseInt(n, 10) / 15 + 'em'; if (n.substring(0, 1) !== '-') { @@ -1017,50 +984,30 @@ const BaseMethods: { [key: string]: ParseMethod } = { BreakAlign(parser: TexParser, name: string) { const top = parser.stack.Top() as sitem.ArrayItem; if (!(top instanceof sitem.ArrayItem)) { - throw new TexError( - 'BreakNotInArray', - '%1 must be used in an alignment environment', - parser.currentCS - ); + texError(COMPONENT, 'BreakNotInArray', parser.currentCS); } const type = parser.GetArgument(name).trim(); switch (type) { case 'c': if (top.First) { - throw new TexError( - 'BreakFirstInEntry', - '%1 must be at the beginning of an alignment entry', - parser.currentCS + '{c}' - ); + texError(COMPONENT, 'BreakFirstInEntry', parser.currentCS + '{c}'); } top.breakAlign.cell = splitAlignArray(parser.GetArgument(name), 1); break; case 'r': if (top.row.length || top.First) { - throw new TexError( - 'BreakFirstInRow', - '%1 must be at the beginning of an alignment row', - parser.currentCS + '{r}' - ); + texError(COMPONENT, 'BreakFirstInRow', parser.currentCS + '{r}'); } top.breakAlign.row = splitAlignArray(parser.GetArgument(name)); break; case 't': if (top.table.length || top.row.length || top.First) { - throw new TexError( - 'BreakFirstInTable', - '%1 must be at the beginning of an alignment', - parser.currentCS + '{t}' - ); + texError(COMPONENT, 'BreakFirstInTable', parser.currentCS + '{t}'); } top.breakAlign.table = splitAlignArray(parser.GetArgument(name)); break; default: - throw new TexError( - 'BreakType', - 'First argument to %1 must be one of c, r, or t', - parser.currentCS - ); + texError(COMPONENT, 'BreakType', parser.currentCS); } }, @@ -1085,7 +1032,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { } if (!node || !node.isToken) { // @test Token Illegal Type, Token Wrong Type - throw new TexError('NotMathMLToken', '%1 is not a token element', kind); + texError(COMPONENT, 'NotMathMLToken', kind); } while (attr !== '') { const match = attr.match( @@ -1093,20 +1040,11 @@ const BaseMethods: { [key: string]: ParseMethod } = { ); if (!match) { // @test Token Invalid Attribute - throw new TexError( - 'InvalidMathMLAttr', - 'Invalid MathML attribute: %1', - attr.split(/[\s\n=]/)[0] - ); + texError(COMPONENT, 'InvalidMathMLAttr', attr.split(/[\s\n=]/)[0]); } if (!node.attributes.hasDefault(match[1]) && !MmlTokenAllow[match[1]]) { // @test Token Unknown Attribute, Token Wrong Attribute - throw new TexError( - 'UnknownAttrForElement', - '%1 is not a recognized attribute for %2', - match[1], - kind - ); + texError(COMPONENT, 'UnknownAttrForElement', match[1], kind); } let value: string | boolean = ParseUtil.mmlFilterAttribute( parser, @@ -1559,11 +1497,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { const c = parser.GetNext(); if (c === '') { // @test Matrix Error - throw new TexError( - 'MissingArgFor', - 'Missing argument for %1', - parser.currentCS - ); + texError(TEX_COMPONENT, 'MissingArgFor', parser.currentCS); } if (c === '{') { // @test Matrix Braces, Matrix Columns, Matrix Rows. @@ -1673,10 +1607,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { // Extra alignment tabs are not allowed in cases // // @test ExtraAlignTab - throw new TexError( - 'ExtraAlignTab', - 'Extra alignment tab in \\cases text' - ); + texError(COMPONENT, 'ExtraAlignTab'); } else if (c === '\\') { // // If the macro is \cr or \\, end the search, otherwise skip the macro @@ -1756,11 +1687,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { // @test Custom Linebreak if (dim && !value) { // @test Dimension Error - throw new TexError( - 'BracketMustBeDimension', - 'Bracket argument to %1 must be a dimension', - parser.currentCS - ); + texError(COMPONENT, 'BracketMustBeDimension', name); } n = value + unit; } @@ -1804,7 +1731,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { const top = parser.stack.Top(); if (!(top instanceof sitem.ArrayItem) || top.Size()) { // @test Misplaced hline - throw new TexError('Misplaced', 'Misplaced %1', parser.currentCS); + texError(TEX_COMPONENT, 'Misplaced', parser.currentCS); } if (!top.table.length) { // @test Enclosed top, Enclosed top bottom @@ -1835,11 +1762,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { top.hfill.push(top.Size()); } else { // @test UnsupportedHFill - throw new TexError( - 'UnsupportedHFill', - 'Unsupported use of %1', - parser.currentCS - ); + texError(COMPONENT, 'UnsupportedHFill', parser.currentCS); } }, @@ -1854,18 +1777,10 @@ const BaseMethods: { [key: string]: ParseMethod } = { const n = parser.GetBrackets(name, '0'); const macro = parser.GetArgument(name); if (c.length !== 1) { - throw new TexError( - 'BadColumnName', - 'Column specifier must be exactly one character: %1', - c - ); + texError(COMPONENT, 'BadColumnName', c); } if (!n.match(/^\d+$/)) { - throw new TexError( - 'PositiveIntegerArg', - 'Argument to %1 must be a positive integer', - n - ); + texError(COMPONENT, 'PositiveIntegerArg', n); } const cparser = parser.configuration.columnParser; cparser.columnHandler[c] = (state: ColumnState) => @@ -1888,7 +1803,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { const env = parser.GetArgument(name); if (env.match(/\\/)) { // @test InvalidEnv - throw new TexError('InvalidEnv', "Invalid environment name '%1'", env); + texError(COMPONENT, 'InvalidEnv', env); } const macro = parser.configuration.handlers .get(HandlerType.ENVIRONMENT) @@ -2025,21 +1940,14 @@ const BaseMethods: { [key: string]: ParseMethod } = { (shift && !UnitUtil.matchDimen(shift)[0]) || (last && !UnitUtil.matchDimen(last)[0]) ) { - throw new TexError( - 'BracketMustBeDimension', - 'Bracket argument to %1 must be a dimension', - name - ); + texError(COMPONENT, 'BracketMustBeDimension', name); } // // Get the indentalign values, if any // const lcr = parser.GetArgument(name); if (lcr && !lcr.match(/^([lcr]{1,3})?$/)) { - throw new TexError( - 'BadAlignment', - 'Alignment must be one to three copies of l, c, or r' - ); + texError(COMPONENT, 'BadAlignment'); } const align = [...lcr].map( (c) => ({ l: 'left', c: 'center', r: 'right' })[c] @@ -2181,7 +2089,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { // @test Label, Ref, Ref Unknown if (parser.tags.label) { // @test Double Label Error - throw new TexError('MultipleCommand', 'Multiple %1', parser.currentCS); + texError(COMPONENT, 'MultipleCommand', parser.currentCS); } parser.tags.label = label; if ( @@ -2189,11 +2097,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { !parser.options['ignoreDuplicateLabels'] ) { // @ Duplicate Label Error - throw new TexError( - 'MultipleLabel', - "Label '%1' multiply defined", - label - ); + texError(COMPONENT, 'MultipleLabel', label); } // TODO: This should be set in the tags structure! parser.tags.labels[label] = new Label(); // will be replaced by tag value later diff --git a/ts/input/tex/base/__locales__/Component.ts b/ts/input/tex/base/__locales__/Component.ts new file mode 100644 index 000000000..8a366032e --- /dev/null +++ b/ts/input/tex/base/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/base + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/base'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/base'); diff --git a/ts/input/tex/base/__locales__/de.json b/ts/input/tex/base/__locales__/de.json new file mode 100644 index 000000000..c5d53b47a --- /dev/null +++ b/ts/input/tex/base/__locales__/de.json @@ -0,0 +1,41 @@ +{ + "AmbiguousUseOf": "Mehrdeutige Verwendung von %1", + "BadAlignment": "Die Ausrichtung muss aus einer bis drei Kopien von l, c oder r bestehen", + "BadBreakAlign": "Ungültiges Ausrichtungszeichen: %1", + "BadColumnName": "Der Spaltenbezeichner muss genau ein Zeichen lang sein: %1", + "BracketMustBeDimension": "Das Klammerargument für %1 muss eine Bemaßung sein", + "BreakFirstInEntry": "%1 muss am Anfang eines Ausrichtungs-Eintrags stehen", + "BreakFirstInRow": "%1 muss am Anfang einer Ausrichtungszeile stehen", + "BreakFirstInTable": "%1 muss am Anfang einer Ausrichtung stehen", + "BreakNotInArray": "%1 muss in einer Ausrichtungsumgebung verwendet werden", + "BreakType": "Das erste Argument für %1 muss c, r oder t sein", + "CantUseHash1": "Das Makroparameterzeichen '#' darf im mathematischen Modus nicht verwendet werden", + "DoubleExponent": "Doppelter Exponent: Verwende Klammern zur Verdeutlichung", + "DoubleExponentPrime": "Prime führt zu doppeltem Exponenten: Verwende Klammern zur Verdeutlichung", + "DoubleSubscripts": "Doppelte Indizes: Verwende Klammern zur Verdeutlichung", + "EnvBadEnd": "\\begin{%1} endete mit \\end{%2}", + "EnvMissingEnd": "Fehlendes \\end{%1}", + "ExtraAlignTab": "Zusätzlicher Ausrichtungs-Tabulator im \\cases-Text", + "ExtraLeftMissingRight": "Überflüssiges \\left oder fehlendes \\right", + "ExtraOpenMissingClose": "Zusätzliche öffnende Klammer oder fehlende schließende Klammer", + "IntegerArg": "Das Argument für %1 muss eine ganze Zahl sein", + "InvalidEnv": "Ungültiger Umgebungsname '%1'", + "InvalidMathMLAttr": "Ungültiges MathML-Attribut: %1", + "MaxTemplateSubs": "Maximale Anzahl an Vorlagenersetzungen überschritten; liegt eine ungültige Verwendung von \\\\ in der Vorlage vor?", + "MisplacedLimits": "%1 ist nur bei Operatoren zulässig", + "MisplacedMoveRoot": "%1 darf nur innerhalb einer Wurzel stehen", + "MissingBoxFor": "Fehlende Box für %1", + "MissingOpenForSub": "Fehlende öffnende Klammer für Tiefstellung", + "MissingOpenForSup": "Fehlende öffnende Klammer für Hochstellung", + "MissingScript": "Fehlendes Argument für Hoch- oder Tiefstellung", + "MultipleCommand": "Mehrere %1", + "MultipleLabel": "Bezeichnung '%1' mehrfach definiert", + "MultipleMoveRoot": "Mehrfache Verwendung von %1", + "NotMathMLToken": "%1 ist kein Token-Element", + "PositiveIntegerArg": "Das Argument für %1 muss eine positive ganze Zahl sein", + "TooManyAligns": "Zu viele Ausrichtungszeichen: %1", + "UndefinedControlSequence": "Undefinierte Steuerungssequenz %1", + "UnknownAttrForElement": "%1 ist kein anerkanntes Attribut für %2", + "UnknownEnv": "Unbekannte Umgebung '%1'", + "UnsupportedHFill": "Nicht unterstützte Verwendung von %1" +} diff --git a/ts/input/tex/base/__locales__/en.json b/ts/input/tex/base/__locales__/en.json new file mode 100644 index 000000000..6b5f3b71e --- /dev/null +++ b/ts/input/tex/base/__locales__/en.json @@ -0,0 +1,41 @@ +{ + "AmbiguousUseOf": "Ambiguous use of %1", + "BadAlignment": "Alignment must be one to three copies of l, c, or r", + "BadBreakAlign": "Invalid alignment character: %1", + "BadColumnName": "Column specifier must be exactly one character: %1", + "BracketMustBeDimension": "Bracket argument to %1 must be a dimension", + "BreakFirstInEntry": "%1 must be at the beginning of an alignment entry", + "BreakFirstInRow": "%1 must be at the beginning of an alignment row", + "BreakFirstInTable": "%1 must be at the beginning of an alignment", + "BreakNotInArray": "%1 must be used in an alignment environment", + "BreakType": "First argument to %1 must be one of c, r, or t", + "CantUseHash1": "You can't use 'macro parameter character #' in math mode", + "DoubleExponent": "Double exponent: use braces to clarify", + "DoubleExponentPrime": "Prime causes double exponent: use braces to clarify", + "DoubleSubscripts": "Double subscripts: use braces to clarify", + "EnvBadEnd": "\\begin{%1} ended with \\end{%2}", + "EnvMissingEnd": "Missing \\end{%1}", + "ExtraAlignTab": "Extra alignment tab in \\cases text", + "ExtraLeftMissingRight": "Extra \\left or missing \\right", + "ExtraOpenMissingClose": "Extra open brace or missing close brace", + "IntegerArg": "The argument to %1 must be an integer", + "InvalidEnv": "Invalid environment name '%1'", + "InvalidMathMLAttr": "Invalid MathML attribute: %1", + "MaxTemplateSubs": "Maximum template substitutions exceeded; is there an invalid use of \\\\ in the template?", + "MisplacedLimits": "%1 is allowed only on operators", + "MisplacedMoveRoot": "%1 can appear only within a root", + "MissingBoxFor": "Missing box for %1", + "MissingOpenForSub": "Missing open brace for subscript", + "MissingOpenForSup": "Missing open brace for superscript", + "MissingScript": "Missing superscript or subscript argument", + "MultipleCommand": "Multiple %1", + "MultipleLabel": "Label '%1' multiply defined", + "MultipleMoveRoot": "Multiple use of %1", + "NotMathMLToken": "%1 is not a token element", + "PositiveIntegerArg": "Argument to %1 must be a positive integer", + "TooManyAligns": "Too many alignment characters: %1", + "UndefinedControlSequence": "Undefined control sequence %1", + "UnknownAttrForElement": "%1 is not a recognized attribute for %2", + "UnknownEnv": "Unknown environment '%1'", + "UnsupportedHFill": "Unsupported use of %1" +} diff --git a/ts/input/tex/bbox/BboxConfiguration.ts b/ts/input/tex/bbox/BboxConfiguration.ts index 37838302e..7700752e6 100644 --- a/ts/input/tex/bbox/BboxConfiguration.ts +++ b/ts/input/tex/bbox/BboxConfiguration.ts @@ -26,7 +26,9 @@ import { Configuration } from '../Configuration.js'; import TexParser from '../TexParser.js'; import { CommandMap } from '../TokenMap.js'; import { ParseMethod } from '../Types.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; // Namespace const BboxMethods: { [key: string]: ParseMethod } = { @@ -50,12 +52,7 @@ const BboxMethods: { [key: string]: ParseMethod } = { // @test Bbox-Padding if (def) { // @test Bbox-Padding-Error - throw new TexError( - 'MultipleBBoxProperty', - '%1 specified twice in %2', - 'Padding', - name - ); + texError(COMPONENT, 'MultipleBBoxProperty', 'Padding', name); } const pad = BBoxPadding(match[1] + match[3]); if (pad) { @@ -71,33 +68,19 @@ const BboxMethods: { [key: string]: ParseMethod } = { // @test Bbox-Background if (background) { // @test Bbox-Background-Error - throw new TexError( - 'MultipleBBoxProperty', - '%1 specified twice in %2', - 'Background', - name - ); + texError(COMPONENT, 'MultipleBBoxProperty', 'Background', name); } background = part; } else if (part.match(/^[-a-z]+:/i)) { // @test Bbox-Frame if (style) { // @test Bbox-Frame-Error - throw new TexError( - 'MultipleBBoxProperty', - '%1 specified twice in %2', - 'Style', - name - ); + texError(COMPONENT, 'MultipleBBoxProperty', 'Style', name); } style = BBoxStyle(part); } else if (part !== '') { // @test Bbox-General-Error - throw new TexError( - 'InvalidBBoxProperty', - '"%1" doesn\'t look like a color, a padding dimension, or a style', - part - ); + texError(COMPONENT, 'InvalidBBoxProperty', part); } } if (def) { diff --git a/ts/input/tex/bbox/__locales__/Component.ts b/ts/input/tex/bbox/__locales__/Component.ts new file mode 100644 index 000000000..5c1b19e9c --- /dev/null +++ b/ts/input/tex/bbox/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/bbox + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/bbox'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/bbox'); diff --git a/ts/input/tex/bbox/__locales__/de.json b/ts/input/tex/bbox/__locales__/de.json new file mode 100644 index 000000000..189d848b1 --- /dev/null +++ b/ts/input/tex/bbox/__locales__/de.json @@ -0,0 +1,4 @@ +{ + "InvalidBBoxProperty": "'%1' sieht nicht nach einer Farbe, einem Abstand oder einem Stil aus", + "MultipleBBoxProperty": "%1 wurde in %2 zweimal angegeben" +} diff --git a/ts/input/tex/bbox/__locales__/en.json b/ts/input/tex/bbox/__locales__/en.json new file mode 100644 index 000000000..38d9d1919 --- /dev/null +++ b/ts/input/tex/bbox/__locales__/en.json @@ -0,0 +1,4 @@ +{ + "InvalidBBoxProperty": "'%1' doesn't look like a color, a padding dimension, or a style", + "MultipleBBoxProperty": "%1 specified twice in %2" +} diff --git a/ts/input/tex/begingroup/BegingroupConfiguration.ts b/ts/input/tex/begingroup/BegingroupConfiguration.ts index f509f990b..d2aaa6e33 100644 --- a/ts/input/tex/begingroup/BegingroupConfiguration.ts +++ b/ts/input/tex/begingroup/BegingroupConfiguration.ts @@ -26,6 +26,7 @@ import { Configuration } from '../Configuration.js'; import { CommandMap } from '../TokenMap.js'; import { BegingroupStack, begingroupStack } from './BegingroupStack.js'; import { BegingroupMethods } from './BegingroupMethods.js'; +export { COMPONENT } from './__locales__/Component.js'; /** * Create the begingroup command map. diff --git a/ts/input/tex/begingroup/BegingroupMethods.ts b/ts/input/tex/begingroup/BegingroupMethods.ts index fcaab96b5..e0b050a4e 100644 --- a/ts/input/tex/begingroup/BegingroupMethods.ts +++ b/ts/input/tex/begingroup/BegingroupMethods.ts @@ -23,7 +23,9 @@ import { CommandMap } from '../TokenMap.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; + +import { COMPONENT } from './__locales__/Component.js'; import BaseMethods from '../base/BaseMethods.js'; import { begingroupStack } from './BegingroupStack.js'; @@ -83,11 +85,7 @@ export const BegingroupMethods = { // Check that \global can be used with the following CS // if (!parser.options.begingroup.allowGlobal.includes(cs)) { - throw new TexError( - 'IllegalGlobal', - 'Invalid use of %1', - parser.currentCS - ); + texError(COMPONENT, 'IllegalGlobal', parser.currentCS); } parser.stack.env.isGlobal = true; }, diff --git a/ts/input/tex/begingroup/BegingroupStack.ts b/ts/input/tex/begingroup/BegingroupStack.ts index 6de5e618d..1443da966 100644 --- a/ts/input/tex/begingroup/BegingroupStack.ts +++ b/ts/input/tex/begingroup/BegingroupStack.ts @@ -31,7 +31,9 @@ import { } from '../TokenMap.js'; import { Token } from '../Token.js'; import { MapHandler, SubHandlers } from '../MapHandler.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; + +import { COMPONENT } from './__locales__/Component.js'; import { NewcommandTables as NT, NewcommandPriority, @@ -170,10 +172,7 @@ export class BegingroupStack { */ public pop() { if (this.i === this.base) { - throw new TexError( - 'MissingBegingroup', - 'Missing \\begingroup or extra \\endgroup' - ); + texError(COMPONENT, 'MissingBegingroup'); } this.handlers.remove(BegingroupStack.handlerConfig, {}); // diff --git a/ts/input/tex/begingroup/__locales__/Component.ts b/ts/input/tex/begingroup/__locales__/Component.ts new file mode 100644 index 000000000..0e9b0ae1e --- /dev/null +++ b/ts/input/tex/begingroup/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/begingroup + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/begingroup'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/begingroup'); diff --git a/ts/input/tex/begingroup/__locales__/de.json b/ts/input/tex/begingroup/__locales__/de.json new file mode 100644 index 000000000..68520ec2c --- /dev/null +++ b/ts/input/tex/begingroup/__locales__/de.json @@ -0,0 +1,4 @@ +{ + "IllegalGlobal": "Ungültige Verwendung von %1", + "MissingBegingroup": "Fehlende \\begingroup oder überflüssige \\endgroup" +} diff --git a/ts/input/tex/begingroup/__locales__/en.json b/ts/input/tex/begingroup/__locales__/en.json new file mode 100644 index 000000000..7e6f351a8 --- /dev/null +++ b/ts/input/tex/begingroup/__locales__/en.json @@ -0,0 +1,4 @@ +{ + "IllegalGlobal": "Invalid use of %1", + "MissingBegingroup": "Missing \\begingroup or extra \\endgroup" +} diff --git a/ts/input/tex/bussproofs/BussproofsConfiguration.ts b/ts/input/tex/bussproofs/BussproofsConfiguration.ts index d7bfc192b..d2d75d9cb 100644 --- a/ts/input/tex/bussproofs/BussproofsConfiguration.ts +++ b/ts/input/tex/bussproofs/BussproofsConfiguration.ts @@ -31,6 +31,7 @@ import { makeBsprAttributes, } from './BussproofsUtil.js'; import './BussproofsMappings.js'; +export { COMPONENT } from './__locales__/Component.js'; export const BussproofsConfiguration = Configuration.create('bussproofs', { [ConfigurationType.HANDLER]: { diff --git a/ts/input/tex/bussproofs/BussproofsItems.ts b/ts/input/tex/bussproofs/BussproofsItems.ts index a9a4f51ad..29b465475 100644 --- a/ts/input/tex/bussproofs/BussproofsItems.ts +++ b/ts/input/tex/bussproofs/BussproofsItems.ts @@ -21,12 +21,14 @@ * @author v.sorge@mathjax.org (Volker Sorge) */ -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { BaseItem, CheckType, StackItem } from '../StackItem.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; import Stack from '../Stack.js'; import * as BussproofsUtil from './BussproofsUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + export class ProofTreeItem extends BaseItem { /** * The current left label. @@ -61,7 +63,7 @@ export class ProofTreeItem extends BaseItem { return [[this.factory.create('mml', node), item], true]; } if (item.isKind('stop')) { - throw new TexError('EnvMissingEnd', 'Missing \\end{%1}', this.getName()); + texError(COMPONENT, 'EnvMissingEnd', this.getName()); } this.innerStack.Push(item); return BaseItem.fail; diff --git a/ts/input/tex/bussproofs/BussproofsMethods.ts b/ts/input/tex/bussproofs/BussproofsMethods.ts index d3f5803b2..f67176707 100644 --- a/ts/input/tex/bussproofs/BussproofsMethods.ts +++ b/ts/input/tex/bussproofs/BussproofsMethods.ts @@ -22,7 +22,7 @@ */ import { ParseMethod } from '../Types.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import TexParser from '../TexParser.js'; import { ParseUtil } from '../ParseUtil.js'; import { UnitUtil } from '../UnitUtil.js'; @@ -30,6 +30,8 @@ import { StackItem } from '../StackItem.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; import * as BussproofsUtil from './BussproofsUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Pads content of an inference rule. * @@ -136,21 +138,12 @@ function createRule( function parseFCenterLine(parser: TexParser, name: string): MmlNode { const dollar = parser.GetNext(); if (dollar !== '$') { - throw new TexError( - 'IllegalUseOfCommand', - 'Use of %1 does not match its definition.', - name - ); + texError(COMPONENT, 'IllegalUseOfCommand', name); } parser.i++; const axiom = parser.GetUpTo(name, '$'); if (!axiom.includes('\\fCenter')) { - throw new TexError( - 'MissingProofCommand', - 'Missing %1 in %2.', - '\\fCenter', - name - ); + texError(COMPONENT, 'MissingProofCommand', '\\fCenter', name); } // Check for fCenter and throw error? const [prem, conc] = axiom.split('\\fCenter'); @@ -216,10 +209,7 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { const top = parser.stack.Top(); // TODO: Label error if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } const content = paddedContent(parser, parser.GetArgument(name)); BussproofsUtil.setProperty(content, 'axiom', true); @@ -236,13 +226,10 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { Inference(parser: TexParser, name: string, n: number) { const top = parser.stack.Top(); if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } if (top.Size() < n) { - throw new TexError('BadProofTree', 'Proof tree badly specified.'); + texError(COMPONENT, 'BadProofTree'); } const rootAtTop = top.getProperty('rootAtTop') as boolean; const childCount = n === 1 && !top.Peek()[0].childNodes.length ? 0 : n; @@ -294,10 +281,7 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { const top = parser.stack.Top(); // Label error if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } const content = ParseUtil.internalMath(parser, parser.GetArgument(name), 0); const label = @@ -319,10 +303,7 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { const top = parser.stack.Top(); // Label error if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } top.setProperty('currentLine', style); if (always) { @@ -340,10 +321,7 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { RootAtTop(parser: TexParser, _name: string, where: boolean) { const top = parser.stack.Top(); if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } top.setProperty('rootAtTop', where); }, @@ -357,10 +335,7 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { AxiomF(parser: TexParser, name: string) { const top = parser.stack.Top(); if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } const line = parseFCenterLine(parser, name); BussproofsUtil.setProperty(line, 'axiom', true); @@ -385,13 +360,10 @@ const BussproofsMethods: { [key: string]: ParseMethod } = { InferenceF(parser: TexParser, name: string, n: number) { const top = parser.stack.Top(); if (top.kind !== 'proofTree') { - throw new TexError( - 'IllegalProofCommand', - 'Proof commands only allowed in prooftree environment.' - ); + texError(COMPONENT, 'IllegalProofCommand'); } if (top.Size() < n) { - throw new TexError('BadProofTree', 'Proof tree badly specified.'); + texError(COMPONENT, 'BadProofTree'); } const rootAtTop = top.getProperty('rootAtTop') as boolean; const childCount = n === 1 && !top.Peek()[0].childNodes.length ? 0 : n; diff --git a/ts/input/tex/bussproofs/__locales__/Component.ts b/ts/input/tex/bussproofs/__locales__/Component.ts new file mode 100644 index 000000000..74bcc29ad --- /dev/null +++ b/ts/input/tex/bussproofs/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/bussproofs + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/bussproofs'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/bussproofs'); diff --git a/ts/input/tex/bussproofs/__locales__/de.json b/ts/input/tex/bussproofs/__locales__/de.json new file mode 100644 index 000000000..76dfe1782 --- /dev/null +++ b/ts/input/tex/bussproofs/__locales__/de.json @@ -0,0 +1,7 @@ +{ + "BadProofTree": "Beweisbaum falsch angegeben.", + "EnvMissingEnd": "\\end{%1} fehlt", + "IllegalProofCommand": "Beweisbefehle sind nur in der prooftree-Umgebung zulässig.", + "IllegalUseOfCommand": "Die Verwendung von %1 entspricht nicht seiner Definition.", + "MissingProofCommand": "%1 fehlt in %2." +} diff --git a/ts/input/tex/bussproofs/__locales__/en.json b/ts/input/tex/bussproofs/__locales__/en.json new file mode 100644 index 000000000..8ecb97ba4 --- /dev/null +++ b/ts/input/tex/bussproofs/__locales__/en.json @@ -0,0 +1,7 @@ +{ + "BadProofTree": "Proof tree badly specified.", + "EnvMissingEnd": "Missing \\end{%1}", + "IllegalProofCommand": "Proof commands only allowed in prooftree environment.", + "IllegalUseOfCommand": "Use of %1 does not match its definition.", + "MissingProofCommand": "Missing %1 in %2." +} diff --git a/ts/input/tex/cases/CasesConfiguration.ts b/ts/input/tex/cases/CasesConfiguration.ts index 6ffdd8695..802e636d8 100644 --- a/ts/input/tex/cases/CasesConfiguration.ts +++ b/ts/input/tex/cases/CasesConfiguration.ts @@ -5,12 +5,14 @@ import { ParseResult, ParseMethod } from '../Types.js'; import { ParseUtil } from '../ParseUtil.js'; import BaseMethods from '../base/BaseMethods.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { BeginItem, EqnArrayItem } from '../base/BaseItems.js'; import { AmsTags } from '../ams/AmsConfiguration.js'; import { StackItem, CheckType } from '../StackItem.js'; import { MmlMtable } from '../../../core/MmlTree/MmlNodes/mtable.js'; import { EmpheqUtil } from '../empheq/EmpheqUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; /** * The StackItem for the numcases environment. @@ -181,10 +183,7 @@ export const CasesMethods = { // // Extra alignment tabs are not allowed in cases // - throw new TexError( - 'ExtraCasesAlignTab', - 'Extra alignment tab in text for numcase environment' - ); + texError(COMPONENT, 'ExtraCasesAlignTab'); } else if (c === '\\' && braces === 0) { // // If the macro is \cr or \\, end the search, otherwise skip the macro diff --git a/ts/input/tex/cases/__locales__/Component.ts b/ts/input/tex/cases/__locales__/Component.ts new file mode 100644 index 000000000..6eb0bedce --- /dev/null +++ b/ts/input/tex/cases/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/cases + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/cases'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/cases'); diff --git a/ts/input/tex/cases/__locales__/de.json b/ts/input/tex/cases/__locales__/de.json new file mode 100644 index 000000000..454506afd --- /dev/null +++ b/ts/input/tex/cases/__locales__/de.json @@ -0,0 +1,3 @@ +{ + "ExtraCasesAlignTab": "Zusätzlicher Ausrichtungs-Tabulator im Text für die numcase-Umgebung" +} diff --git a/ts/input/tex/cases/__locales__/en.json b/ts/input/tex/cases/__locales__/en.json new file mode 100644 index 000000000..85a34fac9 --- /dev/null +++ b/ts/input/tex/cases/__locales__/en.json @@ -0,0 +1,3 @@ +{ + "ExtraCasesAlignTab": "Extra alignment tab in text for numcase environment" +} diff --git a/ts/input/tex/color/ColorConfiguration.ts b/ts/input/tex/color/ColorConfiguration.ts index 524f5d079..4b76f3c0e 100644 --- a/ts/input/tex/color/ColorConfiguration.ts +++ b/ts/input/tex/color/ColorConfiguration.ts @@ -27,6 +27,7 @@ import { Configuration, ParserConfiguration } from '../Configuration.js'; import { ColorMethods } from './ColorMethods.js'; import { ColorModel } from './ColorUtil.js'; import { TeX } from '../../tex.js'; +export { COMPONENT } from './__locales__/Component.js'; /** * The color macros diff --git a/ts/input/tex/color/ColorUtil.ts b/ts/input/tex/color/ColorUtil.ts index be289c801..97f5e0615 100644 --- a/ts/input/tex/color/ColorUtil.ts +++ b/ts/input/tex/color/ColorUtil.ts @@ -21,9 +21,11 @@ * @author i@omardo.com (Omar Al-Ithawi) */ -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { COLORS } from './ColorConstants.js'; +import { COMPONENT } from './__locales__/Component.js'; + type ColorModelProcessor = (def: string) => string; const ColorModelProcessors: Map = new Map< string, @@ -49,7 +51,7 @@ export class ColorModel { private normalizeColor(model: string, def: string): string { if (!model || model === 'named') { if (def.match(/;/)) { - throw new TexError('BadColorValue', 'Invalid color value'); + texError(COMPONENT, 'BadColorValue'); } // Allow to define colors directly by using the CSS format e.g. `#888` return def; @@ -58,11 +60,8 @@ export class ColorModel { const modelProcessor = ColorModelProcessors.get(model); return modelProcessor(def); } - throw new TexError( - 'UndefinedColorModel', - "Color model '%1' not defined", - model - ); + + texError(COMPONENT, 'UndefinedColorModel', model); } /** @@ -100,7 +99,7 @@ export class ColorModel { return COLORS.get(name); } if (name.match(/;/)) { - throw new TexError('BadColorValue', 'Invalid color value'); + texError(COMPONENT, 'BadColorValue', 'Invalid color value'); } // Pass the color name as-is to CSS return name; @@ -132,27 +131,17 @@ ColorModelProcessors.set('rgb', function (rgb: string): string { let RGB: string = '#'; if (rgbParts.length !== 3) { - throw new TexError( - 'ModelArg1', - 'Color values for the %1 model require 3 numbers', - 'rgb' - ); + texError(COMPONENT, 'ModelArg1', 'rgb'); } for (const rgbPart of rgbParts) { if (!rgbPart.match(/^(\d+(\.\d*)?|\.\d+)$/)) { - throw new TexError('InvalidDecimalNumber', 'Invalid decimal number'); + texError(COMPONENT, 'InvalidDecimalNumber'); } const n = parseFloat(rgbPart); if (n < 0 || n > 1) { - throw new TexError( - 'ModelArg2', - 'Color values for the %1 model must be between %2 and %3', - 'rgb', - '0', - '1' - ); + texError(COMPONENT, 'ModelArg2', 'rgb', '0', '1'); } let pn = Math.floor(n * 255).toString(16); @@ -177,27 +166,17 @@ ColorModelProcessors.set('RGB', function (rgb: string): string { let RGB = '#'; if (rgbParts.length !== 3) { - throw new TexError( - 'ModelArg1', - 'Color values for the %1 model require 3 numbers', - 'RGB' - ); + texError(COMPONENT, 'ModelArg1', 'RGB'); } for (const rgbPart of rgbParts) { if (!rgbPart.match(/^\d+$/)) { - throw new TexError('InvalidNumber', 'Invalid number'); + texError(COMPONENT, 'InvalidNumber'); } const n = parseInt(rgbPart); if (n > 255) { - throw new TexError( - 'ModelArg2', - 'Color values for the %1 model must be between %2 and %3', - 'RGB', - '0', - '255' - ); + texError(COMPONENT, 'ModelArg2', 'RGB', '0', '255'); } let pn = n.toString(16); @@ -217,18 +196,12 @@ ColorModelProcessors.set('RGB', function (rgb: string): string { */ ColorModelProcessors.set('gray', function (gray: string): string { if (!gray.match(/^\s*(\d+(\.\d*)?|\.\d+)\s*$/)) { - throw new TexError('InvalidDecimalNumber', 'Invalid decimal number'); + texError(COMPONENT, 'InvalidDecimalNumber'); } const n: number = parseFloat(gray); if (n < 0 || n > 1) { - throw new TexError( - 'ModelArg2', - 'Color values for the %1 model must be between %2 and %3', - 'gray', - '0', - '1' - ); + texError(COMPONENT, 'ModelArg2', 'gray', '0', '1'); } let pn = Math.floor(n * 255).toString(16); if (pn.length < 2) { diff --git a/ts/input/tex/color/__locales__/Component.ts b/ts/input/tex/color/__locales__/Component.ts new file mode 100644 index 000000000..d79509c61 --- /dev/null +++ b/ts/input/tex/color/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/color + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/color'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/color'); diff --git a/ts/input/tex/color/__locales__/de.json b/ts/input/tex/color/__locales__/de.json new file mode 100644 index 000000000..6ea0ad493 --- /dev/null +++ b/ts/input/tex/color/__locales__/de.json @@ -0,0 +1,8 @@ +{ + "BadColorValue": "Ungültiger Farbwert", + "InvalidDecimalNumber": "Ungültige Dezimalzahl", + "InvalidNumber": "Ungültige Zahl", + "ModelArg1": "Farbwerte für das Modell %1 erfordern 3 Zahlen", + "ModelArg2": "Farbwerte für das Modell %1 müssen zwischen %2 und %3 liegen", + "UndefinedColorModel": "Farbmodell '%1' nicht definiert" +} diff --git a/ts/input/tex/color/__locales__/en.json b/ts/input/tex/color/__locales__/en.json new file mode 100644 index 000000000..574e35fc8 --- /dev/null +++ b/ts/input/tex/color/__locales__/en.json @@ -0,0 +1,8 @@ +{ + "BadColorValue": "Invalid color value", + "InvalidDecimalNumber": "Invalid decimal number", + "InvalidNumber": "Invalid number", + "ModelArg1": "Color values for the %1 model require 3 numbers", + "ModelArg2": "Color values for the %1 model must be between %2 and %3", + "UndefinedColorModel": "Color model '%1' not defined" +} diff --git a/ts/input/tex/colortbl/ColortblConfiguration.ts b/ts/input/tex/colortbl/ColortblConfiguration.ts index 67496ffcf..02143cc4c 100644 --- a/ts/input/tex/colortbl/ColortblConfiguration.ts +++ b/ts/input/tex/colortbl/ColortblConfiguration.ts @@ -30,9 +30,11 @@ import { } from '../Configuration.js'; import { CommandMap } from '../TokenMap.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { TeX } from '../../tex.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; /** * Information about table colors. @@ -130,22 +132,14 @@ function TableColor(parser: TexParser, name: string, type: keyof ColorData) { // const top = parser.stack.Top() as ColorArrayItem; if (!(top instanceof ColorArrayItem)) { - throw new TexError( - 'UnsupportedTableColor', - 'Unsupported use of %1', - parser.currentCS - ); + texError(COMPONENT, 'UnsupportedTableColor', parser.currentCS); } // // Check the position of the macro and save the color. // if (type === 'col') { if (top.table.length && top.color.col[top.row.length] !== color) { - throw new TexError( - 'ColumnColorNotTop', - '%1 must be in the top row or preamble', - name - ); + texError(COMPONENT, 'ColumnColorNotTop', name); } top.color.col[top.row.length] = color; // @@ -157,11 +151,7 @@ function TableColor(parser: TexParser, name: string, type: keyof ColorData) { } else { top.color[type] = color; if (type === 'row' && (top.Size() || top.row.length)) { - throw new TexError( - 'RowColorNotFirst', - '%1 must be at the beginning of a row', - name - ); + texError(COMPONENT, 'RowColorNotFirst', name); } } } diff --git a/ts/input/tex/colortbl/__locales__/Component.ts b/ts/input/tex/colortbl/__locales__/Component.ts new file mode 100644 index 000000000..2415e5895 --- /dev/null +++ b/ts/input/tex/colortbl/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/colortbl + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/colortbl'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/colortbl'); diff --git a/ts/input/tex/colortbl/__locales__/de.json b/ts/input/tex/colortbl/__locales__/de.json new file mode 100644 index 000000000..4021cfee9 --- /dev/null +++ b/ts/input/tex/colortbl/__locales__/de.json @@ -0,0 +1,5 @@ +{ + "ColumnColorNotTop": "%1 muss in der obersten Zeile oder im Vorspann stehen", + "RowColorNotFirst": "%1 muss am Anfang einer Zeile stehen", + "UnsupportedTableColor": "%1 wird nicht unterstützt" +} diff --git a/ts/input/tex/colortbl/__locales__/en.json b/ts/input/tex/colortbl/__locales__/en.json new file mode 100644 index 000000000..51f906ece --- /dev/null +++ b/ts/input/tex/colortbl/__locales__/en.json @@ -0,0 +1,5 @@ +{ + "ColumnColorNotTop": "%1 must be in the top row or preamble", + "RowColorNotFirst": "%1 must be at the beginning of a row", + "UnsupportedTableColor": "Unsupported use of %1" +} diff --git a/ts/input/tex/empheq/EmpheqConfiguration.ts b/ts/input/tex/empheq/EmpheqConfiguration.ts index a4f6f7fb9..613fe36c5 100644 --- a/ts/input/tex/empheq/EmpheqConfiguration.ts +++ b/ts/input/tex/empheq/EmpheqConfiguration.ts @@ -26,10 +26,12 @@ import { Configuration } from '../Configuration.js'; import { CommandMap, EnvironmentMap } from '../TokenMap.js'; import { ParseUtil } from '../ParseUtil.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { BeginItem } from '../base/BaseItems.js'; import { EmpheqUtil } from './EmpheqUtil.js'; import ParseMethods from '../ParseMethods.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; /** * The methods that implement the empheq package. @@ -62,12 +64,7 @@ export const EmpheqMethods = { .GetArgument('\\begin{' + begin.getName() + '}') .split(/=/); if (!EmpheqUtil.checkEnv(env)) { - throw new TexError( - 'EmpheqInvalidEnv', - 'Invalid environment "%1" for %2', - env, - begin.getName() - ); + texError(COMPONENT, 'EmpheqInvalidEnv', env, begin.getName()); } begin.setProperty('nestStart', true); if (opts) { diff --git a/ts/input/tex/empheq/__locales__/Component.ts b/ts/input/tex/empheq/__locales__/Component.ts new file mode 100644 index 000000000..a85fd89c3 --- /dev/null +++ b/ts/input/tex/empheq/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/empheq + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/empheq'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/empheq'); diff --git a/ts/input/tex/empheq/__locales__/de.json b/ts/input/tex/empheq/__locales__/de.json new file mode 100644 index 000000000..148636823 --- /dev/null +++ b/ts/input/tex/empheq/__locales__/de.json @@ -0,0 +1,3 @@ +{ + "EmpheqInvalidEnv": "Ungültige Umgebung \"%1\" für %2" +} diff --git a/ts/input/tex/empheq/__locales__/en.json b/ts/input/tex/empheq/__locales__/en.json new file mode 100644 index 000000000..a723204a1 --- /dev/null +++ b/ts/input/tex/empheq/__locales__/en.json @@ -0,0 +1,3 @@ +{ + "EmpheqInvalidEnv": "Invalid environment \"%1\" for %2" +} diff --git a/ts/input/tex/extpfeil/ExtpfeilConfiguration.ts b/ts/input/tex/extpfeil/ExtpfeilConfiguration.ts index aa313ba1e..b1d5b0f40 100644 --- a/ts/input/tex/extpfeil/ExtpfeilConfiguration.ts +++ b/ts/input/tex/extpfeil/ExtpfeilConfiguration.ts @@ -30,7 +30,9 @@ import { ParseMethod } from '../Types.js'; import { AmsMethods } from '../ams/AmsMethods.js'; import { NewcommandUtil } from '../newcommand/NewcommandUtil.js'; import { NewcommandConfig } from '../newcommand/NewcommandConfiguration.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; // Namespace const ExtpfeilMethods: { [key: string]: ParseMethod } = { @@ -45,25 +47,13 @@ const ExtpfeilMethods: { [key: string]: ParseMethod } = { const space = parser.GetArgument(name); const chr = parser.GetArgument(name); if (!cs.match(/^\\([a-z]+|.)$/i)) { - throw new TexError( - 'NewextarrowArg1', - 'First argument to %1 must be a control sequence name', - name - ); + texError(COMPONENT, 'NewextarrowArg1', name); } if (!space.match(/^(\d+),(\d+)$/)) { - throw new TexError( - 'NewextarrowArg2', - 'Second argument to %1 must be two integers separated by a comma', - name - ); + texError(COMPONENT, 'NewextarrowArg2', name); } if (!chr.match(/^(\d+|0x[0-9A-F]+)$/i)) { - throw new TexError( - 'NewextarrowArg3', - 'Third argument to %1 must be a unicode character number', - name - ); + texError(COMPONENT, 'NewextarrowArg3', name); } cs = cs.substring(1); const spaces = space.split(','); diff --git a/ts/input/tex/extpfeil/__locales__/Component.ts b/ts/input/tex/extpfeil/__locales__/Component.ts new file mode 100644 index 000000000..cb8ff0e6c --- /dev/null +++ b/ts/input/tex/extpfeil/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/extpfeil + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/extpfeil'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/extpfeil'); diff --git a/ts/input/tex/extpfeil/__locales__/de.json b/ts/input/tex/extpfeil/__locales__/de.json new file mode 100644 index 000000000..9fedfaacf --- /dev/null +++ b/ts/input/tex/extpfeil/__locales__/de.json @@ -0,0 +1,5 @@ +{ + "NewextarrowArg1": "Das erste Argument für %1 muss ein Name einer Steuerungssequenz sein", + "NewextarrowArg2": "Das zweite Argument für %1 muss aus zwei durch ein Komma getrennten Ganzzahlen bestehen", + "NewextarrowArg3": "Das dritte Argument für %1 muss eine Unicode-Zeichennummer sein" +} diff --git a/ts/input/tex/extpfeil/__locales__/en.json b/ts/input/tex/extpfeil/__locales__/en.json new file mode 100644 index 000000000..135821255 --- /dev/null +++ b/ts/input/tex/extpfeil/__locales__/en.json @@ -0,0 +1,5 @@ +{ + "NewextarrowArg1": "First argument to %1 must be a control sequence name", + "NewextarrowArg2": "Second argument to %1 must be two integers separated by a comma", + "NewextarrowArg3": "Third argument to %1 must be a unicode character number" +} diff --git a/ts/input/tex/html/HtmlConfiguration.ts b/ts/input/tex/html/HtmlConfiguration.ts index 918118a18..c0ea54245 100644 --- a/ts/input/tex/html/HtmlConfiguration.ts +++ b/ts/input/tex/html/HtmlConfiguration.ts @@ -25,6 +25,7 @@ import { HandlerType, ConfigurationType } from '../HandlerTypes.js'; import { Configuration } from '../Configuration.js'; import { CommandMap } from '../TokenMap.js'; import HtmlMethods from './HtmlMethods.js'; +export { COMPONENT } from './__locales__/Component.js'; new CommandMap('html_macros', { data: HtmlMethods.Data, diff --git a/ts/input/tex/html/HtmlMethods.ts b/ts/input/tex/html/HtmlMethods.ts index 9aa89f19e..10c9b38e6 100644 --- a/ts/input/tex/html/HtmlMethods.ts +++ b/ts/input/tex/html/HtmlMethods.ts @@ -26,7 +26,9 @@ import { ParseMethod } from '../Types.js'; import NodeUtil from '../NodeUtil.js'; import { ParseUtil } from '../ParseUtil.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; + +import { COMPONENT } from './__locales__/Component.js'; /** Regexp for matching non-characters as specified by {@link https://infra.spec.whatwg.org/#noncharacter}. */ const nonCharacterRegexp = @@ -63,11 +65,7 @@ const HtmlMethods: { [key: string]: ParseMethod } = { for (const key in data) { // remove illegal attribute names if (!isLegalAttributeName(key)) { - throw new TexError( - 'InvalidHTMLAttr', - 'Invalid HTML attribute: %1', - `data-${key}` - ); + texError(COMPONENT, 'InvalidHTMLAttr', `data-${key}`); } NodeUtil.setAttribute(arg, `data-${key}`, data[key]); } diff --git a/ts/input/tex/html/__locales__/Component.ts b/ts/input/tex/html/__locales__/Component.ts new file mode 100644 index 000000000..fc3eafa07 --- /dev/null +++ b/ts/input/tex/html/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/html + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/html'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/html'); diff --git a/ts/input/tex/html/__locales__/de.json b/ts/input/tex/html/__locales__/de.json new file mode 100644 index 000000000..8ee87dca3 --- /dev/null +++ b/ts/input/tex/html/__locales__/de.json @@ -0,0 +1,3 @@ +{ + "InvalidHTMLAttr": "Ungültiges HTML-Attribut: %1" +} diff --git a/ts/input/tex/html/__locales__/en.json b/ts/input/tex/html/__locales__/en.json new file mode 100644 index 000000000..1dc90b7cf --- /dev/null +++ b/ts/input/tex/html/__locales__/en.json @@ -0,0 +1,3 @@ +{ + "InvalidHTMLAttr": "Invalid HTML attribute: %1" +} diff --git a/ts/input/tex/mathtools/MathtoolsConfiguration.ts b/ts/input/tex/mathtools/MathtoolsConfiguration.ts index aaa1c5583..e01f494fa 100644 --- a/ts/input/tex/mathtools/MathtoolsConfiguration.ts +++ b/ts/input/tex/mathtools/MathtoolsConfiguration.ts @@ -42,6 +42,7 @@ import { } from './MathtoolsMethods.js'; import { MathtoolsTagFormat } from './MathtoolsTags.js'; import { MultlinedItem } from './MathtoolsItems.js'; +export { COMPONENT } from './__locales__/Component.js'; /** * Add any pre-defined paired delimiters, and subclass the configured tag format. diff --git a/ts/input/tex/mathtools/MathtoolsMethods.ts b/ts/input/tex/mathtools/MathtoolsMethods.ts index ae04f2ec1..7045bcee8 100644 --- a/ts/input/tex/mathtools/MathtoolsMethods.ts +++ b/ts/input/tex/mathtools/MathtoolsMethods.ts @@ -29,7 +29,7 @@ import { ParseMethod, ParseResult } from '../Types.js'; import { AmsMethods } from '../ams/AmsMethods.js'; import BaseMethods from '../base/BaseMethods.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import NodeUtil from '../NodeUtil.js'; import { TEXCLASS } from '../../../core/MmlTree/MmlNode.js'; import { length2em, em } from '../../../util/lengths.js'; @@ -47,6 +47,8 @@ import { PrioritizedList } from '../../../util/PrioritizedList.js'; import { MathtoolsTags } from './MathtoolsTags.js'; import { MathtoolsUtil } from './MathtoolsUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + export const LEGACYCONFIG = { [HandlerType.MACRO]: ['mathtools-legacycolonsymbols'], }; @@ -135,11 +137,7 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { width = arg; } if (width && !UnitUtil.matchDimen(width)[0]) { - throw new TexError( - 'BadWidth', - 'Width for %1 must be a dimension', - name - ); + texError(COMPONENT, 'BadWidth', name); } } parser.Push(begin); @@ -167,18 +165,10 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { HandleShove(parser: TexParser, name: string, shove: string) { const top = parser.stack.Top(); if (top.kind !== 'multline' && top.kind !== 'multlined') { - throw new TexError( - 'CommandInMultlined', - '%1 can only appear within the multline or multlined environments', - name - ); + texError(COMPONENT, 'CommandInMultlined', name); } if (top.Size()) { - throw new TexError( - 'CommandAtTheBeginingOfLine', - '%1 must come at the beginning of the line', - name - ); + texError(COMPONENT, 'CommandAtTheBeginingOfLine', name); } top.setProperty('shove', shove); const shift = parser.GetBrackets(name); @@ -468,7 +458,7 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { const box = NewcommandUtil.GetCSname(parser, name + '\\' + cs); const handlers = parser.configuration.handlers; if (handlers.get(HandlerType.MACRO).lookup(cs)) { - throw new TexError('AlreadyDefined', '%1 is already defined', '\\' + cs); + texError(COMPONENT, 'AlreadyDefined', '\\' + cs); } const handler = handlers.retrieve( NewcommandTables.NEW_COMMAND @@ -486,7 +476,7 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { ArrowBetweenLines(parser: TexParser, name: string) { const top = MathtoolsUtil.checkAlignment(parser, name); if (top.Size() || top.row.length) { - throw new TexError('BetweenLines', '%1 must be on a row by itself', name); + texError(COMPONENT, 'BetweenLines', name); } const star = parser.GetStar(); const symbol = parser.GetBrackets(name, '\\Updownarrow'); @@ -936,21 +926,17 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { NewTagForm(parser: TexParser, name: string, renew: boolean = false) { const tags = parser.tags as MathtoolsTags; if (!('mtFormats' in tags)) { - throw new TexError( - 'TagsNotMT', - '%1 can only be used with ams or mathtools tags', - name - ); + texError(COMPONENT, 'TagsNotMT', name); } const id = parser.GetArgument(name).trim(); if (!id) { - throw new TexError('InvalidTagFormID', "Tag form name can't be empty"); + texError(COMPONENT, 'InvalidTagFormID'); } const format = parser.GetBrackets(name, ''); const left = parser.GetArgument(name); const right = parser.GetArgument(name); if (!renew && tags.mtFormats.has(id)) { - throw new TexError('DuplicateTagForm', 'Duplicate tag form: %1', id); + texError(COMPONENT, 'DuplicateTagForm', id); } tags.mtFormats.set(id, [left, right, format]); parser.Push(parser.itemFactory.create('null')); @@ -965,11 +951,7 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { UseTagForm(parser: TexParser, name: string) { const tags = parser.tags as MathtoolsTags; if (!('mtFormats' in tags)) { - throw new TexError( - 'TagsNotMT', - '%1 can only be used with ams or mathtools tags', - name - ); + texError(COMPONENT, 'TagsNotMT', name); } const id = parser.GetArgument(name).trim(); if (!id) { @@ -978,7 +960,7 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { return; } if (!tags.mtFormats.has(id)) { - throw new TexError('UndefinedTagForm', 'Undefined tag form: %1', id); + texError(COMPONENT, 'UndefinedTagForm', id); } tags.mtCurrent = tags.mtFormats.get(id); parser.Push(parser.itemFactory.create('null')); @@ -993,7 +975,7 @@ export const MathtoolsMethods: { [key: string]: ParseMethod } = { SetOptions(parser: TexParser, name: string) { const options = parser.options.mathtools; if (!options['allow-mathtoolsset']) { - throw new TexError('ForbiddenMathtoolsSet', '%1 is disabled', name); + texError(COMPONENT, 'ForbiddenMathtoolsSet', name); } const allowed = {} as { [id: string]: number }; Object.keys(options).forEach((id) => { diff --git a/ts/input/tex/mathtools/MathtoolsTags.ts b/ts/input/tex/mathtools/MathtoolsTags.ts index 187273f9e..13c18a317 100644 --- a/ts/input/tex/mathtools/MathtoolsTags.ts +++ b/ts/input/tex/mathtools/MathtoolsTags.ts @@ -20,11 +20,13 @@ * @author dpvc@mathjax.org (Davide P. Cervone) */ -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { ParserConfiguration } from '../Configuration.js'; import { TeX } from '../../tex.js'; import { AbstractTags, TagsFactory } from '../Tags.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * The type for the Mathtools tags (including their data). */ @@ -88,11 +90,7 @@ export function MathtoolsTagFormat( const forms = jax.parseOptions.options.mathtools.tagforms; for (const form of Object.keys(forms)) { if (!Array.isArray(forms[form]) || forms[form].length !== 3) { - throw new TexError( - 'InvalidTagFormDef', - 'The tag form definition for "%1" should be an array of three strings', - form - ); + texError(COMPONENT, 'InvalidTagFormDef', form); } this.mtFormats.set(form, forms[form] as [string, string, string]); } diff --git a/ts/input/tex/mathtools/MathtoolsUtil.ts b/ts/input/tex/mathtools/MathtoolsUtil.ts index fef511b62..99ac61db7 100644 --- a/ts/input/tex/mathtools/MathtoolsUtil.ts +++ b/ts/input/tex/mathtools/MathtoolsUtil.ts @@ -23,7 +23,7 @@ import { EqnArrayItem } from '../base/BaseItems.js'; import { UnitUtil } from '../UnitUtil.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { lookup } from '../../../util/Options.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; import { HandlerType } from '../HandlerTypes.js'; @@ -31,6 +31,8 @@ import { NewcommandUtil } from '../newcommand/NewcommandUtil.js'; import { MathtoolsMethods } from './MathtoolsMethods.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Utility functions for the Mathtools package. */ @@ -69,11 +71,7 @@ export const MathtoolsUtil = { checkAlignment(parser: TexParser, name: string): EqnArrayItem { const top = parser.stack.Top() as EqnArrayItem; if (top.kind !== EqnArrayItem.prototype.kind) { - throw new TexError( - 'NotInAlignment', - '%1 can only be used in aligment environments', - name - ); + texError(COMPONENT, 'NotInAlignment', name); } return top; }, @@ -90,11 +88,7 @@ export const MathtoolsUtil = { */ addPairedDelims(parser: TexParser, cs: string, args: string[]) { if (parser.configuration.handlers.get(HandlerType.MACRO).contains(cs)) { - throw new TexError( - 'CommadExists', - 'Command %1 already defined', - `\\${cs}` - ); + texError(COMPONENT, 'CommadExists', `\\${cs}`); } NewcommandUtil.addMacro( parser, @@ -131,7 +125,7 @@ export const MathtoolsUtil = { plusOrMinus(name: string, n: string): string { n = n.trim(); if (!n.match(/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/)) { - throw new TexError('NotANumber', 'Argument to %1 is not a number', name); + texError(COMPONENT, 'NotANumber', name); } return n.match(/^[-+]/) ? n : '+' + n; }, diff --git a/ts/input/tex/mathtools/__locales__/Component.ts b/ts/input/tex/mathtools/__locales__/Component.ts new file mode 100644 index 000000000..071d87daf --- /dev/null +++ b/ts/input/tex/mathtools/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/mathtools + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/mathtools'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/mathtools'); diff --git a/ts/input/tex/mathtools/__locales__/de.json b/ts/input/tex/mathtools/__locales__/de.json new file mode 100644 index 000000000..dec337736 --- /dev/null +++ b/ts/input/tex/mathtools/__locales__/de.json @@ -0,0 +1,16 @@ +{ + "AlreadyDefined": "%1 ist bereits definiert", + "BadWidth": "Die Breite für %1 muss eine Abmessung sein", + "BetweenLines": "%1 muss in einer eigenen Zeile stehen", + "BefehlExistiert": "Befehl %1 ist bereits definiert", + "BefehlAmZeilenanfang": "%1 muss am Zeilenanfang stehen", + "BefehlInMultlined": "%1 darf nur innerhalb der Umgebungen multline oder multlined vorkommen", + "DoppelteTagForm": "Doppelte Tag-Form: %1", + "ForbiddenMathtoolsSet": "%1 ist deaktiviert", + "InvalidTagFormDef": "Die Tag-Form-Definition für \"%1\" sollte ein Array aus drei Strings sein", + "InvalidTagFormID": "Der Name der Tag-Form darf nicht leer sein", + "NotANumber": "Das Argument für %1 ist keine Zahl", + "NotInAlignment": "%1 kann nur in Ausrichtungsumgebungen verwendet werden", + "TagsNotMT": "%1 kann nur mit ams- oder mathtools-Tags verwendet werden", + "UndefinedTagForm": "Undefinierte Tag-Form: %1" +} diff --git a/ts/input/tex/mathtools/__locales__/en.json b/ts/input/tex/mathtools/__locales__/en.json new file mode 100644 index 000000000..f43fbbd74 --- /dev/null +++ b/ts/input/tex/mathtools/__locales__/en.json @@ -0,0 +1,16 @@ +{ + "AlreadyDefined": "%1 is already defined", + "BadWidth": "Width for %1 must be a dimension", + "BetweenLines": "%1 must be on a row by itself", + "CommadExists": "Command %1 already defined", + "CommandAtTheBeginingOfLine": "%1 must come at the beginning of the line", + "CommandInMultlined": "%1 can only appear within the multline or multlined environments", + "DuplicateTagForm": "Duplicate tag form: %1", + "ForbiddenMathtoolsSet": "%1 is disabled", + "InvalidTagFormDef": "The tag form definition for \"%1\" should be an array of three strings", + "InvalidTagFormID": "Tag form name can't be empty", + "NotANumber": "Argument to %1 is not a number", + "NotInAlignment": "%1 can only be used in alignment environments", + "TagsNotMT": "%1 can only be used with ams or mathtools tags", + "UndefinedTagForm": "Undefined tag form: %1" +} diff --git a/ts/input/tex/mhchem/MhchemConfiguration.ts b/ts/input/tex/mhchem/MhchemConfiguration.ts index 05c966955..3bef93c15 100644 --- a/ts/input/tex/mhchem/MhchemConfiguration.ts +++ b/ts/input/tex/mhchem/MhchemConfiguration.ts @@ -26,12 +26,14 @@ import { Configuration } from '../Configuration.js'; import { CommandMap, CharacterMap } from '../TokenMap.js'; import { Token } from '../Token.js'; import { ParseMethod } from '../Types.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import TexParser from '../TexParser.js'; import BaseMethods from '../base/BaseMethods.js'; import { AmsMethods } from '../ams/AmsMethods.js'; import { mhchemParser } from '#mhchem/mhchemParser.js'; import { TEXCLASS } from '../../../core/MmlTree/MmlNode.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; /** * Creates mo token elements with the proper attributes @@ -104,8 +106,20 @@ export const MhchemMethods: { [key: string]: ParseMethod } = { for (const [name, pattern] of MhchemReplacements.entries()) { tex = tex.replace(pattern, name as string); } - } catch (err) { - throw new TexError(err[0], err[1]); + } catch ([id, msg]) { + if (id === 'MhchemErrorBond') { + const match = msg.match(/\((.*)\)/); + texError(COMPONENT, id, match[1]); + } + if (id.startsWith('MhchemBug')) { + const match = msg.match(/mhchem bug (.).*\((.*)\)/); + if (match) { + texError(COMPONENT, 'MhchemBug2', match[1], match[2]); + } else { + texError(COMPONENT, 'MhchemBug', id.charAt(9)); + } + } + texError('input/tex', id); } parser.string = tex + parser.string.substring(parser.i); parser.i = 0; diff --git a/ts/input/tex/mhchem/__locales__/Component.ts b/ts/input/tex/mhchem/__locales__/Component.ts new file mode 100644 index 000000000..66593a0d3 --- /dev/null +++ b/ts/input/tex/mhchem/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/mhchem + * + * @author dpvc@mathjax.org (Davide P. Cervone) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/mhchem'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/mhchem'); diff --git a/ts/input/tex/mhchem/__locales__/de.json b/ts/input/tex/mhchem/__locales__/de.json new file mode 100644 index 000000000..bdf85b8e0 --- /dev/null +++ b/ts/input/tex/mhchem/__locales__/de.json @@ -0,0 +1,5 @@ +{ + "MhchemBug": "mhchem-Fehler %1. Bitte melden.", + "MhchemBug2": "mhchem-Fehler %1. Bitte melden. (%2)", + "MhchemErrorBond": "mhchem-Fehler. Unbekannter Bindungstyp (%1)" +} diff --git a/ts/input/tex/mhchem/__locales__/en.json b/ts/input/tex/mhchem/__locales__/en.json new file mode 100644 index 000000000..1fcf478e7 --- /dev/null +++ b/ts/input/tex/mhchem/__locales__/en.json @@ -0,0 +1,5 @@ +{ + "MhchemBug": "mhchem bug %1. Please report.", + "MhchemBug2": "mhchem bug %1. Please report. (%2)", + "MhchemErrorBond": "mhchem Error. Unknown bond type (%1)" +} diff --git a/ts/input/tex/newcommand/NewcommandConfiguration.ts b/ts/input/tex/newcommand/NewcommandConfiguration.ts index 3b1048d32..9530712dc 100644 --- a/ts/input/tex/newcommand/NewcommandConfiguration.ts +++ b/ts/input/tex/newcommand/NewcommandConfiguration.ts @@ -29,6 +29,7 @@ import { NewcommandTables, NewcommandPriority } from './NewcommandUtil.js'; import './NewcommandMappings.js'; import ParseMethods from '../ParseMethods.js'; import * as sm from '../TokenMap.js'; +export { COMPONENT } from './__locales__/Component.js'; /** * Initialize the newcommand maps for delimiters, commands, and environments, diff --git a/ts/input/tex/newcommand/NewcommandItems.ts b/ts/input/tex/newcommand/NewcommandItems.ts index dac92392b..11d6870a3 100644 --- a/ts/input/tex/newcommand/NewcommandItems.ts +++ b/ts/input/tex/newcommand/NewcommandItems.ts @@ -21,9 +21,11 @@ * @author v.sorge@mathjax.org (Volker Sorge) */ -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { CheckType, BaseItem, StackItem } from '../StackItem.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Opening Item dealing with definitions of new environments. It's pushed onto * the stack whenever a user defined environment is encountered and remains @@ -52,18 +54,13 @@ export class BeginEnvItem extends BaseItem { // @test Newenvironment Empty, Newenvironment Align if (item.getName() !== this.getName()) { // @test (missing) \newenvironment{env}{aa}{bb}\begin{env}cc\end{equation} - throw new TexError( - 'EnvBadEnd', - '\\begin{%1} ended with \\end{%2}', - this.getName(), - item.getName() - ); + texError(COMPONENT, 'EnvBadEnd', this.getName(), item.getName()); } return [[this.factory.create('mml', this.toMml())], true]; } if (item.isKind('stop')) { // @test (missing) \newenvironment{env}{aa}{bb}\begin{env}cc - throw new TexError('EnvMissingEnd', 'Missing \\end{%1}', this.getName()); + texError(COMPONENT, 'EnvMissingEnd', this.getName()); } // @test Newenvironment Empty, Newenvironment Align return super.checkItem(item); diff --git a/ts/input/tex/newcommand/NewcommandMethods.ts b/ts/input/tex/newcommand/NewcommandMethods.ts index 545c0f6d9..d43ce9c8f 100644 --- a/ts/input/tex/newcommand/NewcommandMethods.ts +++ b/ts/input/tex/newcommand/NewcommandMethods.ts @@ -23,7 +23,7 @@ import { HandlerType } from '../HandlerTypes.js'; import { ParseResult, ParseMethod } from '../Types.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import TexParser from '../TexParser.js'; import * as sm from '../TokenMap.js'; import { Token } from '../Token.js'; @@ -33,6 +33,8 @@ import { UnitUtil } from '../UnitUtil.js'; import { StackItem } from '../StackItem.js'; import { NewcommandUtil } from './NewcommandUtil.js'; +import { COMPONENT } from './__locales__/Component.js'; + // Namespace const NewcommandMethods: { [key: string]: ParseMethod } = { /** @@ -215,11 +217,7 @@ const NewcommandMethods: { [key: string]: ParseMethod } = { parser.GetNext(); if (params[0] && !NewcommandUtil.MatchParam(parser, params[0])) { // @test Missing Arguments - throw new TexError( - 'MismatchUseDef', - "Use of %1 doesn't match its definition", - name - ); + texError(COMPONENT, 'MismatchUseDef', name); } if (argCount) { for (let i = 0; i < argCount; i++) { diff --git a/ts/input/tex/newcommand/NewcommandUtil.ts b/ts/input/tex/newcommand/NewcommandUtil.ts index c1ade5834..bb7ff956d 100644 --- a/ts/input/tex/newcommand/NewcommandUtil.ts +++ b/ts/input/tex/newcommand/NewcommandUtil.ts @@ -24,12 +24,14 @@ import { HandlerType } from '../HandlerTypes.js'; import { SubHandler } from '../MapHandler.js'; import { UnitUtil } from '../UnitUtil.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import TexParser from '../TexParser.js'; import { Macro, Token } from '../Token.js'; import { Args, Attributes, ParseMethod } from '../Types.js'; import * as tm from '../TokenMap.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Naming constants for the extension mappings. */ @@ -57,11 +59,7 @@ export const NewcommandUtil = { const c = parser.GetNext(); if (c !== '\\') { // @test No CS - throw new TexError( - 'MissingCS', - '%1 must be followed by a control sequence', - cmd - ); + texError(COMPONENT, 'MissingCS', cmd); } const cs = UnitUtil.trimSpaces(parser.GetArgument(cmd)).substring(1); this.checkProtectedMacros(parser, cs); @@ -83,11 +81,7 @@ export const NewcommandUtil = { } if (!cs.match(/^(.|[a-z]+)$/i)) { // @test Illegal CS - throw new TexError( - 'IllegalControlSequenceName', - 'Illegal control sequence name for %1', - name - ); + texError(COMPONENT, 'IllegalControlSequenceName', name); } this.checkProtectedMacros(parser, cs); return cs; @@ -108,11 +102,7 @@ export const NewcommandUtil = { n = UnitUtil.trimSpaces(n); if (!n.match(/^[0-9]+$/)) { // @test Illegal Argument Number - throw new TexError( - 'IllegalParamNumber', - 'Illegal number of parameters specified in %1', - name - ); + texError(COMPONENT, 'IllegalParamNumber', name); } } return n; @@ -145,19 +135,11 @@ export const NewcommandUtil = { c = parser.string.charAt(++parser.i); if (!c.match(/^[1-9]$/)) { // @test Illegal Hash - throw new TexError( - 'CantUseHash2', - 'Illegal use of # in template for %1', - cs - ); + texError(COMPONENT, 'CantUseHash2', cs); } if (parseInt(c) !== ++n) { // @test No Sequence - throw new TexError( - 'SequentialParam', - 'Parameters for %1 must be numbered sequentially', - cs - ); + texError(COMPONENT, 'SequentialParam', cs); } i = parser.i + 1; } else if (c === '{') { @@ -184,11 +166,7 @@ export const NewcommandUtil = { parser.i++; } // @test No Replacement - throw new TexError( - 'MissingReplacementString', - 'Missing replacement string for definition of %1', - cmd - ); + texError(COMPONENT, 'MissingReplacementString', cmd); }, /** @@ -242,7 +220,7 @@ export const NewcommandUtil = { } } // @test Runaway Argument - throw new TexError('RunawayArgument', 'Runaway argument for %1?', name); + texError(COMPONENT, 'RunawayArgument', name); }, /** @@ -306,11 +284,7 @@ export const NewcommandUtil = { */ checkProtectedMacros(parser: TexParser, cs: string) { if (parser.options.protectedMacros?.includes(cs)) { - throw new TexError( - 'ProtectedMacro', - "The control sequence %1 can't be redefined", - `\\${cs}` - ); + texError(COMPONENT, 'ProtectedMacro', `\\${cs}`); } }, diff --git a/ts/input/tex/newcommand/__locales__/Component.ts b/ts/input/tex/newcommand/__locales__/Component.ts new file mode 100644 index 000000000..a7ad030e9 --- /dev/null +++ b/ts/input/tex/newcommand/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/newcommand + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/newcommand'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/newcommand'); diff --git a/ts/input/tex/newcommand/__locales__/de.json b/ts/input/tex/newcommand/__locales__/de.json new file mode 100644 index 000000000..bb1ee2681 --- /dev/null +++ b/ts/input/tex/newcommand/__locales__/de.json @@ -0,0 +1,13 @@ +{ + "CantUseHash2": "Unzulässige Verwendung von # in der Vorlage für %1", + "EnvBadEnd": "\\begin{%1} endete mit \\end{%2}", + "EnvMissingEnd": "Fehlendes \\end{%1}", + "IllegalControlSequenceName": "Ungültiger Name der Steuerungssequenz für %1", + "IllegalParamNumber": "Ungültige Anzahl von Parametern in %1 angegeben", + "MismatchUseDef": "Die Verwendung von %1 stimmt nicht mit der Definition überein", + "MissingCS": "Auf %1 muss eine Steuerungssequenz folgen", + "MissingReplacementString": "Ersetzungszeichenfolge für die Definition von %1 fehlt", + "ProtectedMacro": "Die Steuerungssequenz %1 kann nicht neu definiert werden", + "RunawayArgument": "Fehlerhaftes Argument für %1?", + "SequentialParam": "Parameter für %1 müssen fortlaufend nummeriert sein" +} diff --git a/ts/input/tex/newcommand/__locales__/en.json b/ts/input/tex/newcommand/__locales__/en.json new file mode 100644 index 000000000..c6b2fa866 --- /dev/null +++ b/ts/input/tex/newcommand/__locales__/en.json @@ -0,0 +1,13 @@ +{ + "CantUseHash2": "Illegal use of # in template for %1", + "EnvBadEnd": "\\begin{%1} ended with \\end{%2}", + "EnvMissingEnd": "Missing \\end{%1}", + "IllegalControlSequenceName": "Illegal control sequence name for %1", + "IllegalParamNumber": "Illegal number of parameters specified in %1", + "MismatchUseDef": "Use of %1 doesn't match its definition", + "MissingCS": "%1 must be followed by a control sequence", + "MissingReplacementString": "Missing replacement string for definition of %1", + "ProtectedMacro": "The control sequence %1 can't be redefined", + "RunawayArgument": "Runaway argument for %1?", + "SequentialParam": "Parameters for %1 must be numbered sequentially" +} diff --git a/ts/input/tex/physics/PhysicsConfiguration.ts b/ts/input/tex/physics/PhysicsConfiguration.ts index e11e6767c..dde9672e6 100644 --- a/ts/input/tex/physics/PhysicsConfiguration.ts +++ b/ts/input/tex/physics/PhysicsConfiguration.ts @@ -25,6 +25,7 @@ import { HandlerType, ConfigurationType } from '../HandlerTypes.js'; import { Configuration } from '../Configuration.js'; import { AutoOpen } from './PhysicsItems.js'; import './PhysicsMappings.js'; +export { COMPONENT } from './__locales__/Component.js'; export const PhysicsConfiguration = Configuration.create('physics', { [ConfigurationType.HANDLER]: { diff --git a/ts/input/tex/physics/PhysicsItems.ts b/ts/input/tex/physics/PhysicsItems.ts index 3fab3799c..ede292204 100644 --- a/ts/input/tex/physics/PhysicsItems.ts +++ b/ts/input/tex/physics/PhysicsItems.ts @@ -26,13 +26,14 @@ import { ParseUtil } from '../ParseUtil.js'; import NodeUtil from '../NodeUtil.js'; import TexParser from '../TexParser.js'; import { AbstractMmlTokenNode } from '../../../core/MmlTree/MmlNode.js'; +import { COMPONENT } from './__locales__/Component.js'; export class AutoOpen extends BaseItem { /** * @override */ protected static errors = Object.assign(Object.create(BaseItem.errors), { - stop: ['ExtraOrMissingDelims', 'Extra open or missing close delimiter'], + stop: [COMPONENT, 'ExtraOrMissingDelims'], }); /** diff --git a/ts/input/tex/physics/PhysicsMethods.ts b/ts/input/tex/physics/PhysicsMethods.ts index f53c10041..2c5216a0b 100644 --- a/ts/input/tex/physics/PhysicsMethods.ts +++ b/ts/input/tex/physics/PhysicsMethods.ts @@ -25,7 +25,7 @@ import { HandlerType } from '../HandlerTypes.js'; import { ParseMethod, ParseResult } from '../Types.js'; import BaseMethods from '../base/BaseMethods.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { TEXCLASS, MmlNode } from '../../../core/MmlTree/MmlNode.js'; import { ParseUtil } from '../ParseUtil.js'; import NodeUtil from '../NodeUtil.js'; @@ -33,6 +33,9 @@ import { NodeFactory } from '../NodeFactory.js'; import { Macro } from '../Token.js'; import { AutoOpen } from './PhysicsItems.js'; +import { COMPONENT as TEX_COMPONENT } from '../__locales__/Component.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * Pairs open and closed fences. * @@ -243,11 +246,7 @@ const PhysicsMethods: { [key: string]: ParseMethod } = { } let right = pairs[next]; if (arg && next !== '{') { - throw new TexError( - 'MissingArgFor', - 'Missing argument for %1', - parser.currentCS - ); + texError(TEX_COMPONENT, 'MissingArgFor', parser.currentCS); } if (!right) { const empty = parser.create('node', 'mrow'); @@ -345,20 +344,12 @@ const PhysicsMethods: { [key: string]: ParseMethod } = { big = parser.GetCS(); if (!big.match(biggs)) { // Actually a commutator error arg1 error. - throw new TexError( - 'MissingArgFor', - 'Missing argument for %1', - parser.currentCS - ); + texError(TEX_COMPONENT, 'MissingArgFor', parser.currentCS); } next = parser.GetNext(); } if (next !== '{') { - throw new TexError( - 'MissingArgFor', - 'Missing argument for %1', - parser.currentCS - ); + texError(TEX_COMPONENT, 'MissingArgFor', parser.currentCS); } const arg1 = parser.GetArgument(name); const arg2 = parser.GetArgument(name); @@ -888,7 +879,7 @@ const PhysicsMethods: { [key: string]: ParseMethod } = { const arg = parser.GetArgument(name); const size = parseInt(arg, 10); if (isNaN(size)) { - throw new TexError('InvalidNumber', 'Invalid number'); + texError(COMPONENT, 'InvalidNumber'); } if (size <= 1) { parser.string = '1' + parser.string.slice(parser.i); @@ -925,7 +916,7 @@ const PhysicsMethods: { [key: string]: ParseMethod } = { m.toString() !== arg3 || n.toString() !== arg2 ) { - throw new TexError('InvalidNumber', 'Invalid number'); + texError(COMPONENT, 'InvalidNumber'); } n = n < 1 ? 1 : n; m = m < 1 ? 1 : m; diff --git a/ts/input/tex/physics/__locales__/Component.ts b/ts/input/tex/physics/__locales__/Component.ts new file mode 100644 index 000000000..c5c391e2f --- /dev/null +++ b/ts/input/tex/physics/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/physics + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/physics'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/physics'); diff --git a/ts/input/tex/physics/__locales__/de.json b/ts/input/tex/physics/__locales__/de.json new file mode 100644 index 000000000..46ac00177 --- /dev/null +++ b/ts/input/tex/physics/__locales__/de.json @@ -0,0 +1,4 @@ +{ + "InvalidNumber": "Ungültige Zahl", + "ExtraOrMissingDelims": "Extra open or missing close delimiter" +} diff --git a/ts/input/tex/physics/__locales__/en.json b/ts/input/tex/physics/__locales__/en.json new file mode 100644 index 000000000..0b659d22f --- /dev/null +++ b/ts/input/tex/physics/__locales__/en.json @@ -0,0 +1,4 @@ +{ + "InvalidNumber": "Invalid number", + "ExtraOrMissingDelims": "Extra open or missing close delimiter" +} diff --git a/ts/input/tex/require/RequireConfiguration.ts b/ts/input/tex/require/RequireConfiguration.ts index 1a7408fba..5f7bdf785 100644 --- a/ts/input/tex/require/RequireConfiguration.ts +++ b/ts/input/tex/require/RequireConfiguration.ts @@ -30,7 +30,7 @@ import { import TexParser from '../TexParser.js'; import { CommandMap } from '../TokenMap.js'; import { ParseMethod } from '../Types.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { TeX } from '../../tex.js'; import { MathJax } from '../../../components/startup.js'; @@ -40,6 +40,9 @@ import { mathjax } from '../../../mathjax.js'; import { expandable } from '../../../util/Options.js'; import { MenuMathDocument } from '../../../ui/menu/MenuHandler.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; + /** * The MathJax configuration block (for looking up user-defined package options) */ @@ -168,18 +171,14 @@ export function RequireLoad(parser: TexParser, name: string) { ? allow[name] : options.defaultAllow; if (!allowed) { - throw new TexError( - 'BadRequire', - 'Extension "%1" is not allowed to be loaded', - extension - ); + texError(COMPONENT, 'BadRequire', extension); } const data = Package.packages.get(extension); if (!data) { mathjax.retryAfter(Loader.load(extension).catch((_) => {})); } if (data.hasFailed) { - throw new TexError('RequireFail', 'Extension "%1" failed to load', name); + texError(COMPONENT, 'RequireFail', name); } const require = LOADERCONFIG[extension]?.rendererExtensions; const menu = (MathJax.startup.document as MenuMathDocument)?.menu; @@ -229,11 +228,7 @@ export const RequireMethods: { [key: string]: ParseMethod } = { Require(parser: TexParser, name: string) { const required = parser.GetArgument(name); if (required.match(/[^_a-zA-Z0-9]/) || required === '') { - throw new TexError( - 'BadPackageName', - 'Argument for %1 is not a valid package name', - name - ); + texError(COMPONENT, 'BadPackageName', name); } RequireLoad(parser, required); parser.Push(parser.itemFactory.create('null')); diff --git a/ts/input/tex/require/__locales__/Component.ts b/ts/input/tex/require/__locales__/Component.ts new file mode 100644 index 000000000..5519890a3 --- /dev/null +++ b/ts/input/tex/require/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/require + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/require'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/require'); diff --git a/ts/input/tex/require/__locales__/de.json b/ts/input/tex/require/__locales__/de.json new file mode 100644 index 000000000..017eff802 --- /dev/null +++ b/ts/input/tex/require/__locales__/de.json @@ -0,0 +1,5 @@ +{ + "BadPackageName": "Das Argument für %1 ist kein gültiger Paketname", + "BadRequire": "Die Erweiterung \"%1\" darf nicht geladen werden", + "RequireFail": "Die Erweiterung \"%1\" konnte nicht geladen werden" +} diff --git a/ts/input/tex/require/__locales__/en.json b/ts/input/tex/require/__locales__/en.json new file mode 100644 index 000000000..590ebf565 --- /dev/null +++ b/ts/input/tex/require/__locales__/en.json @@ -0,0 +1,5 @@ +{ + "BadPackageName": "Argument for %1 is not a valid package name", + "BadRequire": "Extension \"%1\" is not allowed to be loaded", + "RequireFail": "Extension \"%1\" failed to load" +} diff --git a/ts/input/tex/setoptions/SetOptionsConfiguration.ts b/ts/input/tex/setoptions/SetOptionsConfiguration.ts index 33e4e2f71..b33595ed1 100644 --- a/ts/input/tex/setoptions/SetOptionsConfiguration.ts +++ b/ts/input/tex/setoptions/SetOptionsConfiguration.ts @@ -30,12 +30,14 @@ import { import { TeX } from '../../tex.js'; import TexParser from '../TexParser.js'; import { CommandMap } from '../TokenMap.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { ParseUtil } from '../ParseUtil.js'; import { Macro } from '../Token.js'; import BaseMethods from '../base/BaseMethods.js'; import { expandable, isObject } from '../../../util/Options.js'; import { PrioritizedList } from '../../../util/PrioritizedList.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; export const SetOptionsUtil = { /** @@ -47,7 +49,7 @@ export const SetOptionsUtil = { */ filterPackage(parser: TexParser, extension: string): boolean { if (extension !== 'tex' && !ConfigurationHandler.get(extension)) { - throw new TexError('NotAPackage', 'Not a defined package: %1', extension); + texError(COMPONENT, 'NotAPackage', extension); } const config = parser.options.setoptions; const options = config.allowOptions[extension]; @@ -55,11 +57,7 @@ export const SetOptionsUtil = { (options === undefined && !config.allowPackageDefault) || options === false ) { - throw new TexError( - 'PackageNotSettable', - 'Options can\'t be set for package "%1"', - extension - ); + texError(COMPONENT, 'PackageNotSettable', extension); } return true; }, @@ -82,35 +80,17 @@ export const SetOptionsUtil = { : null; if (allow === false || (allow === null && !config.allowOptionsDefault)) { if (isTex) { - throw new TexError( - 'TeXOptionNotSettable', - 'Option "%1" is not allowed to be set', - option - ); + texError(COMPONENT, 'TeXOptionNotSettable', option); } else { - throw new TexError( - 'OptionNotSettable', - 'Option "%1" is not allowed to be set for package %2', - option, - extension - ); + texError(COMPONENT, 'OptionNotSettable', option, extension); } } const extOptions = isTex ? parser.options : parser.options[extension]; if (!extOptions || !Object.hasOwn(extOptions, option)) { if (isTex) { - throw new TexError( - 'InvalidTexOption', - 'Invalid TeX option "%1"', - option - ); + texError(COMPONENT, 'InvalidTexOption', option); } else { - throw new TexError( - 'InvalidOptionKey', - 'Invalid option "%1" for package "%2"', - option, - extension - ); + texError(COMPONENT, 'InvalidOptionKey', option, extension); } } return true; diff --git a/ts/input/tex/setoptions/__locales__/Component.ts b/ts/input/tex/setoptions/__locales__/Component.ts new file mode 100644 index 000000000..35630731c --- /dev/null +++ b/ts/input/tex/setoptions/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/setoptions + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/setoptions'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/setoptions'); diff --git a/ts/input/tex/setoptions/__locales__/de.json b/ts/input/tex/setoptions/__locales__/de.json new file mode 100644 index 000000000..46953f849 --- /dev/null +++ b/ts/input/tex/setoptions/__locales__/de.json @@ -0,0 +1,8 @@ +{ + "InvalidOptionKey": "Ungültige Option \"%1\" für das Paket \"%2\"", + "InvalidTexOption": "Ungültige TeX-Option \"%1\"", + "NotAPackage": "Kein definiertes Paket: %1", + "OptionNotSettable": "Die Option \"%1\" darf für das Paket %2 nicht gesetzt werden", + "PackageNotSettable": "Für das Paket \"%1\" können keine Optionen gesetzt werden", + "TeXOptionNotSettable": "Die Option \"%1\" darf nicht gesetzt werden" +} diff --git a/ts/input/tex/setoptions/__locales__/en.json b/ts/input/tex/setoptions/__locales__/en.json new file mode 100644 index 000000000..2d56e407f --- /dev/null +++ b/ts/input/tex/setoptions/__locales__/en.json @@ -0,0 +1,8 @@ +{ + "InvalidOptionKey": "Invalid option \"%1\" for package \"%2\"", + "InvalidTexOption": "Invalid TeX option \"%1\"", + "NotAPackage": "Not a defined package: %1", + "OptionNotSettable": "Option \"%1\" is not allowed to be set for package %2", + "PackageNotSettable": "Options can't be set for package \"%1\"", + "TeXOptionNotSettable": "Option \"%1\" is not allowed to be set" +} diff --git a/ts/input/tex/texhtml/TexHtmlConfiguration.ts b/ts/input/tex/texhtml/TexHtmlConfiguration.ts index 58d157ea8..adb05ce32 100644 --- a/ts/input/tex/texhtml/TexHtmlConfiguration.ts +++ b/ts/input/tex/texhtml/TexHtmlConfiguration.ts @@ -27,11 +27,12 @@ import TexParser from '../TexParser.js'; import { MacroMap } from '../TokenMap.js'; import { ParseMethod } from '../Types.js'; import ParseOptions from '../ParseOptions.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { HTMLDocument } from '../../../handlers/html/HTMLDocument.js'; import { HtmlNode } from '../../../core/MmlTree/MmlNodes/HtmlNode.js'; import { HTMLDomStrings } from '../../../handlers/html/HTMLDomStrings.js'; import { DOMAdaptor } from '../../../core/DOMAdaptor.js'; +import { COMPONENT } from '../__locales__/Component.js'; export const HtmlNodeMethods: { [key: string]: ParseMethod } = { /** @@ -59,12 +60,7 @@ export const HtmlNodeMethods: { [key: string]: ParseMethod } = { const end = (match[1] ? `` : '') + ''; const i = parser.string.slice(parser.i).indexOf(end); if (i < 0) { - throw new TexError( - 'TokenNotFoundForCommand', - 'Could not find %1 for %2', - end, - '<' + match[0] - ); + texError(COMPONENT, 'TokenNotFoundForCommand', end, '<' + match[0]); } const html = parser.string.substring(parser.i, parser.i + i).trim(); parser.i += i + 11 + (match[1] ? 3 + match[1].length : 0); diff --git a/ts/input/tex/textmacros/TextMacrosConfiguration.ts b/ts/input/tex/textmacros/TextMacrosConfiguration.ts index 981362af7..712daeec5 100644 --- a/ts/input/tex/textmacros/TextMacrosConfiguration.ts +++ b/ts/input/tex/textmacros/TextMacrosConfiguration.ts @@ -31,6 +31,8 @@ import { StartItem, StopItem, MmlItem, StyleItem } from '../base/BaseItems.js'; import { TextParser } from './TextParser.js'; import { TextMacrosMethods } from './TextMacrosMethods.js'; import { MmlNode } from '../../../core/MmlTree/MmlNode.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; import './TextMacrosMappings.js'; @@ -62,11 +64,7 @@ export const TextBaseConfiguration = Configuration.create('text-base', { const texParser = parser.texParser; const macro = texParser.lookup(HandlerType.MACRO, name); if (macro && macro._func !== TextMacrosMethods.Macro) { - parser.Error( - 'MathMacro', - '%1 is only supported in math mode', - '\\' + name - ); + parser.Error(COMPONENT, 'MathMacro', '\\' + name); } texParser.parse(HandlerType.MACRO, [parser, name]); }, diff --git a/ts/input/tex/textmacros/TextMacrosMethods.ts b/ts/input/tex/textmacros/TextMacrosMethods.ts index f26a3bd0d..102c330d4 100644 --- a/ts/input/tex/textmacros/TextMacrosMethods.ts +++ b/ts/input/tex/textmacros/TextMacrosMethods.ts @@ -27,6 +27,9 @@ import { retryAfter } from '../../../util/Retries.js'; import { TextParser } from './TextParser.js'; import BaseMethods from '../base/BaseMethods.js'; +import { COMPONENT as TEX_COMPONENT } from '../__locales__/Component.js'; +import { COMPONENT } from './__locales__/Component.js'; + /** * The methods used to implement the text-mode macros */ @@ -89,16 +92,13 @@ export const TextMacrosMethods = { case '}': if (braces === 0) { - parser.Error( - 'ExtraCloseMissingOpen', - 'Extra close brace or missing open brace' - ); + parser.Error(TEX_COMPONENT, 'ExtraCloseMissingOpen'); } braces--; break; } } - parser.Error('MathNotTerminated', 'Math mode is not properly terminated'); + parser.Error(TEX_COMPONENT, 'MathNotTerminated'); }, /** @@ -106,7 +106,7 @@ export const TextMacrosMethods = { * @param {string} c The character that called this function */ MathModeOnly(parser: TextParser, c: string) { - parser.Error('MathModeOnly', "'%1' allowed only in math mode", c); + parser.Error(COMPONENT, 'MathModeOnly', c); }, /** @@ -114,7 +114,7 @@ export const TextMacrosMethods = { * @param {string} c The character that called this function */ Misplaced(parser: TextParser, c: string) { - parser.Error('Misplaced', "Misplaced '%1'", c); + parser.Error(TEX_COMPONENT, 'Misplaced', c); }, /** @@ -143,10 +143,7 @@ export const TextMacrosMethods = { parser.saveText(); parser.stack.env = parser.envStack.pop(); } else { - parser.Error( - 'ExtraCloseMissingOpen', - 'Extra close brace or missing open brace' - ); + parser.Error(TEX_COMPONENT, 'ExtraCloseMissingOpen'); } }, diff --git a/ts/input/tex/textmacros/TextParser.ts b/ts/input/tex/textmacros/TextParser.ts index b7031d3f5..e364fe736 100644 --- a/ts/input/tex/textmacros/TextParser.ts +++ b/ts/input/tex/textmacros/TextParser.ts @@ -22,7 +22,7 @@ */ import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import ParseOptions from '../ParseOptions.js'; import { ParseUtil } from '../ParseUtil.js'; import { StackItem } from '../StackItem.js'; @@ -232,11 +232,11 @@ export class TextParser extends TexParser { /** * Throw an error * - * @param {string} id The id for the message string - * @param {string} message The English version of the message - * @param {string[]} args Any substitution args for the message + * @param {string} component The locale component + * @param {string} id The id for the message string + * @param {string[]} args Any substitution args for the message */ - public Error(id: string, message: string, ...args: string[]) { - throw new TexError(id, message, ...args); + public Error(component: string, id: string, ...args: string[]) { + texError(component, id, ...args); } } diff --git a/ts/input/tex/textmacros/__locales__/Component.ts b/ts/input/tex/textmacros/__locales__/Component.ts new file mode 100644 index 000000000..cfce9e2b0 --- /dev/null +++ b/ts/input/tex/textmacros/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/textmacros + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/textmacros'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/textmacros'); diff --git a/ts/input/tex/textmacros/__locales__/de.json b/ts/input/tex/textmacros/__locales__/de.json new file mode 100644 index 000000000..5af2226a6 --- /dev/null +++ b/ts/input/tex/textmacros/__locales__/de.json @@ -0,0 +1,4 @@ +{ + "MathMacro": "%1 wird nur im mathematischen Modus unterstützt", + "MathModeOnly": "'%1' ist nur im mathematischen Modus zulässig" +} diff --git a/ts/input/tex/textmacros/__locales__/en.json b/ts/input/tex/textmacros/__locales__/en.json new file mode 100644 index 000000000..5d718f440 --- /dev/null +++ b/ts/input/tex/textmacros/__locales__/en.json @@ -0,0 +1,4 @@ +{ + "MathMacro": "%1 is only supported in math mode", + "MathModeOnly": "'%1' allowed only in math mode" +} diff --git a/ts/input/tex/unicode/UnicodeConfiguration.ts b/ts/input/tex/unicode/UnicodeConfiguration.ts index 03833292a..786e10443 100644 --- a/ts/input/tex/unicode/UnicodeConfiguration.ts +++ b/ts/input/tex/unicode/UnicodeConfiguration.ts @@ -25,13 +25,16 @@ import { HandlerType, ConfigurationType } from '../HandlerTypes.js'; import { Configuration } from '../Configuration.js'; import { EnvList } from '../StackItem.js'; import TexParser from '../TexParser.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; import { CommandMap } from '../TokenMap.js'; import { ParseMethod } from '../Types.js'; import { UnitUtil } from '../UnitUtil.js'; import NodeUtil from '../NodeUtil.js'; import { numeric } from '../../../util/Entities.js'; import { Other } from '../base/BaseConfiguration.js'; +import { COMPONENT as TEX_COMPONENT } from '../__locales__/Component.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; const UnicodeCache: { [key: number]: [number, number, string, number] } = {}; @@ -58,19 +61,11 @@ const UnicodeMethods: { [key: string]: ParseMethod } = { } } if (font.match(/;/)) { - throw new TexError( - 'BadFont', - "Font name for %1 can't contain semicolons", - parser.currentCS - ); + texError(COMPONENT, 'BadFont', parser.currentCS); } const n = UnitUtil.trimSpaces(parser.GetArgument(name)).replace(/^0x/, 'x'); if (!n.match(/^(x[0-9A-Fa-f]+|[0-9]+)$/)) { - throw new TexError( - 'BadUnicode', - 'Argument to %1 must be a number', - parser.currentCS - ); + texError(COMPONENT, 'BadUnicode', parser.currentCS); } const N = parseInt(n.match(/^x/) ? '0' + n : n); if (!UnicodeCache[N]) { @@ -111,11 +106,7 @@ const UnicodeMethods: { [key: string]: ParseMethod } = { RawUnicode(parser: TexParser, name: string) { const hex = parser.GetArgument(name).trim(); if (!hex.match(/^[0-9A-F]{1,6}$/)) { - throw new TexError( - 'BadRawUnicode', - 'Argument to %1 must a hexadecimal number with 1 to 6 digits', - parser.currentCS - ); + texError(TEX_COMPONENT, 'BadRawUnicode', parser.currentCS); } const n = parseInt(hex, 16); parser.string = String.fromCodePoint(n) + parser.string.substring(parser.i); @@ -156,11 +147,7 @@ const UnicodeMethods: { [key: string]: ParseMethod } = { parser.i += 2; const cs = [...parser.GetCS()]; if (cs.length > 1) { - throw new TexError( - 'InvalidAlphanumeric', - 'Invalid alphanumeric constant for %1', - parser.currentCS - ); + texError(COMPONENT, 'InvalidAlphanumeric', parser.currentCS); } c = cs[0]; match = ['']; @@ -173,11 +160,7 @@ const UnicodeMethods: { [key: string]: ParseMethod } = { } } if (!c) { - throw new TexError( - 'MissingNumber', - 'Missing numeric constant for %1', - parser.currentCS - ); + texError(COMPONENT, 'MissingNumber', parser.currentCS); } parser.i += match[0].length; if (c >= '0' && c <= '9') { diff --git a/ts/input/tex/unicode/__locales__/Component.ts b/ts/input/tex/unicode/__locales__/Component.ts new file mode 100644 index 000000000..776f460d1 --- /dev/null +++ b/ts/input/tex/unicode/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/unicode + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/unicode'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/unicode'); diff --git a/ts/input/tex/unicode/__locales__/de.json b/ts/input/tex/unicode/__locales__/de.json new file mode 100644 index 000000000..975416c57 --- /dev/null +++ b/ts/input/tex/unicode/__locales__/de.json @@ -0,0 +1,6 @@ +{ + "BadFont": "Der Schriftartenname für %1 darf keine Semikolons enthalten", + "BadUnicode": "Das Argument für %1 muss eine Zahl sein", + "InvalidAlphanumeric": "Ungültige alphanumerische Konstante für %1", + "MissingNumber": "Fehlende numerische Konstante für %1" +} diff --git a/ts/input/tex/unicode/__locales__/en.json b/ts/input/tex/unicode/__locales__/en.json new file mode 100644 index 000000000..6e711af79 --- /dev/null +++ b/ts/input/tex/unicode/__locales__/en.json @@ -0,0 +1,6 @@ +{ + "BadFont": "Font name for %1 can't contain semicolons", + "BadUnicode": "Argument to %1 must be a number", + "InvalidAlphanumeric": "Invalid alphanumeric constant for %1", + "MissingNumber": "Missing numeric constant for %1" +} diff --git a/ts/input/tex/verb/VerbConfiguration.ts b/ts/input/tex/verb/VerbConfiguration.ts index 645aab45c..905193cb2 100644 --- a/ts/input/tex/verb/VerbConfiguration.ts +++ b/ts/input/tex/verb/VerbConfiguration.ts @@ -27,7 +27,10 @@ import { TexConstant } from '../TexConstants.js'; import TexParser from '../TexParser.js'; import { CommandMap } from '../TokenMap.js'; import { ParseMethod } from '../Types.js'; -import TexError from '../TexError.js'; +import { texError } from '../TexError.js'; +import { COMPONENT as TEX_COMPONENT } from '../__locales__/Component.js'; +import { COMPONENT } from './__locales__/Component.js'; +export { COMPONENT }; // Namespace const VerbMethods: { [key: string]: ParseMethod } = { @@ -41,7 +44,7 @@ const VerbMethods: { [key: string]: ParseMethod } = { const c = parser.GetNext(); const start = ++parser.i; if (c === '') { - throw new TexError('MissingArgFor', 'Missing argument for %1', name); + texError(TEX_COMPONENT, 'MissingArgFor', name); } while ( parser.i < parser.string.length && @@ -50,11 +53,7 @@ const VerbMethods: { [key: string]: ParseMethod } = { parser.i++; } if (parser.i === parser.string.length) { - throw new TexError( - 'NoClosingDelim', - "Can't find closing delimiter for %1", - parser.currentCS - ); + texError(COMPONENT, 'NoClosingDelim', parser.currentCS); } const text = parser.string.slice(start, parser.i).replace(/ /g, '\u00A0'); parser.i++; diff --git a/ts/input/tex/verb/__locales__/Component.ts b/ts/input/tex/verb/__locales__/Component.ts new file mode 100644 index 000000000..5b85bb7fe --- /dev/null +++ b/ts/input/tex/verb/__locales__/Component.ts @@ -0,0 +1,28 @@ +/************************************************************* + * + * 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 [tex]/verb + * + * @author v.sorge@mathjax.org (Volker Sorge) + */ + +import { Locale } from '../../../../util/Locale.js'; + +export const COMPONENT = '[tex]/verb'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/verb'); diff --git a/ts/input/tex/verb/__locales__/de.json b/ts/input/tex/verb/__locales__/de.json new file mode 100644 index 000000000..1d6f562fa --- /dev/null +++ b/ts/input/tex/verb/__locales__/de.json @@ -0,0 +1,3 @@ +{ + "NoClosingDelim": "Schließendes Trennzeichen für %1 nicht gefunden" +} diff --git a/ts/input/tex/verb/__locales__/en.json b/ts/input/tex/verb/__locales__/en.json new file mode 100644 index 000000000..a34910e01 --- /dev/null +++ b/ts/input/tex/verb/__locales__/en.json @@ -0,0 +1,3 @@ +{ + "NoClosingDelim": "Can't find closing delimiter for %1" +} diff --git a/ts/mathjax.ts b/ts/mathjax.ts index f57974a4b..739e9f9d0 100644 --- a/ts/mathjax.ts +++ b/ts/mathjax.ts @@ -27,6 +27,7 @@ import { handleRetriesFor, retryAfter } from './util/Retries.js'; import { OptionList } from './util/Options.js'; import { MathDocument } from './core/MathDocument.js'; import { context } from './util/context.js'; +import { json } from '#root/json.js'; /*****************************************************************/ /** @@ -77,4 +78,12 @@ export const mathjax = { * When asyncLoad uses require(), it actually operates synchronously and this is true */ asyncIsSynchronous: false, + + /** + * function to use for loading json files in components + * + * @param {string} file The name of the JSON file to load + * @returns {Promise} A promise resolving to the JSON data + */ + json: json, }; diff --git a/ts/ui/menu/MJContextMenu.ts b/ts/ui/menu/MJContextMenu.ts index 9e7abf2c8..066a74a3d 100644 --- a/ts/ui/menu/MJContextMenu.ts +++ b/ts/ui/menu/MJContextMenu.ts @@ -24,6 +24,7 @@ import { MathItem } from '../../core/MathItem.js'; import { OptionList } from '../../util/Options.js'; import { JaxList } from './Menu.js'; +import { localize } from './__locales__/Component.js'; import { ExplorerMathItem } from '../../a11y/explorer.js'; import { @@ -167,7 +168,9 @@ export class MJContextMenu extends ContextMenu { const input = this.mathItem.inputJax.name; const original = this.findID('Show', 'Original'); original.content = - input === 'MathML' ? 'Original MathML' : input + ' Commands'; + input === 'MathML' + ? localize('OriginalMathML') + : localize('Commands', input); const clipboard = this.findID('Copy', 'Original'); clipboard.content = original.content; } @@ -187,8 +190,8 @@ export class MJContextMenu extends ContextMenu { */ protected getSpeechMenu() { const speech = this.mathItem.outputData.speech; - this.findID('Show', 'Speech')[speech ? 'enable' : 'disable'](); - this.findID('Copy', 'Speech')[speech ? 'enable' : 'disable'](); + this.findID('Show', 'SpeechText')[speech ? 'enable' : 'disable'](); + this.findID('Copy', 'SpeechText')[speech ? 'enable' : 'disable'](); } /** @@ -196,8 +199,8 @@ export class MJContextMenu extends ContextMenu { */ protected getBrailleMenu() { const braille = this.mathItem.outputData.braille; - this.findID('Show', 'Braille')[braille ? 'enable' : 'disable'](); - this.findID('Copy', 'Braille')[braille ? 'enable' : 'disable'](); + this.findID('Show', 'BrailleCode')[braille ? 'enable' : 'disable'](); + this.findID('Copy', 'BrailleCode')[braille ? 'enable' : 'disable'](); } /** @@ -205,8 +208,8 @@ export class MJContextMenu extends ContextMenu { */ protected getSvgMenu() { const svg = this.jax.SVG; - this.findID('Show', 'SVG')[svg ? 'enable' : 'disable'](); - this.findID('Copy', 'SVG')[svg ? 'enable' : 'disable'](); + this.findID('Show', 'SvgImage')[svg ? 'enable' : 'disable'](); + this.findID('Copy', 'SvgImage')[svg ? 'enable' : 'disable'](); } /** diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts index ebee11f7b..a7aebd3b6 100644 --- a/ts/ui/menu/Menu.ts +++ b/ts/ui/menu/Menu.ts @@ -46,9 +46,14 @@ import { RadioCompare } from './RadioCompare.js'; import { MmlVisitor } from './MmlVisitor.js'; import { MenuMathDocument } from './MenuHandler.js'; import * as MenuUtil from './MenuUtil.js'; +import { locales } from './locales.js'; import { Parser, Rule, CssStyles, Submenu } from './mj-context-menu.js'; +import { Locale } from '../../util/Locale.js'; +import { COMPONENT, localize } from './__locales__/Component.js'; +export { COMPONENT }; + /*==========================================================================*/ /** @@ -100,6 +105,7 @@ export interface MenuSettings { infoType: boolean; inTabOrder: boolean; locale: string; + language: string; magnification: string; magnify: string; speech: boolean; @@ -130,6 +136,11 @@ export class Menu { */ public static MENU_STORAGE = 'MathJax-Menu-Settings'; + /** + * The key for the localStorage for the locale settings + */ + public static LOCALE_STORAGE = 'MathJax-locale'; + /** * The options for the menu, including the default settings, the various output jax * and the list of annotation types and their encodings @@ -143,6 +154,7 @@ export class Menu { zoom: 'NoZoom', zscale: '200%', renderer: 'CHTML', + language: 'en', alt: true, cmd: false, ctrl: false, @@ -302,15 +314,18 @@ export class Menu { // Add the input and output jax and the document type // lines.push( - 'Input Jax: ' + this.document.inputJax.map((jax) => jax.name).join(', ') + localize( + 'InputJax', + this.document.inputJax.map((jax) => jax.name).join(', ') + ), + localize('OutputJax', this.document.outputJax.name), + localize('DocType', this.document.kind) ); - lines.push('Output Jax: ' + this.document.outputJax.name); - lines.push('Document Type: ' + this.document.kind); // // Add the loaded packages and their versions // if (MathJax && MathJax.loader) { - lines.push('
Modules Loaded:'); + lines.push('
' + localize('Modules')); const Package = MathJax._.components.package.Package; const versions = (MathJax as any).loader.versions; for (const name of Array.from(Package.packages.keys()).sort( @@ -391,41 +406,8 @@ export class Menu { */ protected help() { InfoDialog.post({ - title: 'MathJax Help', - message: [ - '

MathJax is a JavaScript library that allows page', - ' authors to include mathematics within their web pages.', - " As a reader, you don't need to do anything to make that happen.

", - '

Browsers: MathJax works with all modern browsers including', - ' Edge, Firefox, Chrome, Safari, Opera, and most mobile browsers.

', - '

Math Menu: MathJax adds a contextual menu to equations.', - ' Right-click or CTRL-click on any mathematics to access the menu.

', - '
', - "

Show Math As: These options allow you to view the formula's", - ' source markup (as MathML or in its original format).

', - "

Copy to Clipboard: These options copy the formula's source markup,", - ' as MathML or in its original format, to the clipboard', - ' (in browsers that support that).

', - '

Math Settings: These give you control over features of MathJax,', - ' such the size of the mathematics, the mechanism used to display equations,', - ' how to handle equations that are too wide, and the language to use for', - " MathJax's menus and error messages (not yet implemented in v4).", - '

', - '

Accessibility: MathJax can work with screen', - ' readers to make mathematics accessible to the visually impaired.', - ' Turn on speech or braille generation to enable creation of speech strings', - ' and the ability to investigate expressions interactively. You can control', - ' the style of the explorer in its menu.

', - '
', - '

Math Zoom: If you are having difficulty reading an', - ' equation, MathJax can enlarge it to help you see it better, or', - ' you can scale all the math on the page to make it larger.', - ' Turn these features on in the Math Settings menu.

', - "

Preferences: MathJax uses your browser's localStorage database", - ' to save the preferences set via this menu locally in your browser. These', - ' are not used to track you, and are not transferred or used remotely by', - ' MathJax in any way.

', - ].join('\n'), + title: localize('HelpTitle'), + message: localize('HelpMessage'), adaptor: this.document.adaptor, extraNodes: [ this.document.adaptor.node( @@ -442,7 +424,7 @@ export class Menu { */ protected mathMLCode() { CopyDialog.post({ - title: 'MathJax MathML Expression', + title: localize('MmlTitle'), message: this.menu.mathItem ? this.toMML(this.menu.mathItem) : '', adaptor: this.document.adaptor, code: true, @@ -454,7 +436,7 @@ export class Menu { */ protected originalText() { CopyDialog.post({ - title: 'MathJax Original Source', + title: localize('SourceTitle'), message: this.menu.mathItem?.math ?? '', adaptor: this.document.adaptor, code: true, @@ -466,7 +448,7 @@ export class Menu { */ protected annotationBox() { CopyDialog.post({ - title: 'MathJax Annotation Text', + title: localize('AnnotationTitle'), message: AnnotationMenu.annotation, adaptor: this.document.adaptor, code: true, @@ -478,7 +460,7 @@ export class Menu { */ public async svgImage() { CopyDialog.post({ - title: 'MathJax SVG Image', + title: localize('SvgTitle'), message: await this.toSVG(this.menu.mathItem), adaptor: this.document.adaptor, code: true, @@ -490,7 +472,7 @@ export class Menu { */ protected speechText() { CopyDialog.post({ - title: 'MathJax Speech Text', + title: localize('SpeechTitle'), message: this.menu.mathItem?.outputData?.speech ?? '', adaptor: this.document.adaptor, code: true, @@ -498,11 +480,11 @@ export class Menu { } /** - * The "Show As Speech Text" info box + * The "Show As Braille Text" info box */ protected brailleText() { CopyDialog.post({ - title: 'MathJax Braille Text', + title: localize('BrailleTitle'), message: this.menu.mathItem?.outputData?.braille ?? '', adaptor: this.document.adaptor, code: true, @@ -514,7 +496,7 @@ export class Menu { */ protected errorMessage() { CopyDialog.post({ - title: 'MathJax Error Message', + title: localize('ErrorTitle'), message: this.menu.mathItem ? this.menu.errorMsg : '', adaptor: this.document.adaptor, code: true, @@ -535,7 +517,7 @@ export class Menu { text = `
${zoom.outerHTML}
`; } InfoDialog.post({ - title: 'MathJax Zoomed Expression', + title: localize('ZoomTitle'), message: text, adaptor: this.document.adaptor, styles: { @@ -573,6 +555,7 @@ export class Menu { */ protected initSettings() { this.settings = this.options.settings; + this.settings.language = MathJax.config.locale ?? Locale.current; this.jax = this.options.jax; const jax = this.document.outputJax; this.jax[jax.name] = jax; @@ -614,6 +597,7 @@ export class Menu { this.variable('overflow', (overflow) => this.setOverflow(overflow) ), + this.variable('language', (locale) => this.setLanguage(locale)), this.variable('breakInline', (breaks) => this.setInlineBreaks(breaks) ), @@ -680,262 +664,216 @@ export class Menu { ), ], items: [ - this.submenu('Show', 'Show Math As', [ - this.command('MathMLcode', 'MathML Code', () => this.mathMLCode()), - this.command('Original', 'Original Form', () => this.originalText()), + this.submenu('Show', [ + this.command('MathMLcode', () => this.mathMLCode()), + this.command('Original', () => this.originalText()), this.rule(), - this.command('Speech', 'Speech Text', () => this.speechText(), { + this.command('SpeechText', () => this.speechText(), { disabled: true, }), - this.command('Braille', 'Braille Code', () => this.brailleText(), { + this.command('BrailleCode', () => this.brailleText(), { disabled: true, }), - this.command('SVG', 'SVG Image', () => this.svgImage(), { + this.command('SvgImage', () => this.svgImage(), { disabled: true, }), - this.submenu('ShowAnnotation', 'Annotation'), + this.submenu('ShowAnnotation'), this.rule(), - this.command('Error', 'Error Message', () => this.errorMessage(), { + this.command('Error', () => this.errorMessage(), { disabled: true, }), ]), - this.submenu('Copy', 'Copy to Clipboard', [ - this.command('MathMLcode', 'MathML Code', () => this.copyMathML()), - this.command('Original', 'Original Form', () => this.copyOriginal()), + this.submenu('Copy', [ + this.command('MathMLcode', () => this.copyMathML()), + this.command('Original', () => this.copyOriginal()), this.rule(), - this.command('Speech', 'Speech Text', () => this.copySpeechText(), { + this.command('SpeechText', () => this.copySpeechText(), { disabled: true, }), - this.command( - 'Braille', - 'Braille Code', - () => this.copyBrailleText(), - { disabled: true } - ), - this.command('SVG', 'SVG Image', () => this.copySvgImage(), { + this.command('BrailleCode', () => this.copyBrailleText(), { + disabled: true, + }), + this.command('SvgImage', () => this.copySvgImage(), { disabled: true, }), - this.submenu('CopyAnnotation', 'Annotation'), + this.submenu('CopyAnnotation'), this.rule(), - this.command( - 'Error', - 'Error Message', - () => this.copyErrorMessage(), - { disabled: true } - ), + this.command('Error', () => this.copyErrorMessage(), { + disabled: true, + }), ]), this.rule(), - this.submenu('Settings', 'Math Settings', [ + this.submenu('Settings', [ this.submenu( 'Renderer', - 'Math Renderer', - this.radioGroup('renderer', [['CHTML'], ['SVG']]) + this.radioGroup('renderer', ['CHTML', 'SVG']) ), - this.submenu('Overflow', 'Wide Expressions', [ + this.submenu('WideExpressions', [ this.radioGroup('overflow', [ - ['Overflow'], - ['Scroll'], - ['Linebreak'], - ['Scale'], - ['Truncate'], - ['Elide'], + 'Overflow', + 'Scroll', + 'Linebreak', + 'Scale', + 'Truncate', + 'Elide', ]), this.rule(), - this.checkbox('BreakInline', 'Allow In-line Breaks', 'breakInline'), + this.checkbox('BreakInline', 'breakInline'), ]), this.rule(), - this.submenu('MathmlIncludes', 'MathML/SVG has', [ - this.checkbox('showSRE', 'Semantic attributes', 'showSRE'), - this.checkbox('showTex', 'LaTeX attributes', 'showTex'), - this.checkbox('texHints', 'TeX hints', 'texHints'), - this.checkbox('semantics', 'Original as annotation', 'semantics'), + this.submenu('MathmlIncludes', [ + this.checkbox('showSRE', 'showSRE'), + this.checkbox('showTex', 'showTex'), + this.checkbox('texHints', 'texHints'), + this.checkbox('semantics', 'semantics'), ]), - this.submenu('Language', 'Language'), + this.submenu('Language', this.languageSubmenu()), this.rule(), - this.submenu('ZoomTrigger', 'Zoom Trigger', [ - this.command('ZoomNow', 'Zoom Once Now', () => this.zoom(null, '')), + this.submenu('ZoomTrigger', [ + this.command('ZoomNow', () => this.zoom(null, '')), this.rule(), - this.radioGroup('zoom', [ - ['Click'], - ['DoubleClick', 'Double-Click'], - ['NoZoom', 'No Zoom'], - ]), + this.radioGroup('zoom', ['Click', 'DoubleClick', 'NoZoom']), this.rule(), - this.label('TriggerRequires', 'Trigger Requires:'), - this.checkbox( - MenuUtil.isMac ? 'Option' : 'Alt', - MenuUtil.isMac ? 'Option' : 'Alt', - 'alt' - ), - this.checkbox('Command', 'Command', 'cmd', { + this.label('TriggerRequires'), + this.checkbox(MenuUtil.isMac ? 'Option' : 'Alt', 'alt'), + this.checkbox('Command', 'cmd', { hidden: !MenuUtil.isMac, }), - this.checkbox('Control', 'Control', 'ctrl', { + this.checkbox('Control', 'ctrl', { hidden: MenuUtil.isMac, }), - this.checkbox('Shift', 'Shift', 'shift'), + this.checkbox('Shift', 'shift'), ]), this.submenu( 'ZoomFactor', - 'Zoom Factor', this.radioGroup('zscale', [ - ['150%'], - ['175%'], - ['200%'], - ['250%'], - ['300%'], - ['400%'], + '150%', + '175%', + '200%', + '250%', + '300%', + '400%', ]) ), this.rule(), - this.command('Scale', 'Scale All Math...', () => this.scaleAllMath()), + this.command('ScaleAllMath', () => this.scaleAllMath()), this.rule(), - this.command('Reset', 'Reset to defaults', () => - this.resetDefaults() - ), + this.command('Reset', () => this.resetDefaults()), ]), this.rule(), - this.label('Accessibility', '\xA0\xA0 Accessibility:'), - this.submenu('Speech', '\xA0 \xA0 Speech', [ - this.checkbox('Generate', 'Generate', 'speech'), - this.checkbox('Subtitles', 'Show Subtitles', 'subtitles'), - this.checkbox('Auto Voicing', 'Auto Voicing', 'voicing'), + this.label('Accessibility'), + this.submenu('Speech', [ + this.checkbox('Generate', 'speech'), + this.checkbox('Subtitles', 'subtitles'), + this.checkbox('AutoVoicing', 'voicing'), this.rule(), - this.label('Rules', 'Rules:'), + this.label('Rules'), this.submenu( - 'Mathspeak', 'Mathspeak', this.radioGroup('speechRules', [ - ['mathspeak-default', 'Verbose'], - ['mathspeak-brief', 'Brief'], - ['mathspeak-sbrief', 'Superbrief'], + 'mathspeak-default', + 'mathspeak-brief', + 'mathspeak-sbrief', ]) ), this.submenu( 'Clearspeak', - 'Clearspeak', - this.radioGroup('speechRules', [['clearspeak-default', 'Auto']]) + this.radioGroup('speechRules', ['clearspeak-default']) ), this.rule(), - this.submenu('A11yLanguage', 'Language'), + this.submenu('A11yLanguage'), ]), - this.submenu('Braille', '\xA0 \xA0 Braille', [ - this.checkbox('Generate', 'Generate', 'braille'), - this.checkbox('Subtitles', 'Show Subtitles', 'viewBraille'), - this.checkbox('BrailleSpeech', 'Replace Speech', 'brailleSpeech', { + this.submenu('Braille', [ + this.checkbox('Generate', 'braille'), + this.checkbox('Subtitles', 'viewBraille'), + this.checkbox('BrailleSpeech', 'brailleSpeech', { hidden: true, }), - this.checkbox( - 'BrailleCombine', - 'Combine with Speech', - 'brailleCombine' - ), + this.checkbox('BrailleCombine', 'brailleCombine'), this.rule(), - this.label('Code', 'Code Format:'), - this.radioGroup('brailleCode', [ - ['nemeth', 'Nemeth'], - ['ueb', 'UEB'], - ['euro', 'Euro'], - ]), + this.label('Code'), + this.radioGroup('brailleCode', ['nemeth', 'ueb', 'euro']), ]), - this.submenu('Explorer', '\xA0 \xA0 Explorer', [ - this.submenu('Highlight', 'Highlight', [ + this.submenu('Explorer', [ + this.submenu('Highlight', [ this.submenu( - 'Background', 'Background', this.radioGroup('backgroundColor', [ - ['Blue'], - ['Red'], - ['Green'], - ['Yellow'], - ['Cyan'], - ['Magenta'], - ['White'], - ['Black'], + 'Blue', + 'Red', + 'Green', + 'Yellow', + 'Cyan', + 'Magenta', + 'White', + 'Black', ]) ), { type: 'slider', variable: 'backgroundOpacity', content: ' ' }, this.submenu( - 'Foreground', 'Foreground', this.radioGroup('foregroundColor', [ - ['Black'], - ['White'], - ['Magenta'], - ['Cyan'], - ['Yellow'], - ['Green'], - ['Red'], - ['Blue'], + 'Black', + 'White', + 'Magenta', + 'Cyan', + 'Yellow', + 'Green', + 'Red', + 'Blue', ]) ), { type: 'slider', variable: 'foregroundOpacity', content: ' ' }, this.rule(), - this.radioGroup('highlight', [['None'], ['Hover'], ['Flame']]), + this.radioGroup('highlight', ['None', 'Hover', 'Flame']), this.rule(), - this.checkbox('TreeColoring', 'Tree Coloring', 'treeColoring'), + this.checkbox('TreeColoring', 'treeColoring'), ]), - this.submenu('Magnification', 'Magnification', [ - this.radioGroup('magnification', [ - ['None'], - ['Keyboard'], - ['Mouse'], - ]), + this.submenu('Magnification', [ + this.radioGroup('magnification', ['None', 'Keyboard', 'Mouse']), this.rule(), - this.radioGroup('magnify', [ - ['200%'], - ['300%'], - ['400%'], - ['500%'], - ]), + this.radioGroup('magnify', ['200%', '300%', '400%', '500%']), ]), - this.submenu('Semantic Info', 'Semantic Info', [ - this.checkbox('Type', 'Type', 'infoType'), - this.checkbox('Role', 'Role', 'infoRole'), - this.checkbox('Prefix', 'Prefix', 'infoPrefix'), + this.submenu('SemanticInfo', [ + this.checkbox('Type', 'infoType'), + this.checkbox('Role', 'infoRole'), + this.checkbox('Prefix', 'infoPrefix'), ]), this.rule(), - this.submenu('Role Description', 'Describe math as', [ + this.submenu('RoleDescription', [ this.radioGroup('roleDescription', [ - ['MathJax expression'], - ['MathJax'], - ['math'], - ['clickable math'], - ['explorable math'], - ['none'], + 'MathJax expression', + 'MathJax', + 'math', + 'clickable math', + 'explorable math', + 'none', ]), ]), - this.checkbox('Math Help', 'Help message on focus', 'help'), + this.checkbox('MathHelp', 'help'), ]), - this.submenu('Options', '\xA0 \xA0 Options', [ - this.checkbox('Enrich', 'Semantic Enrichment', 'enrich'), - this.checkbox('Collapsible', 'Collapsible Math', 'collapsible'), - this.checkbox('AutoCollapse', 'Auto Collapse', 'autocollapse', { + this.submenu('Options', [ + this.checkbox('Enrich', 'enrich'), + this.checkbox('Collapsible', 'collapsible'), + this.checkbox('AutoCollapse', 'autocollapse', { disabled: true, }), this.rule(), - this.checkbox('InTabOrder', 'Include in Tab Order', 'inTabOrder'), - this.submenu('TabSelects', 'Tabbing Focuses on', [ - this.radioGroup('tabSelects', [ - ['all', 'Whole Expression'], - ['last', 'Last Explored Node'], - ]), + this.checkbox('InTabOrder', 'inTabOrder'), + this.submenu('TabSelects', [ + this.radioGroup('tabSelects', ['all', 'last']), ]), this.rule(), - this.checkbox( - 'AssistiveMml', - 'Include Hidden MathML', - 'assistiveMml' - ), + this.checkbox('AssistiveMml', 'assistiveMml'), ]), this.rule(), - this.command('About', 'About MathJax', () => this.about()), - this.command('Help', 'MathJax Help', () => this.help()), + this.command('About', () => this.about()), + this.command('Help', () => this.help()), ], }) as MJContextMenu; const menu = this.menu; menu.settings = this.settings; - menu.findID('Settings', 'Overflow', 'Elide').disable(); + menu.findID('Settings', 'WideExpressions', 'Elide').disable(); menu.findID('Braille', 'ueb').hide(); menu.setJax(this.jax); this.checkLoadableItems(); @@ -1025,7 +963,7 @@ export class Menu { Object.assign(this.settings, settings); this.setA11y(settings); } catch (err) { - console.log('MathJax localStorage error: ' + err.message); + console.log(localize('StorageError', err.message)); } } @@ -1045,8 +983,9 @@ export class Menu { } else { localStorage.removeItem(Menu.MENU_STORAGE); } + localStorage.setItem(Menu.LOCALE_STORAGE, this.settings.language); } catch (err) { - console.log('MathJax localStorage error: ' + err.message); + console.log(localize('StorageError', err.message)); } } @@ -1151,7 +1090,7 @@ export class Menu { this.loadComponent('output/' + name, () => { const startup = MathJax.startup; if (!(name in startup.constructors)) { - return fail(new Error(`Component ${name} not loaded`)); + return fail(new Error(localize('ComponentNotLoaded', name))); } startup.useOutput(name, true); startup.output = this.applyRendererOptions(startup.getOutputJax()); @@ -1363,6 +1302,16 @@ export class Menu { this.rerender(STATE.COMPILED); } + /** + * @param {string} locale The interface language locale + */ + protected setLanguage(locale: string) { + Locale.setLocale(locale).then(() => { + this.initMenu(); + this.rerender(STATE.COMPILED); + }); + } + /** * Rerender when the role description changes */ @@ -1448,10 +1397,7 @@ export class Menu { const scale = (parseFloat(this.settings.scale) * 100) .toFixed(1) .replace(/.0$/, ''); - const percent = prompt( - 'Scale all mathematics (compared to surrounding text) by', - scale + '%' - ); + const percent = prompt(localize('ScalePrompt'), scale + '%'); if (this.current) { const speech = (this.menu.mathItem as ExplorerMathItem).explorers.speech; speech.refocus = this.current; @@ -1463,10 +1409,10 @@ export class Menu { if (scale) { this.menu.pool.lookup('scale').setValue(String(scale)); } else { - alert('The scale should not be zero'); + alert(localize('ScaleNonZero')); } } else { - alert('The scale should be a percentage (e.g., 120%)'); + alert(localize('ScalePercent')); } } } @@ -1627,7 +1573,7 @@ export class Menu { protected async toSVG(math: HTMLMATHITEM): Promise { const jax = this.jax.SVG; if (!jax) { - return "SVG can't be produced.
Try switching to SVG output first."; + return localize('NoSvgProduced'); } const adaptor = jax.adaptor; const cache = jax.options.fontCache; @@ -1950,18 +1896,32 @@ export class Menu { }; } + /** + * Create the Languages submenu entries. + * + * @returns {object[]} The submenu definitions + */ + public languageSubmenu(): object[] { + return (locales as [string, string][]).map(([locale, name]) => { + return { + type: 'radio', + id: locale, + content: `${name} (${locale})`, + variable: 'language', + }; + }); + } + /** * Create JSON for a submenu item * * @param {string} id The id for the item - * @param {string} content The content for the item * @param {any[]} entries The JSON for the entries * @param {boolean=} disabled True if this item is diabled initially * @returns {object} The JSON for the submenu item */ public submenu( id: string, - content: string, entries: any[] = [], disabled: boolean = false ): object { @@ -1976,7 +1936,7 @@ export class Menu { return { type: 'submenu', id, - content, + content: localize(id), menu: { items }, disabled: items.length === 0 || disabled, }; @@ -1986,17 +1946,12 @@ export class Menu { * Create JSON for a command item * * @param {string} id The id for the item - * @param {string} content The content for the item * @param {() => void} action The action function for the command * @param {object} other Other values to include in the generated JSON object * @returns {object} The JSON for the command item */ - public command( - id: string, - content: string, - action: () => void, - other: object = {} - ): object { + public command(id: string, action: () => void, other: object = {}): object { + const content = localize(id); return Object.assign({ type: 'command', id, content, action }, other); } @@ -2004,47 +1959,37 @@ export class Menu { * Create JSON for a checkbox item * * @param {string} id The id for the item - * @param {string} content The content for the item * @param {string} variable The (pool) variable to attach to this checkbox * @param {object} other Other values to include in the generated JSON object * @returns {object} The JSON for the checkbox item */ - public checkbox( - id: string, - content: string, - variable: string, - other: object = {} - ): object { + public checkbox(id: string, variable: string, other: object = {}): object { + const content = localize(id); return Object.assign({ type: 'checkbox', id, content, variable }, other); } /** * Create JSON for a group of connected radio buttons * - * @param {string} variable The (pool) variable to attach to each radio button - * @param {string[][]} radios An array of [string] or [string, string], giving the id and content - * for each radio button (if only one string is given it is used for both) - * @returns {object[]} An array of JSON objects for radion buttons + * @param {string} variable The (pool) variable to attach to each radio button + * @param {string[]} radios An array of [string] or [string, string], giving the id and content + * for each radio button (if only one string is given it is used for both) + * @returns {object[]} An array of JSON objects for radion buttons */ - public radioGroup(variable: string, radios: string[][]): object[] { - return radios.map((def) => this.radio(def[0], def[1] || def[0], variable)); + public radioGroup(variable: string, radios: string[]): object[] { + return radios.map((item) => this.radio(item, variable)); } /** * Create JSON for a radio button item * * @param {string} id The id for the item - * @param {string} content The content for the item * @param {string} variable The (pool) variable to attach to this radio button * @param {object} other Other values to include in the generated JSON object * @returns {object} The JSON for the radio button item */ - public radio( - id: string, - content: string, - variable: string, - other: object = {} - ): object { + public radio(id: string, variable: string, other: object = {}): object { + const content = localize(id); return Object.assign({ type: 'radio', id, content, variable }, other); } @@ -2052,10 +1997,10 @@ export class Menu { * Create JSON for a label item * * @param {string} id The id for the item - * @param {string} content The content for the item * @returns {object} The JSON for the label item */ - public label(id: string, content: string): object { + public label(id: string): object { + const content = localize(id); return { type: 'label', id, content }; } diff --git a/ts/ui/menu/__locales__/Component.ts b/ts/ui/menu/__locales__/Component.ts new file mode 100644 index 000000000..fbd032936 --- /dev/null +++ b/ts/ui/menu/__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 ui/menu + * + * @author dpvc@mathjax.org (Davide P. Cervone + */ + +import { Locale, namedData } from '../../../util/Locale.js'; + +export const COMPONENT = 'ui/menu'; + +Locale.registerLocaleFiles(COMPONENT, '../ts/ui/menu'); + +/** + * 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/ui/menu/__locales__/de.json b/ts/ui/menu/__locales__/de.json new file mode 100644 index 000000000..0badab345 --- /dev/null +++ b/ts/ui/menu/__locales__/de.json @@ -0,0 +1,146 @@ +{ + "150%": "150%", + "175%": "175%", + "200%": "200%", + "250%": "250%", + "300%": "300%", + "400%": "400%", + "500%": "500%", + "A11yLanguage": "Sprache", + "About": "Über MathJax", + "Accessibility": "\u00A0\u00A0 Barrierefreiheit:", + "Alt": "Alt", + "AssistiveMml": "Verstecktes MathML einbeziehen", + "AutoCollapse": "Automatisches Ausblenden", + "AutoVoicing": "Automatische Sprachausgabe", + "Background": "Hintergrund", + "Black": "Schwarz", + "Blue": "Blau", + "Braille": "\u00A0 \u00A0 Braille", + "BrailleCode": "Braille-Code", + "BrailleCombine": "Mit Sprachausgabe kombinieren", + "BrailleSpeech": "Sprachausgabe ersetzen", + "BreakInline": "Inline-Zeilenumbrüche zulassen", + "CHTML": "CHTML", + "Clearspeak": "Clearspeak", + "Click": "Klicken", + "Code": "Code-Format:", + "Collapsible": "Zusammenklappbare Mathematik", + "Command": "Befehl", + "Control": "Steuerelement", + "Copy": "In Zwischenablage kopieren", + "CopyAnnotation": "Anmerkung", + "Cyan": "Cyan", + "DoubleClick": "Doppelklick", + "Elide": "Elide", + "Enrich": "Semantische Anreicherung", + "Error": "Fehlermeldung", + "Explorer": " \u00A0 \u00A0 Explorer", + "Flame": "Flame", + "Foreground": "Foreground", + "Generate": "Generate", + "Green": "Green", + "Help": "MathJax-Hilfe", + "Highlight": "Highlight", + "Hover": "Hover", + "InTabOrder": "In Tab-Reihenfolge einbeziehen", + "Keyboard": "Tastatur", + "Language": "Sprache", + "Linebreak": "Zeilenumbruch", + "Magenta": "Magenta", + "Magnification": "Vergrößerung", + "MathHelp": "Hilfemeldung bei Fokus", + "MathJax": "MathJax", + "MathJax expression": "MathJax-Ausdruck", + "MathMLcode": "MathML-Code", + "MathmlIncludes": "MathML/SVG enthält", + "Mathspeak": "Mathspeak", + "Mouse": "Maus", + "NoZoom": "Kein Zoom", + "None": "Keine", + "Option": "Option", + "Options": "\u00A0 \u00A0 Optionen", + "Original": "Originalform", + "Overflow": "Überlauf", + "Prefix": "Präfix", + "Rot": "Rot", + "Renderer": "Mathematik-Renderer", + "Reset": "Auf Standardwerte zurücksetzen", + "Role": "Rolle", + "RoleDescription": "Mathematik beschreiben als", + "Rules": "Regeln:", + "SVG": "SVG", + "Scale": "Skalierung", + "ScaleAllMath": "Alle mathematischen Formeln skalieren...", + "Scroll": "Scrollen", + "SemanticInfo": "Semantische Informationen", + "Settings": "Einstellungen für mathematische Formeln", + "Shift": "Umschalt", + "Show": "Mathematik anzeigen als", + "ShowAnnotation": "Anmerkung", + "Speech": "\u00A0 \u00A0 Sprache", + "SpeechText": "Sprechtext", + "Subtitles": "Untertitel anzeigen", + "SvgImage": "SVG-Bild", + "TabSelects": "Tabulator-Fokus auf", + "TreeColoring": "Baumfärbung", + "TriggerRequires": "Trigger erfordert:", + "Truncate": "Kürzen", + "Type": "Typ", + "White": "Weiß", + "WideExpressions": "Breite Ausdrücke", + "Yellow": "Gelb", + "ZoomFactor": "Zoomfaktor", + "ZoomNow": "Jetzt einmal zoomen", + "ZoomTrigger": "Zoom-Auslöser", + "all": "Gesamter Ausdruck", + "clearspeak-default": "Auto", + "clickable math": "anklickbare Mathematik", + "euro": "Euro", + "explorable math": "Erkundbare Mathematik", + "last": "Zuletzt erkundeter Knoten", + "math": "Mathematik", + "mathspeak-brief": "Kurz", + "mathspeak-default": "Ausführlich", + "mathspeak-sbrief": "Superkurz", + "nemeth": "Nemeth", + "none": "none", + "semantics": "Original als Anmerkung", + "showSRE": "Semantische Attribute", + "showTex": "LaTeX-Attribute", + "texHints": "TeX-Hinweise", + "ueb": "UEB", + + "OriginalMathML": "Original MathML", + "Commands": "%1 Befehle", + + "InputJax": "Input Jax: %1", + "OutputJax": "Output Jax: %1", + "DocType": "Dokumenttyp: %1", + "Modules": "Geladene Module:", + + "HelpTitle": "MathJax-Hilfe", + "HelpMessage": "

MathJax ist eine JavaScript-Bibliothek, die es Seitenautoren ermöglicht, mathematische Formeln in ihre Webseiten einzubinden. Als Leser müssen Sie nichts tun, um dies zu nutzen.

Browser: MathJax funktioniert mit allen modernen Browsern, einschließlich Edge, Firefox, Chrome, Safari, Opera und den meisten mobilen Browsern.

Mathematik-Menü: MathJax fügt den Formeln ein Kontextmenü hinzu. Klicken Sie mit der rechten Maustaste oder bei gedrückter STRG-Taste auf eine beliebige mathematische Formel, um das Menü aufzurufen.

Mathematik anzeigen als: Mit diesen Optionen können Sie den Quellcode der Formel (als MathML oder im Originalformat) anzeigen.

In die Zwischenablage kopieren: Diese Optionen kopieren den Quellcode der Formel als MathML oder im Originalformat in die Zwischenablage (in Browsern, die dies unterstützen).

Mathematik-Einstellungen: Hiermit können Sie Funktionen von MathJax steuern, wie z. B. die Größe der mathematischen Ausdrücke, den Mechanismus zur Darstellung von Gleichungen und den Umgang mit zu breiten Gleichungen, sowie die Sprache, die für die Menüs und Fehlermeldungen von MathJax verwendet werden soll (in Version 4 noch nicht implementiert).

Barrierefreiheit: MathJax kann mit Bildschirmleseprogrammen zusammenarbeiten, um Mathematik für Sehbehinderte zugänglich zu machen. Aktivieren Sie die Sprach- oder Braille-Generierung, um die Erstellung von Sprachausgaben und die Möglichkeit zur interaktiven Untersuchung von Ausdrücken zu ermöglichen. Sie können den Stil des Explorers über dessen Menü steuern.

Mathematik-Zoom: Wenn Sie Schwierigkeiten haben, eine Gleichung zu lesen, MathJax kann die Darstellung vergrößern, damit Sie sie besser erkennen können, oder Sie können alle mathematischen Formeln auf der Seite vergrößern. Aktivieren Sie diese Funktionen im Menü Mathematik-Einstellungen.

Einstellungen: MathJax nutzt die localStorage-Datenbank Ihres Browsers, um die über dieses Menü festgelegten Einstellungen lokal in Ihrem Browser zu speichern. Diese werden nicht dazu verwendet, Sie zu verfolgen, und werden von MathJax in keiner Weise übertragen oder aus der Ferne genutzt.

", + + "MmlTitle": "MathJax-MathML-Ausdruck", + "SourceTitle": "MathJax-Originalquelle", + "AnnotationTitle": "MathJax-Anmerkungstext", + "SvgTitle": "MathJax-SVG-Bild", + "SpeechTitle": "MathJax-Sprechtext", + "BrailleTitle": "MathJax-Braille-Text", + "ErrorTitle": "MathJax-Fehlermeldung", + "ZoomTitle": "MathJax-vergrößerter Ausdruck", + + "StorageError": "MathJax-localStorage-Fehler: %1", + "ComponentNotLoaded": "Komponente %1 nicht geladen", + "ScalePrompt": "Alle mathematischen Formeln (im Vergleich zum umgebenden Text) um skalieren", + "ScaleNonZero": "Der Skalierungsfaktor darf nicht Null sein", + "ScalePercent": "Der Skalierungsfaktor muss ein Prozentsatz sein (z. B. 120 %)", + "NoSvgProduced": "SVG kann nicht erzeugt werden.
Versuchen Sie zunächst, zur SVG-Ausgabe zu wechseln.", + + "ClearspeakTitle": "Clearspeak-Einstellungen", + "SelectPrefs": "Einstellungen auswählen", + "NoPrefs": "Keine Einstellungen", + "CurrentPrefs": "Aktuelle Einstellungen", + "PrefsFor": "Einstellungen für %1" +} diff --git a/ts/ui/menu/__locales__/en.json b/ts/ui/menu/__locales__/en.json new file mode 100644 index 000000000..83b2af7ea --- /dev/null +++ b/ts/ui/menu/__locales__/en.json @@ -0,0 +1,146 @@ +{ + "150%": "150%", + "175%": "175%", + "200%": "200%", + "250%": "250%", + "300%": "300%", + "400%": "400%", + "500%": "500%", + "A11yLanguage": "Language", + "About": "About MathJax", + "Accessibility": "\u00A0\u00A0 Accessibility:", + "Alt": "Alt", + "AssistiveMml": "Include Hidden MathML", + "AutoCollapse": "Auto Collapse", + "AutoVoicing": "Auto Voicing", + "Background": "Background", + "Black": "Black", + "Blue": "Blue", + "Braille": "\u00A0 \u00A0 Braille", + "BrailleCode": "Braille Code", + "BrailleCombine": "Combine with Speech", + "BrailleSpeech": "Replace Speech", + "BreakInline": "Allow In-line Breaks", + "CHTML": "CHTML", + "Clearspeak": "Clearspeak", + "Click": "Click", + "Code": "Code Format:", + "Collapsible": "Collapsible Math", + "Command": "Command", + "Control": "Control", + "Copy": "Copy to Clipboard", + "CopyAnnotation": "Annotation", + "Cyan": "Cyan", + "DoubleClick": "Double-Click", + "Elide": "Elide", + "Enrich": "Semantic Enrichment", + "Error": "Error Message", + "Explorer": "\u00A0 \u00A0 Explorer", + "Flame": "Flame", + "Foreground": "Foreground", + "Generate": "Generate", + "Green": "Green", + "Help": "MathJax Help", + "Highlight": "Highlight", + "Hover": "Hover", + "InTabOrder": "Include in Tab Order", + "Keyboard": "Keyboard", + "Language": "Language", + "Linebreak": "Linebreak", + "Magenta": "Magenta", + "Magnification": "Magnification", + "MathHelp": "Help message on focus", + "MathJax": "MathJax", + "MathJax expression": "MathJax expression", + "MathMLcode": "MathML Code", + "MathmlIncludes": "MathML/SVG has", + "Mathspeak": "Mathspeak", + "Mouse": "Mouse", + "NoZoom": "No Zoom", + "None": "None", + "Option": "Option", + "Options": "\u00A0 \u00A0 Options", + "Original": "Original Form", + "Overflow": "Overflow", + "Prefix": "Prefix", + "Red": "Red", + "Renderer": "Math Renderer", + "Reset": "Reset to defaults", + "Role": "Role", + "RoleDescription": "Describe math as", + "Rules": "Rules:", + "SVG": "SVG", + "Scale": "Scale", + "ScaleAllMath": "Scale All Math...", + "Scroll": "Scroll", + "SemanticInfo": "Semantic Info", + "Settings": "Math Settings", + "Shift": "Shift", + "Show": "Show Math As", + "ShowAnnotation": "Annotation", + "Speech": "\u00A0 \u00A0 Speech", + "SpeechText": "Speech Text", + "Subtitles": "Show Subtitles", + "SvgImage": "SVG Image", + "TabSelects": "Tabbing Focuses on", + "TreeColoring": "Tree Coloring", + "TriggerRequires": "Trigger Requires:", + "Truncate": "Truncate", + "Type": "Type", + "White": "White", + "WideExpressions": "Wide Expressions", + "Yellow": "Yellow", + "ZoomFactor": "Zoom Factor", + "ZoomNow": "Zoom Once Now", + "ZoomTrigger": "Zoom Trigger", + "all": "Whole Expression", + "clearspeak-default": "Auto", + "clickable math": "clickable math", + "euro": "Euro", + "explorable math": "explorable math", + "last": "Last Explored Node", + "math": "math", + "mathspeak-brief": "Brief", + "mathspeak-default": "Verbose", + "mathspeak-sbrief": "Superbrief", + "nemeth": "Nemeth", + "none": "none", + "semantics": "Original as annotation", + "showSRE": "Semantic attributes", + "showTex": "LaTeX attributes", + "texHints": "TeX hints", + "ueb": "UEB", + + "OriginalMathML": "Original MathML", + "Commands": "%1 Commands", + + "InputJax": "Input Jax: %1", + "OutputJax": "Outut Jax: %1", + "DocType": "Document Type: %1", + "Modules": "Modules Loaded:", + + "HelpTitle": "MathJax Help", + "HelpMessage": "

MathJax is a JavaScript library that allows page authors to include mathematics within their web pages. As a reader, you don't need to do anything to make that happen.

Browsers: MathJax works with all modern browsers including Edge, Firefox, Chrome, Safari, Opera, and most mobile browsers.

Math Menu: MathJax adds a contextual menu to equations. Right-click or CTRL-click on any mathematics to access the menu.

Show Math As: These options allow you to view the formula's source markup (as MathML or in its original format).

Copy to Clipboard: These options copy the formula's source markup, as MathML or in its original format, to the clipboard (in browsers that support that).

Math Settings: These give you control over features of MathJax, such the size of the mathematics, the mechanism used to display equations, how to handle equations that are too wide, and the language to use for MathJax's menus and error messages (not yet implemented in v4).

Accessibility: MathJax can work with screen readers to make mathematics accessible to the visually impaired. Turn on speech or braille generation to enable creation of speech strings and the ability to investigate expressions interactively. You can control the style of the explorer in its menu.

Math Zoom: If you are having difficulty reading an equation, MathJax can enlarge it to help you see it better, or you can scale all the math on the page to make it larger. Turn these features on in the Math Settings menu.

Preferences: MathJax uses your browser's localStorage database to save the preferences set via this menu locally in your browser. These are not used to track you, and are not transferred or used remotely by MathJax in any way.

", + + "MmlTitle": "MathJax MathML Expression", + "SourceTitle": "MathJax Original Source", + "AnnotationTitle": "MathJax Annotation Text", + "SvgTitle": "MathJax SVG Image", + "SpeechTitle": "MathJax Speech Text", + "BrailleTitle": "MathJax Braille Text", + "ErrorTitle": "MathJax Error Message", + "ZoomTitle": "MathJax Zoomed Expression", + + "StorageError": "MathJax localStorage error: %1", + "ComponentNotLoaded": "Component %1 not loaded", + "ScalePrompt": "Scale all mathematics (compared to surrounding text) by", + "ScaleNonZero": "The scale should not be zero", + "ScalePercent": "The scale should be a percentage (e.g., 120%)", + "NoSvgProduced": "SVG can't be produced.
Try switching to SVG output first.", + + "ClearspeakTitle": "Clearspeak Preferences", + "SelectPrefs": "Select Preferences", + "NoPrefs": "No Preferences", + "CurrentPrefs": "Current Preferences", + "PrefsFor": "Preferences for %1" +} diff --git a/ts/ui/menu/locales.ts b/ts/ui/menu/locales.ts new file mode 100644 index 000000000..97784faad --- /dev/null +++ b/ts/ui/menu/locales.ts @@ -0,0 +1,27 @@ +/************************************************************* + * + * 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 Lists the locales available in the Language menu. + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +export const locales = [ + ['en', 'English'], + ['de', 'Deutsch'], +]; diff --git a/ts/util/AsyncLoad.ts b/ts/util/AsyncLoad.ts index 2d4c26166..958ba70c5 100644 --- a/ts/util/AsyncLoad.ts +++ b/ts/util/AsyncLoad.ts @@ -44,3 +44,32 @@ export function asyncLoad(name: string): Promise { } }); } + +/** + * Used to look up Package object, if it is in use + */ +declare const MathJax: any; + +/** + * Resolve a file name to a full path or URL + * + * @param {string} name The file name to resolve + * @param {(string)=>string} relative Function to get absolute path from relative one + * @param {(string)=>string} absolute Function to fix up absolute path + * @returns {string} The full path name + */ +export function resolvePath( + name: string, + relative: (name: string) => string, + absolute: (name: string) => string = (name) => name +): string { + const Package = + typeof MathJax === 'undefined' + ? null + : MathJax._?.components?.package?.Package; + return name.charAt(0) === '[' && Package + ? Package.resolvePath(name) + : name.charAt(0) === '.' + ? relative(name) + : absolute(name); +} diff --git a/ts/util/Locale.ts b/ts/util/Locale.ts new file mode 100644 index 000000000..c65083059 --- /dev/null +++ b/ts/util/Locale.ts @@ -0,0 +1,309 @@ +/************************************************************* + * + * Copyright (c) 2024 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 Implements the locale framework + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +import { asyncLoad } from './AsyncLoad.js'; + +/** + * The various object map types + */ +export type messageData = { [id: string]: string }; +export type localeData = { [locale: string]: messageData }; +export type componentData = { [component: string]: localeData }; +export type namedData = { [name: string | number]: string }; + +/** + * The Locale class for handling localized messages + */ +export class Locale { + /** + * The current locale + */ + public static current: string = 'en'; + + /** + * The default locale for when a message has no current localization + */ + public static default: string = 'en'; + + /** + * True when the core component has been loaded (and so the Package path resolution is available) + */ + public static isComponent: boolean = false; + + /** + * The localized message strings, per component and locale, + * with the default message for localeError() below. + */ + protected static data: componentData = { + locale: { + en: { + LocaleJsonError: "MathJax(%1): Can't load '%2': %3", + LocaleMessageNotFound: + "MathJax(Locale): No localized or default version for message with id '%1' from '%2'", + }, + de: { + LocaleJsonError: "MathJax(%1): '%2' kann nicht geladen werden: %3", + LocaleMessageNotFound: + "MathJax(Locale): Keine lokalisierte oder Standardversion für die Meldung mit der ID '%1' aus '%2'", + }, + }, + }; + + /** + * The locale files to load for each locale (as registered by the components) + */ + protected static locations: { [component: string]: [string, Set] } = + Object.create(null); + + /** + * Registers a given component's locale directory + * + * @param {string} component The component's name (e.g., [tex]/bbox) + * @param {string} prefix The directory where the locales are located + */ + public static registerLocaleFiles( + component: string, + prefix: string = component + ) { + this.locations[component] = [ + `${this.isComponent ? component : prefix}/__locales__`, + new Set(), + ]; + } + + /** + * Register a set of messages for a given component and locale (called when the localization + * files are loaded). + * + * @param {string} component The component's name (e.g., [tex]/bbox) + * @param {string} locale The locale for the messages + * @param {messageData} data The messages indexed by their IDs + */ + public static registerMessages( + component: string, + locale: string, + data: messageData + ) { + if (!this.data[component]) { + this.data[component] = Object.create(null); + } + const cdata = this.data[component]; + if (!cdata[locale]) { + cdata[locale] = Object.create(null); + } + Object.assign(cdata[locale], data); + } + + /** + * Get a message string and insert any arguments. The arguments can be positional, or a + * mapping of names to values. E.g. + * + * Locale.message('[my]/test', 'Hello', {name: 'World'})); + * Locale.message('[my]/test', 'FooBar', 'Foo')); + * + * @param {string} component The component whose message is requested + * @param {string} id The id of the message + * @param {string|namedData} data The first argument or the object of names arguments + * @param {string[]} args Any additional string arguments (if data is a string) + * @returns {string} The localized message with arguments substituted in + */ + public static message( + component: string, + id: string, + data: string | namedData = {}, + ...args: string[] + ): string { + if (component) { + const message = this.lookupMessage(component, id); + return this.processMessage(message, data, ...args); + } + if (typeof data !== 'string') { + return ''; + } + return this.processMessage(data, ...args); + } + + /** + * Process a message string by substituting the given arguments. The arguments + * can be positional, or a data mapping of names to values. + * + * @param {string} message The message string to process. + * @param {string| namedData} data The first argument or the object of + * names arguments + * @param {string[]} args Additional arguments (if data is a string) + * @returns {string} The processed message string with arguments substituted + */ + public static processMessage( + message: string, + data: string | namedData = {}, + ...args: string[] + ): string { + if (typeof data !== 'object') { + data = { 1: data }; + for (let i = 0; i < args.length; i++) { + data[i + 2] = args[i]; + } + } + data['%'] = '%'; + return this.substituteArguments(message, data); + } + + /** + * Find a localized message string, or use the default if not available + * + * @param {string} component The component for this message + * @param {string} id The id of the message + * @returns {string} The message string to use + */ + public static lookupMessage(component: string, id: string): string { + return ( + this.data[component]?.[this.current]?.[id] || + this.data[component]?.[this.default]?.[id] || + this.substituteArguments( + this.data.locale[this.current]?.['LocaleMessageNotFound'] || + this.data.locale[this.default]?.['LocaleMessageNotFound'] || + '', + { 1: id, 2: component } + ) + ); + } + + /** + * Substitue arguments into a message string + * + * @param {string} message The original message string + * @param {namedData} data The mapping of markers to values + * @returns {string} The final string with substitutions made + */ + protected static substituteArguments( + message: string, + data: namedData + ): string { + const parts = message.split(/%(%|\d+|[a-z]+|\{.*?\})/); + for (let i = 1; i < parts.length; i += 2) { + const id = parts[i].replace(/^\{(.*)\}$/, '$1'); + parts[i] = data[id] ?? ''; + } + return parts.join(''); + } + + /** + * Throw an error with a given string substituting the given parameters + * + * @param {string} component The component whose message is requested + * @param {string} id The id of the message + * @param {string|namedData} data The first argument or the object of names arguments + * @param {string[]} args Any additional string arguments (if data is a string) + */ + public static error( + component: string, + id: string, + data: string | namedData, + ...args: string[] + ) { + throw Error(this.message(component, id, data, ...args)); + } + + /** + * Set the locale to the given one (or use the current one), and load + * any needed files (or newly registered files for the current locale). + * + * @param {string} locale The local to use (or use the current one) + * @returns {Promise} A promise that resolves when the locale files have been loaded + */ + public static async setLocale( + locale: string = this.current + ): Promise { + this.current = locale; + const promises = []; + for (const [component, [directory, loaded]] of Object.entries( + this.locations + )) { + if (!loaded.has(locale)) { + loaded.add(locale); + promises.push( + this.getLocaleData(component, locale, `${directory}/${locale}.json`) + ); + } + } + return Promise.all(promises); + } + + /** + * Load a localization file and register its contents + * + * @param {string} component The component whose localization is being loaded + * @param {string} locale The locale being loaded + * @param {string} file The file to load for that localization + * @returns {Promise} A promise that resolves when the file is loaded and registered + */ + protected static async getLocaleData( + component: string, + locale: string, + file: string + ): Promise { + return asyncLoad(file) + .then((data: messageData) => + this.registerMessages(component, locale, data) + ) + .catch((error) => this.localeError(component, locale, error)); + } + + /** + * Report an error thrown when loading a component's locale file, and fall + * back to loading the default locale if the failed locale was not the default. + * + * @param {string} component The component whose localization is being loaded + * @param {string} locale The locale being loaded + * @param {Error} error The Error object causing the issue + * @returns {Promise|void} A promise for loading the default locale, or void + */ + protected static localeError( + component: string, + locale: string, + error: Error + ): Promise | void { + const message = this.message( + 'locale', + 'LocaleJsonError', + component, + `${locale}.json`, + error.message + ); + console.error(message); + if (locale !== this.default) { + const location = this.locations[component]; + if (location) { + const [directory, loaded] = location; + if (!loaded.has(this.default)) { + loaded.add(this.default); + return this.getLocaleData( + component, + this.default, + `${directory}/${this.default}.json` + ); + } + } + } + } +} diff --git a/ts/util/asyncLoad/esm.ts b/ts/util/asyncLoad/esm.ts index 0fe350353..529cf7330 100644 --- a/ts/util/asyncLoad/esm.ts +++ b/ts/util/asyncLoad/esm.ts @@ -23,15 +23,28 @@ import { mathjax } from '../../mathjax.js'; import { context } from '../context.js'; +import { resolvePath } from '../AsyncLoad.js'; + +import { readFileSync } from 'node:fs'; +const { resolve } = import.meta as any as { resolve: (file: string) => string }; +const RESOLVE = resolve || ((file: string) => file); let root = context .path(new URL(import.meta.url, 'file://').href) .replace(/\/util\/asyncLoad\/esm.js$/, '/'); +mathjax.json = async (name: string) => { + return JSON.parse( + String(readFileSync(new URL(RESOLVE(name), 'file://').pathname)) + ); +}; + if (!mathjax.asyncLoad) { mathjax.asyncLoad = async (name: string) => { - const file = name.charAt(0) === '.' ? new URL(name, root).href : name; - return import(file).then((result) => result.default ?? result); + const file = resolvePath(name, (name) => new URL(name, root).pathname); + return (file.match(/\.json$/) ? mathjax.json(file) : import(file)).then( + (result) => result.default ?? result + ); }; } diff --git a/ts/util/asyncLoad/fs.d.ts b/ts/util/asyncLoad/fs.d.ts new file mode 100644 index 000000000..5f5691725 --- /dev/null +++ b/ts/util/asyncLoad/fs.d.ts @@ -0,0 +1,3 @@ +declare module 'node:fs' { + export function readFileSync(file: string): any; +} diff --git a/ts/util/asyncLoad/node-import.cjs b/ts/util/asyncLoad/node-import.cjs index 9c7d5a44c..7e2f6ee18 100644 --- a/ts/util/asyncLoad/node-import.cjs +++ b/ts/util/asyncLoad/node-import.cjs @@ -22,15 +22,22 @@ */ const { mathjax } = require('../../mathjax.js'); +const { resolvePath } = require('../AsyncLoad.js'); const path = require('path'); const { dirname } = require('#source/source.cjs'); let root = path.resolve(dirname, '..', '..', 'cjs'); +mathjax.json = async function readJsonFile(name) { + return require(name); +}; + if (!mathjax.asyncLoad) { - mathjax.asyncLoad = async (name) => { - const file = name.charAt(0) === '.' ? path.resolve(root, name) : name; - return import(file).then((result) => result?.default || result); + mathjax.asyncLoad = (name) => { + const file = resolvePath(name, (name) => path.resolve(root, name)); + return (file.match(/\.json$/) ? mathjax.json(file) : import(file)).then( + (result) => result?.default ?? result + ); }; } @@ -42,4 +49,4 @@ exports.setBaseURL = function (URL) { if (!root.match(/\/$/)) { root += '/'; } -} +}; diff --git a/ts/util/asyncLoad/node.ts b/ts/util/asyncLoad/node.ts index c185eb209..70abee6a2 100644 --- a/ts/util/asyncLoad/node.ts +++ b/ts/util/asyncLoad/node.ts @@ -22,6 +22,7 @@ */ import { mathjax } from '../../mathjax.js'; +import { resolvePath } from '../AsyncLoad.js'; import * as path from 'path'; import { dirname } from '#source/source.cjs'; @@ -29,11 +30,15 @@ declare const require: (name: string) => any; let root = path.resolve(dirname, '..', '..', 'cjs'); -if (!mathjax.asyncLoad && typeof require !== 'undefined') { - mathjax.asyncLoad = (name: string) => { - return require(name.charAt(0) === '.' ? path.resolve(root, name) : name); - }; - mathjax.asyncIsSynchronous = true; +if (typeof require !== 'undefined') { + mathjax.json = async (name: string) => require(name); + + if (!mathjax.asyncLoad) { + mathjax.asyncLoad = (name: string) => { + return require(resolvePath(name, (name) => path.resolve(root, name))); + }; + mathjax.asyncIsSynchronous = true; + } } /** diff --git a/ts/util/asyncLoad/system.ts b/ts/util/asyncLoad/system.ts index 1cb1d327a..d4dbef053 100644 --- a/ts/util/asyncLoad/system.ts +++ b/ts/util/asyncLoad/system.ts @@ -23,6 +23,7 @@ import { mathjax } from '../../mathjax.js'; import { context } from '../context.js'; +import { resolvePath } from '../AsyncLoad.js'; declare const System: { import: (name: string, url?: string) => any }; declare const __dirname: string; @@ -36,9 +37,11 @@ let root = if (!mathjax.asyncLoad && typeof System !== 'undefined' && System.import) { mathjax.asyncLoad = (name: string) => { - const file = ( - name.charAt(0) === '.' ? new URL(name, root) : new URL(name, 'file://') - ).href; + const file = resolvePath( + name, + (name) => new URL(name, root).href, + (name) => new URL(name, 'file://').href + ); return System.import(file).then((result: any) => result.default ?? result); }; } diff --git a/tsconfig/components.json b/tsconfig/components.json index 41fedf842..8c13a152f 100644 --- a/tsconfig/components.json +++ b/tsconfig/components.json @@ -1,6 +1,7 @@ { "extends": "./cjs.json", "compilerOptions": { + "removeComments": false, "allowJs": true, "sourceMap": false, "declaration": false,