diff --git a/components/bin/copy b/components/bin/copy index a1429ddda..b8d7a206a 100755 --- a/components/bin/copy +++ b/components/bin/copy @@ -50,13 +50,6 @@ const config = require(json).copy; if (!config) process.exit(); process.chdir(path.dirname(json)); -/** - * Redirect the bundle, if not the default - */ -if (bundle !== 'bundle') { - config.to = config.to.replace(/\/bundle(\/|$)/, '/' + bundle + '$1'); -} - /** * Get the directory for node modules (either the parent of the MathJax directory, * or the MathJax node_modules directory, if it exists). @@ -64,6 +57,13 @@ if (bundle !== 'bundle') { const parent = path.resolve(__dirname, '..', '..'); const nodeDir = (dir => (fs.existsSync(dir) ? dir : path.resolve(parent, '..')))(path.join(parent, 'node_modules')); +/** + * Other top-level directories + */ +const tsDir = path.resolve(parent, 'ts'); +const jsDir = path.resolve(parent, target); +const bundleDir = path.resolve(parent, bundle); + /** * Copy a file or directory tree * @@ -87,12 +87,21 @@ function copyFile(from, to, name, space) { } } +function resolvePaths(name) { + return path.resolve( + process.cwd(), + name.replace(/^\[node\]/, nodeDir) + .replace(/^\[ts\]/, tsDir) + .replace(/^\[js\]/, jsDir) + .replace(/^\[bundle\]/, bundleDir) + ); +} + /** * Copy the given files */ -const wd = process.cwd(); -const to = path.resolve(wd, config.to); -const from = path.resolve(wd, config.from.replace(/\[node\]/, nodeDir)); +const to = resolvePaths(config.to); +const from = resolvePaths(config.from); for (const name of config.copy) { copyFile(from, to, name, ''); } diff --git a/components/mjs/core/core.js b/components/mjs/core/core.js index 55bc14ed5..f6b3ab7cf 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); @@ -10,5 +12,13 @@ if (MathJax.startup) { MathJax.startup.useAdaptor('browserAdaptor'); } if (MathJax.loader) { - MathJax._.mathjax.mathjax.asyncLoad = (name => MathJax.loader.load(name)); + MathJax._.mathjax.mathjax.asyncLoad = (name => { + if (name.match(/\.json$/)) { + if (name.charAt(0) === '[') { + name = Package.resolvePath(name); + } + return fetch(name).then(response => response.json()); + } + return MathJax.loader.load(name); + }); } 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/mml/extensions/mml3/config.json b/components/mjs/input/mml/extensions/mml3/config.json index 4a0f06a9a..8afb7362f 100644 --- a/components/mjs/input/mml/extensions/mml3/config.json +++ b/components/mjs/input/mml/extensions/mml3/config.json @@ -6,8 +6,8 @@ "excludeSubdirs": true }, "copy": { - "to": "../../../../../../bundle/input/mml/extensions", - "from": "../../../../../../ts/input/mathml/mml3", + "to": "[bundle]/input/mml/extensions", + "from": "[ts]/input/mathml/mml3", "copy": [ "mml3.sef.json" ] 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/config.json b/components/mjs/node-main/config.json index 28cd82bb6..b9131328b 100644 --- a/components/mjs/node-main/config.json +++ b/components/mjs/node-main/config.json @@ -1,6 +1,6 @@ { "copy": { - "to": "../../../bundle", + "to": "[bundle]", "from": ".", "copy": [ "node-main.mjs", diff --git a/components/mjs/node-main/node-main-setup.mjs b/components/mjs/node-main/node-main-setup.mjs index f143f4233..4f0bf71e4 100644 --- a/components/mjs/node-main/node-main-setup.mjs +++ b/components/mjs/node-main/node-main-setup.mjs @@ -5,3 +5,4 @@ const path = require("path"); if (!global.MathJax) global.MathJax = {}; global.MathJax.__dirname = path.dirname(new URL(import.meta.url).pathname); +global.MathJax.__js = 'mjs'; \ No newline at end of file diff --git a/components/mjs/node-main/node-main.cjs b/components/mjs/node-main/node-main.cjs index 8978bb318..37d28e27f 100644 --- a/components/mjs/node-main/node-main.cjs +++ b/components/mjs/node-main/node-main.cjs @@ -1,5 +1,6 @@ if (!global.MathJax) global.MathJax = {}; global.MathJax.__dirname = __dirname; +global.MathJax.__js = 'cjs'; module.exports = require('./node-main.js'); diff --git a/components/mjs/node-main/node-main.js b/components/mjs/node-main/node-main.js index 83aae0afa..5033354b1 100644 --- a/components/mjs/node-main/node-main.js +++ b/components/mjs/node-main/node-main.js @@ -21,14 +21,15 @@ 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 '../core/core.js'; import '../adaptors/liteDOM/liteDOM.js'; import {source} from '../source.js'; const path = eval('require("path")'); // get path from node, not webpack -const dir = global.MathJax.config.__dirname; // set up by node-main.mjs or node-main.cjs +const fs = eval('require("fs").promises'); +const dir = MathJax.config.__dirname; // set up by node-main.mjs or node-main.cjs /* * Set up the initial configuration @@ -45,25 +46,28 @@ combineDefaults(MathJax.config, 'output', {font: 'mathjax-modern'}); */ Loader.preLoad('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/mathmaps'; - CONFIG.paths.mathjax = path.dirname(dir); + CONFIG.paths.mathjax = path.resolve(dir, '..', '..', '..', MathJax.config.__js); combineDefaults(CONFIG, 'source', source); - // - // 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); - }; } else { CONFIG.paths.mathjax = dir; } +/* + * Set the asynchronous loader to handle json files + */ +MathJax._.mathjax.mathjax.asyncLoad = function (name) { + const file = resolvePath(name, (name) => path.resolve(CONFIG.paths.mathjax, name)); + return file.match(/\.json$/) ? + fs.readFile(file).then((json) => JSON.parse(json)) : + MathJax.config.loader.require(file); +}; + /* * The initialization function. Use as: * diff --git a/components/mjs/sre/config.json b/components/mjs/sre/config.json index 5cca64ca2..ba603b8d9 100644 --- a/components/mjs/sre/config.json +++ b/components/mjs/sre/config.json @@ -1,6 +1,6 @@ { "copy": { - "to": "../../../bundle/sre", + "to": "[bundle]/sre", "from": "[node]/speech-rule-engine/lib", "copy": [ "mathmaps" diff --git a/package.json b/package.json index fdef16b2f..cfcd42dca 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,10 @@ "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; }; 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; }; 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 \\\\033[32m$1\\\\033[0m; }; log", diff --git a/ts/components/startup.ts b/ts/components/startup.ts index a8307bd77..f3847c45c 100644 --- a/ts/components/startup.ts +++ b/ts/components/startup.ts @@ -36,6 +36,7 @@ import {CommonOutputJax} from '../output/common.js'; import {DOMAdaptor} from '../core/DOMAdaptor.js'; import {PrioritizedList} from '../util/PrioritizedList.js'; import {OptionList, OPTIONS} from '../util/Options.js'; +import {Locale} from '../util/Locale.js'; import {TeX} from '../input/tex.js'; @@ -107,6 +108,7 @@ export interface MathJaxObject extends MJObject { toMML(node: MmlNode): string; defaultReady(): void; defaultPageReady(): Promise; + setLocale(): Promise; getComponents(): void; makeMethods(): void; makeTypesetMethods(): void; @@ -293,7 +295,8 @@ export namespace Startup { export function defaultReady() { getComponents(); makeMethods(); - pagePromise + setLocale() + .then(() => pagePromise) .then(() => CONFIG.pageReady()) // usually the initial typesetting call .then(() => promiseResolve()) .catch((err) => promiseReject(err)); @@ -315,6 +318,13 @@ export namespace Startup { .then(() => promiseResolve()); } + /** + * Set the locale and load any needed locale data files. + */ + export function setLocale() { + return Locale.setLocale(MathJax.config.locale || 'en'); + } + /** * Perform the typesetting with handling of retries */ diff --git a/ts/input/tex/bbox/BboxConfiguration.ts b/ts/input/tex/bbox/BboxConfiguration.ts index 783a54e6f..ad1897e4c 100644 --- a/ts/input/tex/bbox/BboxConfiguration.ts +++ b/ts/input/tex/bbox/BboxConfiguration.ts @@ -28,82 +28,101 @@ 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'; -// Namespace -const BboxMethods: {[key: string]: ParseMethod} = { +/** + * The component name + */ +export const COMPONENT = '[tex]/bbox'; /** - * Implements MathJax Bbox macro to pad and colorize background boxes. - * @param {TexParser} parser The current tex parser. - * @param {string} name The name of the calling macro. + * Register the locales */ -BBox(parser: TexParser, name: string) { - const bbox = parser.GetBrackets(name, ''); - let math = parser.ParseArg(name); - const parts = bbox.split(/,/); - let def, background, style; - for (let i = 0, m = parts.length; i < m; i++) { - const part = parts[i].trim(); - const match = part.match(/^(\.\d+|\d+(\.\d*)?)(pt|em|ex|mu|px|in|cm|mm)$/); - if (match) { - // @test Bbox-Padding - if (def) { - // @test Bbox-Padding-Error - throw new TexError('MultipleBBoxProperty', '%1 specified twice in %2', 'Padding', name); - } - const pad = BBoxPadding(match[1] + match[3]); - if (pad) { +Locale.registerLocaleFiles(COMPONENT); + +/** + * 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} = { + + /** + * Implements MathJax Bbox macro to pad and colorize background boxes. + * @param {TexParser} parser The current tex parser. + * @param {string} name The name of the calling macro. + */ + BBox(parser: TexParser, name: string) { + const bbox = parser.GetBrackets(name, ''); + let math = parser.ParseArg(name); + const parts = bbox.split(/,/); + let def, background, style; + for (let i = 0, m = parts.length; i < m; i++) { + const part = parts[i].trim(); + const match = part.match(/^(\.\d+|\d+(\.\d*)?)(pt|em|ex|mu|px|in|cm|mm)$/); + if (match) { // @test Bbox-Padding - def = { - height: '+' + pad, - depth: '+' + pad, - lspace: pad, - width: '+' + (2 * parseInt(match[1], 10)) + match[3] - }; + if (def) { + // @test Bbox-Padding-Error + bboxError('MultipleBBoxProperty', 'Padding', name); + } + const pad = BBoxPadding(match[1] + match[3]); + if (pad) { + // @test Bbox-Padding + def = { + height: '+' + pad, + depth: '+' + pad, + lspace: pad, + width: '+' + (2 * parseInt(match[1], 10)) + match[3] + }; + } + } else if (part.match(/^([a-z0-9]+|\#[0-9a-f]{6}|\#[0-9a-f]{3})$/i)) { + // @test Bbox-Background + if (background) { + // @test Bbox-Background-Error + bboxError('MultipleBBoxProperty', 'Background', name); + } + background = part; + } else if (part.match(/^[-a-z]+:/i)) { + // @test Bbox-Frame + if (style) { + // @test Bbox-Frame-Error + bboxError('MultipleBBoxProperty', 'Style', name); + } + style = BBoxStyle(part); + } else if (part !== '') { + // @test Bbox-General-Error + bboxError('InvalidBBoxProperty', part); } - } else if (part.match(/^([a-z0-9]+|\#[0-9a-f]{6}|\#[0-9a-f]{3})$/i)) { - // @test Bbox-Background + } + if (def) { + // @test Bbox-Padding + math = parser.create('node', 'mpadded', [math], def); + } + if (background || style) { + def = {}; if (background) { - // @test Bbox-Background-Error - throw new TexError('MultipleBBoxProperty', '%1 specified twice in %2', - 'Background', name); + // @test Bbox-Background + Object.assign(def, {mathbackground: background}); } - 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); + // @test Bbox-Frame + Object.assign(def, {style: style}); } - 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); + math = parser.create('node', 'mstyle', [math], def); } + parser.Push(math); } - if (def) { - // @test Bbox-Padding - math = parser.create('node', 'mpadded', [math], def); - } - if (background || style) { - def = {}; - if (background) { - // @test Bbox-Background - Object.assign(def, {mathbackground: background}); - } - if (style) { - // @test Bbox-Frame - Object.assign(def, {style: style}); - } - math = parser.create('node', 'mstyle', [math], def); - } - parser.Push(math); -}, } @@ -116,10 +135,8 @@ let BBoxPadding = function(pad: string) { return pad; }; - new CommandMap('bbox', {bbox: BboxMethods.BBox}); - export const BboxConfiguration = Configuration.create( 'bbox', {[ConfigurationType.HANDLER]: {[HandlerType.MACRO]: ['bbox']}} ); 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 5d6815f8f..3106bdb9c 100644 --- a/ts/input/tex/require/RequireConfiguration.ts +++ b/ts/input/tex/require/RequireConfiguration.ts @@ -22,8 +22,7 @@ * @author dpvc@mathjax.org (Davide P. Cervone) */ -import { - HandlerType, ConfigurationType} from '../HandlerTypes.js'; +import {HandlerType, ConfigurationType} from '../HandlerTypes.js'; import {Configuration, ParserConfiguration, ConfigurationHandler} from '../Configuration.js'; import TexParser from '../TexParser.js'; import {CommandMap} from '../TokenMap.js'; @@ -37,6 +36,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) @@ -151,7 +151,7 @@ export function RequireLoad(parser: TexParser, name: string) { throw new TexError('BadRequire', 'Extension "%1" is not allowed to be loaded', extension); } if (!Package.packages.has(extension)) { - mathjax.retryAfter(Loader.load(extension)); + mathjax.retryAfter(Loader.load(extension).then(() => Locale.setLocale())); } const require = LOADERCONFIG[extension]?.rendererExtensions; (MathJax.startup.document as MenuMathDocument)?.menu?.addRequiredExtensions?.(require); diff --git a/ts/util/AsyncLoad.ts b/ts/util/AsyncLoad.ts index 470eb4414..41dea6297 100644 --- a/ts/util/AsyncLoad.ts +++ b/ts/util/AsyncLoad.ts @@ -42,3 +42,26 @@ 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 + * @return {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..e22540e45 --- /dev/null +++ b/ts/util/Locale.ts @@ -0,0 +1,223 @@ +/************************************************************* + * + * 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. + */ + +/** + * @fileoverview 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 + */ + protected static data: componentData = {}; + + /** + * 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', 'Hello %{name}!', {name: 'World'})); + * Locale.message('[my]/test', 'FooBar', '%1 bar', 'Foo')); + * + * @param{string} component The component whose message is requested + * @param{string} id The id of the message + * @param{string|namedDat} data The first argument or the object of names arguments + * @param{srting[]} ...args Any additional string arguments (if data is a string) + * @return{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 + * @return{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}'`; + } + + /** + * Substitue arguments into a message string + * + * @param{string} message The original message string + * @param{namedData} data The mapping of markers to values + * @return{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|namedDat} data The first argument or the object of names arguments + * @param{srting[]} ...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) + * @return{Promise} A promise that resolves when the locale files have been loaded + */ + public static async setLocale(locale: string = this.current): Promise { + const promises = []; + for (const [component, [directory, loaded]] of Object.entries(this.locations)) { + if (!loaded.has(locale)) { + loaded.add(locale); + promises.push(this.getLocaleData(component, this.current, `${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 + * @return{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( + 'core', + 'LocaleJsonError', + "MathJax(%1): Can't load '%2': %3", + component, + `${locale}.json`, + error.message + ); + console.log(message); + } + +} diff --git a/ts/util/asyncLoad/esm.ts b/ts/util/asyncLoad/esm.ts index 2c819d6fd..cfaa74e66 100644 --- a/ts/util/asyncLoad/esm.ts +++ b/ts/util/asyncLoad/esm.ts @@ -22,12 +22,13 @@ */ import {mathjax} from '../../mathjax.js'; +import {resolvePath} from '../AsyncLoad.js'; let root = new URL(import.meta.url).href.replace(/\/util\/asyncLoad\/esm.js$/, '/'); if (!mathjax.asyncLoad) { mathjax.asyncLoad = async (name: string) => { - const file = (name.charAt(0) === '.' ? new URL(name, root).pathname : name); + const file = resolvePath(name, (name) => new URL(name, root).pathname); return import(file).then((result) => result?.default || result); }; } diff --git a/ts/util/asyncLoad/node.ts b/ts/util/asyncLoad/node.ts index c9b8d3696..e49ff2099 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 {src} from '#source/source.cjs'; @@ -31,7 +32,7 @@ let root = path.resolve(src, '..', '..', 'cjs'); if (!mathjax.asyncLoad && typeof require !== 'undefined') { mathjax.asyncLoad = (name: string) => { - return require(name.charAt(0) === '.' ? path.resolve(root, name) : name); + 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 19db5dc0c..0f2423aa8 100644 --- a/ts/util/asyncLoad/system.ts +++ b/ts/util/asyncLoad/system.ts @@ -22,6 +22,7 @@ */ import {mathjax} from '../../mathjax.js'; +import {resolvePath} from '../AsyncLoad.js'; declare var System: {import: (name: string, url?: string) => any}; declare var __dirname: string; @@ -30,7 +31,7 @@ let root = 'file://' + __dirname.replace(/\/[^\/]*\/[^\/]*$/, '/'); 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); }; }