diff --git a/packages/core/src/Client.ts b/packages/core/src/Client.ts index 9d5059158c..152620a9dc 100644 --- a/packages/core/src/Client.ts +++ b/packages/core/src/Client.ts @@ -67,68 +67,6 @@ class Client { } }; - /** - * Defines the language of the client, eg. `en` for english, `de` for german etc. - * The special value `none` will disable all built-in internationalization and - * resource loading. See {@link Translations.getSpecialBundle} for handling identifiers - * with and without a dash. - * - * If internationalization is disabled, then the following variables should be - * overridden to reflect the current language of the system. These variables are - * cleared when i18n is disabled. - * {@link Editor.askZoomResource}, {@link Editor.lastSavedResource}, - * {@link Editor.currentFileResource}, {@link Editor.propertiesResource}, - * {@link Editor.tasksResource}, {@link Editor.helpResource}, {@link Editor.outlineResource}, - * {@link ElbowEdgeHandler#doubleClickOrientationResource}, {@link utils.errorResource}, - * {@link utils.closeResource}, {@link GraphSelectionModel#doneResource}, - * {@link GraphSelectionModel#updatingSelectionResource}, {@link GraphView#doneResource}, - * {@link GraphView#updatingDocumentResource}, {@link CellRenderer#collapseExpandResource}, - * {@link Graph#containsValidationErrorsResource} and - * {@link Graph#alreadyConnectedResource}. - */ - static language = typeof window !== 'undefined' ? navigator.language : 'en'; - - static setLanguage = (value: string | undefined | null) => { - if (typeof value !== 'undefined' && value != null) { - Client.language = value; - } else { - Client.language = navigator.language; - } - }; - - /** - * Defines the default language which is used in the common resource files. Any - * resources for this language will only load the common resource file, but not - * the language-specific resource file. - * @default 'en' - */ - static defaultLanguage = 'en'; - - static setDefaultLanguage = (value: string | undefined | null) => { - if (typeof value !== 'undefined' && value != null) { - Client.defaultLanguage = value; - } else { - Client.defaultLanguage = 'en'; - } - }; - - /** - * Defines the optional array of all supported language extensions. The default - * language does not have to be part of this list. See - * {@link Translations#isLanguageSupported}. - * - * This is used to avoid unnecessary requests to language files, ie. if a 404 - * will be returned. - * @default null - */ - static languages: string[] | null = null; - - static setLanguages = (value: string[] | null | undefined) => { - if (typeof value !== 'undefined' && value != null) { - Client.languages = value; - } - }; - /** * True if the current browser is Microsoft Edge. */ diff --git a/packages/core/src/editor/Editor.ts b/packages/core/src/editor/Editor.ts index 935c3afd3a..2a901a3967 100644 --- a/packages/core/src/editor/Editor.ts +++ b/packages/core/src/editor/Editor.ts @@ -20,7 +20,6 @@ import EditorPopupMenu from './EditorPopupMenu'; import UndoManager from '../view/undoable_changes/UndoManager'; import EditorKeyHandler from './EditorKeyHandler'; import EventSource from '../view/event/EventSource'; -import Translations from '../util/Translations'; import Client from '../Client'; import CompactTreeLayout from '../view/layout/CompactTreeLayout'; import { EditorToolbar } from './EditorToolbar'; @@ -56,9 +55,13 @@ import ConnectionHandler from '../view/handler/ConnectionHandler'; import { show } from '../util/printUtils'; import PanningHandler from '../view/handler/PanningHandler'; import { cloneCell } from '../util/cellArrayUtils'; +import { GlobalConfig } from '../util/config'; // TODO disabled side effects, so editor resources are not loaded by default // This should be done in a different way + +// This can be done on demand by calling Translations.loadResources +// Taken from https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/editor/mxEditor.js#L394-L405 /** * Installs the required language resources at class * loading time. @@ -461,8 +464,7 @@ export class Editor extends EventSource { * key does not exist then the value is used as the error message. Default is 'askZoom'. * @default 'askZoom' */ - // askZoomResource: 'askZoom' | ''; - askZoomResource = Client.language !== 'none' ? 'askZoom' : ''; + askZoomResource = GlobalConfig.i18n.isEnabled() ? 'askZoom' : ''; /** * Group: Controls and Handlers @@ -472,14 +474,14 @@ export class Editor extends EventSource { * this key does not exist then the value is used as the error message. Default is 'lastSaved'. * @default 'lastSaved'. */ - lastSavedResource = Client.language !== 'none' ? 'lastSaved' : ''; + lastSavedResource = GlobalConfig.i18n.isEnabled() ? 'lastSaved' : ''; /** * Specifies the resource key for the current file info. If the resource for * this key does not exist then the value is used as the error message. Default is 'currentFile'. * @default 'currentFile' */ - currentFileResource = Client.language !== 'none' ? 'currentFile' : ''; + currentFileResource = GlobalConfig.i18n.isEnabled() ? 'currentFile' : ''; /** * Specifies the resource key for the properties window title. If the @@ -487,7 +489,7 @@ export class Editor extends EventSource { * error message. Default is 'properties'. * @default 'properties' */ - propertiesResource = Client.language !== 'none' ? 'properties' : ''; + propertiesResource = GlobalConfig.i18n.isEnabled() ? 'properties' : ''; /** * Specifies the resource key for the tasks window title. If the @@ -495,7 +497,7 @@ export class Editor extends EventSource { * error message. Default is 'tasks'. * @default 'tasks' */ - tasksResource = Client.language !== 'none' ? 'tasks' : ''; + tasksResource = GlobalConfig.i18n.isEnabled() ? 'tasks' : ''; /** * Specifies the resource key for the help window title. If the @@ -503,7 +505,7 @@ export class Editor extends EventSource { * error message. Default is 'help'. * @default 'help' */ - helpResource = Client.language !== 'none' ? 'help' : ''; + helpResource = GlobalConfig.i18n.isEnabled() ? 'help' : ''; /** * Specifies the resource key for the outline window title. If the @@ -511,7 +513,7 @@ export class Editor extends EventSource { * error message. Default is 'outline'. * @default 'outline' */ - outlineResource = Client.language !== 'none' ? 'outline' : ''; + outlineResource = GlobalConfig.i18n.isEnabled() ? 'outline' : ''; /** * Reference to the {@link MaxWindow} that contains the outline. The {@link outline} @@ -1249,7 +1251,7 @@ export class Editor extends EventSource { this.addAction('zoom', (editor: Editor) => { const current = editor.graph.getView().scale * 100; const preInput = prompt( - Translations.get(editor.askZoomResource) || editor.askZoomResource, + GlobalConfig.i18n.get(editor.askZoomResource) || editor.askZoomResource, String(current) ); @@ -1745,7 +1747,7 @@ export class Editor extends EventSource { const tstamp = new Date().toLocaleString(); this.setStatus( `${ - Translations.get(this.lastSavedResource) || this.lastSavedResource + GlobalConfig.i18n.get(this.lastSavedResource) || this.lastSavedResource }: ${tstamp}` ); }); @@ -1754,7 +1756,7 @@ export class Editor extends EventSource { // when new files are opened this.addListener(InternalEvent.OPEN, () => { this.setStatus( - `${Translations.get(this.currentFileResource) || this.currentFileResource}: ${ + `${GlobalConfig.i18n.get(this.currentFileResource) || this.currentFileResource}: ${ this.filename }` ); @@ -2078,7 +2080,7 @@ export class Editor extends EventSource { // Displays the contents in a window and stores a reference to the // window for later hiding of the window this.properties = new MaxWindow( - Translations.get(this.propertiesResource) || this.propertiesResource, + GlobalConfig.i18n.get(this.propertiesResource) || this.propertiesResource, node, x, y, @@ -2265,7 +2267,7 @@ export class Editor extends EventSource { div.style.paddingLeft = '20px'; const w = document.body.clientWidth; const wnd = new MaxWindow( - Translations.get(this.tasksResource) || this.tasksResource, + GlobalConfig.i18n.get(this.tasksResource) || this.tasksResource, div, w - 220, this.tasksTop, @@ -2334,7 +2336,10 @@ export class Editor extends EventSource { showHelp(tasks: any | null = null): void { if (this.help == null) { const frame = document.createElement('iframe'); - frame.setAttribute('src', (Translations.get('urlHelp') || this.urlHelp)); + frame.setAttribute( + 'src', + (GlobalConfig.i18n.get('urlHelp') || this.urlHelp) + ); frame.setAttribute('height', '100%'); frame.setAttribute('width', '100%'); frame.setAttribute('frameBorder', '0'); @@ -2344,7 +2349,7 @@ export class Editor extends EventSource { const h = document.body.clientHeight || document.documentElement.clientHeight; const wnd = new MaxWindow( - Translations.get(this.helpResource) || this.helpResource, + GlobalConfig.i18n.get(this.helpResource) || this.helpResource, frame, (w - this.helpWidth) / 2, (h - this.helpHeight) / 3, @@ -2398,7 +2403,7 @@ export class Editor extends EventSource { div.style.cursor = 'move'; const wnd = new MaxWindow( - Translations.get(this.outlineResource) || this.outlineResource, + GlobalConfig.i18n.get(this.outlineResource) || this.outlineResource, div, 600, 480, diff --git a/packages/core/src/editor/EditorPopupMenu.ts b/packages/core/src/editor/EditorPopupMenu.ts index 6d7f01a6c7..514f5f9901 100644 --- a/packages/core/src/editor/EditorPopupMenu.ts +++ b/packages/core/src/editor/EditorPopupMenu.ts @@ -19,10 +19,9 @@ limitations under the License. import Cell from '../view/cell/Cell'; import MaxPopupMenu from '../gui/MaxPopupMenu'; import { getTextContent } from '../util/domUtils'; -import Translations from '../util/Translations'; import Editor from './Editor'; - import { PopupMenuItem } from '../types'; +import { GlobalConfig } from '../util/config'; /** * Creates popupmenus for mouse events. This object holds an XML node which is a description of the popup menu to be created. In {@link createMenu}, the configuration is applied to the context and the resulting menu items are added to the menu dynamically. See {@link createMenu} for a description of the configuration format. @@ -185,7 +184,7 @@ export class EditorPopupMenu { if (condition == null || conditions[condition]) { let as = item.getAttribute('as'); - as = Translations.get(as) || as; + as = GlobalConfig.i18n.get(as) || as; const funct = eval(getTextContent((item))); const action = item.getAttribute('action'); let icon = item.getAttribute('icon'); diff --git a/packages/core/src/gui/MaxForm.ts b/packages/core/src/gui/MaxForm.ts index 8c583487b3..7fc6c07d3e 100644 --- a/packages/core/src/gui/MaxForm.ts +++ b/packages/core/src/gui/MaxForm.ts @@ -19,7 +19,7 @@ limitations under the License. import Client from '../Client'; import InternalEvent from '../view/event/InternalEvent'; import { write, writeln } from '../util/domUtils'; -import Translations from '../util/Translations'; +import { GlobalConfig } from '../util/config'; /** * A simple class for creating HTML forms. @@ -65,7 +65,7 @@ class MaxForm { // Adds the ok button let button = document.createElement('button'); - write(button, Translations.get('ok') || 'OK'); + write(button, GlobalConfig.i18n.get('ok') || 'OK'); td.appendChild(button); InternalEvent.addListener(button, 'click', () => { @@ -74,7 +74,7 @@ class MaxForm { // Adds the cancel button button = document.createElement('button'); - write(button, Translations.get('cancel') || 'Cancel'); + write(button, GlobalConfig.i18n.get('cancel') || 'Cancel'); td.appendChild(button); InternalEvent.addListener(button, 'click', () => { diff --git a/packages/core/src/gui/MaxWindow.ts b/packages/core/src/gui/MaxWindow.ts index 0b34a48ef8..4b086d55fe 100644 --- a/packages/core/src/gui/MaxWindow.ts +++ b/packages/core/src/gui/MaxWindow.ts @@ -24,7 +24,7 @@ import InternalEvent from '../view/event/InternalEvent'; import Client from '../Client'; import { NODETYPE } from '../util/Constants'; import { br, write } from '../util/domUtils'; -import Translations from '../util/Translations'; +import { GlobalConfig } from '../util/config'; import { getClientX, getClientY } from '../util/EventUtils'; import { htmlEntities } from '../util/StringUtils'; import { utils } from '../util/Utils'; @@ -1058,7 +1058,7 @@ export const error = ( const w = document.body.clientWidth; const h = document.body.clientHeight || document.documentElement.clientHeight; const warn = new MaxWindow( - Translations.get(utils.errorResource) || utils.errorResource, + GlobalConfig.i18n.get(utils.errorResource) || utils.errorResource, div, (w - width) / 2, h / 4, @@ -1080,7 +1080,7 @@ export const error = ( warn.destroy(); }); - write(button, Translations.get(utils.closeResource) || utils.closeResource); + write(button, GlobalConfig.i18n.get(utils.closeResource) || utils.closeResource); tmp.appendChild(button); div.appendChild(tmp); diff --git a/packages/core/src/util/Translations.ts b/packages/core/src/i18n/Translations.ts similarity index 85% rename from packages/core/src/util/Translations.ts rename to packages/core/src/i18n/Translations.ts index fb7579faa9..fade630ea0 100644 --- a/packages/core/src/util/Translations.ts +++ b/packages/core/src/i18n/Translations.ts @@ -17,9 +17,24 @@ limitations under the License. */ import Client from '../Client'; -import { NONE } from './Constants'; -import { get, load } from './MaxXmlRequest'; -import type MaxXmlRequest from './MaxXmlRequest'; +import { NONE } from '../util/Constants'; +import { get, load } from '../util/MaxXmlRequest'; +import type MaxXmlRequest from '../util/MaxXmlRequest'; +import type { I18nProvider } from './api'; +import { TranslationsConfig } from './config'; + +// TODO make available in the root index.ts +// TODO find a better name +export class TranslationsAsI18n implements I18nProvider { + isEnabled(): boolean { + return TranslationsConfig.isI18nEnabled(); + } + get(key: string) { + return Translations.get(key); + } +} + +// mxGraph source code: https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/util/mxResources.js /** * Implements internationalization. You can provide any number of @@ -52,7 +67,7 @@ import type MaxXmlRequest from './MaxXmlRequest'; * argument passed to {@link get}. The placeholder {1} maps to the first * element in the array (at index 0). * - * See {@link Client.language} for more information on specifying the default + * See {@link I18nConfig.getLanguage} for more information on specifying the default * language or disabling all loading of resources. * * Lines that start with a # sign will be ignored. @@ -66,13 +81,9 @@ import type MaxXmlRequest from './MaxXmlRequest'; * See {@link resourcesEncoded} to disable this. If you disable this, make sure that * your files are UTF-8 encoded. * - * ## Asynchronous loading - * - * TODO the following is taken from mxGraph and is probably no longer accurate + * ## Loading default resources * - * By default, the core adds two resource files synchronously at load time. - * To load these files asynchronously, set {@link LoadResources} to false - * before loading Client and use {@link loadResources} instead. + * Call {@link loadResources} to load the default resources file for both {@link Graph} and {@link Editor}. */ class Translations { /* @@ -87,32 +98,32 @@ class Translations { static extension = '.txt'; /** - * Specifies whether values in resource files are encoded with \u or - * percentage. Default is false. + * Specifies whether values in resource files are encoded with `\u` or percentage. + * @default false */ static resourcesEncoded = false; /** * Specifies if the default file for a given basename should be loaded. - * Default is true. + * @default true */ static loadDefaultBundle = true; /** - * Specifies if the specific language file file for a given basename should - * be loaded. Default is true. + * Specifies if the specific language file for a given basename should be loaded. + * @default true */ static loadSpecialBundle = true; /** * Hook for subclassers to disable support for a given language. This - * implementation returns true if lan is in . + * implementation returns true if `lan` is in {@link I18nConfig.getLanguage()s}. * * @param lan The current language. */ static isLanguageSupported = (lan: string): boolean => { - if (Client.languages != null) { - return Client.languages.indexOf(lan) >= 0; + if (TranslationsConfig.languages != null) { + return TranslationsConfig.languages.indexOf(lan) >= 0; } return true; }; @@ -135,21 +146,21 @@ class Translations { /** * Hook for subclassers to return the URL for the special bundle. This * implementation returns `basename + '_' + lan + ` or `null` if - * {@link Translations.loadSpecialBundle} is `false` or `lan` equals {@link Client.defaultLanguage}. + * {@link loadSpecialBundle} is `false` or `lan` equals {@link TranslationsConfig.defaultLanguage}. * - * If {@link Translations#languages} is not null and {@link Client.language} contains - * a dash, then this method checks if {@link Translations.isLanguageSupported} returns `true` + * If {@link languages} is not `null` and {@link I18nConfig.getLanguage} contains + * a dash, then this method checks if {@link isLanguageSupported} returns `true` * for the full language (including the dash). If that returns false the * first part of the language (up to the dash) will be tried as an extension. * - * If {@link Translations#language} is null then the first part of the language is + * If {@link languages} is `null` then the first part of the language is * used to maintain backwards compatibility. * * @param basename The basename for which the file should be loaded. * @param lan The language for which the file should be loaded. */ static getSpecialBundle = (basename: string, lan: string): string | null => { - if (Client.languages == null || !Translations.isLanguageSupported(lan)) { + if (TranslationsConfig.languages == null || !Translations.isLanguageSupported(lan)) { const dash = lan.indexOf('-'); if (dash > 0) { @@ -160,7 +171,7 @@ class Translations { if ( Translations.loadSpecialBundle && Translations.isLanguageSupported(lan) && - lan != Client.defaultLanguage + lan != TranslationsConfig.defaultLanguage ) { return `${basename}_${lan}${Translations.extension}`; } @@ -191,7 +202,11 @@ class Translations { callback: Function | null = null ): void => { lan = - lan != null ? lan : Client.language != null ? Client.language.toLowerCase() : NONE; + lan != null + ? lan + : TranslationsConfig.getLanguage() != null + ? TranslationsConfig.getLanguage().toLowerCase() + : NONE; if (lan !== NONE) { const defaultBundle = Translations.getDefaultBundle(basename, lan); diff --git a/packages/core/src/i18n/api.ts b/packages/core/src/i18n/api.ts new file mode 100644 index 0000000000..c735801572 --- /dev/null +++ b/packages/core/src/i18n/api.ts @@ -0,0 +1,52 @@ +/* +Copyright 2025-present The maxGraph project Contributors + +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. +*/ + +// TODO create a new JSDoc category, use it everywhere and configure it for use by typedoc + +/** + * @experimental subject to change or removal. The XXX system may be modified in the future without prior notice. + * @since 0.xx.0 + */ +// TODO find a better name (same for concrete implementations) +// TODO move to the root types.ts file for consistency with other types +export interface I18nProvider { + /** + * Returns whether internationalization is enabled. + */ + isEnabled(): boolean; + + // TODO find a better name for the method + get(key: string, params?: any[] | null, defaultValue?: string | null): string | null; +} + +/** + * A {@link I18nProvider} that does nothing. + * + * @experimental subject to change or removal. The XXX system may be modified in the future without prior notice. + * @since 0.XXX.0 + * @category YYYY + */ +export class NoOpI18n implements I18nProvider { + isEnabled() { + return false; + } + + get(_key: string) { + // TODO temp implementation + console.warn('NoOpI18n.get() called with key' + _key); + return null; + } +} diff --git a/packages/core/src/i18n/config.ts b/packages/core/src/i18n/config.ts new file mode 100644 index 0000000000..b1df7259ac --- /dev/null +++ b/packages/core/src/i18n/config.ts @@ -0,0 +1,104 @@ +/* +Copyright 2025-present The maxGraph project Contributors + +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. +*/ + +function getNavigatorLanguage() { + return typeof window !== 'undefined' ? navigator.language : 'en'; +} + +// Implementation extracted from Client +// TODO find a better name or make anonymous +class TranslationsConfigBase { + private language = getNavigatorLanguage(); + + /** + * Returns whether internationalization is enabled. + */ + isI18nEnabled(): boolean { + // TODO use the NONE constant here + return this.getLanguage() !== 'none'; + } + + /** + * Defines the language of the client, eg. `en` for english, `de` for german etc. + * The special value `none` will disable all built-in internationalization and + * resource loading. See {@link Translations.getSpecialBundle} for handling identifiers + * with and without a dash. + * + * If internationalization is disabled, then the following variables should be + * overridden to reflect the current language of the system. These variables are + * cleared when i18n is disabled. + * {@link Editor.askZoomResource}, {@link Editor.lastSavedResource}, + * {@link Editor.currentFileResource}, {@link Editor.propertiesResource}, + * {@link Editor.tasksResource}, {@link Editor.helpResource}, {@link Editor.outlineResource}, + * {@link ElbowEdgeHandler#doubleClickOrientationResource}, {@link utils.errorResource}, + * {@link utils.closeResource}, {@link GraphSelectionModel#doneResource}, + * {@link GraphSelectionModel#updatingSelectionResource}, {@link GraphView#doneResource}, + * {@link GraphView#updatingDocumentResource}, {@link CellRenderer#collapseExpandResource}, + * {@link Graph#containsValidationErrorsResource} and + * {@link Graph#alreadyConnectedResource}. + */ + getLanguage(): string { + return this.language; + } + + setLanguage(value: string | undefined | null): void { + if (typeof value !== 'undefined' && value != null) { + this.language = value; + } else { + this.language = getNavigatorLanguage(); + } + } + + /** + * Defines the optional array of all supported language extensions. The default + * language does not have to be part of this list. See + * {@link Translations#isLanguageSupported}. + * + * This is used to avoid unnecessary requests to language files, ie. if a 404 + * will be returned. + * @default null + */ + languages: string[] | null = null; + + setLanguages(value: string[] | null | undefined): void { + if (typeof value !== 'undefined' && value != null) { + this.languages = value; + } + } + + /** + * Defines the default language which is used in the common resource files. Any + * resources for this language will only load the common resource file, but not + * the language-specific resource file. + * @default 'en' + */ + defaultLanguage = 'en'; + + setDefaultLanguage(value: string | undefined | null): void { + if (typeof value !== 'undefined' && value != null) { + this.defaultLanguage = value; + } else { + this.defaultLanguage = 'en'; + } + } +} + +/** + * Global configuration for {@link Translations}. + */ +// TODO export in root index.ts +// TODO add reference in documentation about global configuration +export const TranslationsConfig = new TranslationsConfigBase(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index acfee0a0ca..ca6ee6d5b9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -128,7 +128,7 @@ export { default as StencilShapeRegistry } from './view/geometry/node/StencilSha export * as constants from './util/Constants'; export { default as Guide } from './view/other/Guide'; -export { default as Translations } from './util/Translations'; +export { default as Translations } from './i18n/Translations'; export * as cellArrayUtils from './util/cellArrayUtils'; export * as cloneUtils from './util/cloneUtils'; diff --git a/packages/core/src/serialization/codecs/editor/EditorCodec.ts b/packages/core/src/serialization/codecs/editor/EditorCodec.ts index 43f53d4541..8b1ed745d7 100644 --- a/packages/core/src/serialization/codecs/editor/EditorCodec.ts +++ b/packages/core/src/serialization/codecs/editor/EditorCodec.ts @@ -18,7 +18,8 @@ import ObjectCodec from '../../ObjectCodec'; import Editor from '../../../editor/Editor'; import type Codec from '../../Codec'; import MaxWindow from '../../../gui/MaxWindow'; -import Translations from '../../../util/Translations'; +import Translations from '../../../i18n/Translations'; +import { GlobalConfig } from '../../../util/config'; import { addLinkToHead, getChildNodes } from '../../../util/domUtils'; /** @@ -170,7 +171,7 @@ export class EditorCodec extends ObjectCodec { } const wnd = new MaxWindow( - Translations.get(as) || as, + GlobalConfig.i18n.get(as) || as, element, x, y, @@ -196,6 +197,8 @@ export class EditorCodec extends ObjectCodec { //editor.setMapContainer(element); } } else if (tmp.nodeName === 'resource') { + // The "resource" content is specific to the Translations implementation, so use it here + // TODO see if we can find an alternative Translations.add(tmp.getAttribute('basename')); } else if (tmp.nodeName === 'stylesheet') { addLinkToHead('stylesheet', tmp.getAttribute('name')); diff --git a/packages/core/src/serialization/codecs/editor/EditorToolbarCodec.ts b/packages/core/src/serialization/codecs/editor/EditorToolbarCodec.ts index 3f7c965213..ef115fcd03 100644 --- a/packages/core/src/serialization/codecs/editor/EditorToolbarCodec.ts +++ b/packages/core/src/serialization/codecs/editor/EditorToolbarCodec.ts @@ -24,7 +24,6 @@ import { convertPoint } from '../../../util/styleUtils'; import { getClientX, getClientY } from '../../../util/EventUtils'; import InternalEvent from '../../../view/event/InternalEvent'; import { getChildNodes, getTextContent } from '../../../util/domUtils'; -import Translations from '../../../util/Translations'; /** * Custom codec for configuring {@link EditorToolbar}s. @@ -144,7 +143,7 @@ export class EditorToolbarCodec extends ObjectCodec { into.toolbar.addLine(); } else if (node.nodeName === 'add') { let as = node.getAttribute('as'); - as = Translations.get(as) || as; + as = GlobalConfig.i18n.get(as) || as; const icon = node.getAttribute('icon'); const pressedIcon = node.getAttribute('pressedIcon'); const action = node.getAttribute('action'); diff --git a/packages/core/src/util/config.ts b/packages/core/src/util/config.ts index d2dae70e27..bbb8b89476 100644 --- a/packages/core/src/util/config.ts +++ b/packages/core/src/util/config.ts @@ -23,6 +23,8 @@ import { SHADOWCOLOR, } from './Constants'; import { shallowCopy } from './cloneUtils'; +import type { I18nProvider } from '../i18n/api'; +import { NoOpI18n } from '../i18n/api'; /** * Global configuration for maxGraph. @@ -51,6 +53,10 @@ export const GlobalConfig = { * @default {@link NoOpLogger} */ logger: new NoOpLogger() as Logger, + + // TODO find a better name for the property + // TODO add JSDoc + i18n: new NoOpI18n() as I18nProvider, }; /** diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index 1b5b494106..b6f3db23de 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -60,6 +60,7 @@ import { registerDefaultEdgeMarkers } from './geometry/edge/MarkerShape'; import { registerDefaultStyleElements } from './style/register'; import { applyGraphMixins } from './mixins/_graph-mixins-apply'; import { getDefaultPlugins } from './plugins'; +import { GlobalConfig } from '../util/config'; /** * Extends {@link EventSource} to implement a graph component for the browser. This is the main class of the package. @@ -382,7 +383,9 @@ class Graph extends EventSource { * for this key does not exist then the value is used as the error message. * @default 'alreadyConnected' */ - alreadyConnectedResource: string = Client.language != 'none' ? 'alreadyConnected' : ''; + alreadyConnectedResource: string = GlobalConfig.i18n.isEnabled() + ? 'alreadyConnected' + : ''; /** * Specifies the resource key for the warning message to be displayed when @@ -390,8 +393,9 @@ class Graph extends EventSource { * key does not exist then the value is used as the warning message. * @default 'containsValidationErrors' */ - containsValidationErrorsResource: string = - Client.language != 'none' ? 'containsValidationErrors' : ''; + containsValidationErrorsResource: string = GlobalConfig.i18n.isEnabled() + ? 'containsValidationErrors' + : ''; // =================================================================================================================== // Group: "Create Class Instance" factory functions. diff --git a/packages/core/src/view/GraphSelectionModel.ts b/packages/core/src/view/GraphSelectionModel.ts index d33f802f98..068a74e289 100644 --- a/packages/core/src/view/GraphSelectionModel.ts +++ b/packages/core/src/view/GraphSelectionModel.ts @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Client from '../Client'; import EventSource from '../view/event/EventSource'; import { Graph } from './Graph'; import Cell from './cell/Cell'; @@ -24,6 +23,7 @@ import SelectionChange from './undoable_changes/SelectionChange'; import UndoableEdit from './undoable_changes/UndoableEdit'; import EventObject from './event/EventObject'; import InternalEvent from './event/InternalEvent'; +import { GlobalConfig } from '../util/config'; /** * Class: mxGraphSelectionModel @@ -79,14 +79,14 @@ class GraphSelectionModel extends EventSource { * If the resource for this key does not exist then the value is used as * the status message. Default is 'done'. */ - doneResource = Client.language !== 'none' ? 'done' : ''; + doneResource = GlobalConfig.i18n.isEnabled() ? 'done' : ''; /** * Specifies the resource key for the status message while the selection is * being updated. If the resource for this key does not exist then the * value is used as the status message. Default is 'updatingSelection'. */ - updatingSelectionResource = Client.language !== 'none' ? 'updatingSelection' : ''; + updatingSelectionResource = GlobalConfig.i18n.isEnabled() ? 'updatingSelection' : ''; /** * Specifies if only one selected item at a time is allowed. diff --git a/packages/core/src/view/GraphView.ts b/packages/core/src/view/GraphView.ts index 2d205af4b3..766a6b4d05 100644 --- a/packages/core/src/view/GraphView.ts +++ b/packages/core/src/view/GraphView.ts @@ -119,14 +119,14 @@ export class GraphView extends EventSource { * If the resource for this key does not exist then the value is used as * the status message. Default is 'done'. */ - doneResource = Client.language !== 'none' ? 'done' : ''; + doneResource = GlobalConfig.i18n.isEnabled() ? 'done' : ''; /** * Specifies the resource key for the status message while the document is * being updated. If the resource for this key does not exist then the * value is used as the status message. Default is 'updatingDocument'. */ - updatingDocumentResource = Client.language !== 'none' ? 'updatingDocument' : ''; + updatingDocumentResource = GlobalConfig.i18n.isEnabled() ? 'updatingDocument' : ''; /** * Specifies if string values in cell styles should be evaluated using diff --git a/packages/core/src/view/geometry/node/StencilShape.ts b/packages/core/src/view/geometry/node/StencilShape.ts index a8418339ae..67d47f29ee 100644 --- a/packages/core/src/view/geometry/node/StencilShape.ts +++ b/packages/core/src/view/geometry/node/StencilShape.ts @@ -19,7 +19,7 @@ limitations under the License. import ConnectionConstraint from '../../other/ConnectionConstraint'; import Rectangle from '../Rectangle'; import Shape from '../Shape'; -import Translations from '../../../util/Translations'; +import { GlobalConfig } from '../../../util/config'; import { getValue, isNotNullish } from '../../../util/Utils'; import { ALIGN, @@ -174,7 +174,7 @@ class StencilShape extends Shape { const loc = node.getAttribute('localized'); if ((StencilShapeConfig.defaultLocalized && !loc) || loc === '1') { - result = Translations.get(result); + result = GlobalConfig.i18n.get(result); } return result; } diff --git a/packages/core/src/view/handler/ElbowEdgeHandler.ts b/packages/core/src/view/handler/ElbowEdgeHandler.ts index 9ca6187d60..78b366a6a6 100644 --- a/packages/core/src/view/handler/ElbowEdgeHandler.ts +++ b/packages/core/src/view/handler/ElbowEdgeHandler.ts @@ -20,10 +20,9 @@ import EdgeHandler from './EdgeHandler'; import { CURSOR, EDGESTYLE, ELBOW } from '../../util/Constants'; import InternalEvent from '../event/InternalEvent'; import Point from '../geometry/Point'; -import Translations from '../../util/Translations'; +import { GlobalConfig } from '../../util/config'; import Rectangle from '../geometry/Rectangle'; import { intersects } from '../../util/mathUtils'; -import Client from '../../Client'; import { isConsumed } from '../../util/EventUtils'; import CellState from '../cell/CellState'; import { HandleConfig } from './config'; @@ -57,8 +56,9 @@ class ElbowEdgeHandler extends EdgeHandler { * exist then the value is used as the error message. * @default 'doubleClickOrientation'. */ - doubleClickOrientationResource = - Client.language !== 'none' ? 'doubleClickOrientation' : ''; + doubleClickOrientationResource = GlobalConfig.i18n.isEnabled() + ? 'doubleClickOrientation' + : ''; /** * Overrides {@link EdgeHandler.createBends} to create custom bends. @@ -132,7 +132,7 @@ class ElbowEdgeHandler extends EdgeHandler { (node === this.bends[1].node || node.parentNode === this.bends[1].node) ) { tip = this.doubleClickOrientationResource; - tip = Translations.get(tip) || tip; // translate + tip = GlobalConfig.i18n.get(tip) || tip; // translate } return tip; diff --git a/packages/core/src/view/mixins/FoldingMixin.ts b/packages/core/src/view/mixins/FoldingMixin.ts index eb04dee37c..db05303dab 100644 --- a/packages/core/src/view/mixins/FoldingMixin.ts +++ b/packages/core/src/view/mixins/FoldingMixin.ts @@ -24,6 +24,7 @@ import { getValue } from '../../util/Utils'; import { toRadians } from '../../util/mathUtils'; import Rectangle from '../geometry/Rectangle'; import type { Graph } from '../Graph'; +import { GlobalConfig } from '../../util/config'; type PartialGraph = Pick< Graph, @@ -63,7 +64,7 @@ export const FoldingMixin: PartialType = { collapseToPreferredSize: true, }, - collapseExpandResource: Client.language != 'none' ? 'collapse-expand' : '', + collapseExpandResource: GlobalConfig.i18n.isEnabled() ? 'collapse-expand' : '', getCollapseExpandResource() { return this.collapseExpandResource; diff --git a/packages/core/src/view/mixins/TooltipMixin.ts b/packages/core/src/view/mixins/TooltipMixin.ts index 2b49483a94..ed2fc0fb98 100644 --- a/packages/core/src/view/mixins/TooltipMixin.ts +++ b/packages/core/src/view/mixins/TooltipMixin.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { htmlEntities } from '../../util/StringUtils'; -import Translations from '../../util/Translations'; +import { GlobalConfig } from '../../util/config'; import type Shape from '../geometry/Shape'; import type Cell from '../cell/Cell'; import type { Graph } from '../Graph'; @@ -40,7 +40,7 @@ export const TooltipMixin: PartialType = { (node === state.control.node || node.parentNode === state.control.node) ) { tip = this.getCollapseExpandResource(); - tip = htmlEntities(Translations.get(tip) || tip, true).replace(/\\n/g, '
'); + tip = htmlEntities(GlobalConfig.i18n.get(tip) || tip, true).replace(/\\n/g, '
'); } if (!tip && state.overlays) { diff --git a/packages/core/src/view/mixins/ValidationMixin.ts b/packages/core/src/view/mixins/ValidationMixin.ts index 6e0314585b..46c2ec4986 100644 --- a/packages/core/src/view/mixins/ValidationMixin.ts +++ b/packages/core/src/view/mixins/ValidationMixin.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type Cell from '../cell/Cell'; -import Translations from '../../util/Translations'; +import { GlobalConfig } from '../../util/config'; import { isNode } from '../../util/domUtils'; import type { Graph } from '../Graph'; @@ -89,7 +89,7 @@ export const ValidationMixin: PartialType = { // Checks if the source and target are not connected by another edge if (tmp.length > 1 || (tmp.length === 1 && tmp[0] !== edge)) { error += `${ - Translations.get(this.getAlreadyConnectedResource()) || + GlobalConfig.i18n.get(this.getAlreadyConnectedResource()) || this.getAlreadyConnectedResource() }\n`; } @@ -172,7 +172,7 @@ export const ValidationMixin: PartialType = { // Adds error for invalid children if collapsed (children invisible) if (cell && cell.isCollapsed() && !isValid) { warning += `${ - Translations.get(this.getContainsValidationErrorsResource()) || + GlobalConfig.i18n.get(this.getContainsValidationErrorsResource()) || this.getContainsValidationErrorsResource() }\n`; } diff --git a/packages/core/src/view/other/Multiplicity.ts b/packages/core/src/view/other/Multiplicity.ts index aaada6706e..6b2ca1bfff 100644 --- a/packages/core/src/view/other/Multiplicity.ts +++ b/packages/core/src/view/other/Multiplicity.ts @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Translations from '../../util/Translations'; +import { GlobalConfig } from '../../util/config'; import { isNode } from '../../util/domUtils'; import Cell from '../cell/Cell'; import { Graph } from '../Graph'; @@ -60,8 +60,8 @@ class Multiplicity { this.min = min ?? 0; this.max = max ?? Number.MAX_VALUE; this.validNeighbors = validNeighbors; - this.countError = Translations.get(countError) || countError; - this.typeError = Translations.get(typeError) || typeError; + this.countError = GlobalConfig.i18n.get(countError) || countError; + this.typeError = GlobalConfig.i18n.get(typeError) || typeError; this.validNeighborsAllowed = validNeighborsAllowed; } diff --git a/packages/ts-example-without-defaults/vite.config.js b/packages/ts-example-without-defaults/vite.config.js index e9fb486f2b..455cee5db8 100644 --- a/packages/ts-example-without-defaults/vite.config.js +++ b/packages/ts-example-without-defaults/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 442, // @maxgraph/core + chunkSizeWarningLimit: 437, // @maxgraph/core }, }; }); diff --git a/packages/ts-example/vite.config.js b/packages/ts-example/vite.config.js index 188efe1495..61e5ff87a3 100644 --- a/packages/ts-example/vite.config.js +++ b/packages/ts-example/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 446, // @maxgraph/core + chunkSizeWarningLimit: 441, // @maxgraph/core }, }; });