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/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/startup/init.js b/components/mjs/startup/init.js index 14c91abdb..4da1e1770 100644 --- a/components/mjs/startup/init.js +++ b/components/mjs/startup/init.js @@ -5,6 +5,7 @@ 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', 'core'); @@ -14,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 5f4f169f0..01e89b577 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,9 @@ "=============================================================================== 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' 'ts/input/tex/*/__locales__/*.json' $1/input/tex/extensions; }; 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 MathML3 extension json'; pnpm copyfiles -u 1 ts/input/mathml/mml3/mml3.sef.json $1; }; copy", "copy:pkg": "copy() { pnpm -s log:single \"Copying package.json to $1\"; pnpm copyfiles -u 2 components/bin/package.json $1; }; copy", diff --git a/ts/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/loader.ts b/ts/components/loader.ts index 4ebd2ef48..d29382859 100644 --- a/ts/components/loader.ts +++ b/ts/components/loader.ts @@ -41,6 +41,7 @@ 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. @@ -201,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. diff --git a/ts/components/startup.ts b/ts/components/startup.ts index 7bcd89a07..00e7be451 100644 --- a/ts/components/startup.ts +++ b/ts/components/startup.ts @@ -43,7 +43,6 @@ 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'; @@ -308,8 +307,7 @@ export abstract class Startup { public static defaultReady() { Startup.getComponents(); Startup.makeMethods(); - Startup.setLocale() - .then(() => Startup.pagePromise) + Startup.pagePromise .then(() => CONFIG.pageReady()) // usually the initial typesetting call .then(() => Startup.promiseResolve()) .catch((err) => Startup.promiseReject(err)); @@ -339,15 +337,6 @@ 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/require/RequireConfiguration.ts b/ts/input/tex/require/RequireConfiguration.ts index 88cb08bc9..a44178a30 100644 --- a/ts/input/tex/require/RequireConfiguration.ts +++ b/ts/input/tex/require/RequireConfiguration.ts @@ -39,7 +39,6 @@ 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'; import { COMPONENT } from './__locales__/Component.js'; export { COMPONENT }; @@ -176,11 +175,7 @@ export function RequireLoad(parser: TexParser, name: string) { } const data = Package.packages.get(extension); if (!data) { - mathjax.retryAfter( - Loader.load(extension) - .then(() => Locale.setLocale()) - .catch((_) => {}) - ); + mathjax.retryAfter(Loader.load(extension).catch((_) => {})); } if (data.hasFailed) { throw new TexError(COMPONENT, 'RequireFail', name); 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/Locale.ts b/ts/util/Locale.ts index 1ac72a89d..db5172039 100644 --- a/ts/util/Locale.ts +++ b/ts/util/Locale.ts @@ -73,7 +73,7 @@ export class Locale { * 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 @@ -97,7 +97,7 @@ export class Locale { * * @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 + * @param {messageData} data The messages indexed by their IDs */ public static registerMessages( component: string, @@ -105,11 +105,11 @@ export class Locale { data: messageData ) { if (!this.data[component]) { - this.data[component] = {}; + this.data[component] = Object.create(null); } const cdata = this.data[component]; if (!cdata[locale]) { - cdata[locale] = {}; + cdata[locale] = Object.create(null); } Object.assign(cdata[locale], data); }