From eaf6da5741a6a4753292d0a0124f3ccb45d6d2f5 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Tue, 23 Dec 2025 10:01:41 -0500 Subject: [PATCH 1/3] Initial implementation of Locale framework, with updates to asyncLoad to handle json files, and updates to testsuite. --- components/json.cjs | 2 + components/mjs/core/core.js | 17 +- components/mjs/core/locale.js | 4 + .../mjs/input/tex/extensions/bbox/config.json | 5 + components/mjs/node-main/node-main.js | 31 ++- components/mjs/require/config.json | 3 +- package.json | 5 +- testsuite/lib/AsyncLoad.child.json | 3 + testsuite/lib/component/locales/en.json | 3 + testsuite/lib/component/locales/test.json | 8 + testsuite/package.json | 2 +- testsuite/pnpm-lock.yaml | 10 +- testsuite/src/node-main.d.ts | 3 + testsuite/src/setupTex.ts | 8 +- testsuite/src/source.d.ts | 3 + testsuite/tests/input/tex/Bbox.test.ts | 4 +- testsuite/tests/input/tex/Require.test.ts | 2 +- testsuite/tests/util/AsyncLoad.test.ts | 26 +- testsuite/tests/util/Locale.test.js | 15 + testsuite/tests/util/Locale.test.ts | 74 +++++ testsuite/tests/util/asyncLoad/esm.test.ts | 7 + testsuite/tests/util/asyncLoad/node.test.ts | 15 + ts/components/loader.ts | 26 +- ts/components/startup.ts | 14 +- ts/input/tex/bbox/BboxConfiguration.ts | 50 ++-- ts/input/tex/bbox/locales/en.json | 4 + ts/input/tex/require/RequireConfiguration.ts | 7 +- ts/mathjax.ts | 8 + ts/util/AsyncLoad.ts | 29 ++ ts/util/Locale.ts | 257 ++++++++++++++++++ ts/util/asyncLoad/esm.ts | 17 +- ts/util/asyncLoad/fs.d.ts | 3 + ts/util/asyncLoad/node-import.cjs | 15 +- ts/util/asyncLoad/node.ts | 15 +- ts/util/asyncLoad/system.ts | 9 +- 35 files changed, 612 insertions(+), 92 deletions(-) create mode 100644 components/json.cjs create mode 100644 components/mjs/core/locale.js create mode 100644 testsuite/lib/AsyncLoad.child.json create mode 100644 testsuite/lib/component/locales/en.json create mode 100644 testsuite/lib/component/locales/test.json create mode 100644 testsuite/src/node-main.d.ts create mode 100644 testsuite/src/source.d.ts create mode 100644 testsuite/tests/util/Locale.test.js create mode 100644 testsuite/tests/util/Locale.test.ts create mode 100644 ts/input/tex/bbox/locales/en.json create mode 100644 ts/util/Locale.ts create mode 100644 ts/util/asyncLoad/fs.d.ts 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..f8ca8ecef 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,16 @@ 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$/)) { + if (name.charAt(0) === '[') { + 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]); + }); } 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/input/tex/extensions/bbox/config.json b/components/mjs/input/tex/extensions/bbox/config.json index 3c233eb2b..992885a51 100644 --- a/components/mjs/input/tex/extensions/bbox/config.json +++ b/components/mjs/input/tex/extensions/bbox/config.json @@ -4,6 +4,11 @@ "component": "input/tex/extensions/bbox", "targets": ["input/tex/bbox"] }, + "copy": { + "to": "[bundle]/input/tex/extensions/bbox", + "from": "[ts]/input/tex/bbox", + "copy": ["locales"] + }, "webpack": { "name": "input/tex/extensions/bbox", "libs": [ diff --git a/components/mjs/node-main/node-main.js b/components/mjs/node-main/node-main.js index e09f18270..e9ef24bdd 100644 --- a/components/mjs/node-main/node-main.js +++ b/components/mjs/node-main/node-main.js @@ -21,16 +21,16 @@ 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'; -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 fs = REQUIRE("fs").promises; const dir = context.path(MathJax.config.__dirname); // set up by node-main.mjs or node-main.cjs /* @@ -48,23 +48,26 @@ 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; + +/* + * Set the asynchronous loader to handle json files + */ MathJax._.mathjax.mathjax.asyncLoad = function (name) { - return REQUIRE(name.charAt(0) === '.' ? path.resolve(ROOT, name) : - name.charAt(0) === '[' ? Package.resolvePath(name) : name); + const file = resolvePath(name, (name) => path.resolve(CONFIG.paths.mathjax, name)); + return file.match(/\.json$/) + ? fs.readFile(REQUIRE.resolve(file)).then((json) => JSON.parse(json)) + : REQUIRE(file); }; /* 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/package.json b/package.json index c80f1d28d..8ff5c2e21 100644 --- a/package.json +++ b/package.json @@ -80,10 +80,11 @@ "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": "pnpm -s log:single 'Copying TeX extension locales'; copy() { 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/package.json b/testsuite/package.json index b77609b5d..071c19c5d 100644 --- a/testsuite/package.json +++ b/testsuite/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@jest/globals": "^29.7.0", - "@mathjax/mathjax-bbm-font-extension": "^4.0.0", + "@mathjax/mathjax-bbm-font-extension": "^4.1.0", "@types/jest": "^29.5.14", "jest": "^29.7.0", "ts-jest": "^29.3.4", diff --git a/testsuite/pnpm-lock.yaml b/testsuite/pnpm-lock.yaml index 6d5af4376..72381b095 100644 --- a/testsuite/pnpm-lock.yaml +++ b/testsuite/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^29.7.0 version: 29.7.0 '@mathjax/mathjax-bbm-font-extension': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^4.1.0 + version: 4.1.0 '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -296,8 +296,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@mathjax/mathjax-bbm-font-extension@4.0.0': - resolution: {integrity: sha512-4ElFHV3T4oXP9DhyLXcvnAfqqmjf4lINFvShXFkcx7/va/TUL3EpZm/s130JQH3Sp5xX6UG09wY2/dcLONLfjQ==} + '@mathjax/mathjax-bbm-font-extension@4.1.0': + resolution: {integrity: sha512-GyZxYGZCyHXpKmxgrgIaJDePx8zkahavRfjzWBOzKO/OC+Y7Paqkf90VnTXF6FRHtgz4iMGDsqAfQngK/lyiqg==} '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1643,7 +1643,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@mathjax/mathjax-bbm-font-extension@4.0.0': {} + '@mathjax/mathjax-bbm-font-extension@4.1.0': {} '@sinclair/typebox@0.27.8': {} 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 8f8b31252..a98b886ed 100644 --- a/testsuite/src/setupTex.ts +++ b/testsuite/src/setupTex.ts @@ -13,13 +13,10 @@ import {mathjax} from '#js/mathjax.js'; import {OptionList} from '#js/util/Options.js'; import {tmpJsonFile} from '#src/constants.js'; import * as fs from 'fs'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import {init} from '#source/node-main/node-main.mjs'; 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; @@ -165,6 +162,7 @@ export function setupTex(packages: PackageList = ['base'], options: OptionList = const html = new HTMLDocument('', adaptor, {InputJax: tex}); convert = (expr: string, display: boolean) => toMathML(html.convert(expr, {display: display, end: STATE.CONVERT})); + return Locale.setLocale(); } /** @@ -189,6 +187,7 @@ export function setupTexRender(packages: PackageList = ['base'], options: Option html.findMath().compile(); return toMathML((Array.from(html.math)[0] as MATHITEM).root); } + return Locale.setLocale(); } /** @@ -263,6 +262,7 @@ export function setupTexWithOutput(packages: string[] = ['base'], options: Optio 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..d96c04162 --- /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/Bbox.test.ts b/testsuite/tests/input/tex/Bbox.test.ts index b68cd6cda..200ceb630 100644 --- a/testsuite/tests/input/tex/Bbox.test.ts +++ b/testsuite/tests/input/tex/Bbox.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeEach, describe, it } from '@jest/globals'; import { getTokens, toXmlMatch, setupTex, tex2mml, expectTexError } from '#helpers'; import '#js/input/tex/bbox/BboxConfiguration'; -beforeEach(() => setupTex(['base', 'bbox'])); +beforeEach(async () => setupTex(['base', 'bbox'])); /**********************************************************************************/ /**********************************************************************************/ @@ -118,7 +118,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`); + .toBe(`'22-11=color' doesn't look like a color, a padding dimension, or a style`); }); /********************************************************************************/ diff --git a/testsuite/tests/input/tex/Require.test.ts b/testsuite/tests/input/tex/Require.test.ts index 1e53e06e3..da72b5106 100644 --- a/testsuite/tests/input/tex/Require.test.ts +++ b/testsuite/tests/input/tex/Require.test.ts @@ -18,7 +18,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/util/AsyncLoad.test.ts b/testsuite/tests/util/AsyncLoad.test.ts index 9e3fe19a3..5f307ea04 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()', () => { @@ -36,4 +36,28 @@ describe('asyncLoad()', () => { }); + 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.js b/testsuite/tests/util/Locale.test.js new file mode 100644 index 000000000..445de284c --- /dev/null +++ b/testsuite/tests/util/Locale.test.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var globals_1 = require("@jest/globals"); +var Locale = new Locale(); +/**********************************************************************************/ +/**********************************************************************************/ +(0, globals_1.describe)('Locale', function () { + /********************************************************************************/ + (0, globals_1.test)('registerLocaleFiles', function () { + Locale.registerLocaleFiles('test'); + (0, globals_1.expect)(Local.locations.text).toEqual({}); + }); +}); +/**********************************************************************************/ +/**********************************************************************************/ diff --git a/testsuite/tests/util/Locale.test.ts b/testsuite/tests/util/Locale.test.ts new file mode 100644 index 000000000..a0a3d15ba --- /dev/null +++ b/testsuite/tests/util/Locale.test.ts @@ -0,0 +1,74 @@ +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('de')).rejects + .toMatch("MathJax(component): Can't load 'de.json': 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("No localized or default version for message with id 'Id2' from 'component'"); + expect(Locale.message('undefined', 'Id1')) + .toBe("No localized or default version for message with id 'Id1' from 'undefined'"); + expect(() => Locale.error('component', 'error', 'x')).toThrow('Error in x'); + }); + + /********************************************************************************/ + + 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; + }); + + /********************************************************************************/ + +}); + + +/**********************************************************************************/ +/**********************************************************************************/ diff --git a/testsuite/tests/util/asyncLoad/esm.test.ts b/testsuite/tests/util/asyncLoad/esm.test.ts index c29bcea23..450f411d7 100644 --- a/testsuite/tests/util/asyncLoad/esm.test.ts +++ b/testsuite/tests/util/asyncLoad/esm.test.ts @@ -12,8 +12,11 @@ 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 @@ -21,6 +24,10 @@ describe('asyncLoad() for esm', () => { 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 .then((result: any) => result.VERSION)).resolves.toBe(mathjax.version); await expect(asyncLoad('@mathjax/src/js/components/version.js') // load from module diff --git a/testsuite/tests/util/asyncLoad/node.test.ts b/testsuite/tests/util/asyncLoad/node.test.ts index 3d7a6c080..7bdf7691f 100644 --- a/testsuite/tests/util/asyncLoad/node.test.ts +++ b/testsuite/tests/util/asyncLoad/node.test.ts @@ -14,8 +14,11 @@ 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 @@ -23,6 +26,10 @@ describe('asyncLoad() for node', () => { 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 .then((result: any) => result.VERSION)).resolves.toBe(mathjax.version); await expect(asyncLoad('@mathjax/src/js/components/version.js') // load from module @@ -30,6 +37,14 @@ describe('asyncLoad() for node', () => { 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 () => { diff --git a/ts/components/loader.ts b/ts/components/loader.ts index 1ed0ae9ab..e93bbf0aa 100644 --- a/ts/components/loader.ts +++ b/ts/components/loader.ts @@ -45,11 +45,12 @@ import { context } from '../util/context.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 +100,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 +110,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 +121,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 +133,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(/\.[^/]+$/)) { @@ -411,6 +400,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/startup.ts b/ts/components/startup.ts index a85839459..508a622e6 100644 --- a/ts/components/startup.ts +++ b/ts/components/startup.ts @@ -43,6 +43,7 @@ import { DOMAdaptor } from '../core/DOMAdaptor.js'; import { PrioritizedList } from '../util/PrioritizedList.js'; import { OptionList, OPTIONS } from '../util/Options.js'; import { context } from '../util/context.js'; +import { Locale } from '../util/Locale.js'; import { TeX } from '../input/tex.js'; @@ -117,6 +118,7 @@ export interface MathJaxObject extends MJObject { defaultReady(): void; defaultPageReady(): Promise; defaultOptionError(message: string, key: string): void; + setLocale(): Promise; getComponents(): void; makeMethods(): void; makeTypesetMethods(): void; @@ -306,7 +308,8 @@ export abstract class Startup { public static defaultReady() { Startup.getComponents(); Startup.makeMethods(); - Startup.pagePromise + Startup.setLocale() + .then(() => Startup.pagePromise) .then(() => CONFIG.pageReady()) // usually the initial typesetting call .then(() => Startup.promiseResolve()) .catch((err) => Startup.promiseReject(err)); @@ -336,6 +339,15 @@ export abstract class Startup { .then(() => Startup.promiseResolve()); } + /** + * Set the locale and load any needed locale data files. + * + * @returns {Promise} A promise for when the locale is loaded and ready. + */ + public static setLocale(): Promise { + return Locale.setLocale(MathJax.config.locale || 'en'); + } + /** * The default OptionError function */ diff --git a/ts/input/tex/bbox/BboxConfiguration.ts b/ts/input/tex/bbox/BboxConfiguration.ts index 7b64601b0..35239652a 100644 --- a/ts/input/tex/bbox/BboxConfiguration.ts +++ b/ts/input/tex/bbox/BboxConfiguration.ts @@ -27,6 +27,29 @@ import TexParser from '../TexParser.js'; import { CommandMap } from '../TokenMap.js'; import { ParseMethod } from '../Types.js'; import TexError from '../TexError.js'; +import { Locale } from '../../../util/Locale.js'; + +/** + * The component name + */ +export const COMPONENT = '[tex]/bbox'; + +/** + * Register the locales + */ +Locale.registerLocaleFiles(COMPONENT, '../ts/input/tex/bbox'); + +/** + * Throw a TexError for this component (eventually, TexError will handle the message directly). + * + * @param {string} id The ID of the error message + * @param {string[]} args The values to substitute into the message + */ +function bboxError(id: string, ...args: string[]) { + const error = new TexError('', ''); + error.message = Locale.message(COMPONENT, id, ...args); + throw error; +} // Namespace const BboxMethods: { [key: string]: ParseMethod } = { @@ -50,12 +73,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 - ); + bboxError('MultipleBBoxProperty', 'Padding', name); } const pad = BBoxPadding(match[1] + match[3]); if (pad) { @@ -71,33 +89,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 - ); + bboxError('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 - ); + bboxError('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 - ); + bboxError('InvalidBBoxProperty', part); } } if (def) { diff --git a/ts/input/tex/bbox/locales/en.json b/ts/input/tex/bbox/locales/en.json new file mode 100644 index 000000000..e06b8dfba --- /dev/null +++ b/ts/input/tex/bbox/locales/en.json @@ -0,0 +1,4 @@ +{ + "MultipleBBoxProperty": "%1 specified twice in %2", + "InvalidBBoxProperty": "'%1' doesn't look like a color, a padding dimension, or a style" +} diff --git a/ts/input/tex/require/RequireConfiguration.ts b/ts/input/tex/require/RequireConfiguration.ts index 91e42be9e..c72acdcac 100644 --- a/ts/input/tex/require/RequireConfiguration.ts +++ b/ts/input/tex/require/RequireConfiguration.ts @@ -39,6 +39,7 @@ import { Loader, CONFIG as LOADERCONFIG } from '../../../components/loader.js'; import { mathjax } from '../../../mathjax.js'; import { expandable } from '../../../util/Options.js'; import { MenuMathDocument } from '../../../ui/menu/MenuHandler.js'; +import { Locale } from '../../../util/Locale.js'; /** * The MathJax configuration block (for looking up user-defined package options) @@ -176,7 +177,11 @@ export function RequireLoad(parser: TexParser, name: string) { } const data = Package.packages.get(extension); if (!data) { - mathjax.retryAfter(Loader.load(extension).catch((_) => {})); + mathjax.retryAfter( + Loader.load(extension) + .then(() => Locale.setLocale()) + .catch((_) => {}) + ); } if (data.hasFailed) { throw new TexError('RequireFail', 'Extension "%1" failed to load', name); diff --git a/ts/mathjax.ts b/ts/mathjax.ts index 19b57f98e..99e5e3bd5 100644 --- a/ts/mathjax.ts +++ b/ts/mathjax.ts @@ -77,4 +77,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: (file: string) => fetch(file).then((data) => data.json()), }; diff --git a/ts/util/AsyncLoad.ts b/ts/util/AsyncLoad.ts index 5b6ca4c67..23e4a660a 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..bb5a3b381 --- /dev/null +++ b/ts/util/Locale.ts @@ -0,0 +1,257 @@ +/************************************************************* + * + * 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", + }, + }, + }; + + /** + * The locale files to load for each locale (as registered by the components) + */ + protected static locations: { [component: string]: [string, Set] } = + {}; + + /** + * 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 byu their IDs + */ + public static registerMessages( + component: string, + locale: string, + data: messageData + ) { + if (!this.data[component]) { + this.data[component] = {}; + } + const cdata = this.data[component]; + if (!cdata[locale]) { + cdata[locale] = {}; + } + 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 { + const message = this.lookupMessage(component, id); + if (typeof data === 'string') { + 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] || + `No localized or default version for message with id '${id}' from '${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 + * + * @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 + */ + protected static localeError( + component: string, + locale: string, + error: Error + ) { + const message = this.message( + 'locale', + 'LocaleJsonError', + component, + `${locale}.json`, + error.message + ); + console.error(message); + } +} diff --git a/ts/util/asyncLoad/esm.ts b/ts/util/asyncLoad/esm.ts index ae805e290..b91acb570 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 b5ed1d464..41c4b94bb 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 5ba06e0c7..190adc151 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 c950babe9..4baa187ff 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); }; } From 60bbe811197a6ce96b0f3910ae922442eb0f62ea Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Wed, 31 Dec 2025 16:37:22 -0500 Subject: [PATCH 2/3] Remove unneeded Locale.test.js file --- testsuite/tests/util/Locale.test.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 testsuite/tests/util/Locale.test.js diff --git a/testsuite/tests/util/Locale.test.js b/testsuite/tests/util/Locale.test.js deleted file mode 100644 index 445de284c..000000000 --- a/testsuite/tests/util/Locale.test.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -var globals_1 = require("@jest/globals"); -var Locale = new Locale(); -/**********************************************************************************/ -/**********************************************************************************/ -(0, globals_1.describe)('Locale', function () { - /********************************************************************************/ - (0, globals_1.test)('registerLocaleFiles', function () { - Locale.registerLocaleFiles('test'); - (0, globals_1.expect)(Local.locations.text).toEqual({}); - }); -}); -/**********************************************************************************/ -/**********************************************************************************/ From 75a090845788bf6738fee9823aeb3d202c1d2639 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Fri, 16 Jan 2026 15:24:44 -0500 Subject: [PATCH 3/3] Update test framework to copy locale and other assets --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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