Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ _**Note:** Yet to be released breaking changes appear here._
**Breaking Changes**:
- `StylesheetCodec.allowEval` is now set to `false` by default to prevent unwanted use of the eval function, as it carries a possible security risk.
- `Utils.copyTextToClipboard` is no longer available. It was intended to be internal and had been made public by mistake.
- The built-in `Translations` class is no longer used by default. To use it, call `GlobalConfig.i18n = new TranslationsAsI18n()`

## 0.16.0

Expand Down
37 changes: 17 additions & 20 deletions packages/core/src/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '../i18n/Translations';
import Client from '../Client';
import CompactTreeLayout from '../view/layout/CompactTreeLayout';
import { EditorToolbar } from './EditorToolbar';
Expand Down Expand Up @@ -56,9 +55,9 @@ import type ConnectionHandler from '../view/plugins/ConnectionHandler';
import { show } from '../util/printUtils';
import type PanningHandler from '../view/plugins/PanningHandler';
import { cloneCell } from '../util/cellArrayUtils';
import { TranslationsConfig } from '../i18n/config';
import type MaxPopupMenu from '../gui/MaxPopupMenu';
import { isNullish } from '../internal/utils';
import { isI18nEnabled, translate } from '../internal/i18n-utils';

/**
* Extends {@link EventSource} to implement an application wrapper for a graph that
Expand Down Expand Up @@ -445,7 +444,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 = TranslationsConfig.isEnabled() ? 'askZoom' : '';
askZoomResource = isI18nEnabled() ? 'askZoom' : '';

// =====================================================================================
// Group: Controls and Handlers
Expand All @@ -456,46 +455,46 @@ 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 = TranslationsConfig.isEnabled() ? 'lastSaved' : '';
lastSavedResource = isI18nEnabled() ? '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 = TranslationsConfig.isEnabled() ? 'currentFile' : '';
currentFileResource = isI18nEnabled() ? 'currentFile' : '';

/**
* Specifies the resource key for the properties window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'properties'.
* @default 'properties'
*/
propertiesResource = TranslationsConfig.isEnabled() ? 'properties' : '';
propertiesResource = isI18nEnabled() ? 'properties' : '';

/**
* Specifies the resource key for the tasks window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'tasks'.
* @default 'tasks'
*/
tasksResource = TranslationsConfig.isEnabled() ? 'tasks' : '';
tasksResource = isI18nEnabled() ? 'tasks' : '';

/**
* Specifies the resource key for the help window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'help'.
* @default 'help'
*/
helpResource = TranslationsConfig.isEnabled() ? 'help' : '';
helpResource = isI18nEnabled() ? 'help' : '';

/**
* Specifies the resource key for the outline window title. If the
* resource for this key does not exist then the value is used as the
* error message. Default is 'outline'.
* @default 'outline'
*/
outlineResource = TranslationsConfig.isEnabled() ? 'outline' : '';
outlineResource = isI18nEnabled() ? 'outline' : '';

/**
* Reference to the {@link MaxWindow} that contains the outline.
Expand Down Expand Up @@ -1230,7 +1229,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,
translate(editor.askZoomResource) || editor.askZoomResource,
String(current)
);

Expand Down Expand Up @@ -1716,16 +1715,14 @@ export class Editor extends EventSource {
this.addListener(InternalEvent.SAVE, () => {
const timestamp = new Date().toLocaleString();
this.setStatus(
`${
Translations.get(this.lastSavedResource) || this.lastSavedResource
}: ${timestamp}`
`${translate(this.lastSavedResource) || this.lastSavedResource}: ${timestamp}`
);
});

// Updates the statusbar to display the filename when new files are opened
this.addListener(InternalEvent.OPEN, () => {
this.setStatus(
`${Translations.get(this.currentFileResource) || this.currentFileResource}: ${
`${translate(this.currentFileResource) || this.currentFileResource}: ${
this.filename
}`
);
Expand Down Expand Up @@ -2040,7 +2037,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,
translate(this.propertiesResource) || this.propertiesResource,
node,
x,
y,
Expand Down Expand Up @@ -2220,7 +2217,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,
translate(this.tasksResource) || this.tasksResource,
div,
w - 220,
this.tasksTop,
Expand Down Expand Up @@ -2282,14 +2279,14 @@ export class Editor extends EventSource {
/**
* Shows the help window. If the help window does not exist
* then it is created using an iframe pointing to the resource
* for the <code>urlHelp</code> key or {@link urlHelp} if the resource
* for the `urlHelp` key or {@link urlHelp} if the resource
* is undefined.
* @param tasks
*/
showHelp(tasks: any | null = null): void {
if (this.help == null) {
const frame = document.createElement('iframe');
frame.setAttribute('src', <string>(Translations.get('urlHelp') || this.urlHelp));
frame.setAttribute('src', (translate('urlHelp') || this.urlHelp)!);
frame.setAttribute('height', '100%');
frame.setAttribute('width', '100%');
frame.setAttribute('frameBorder', '0');
Expand All @@ -2299,7 +2296,7 @@ export class Editor extends EventSource {
const h = document.body.clientHeight || document.documentElement.clientHeight;

const wnd = new MaxWindow(
Translations.get(this.helpResource) || this.helpResource,
translate(this.helpResource) || this.helpResource,
frame,
(w - this.helpWidth) / 2,
(h - this.helpHeight) / 3,
Expand Down Expand Up @@ -2353,7 +2350,7 @@ export class Editor extends EventSource {
div.style.cursor = 'move';

const wnd = new MaxWindow(
Translations.get(this.outlineResource) || this.outlineResource,
translate(this.outlineResource) || this.outlineResource,
div,
600,
480,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/editor/EditorPopupMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ limitations under the License.
import Cell from '../view/cell/Cell';
import MaxPopupMenu from '../gui/MaxPopupMenu';
import { getTextContent } from '../util/domUtils';
import Translations from '../i18n/Translations';
import Editor from './Editor';
import { PopupMenuItem } from '../types';
import { doEval, isNullish } from '../internal/utils';
import { translate } from '../internal/i18n-utils';

/**
* Creates popupmenus for mouse events.
Expand Down Expand Up @@ -192,7 +192,7 @@ export class EditorPopupMenu {

if (isNullish(condition) || conditions[condition]) {
let as = item.getAttribute('as')!;
as = Translations.get(as) || as;
as = translate(as) || as;
const funct = doEval(getTextContent(<Text>(<unknown>item)));
const action = item.getAttribute('action');
let icon = item.getAttribute('icon');
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/gui/MaxForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ limitations under the License.
import Client from '../Client';
import InternalEvent from '../view/event/InternalEvent';
import { write, writeln } from '../util/domUtils';
import Translations from '../i18n/Translations';

import { translate } from '../internal/i18n-utils';

/**
* A simple class for creating HTML forms.
Expand Down Expand Up @@ -65,7 +66,7 @@ class MaxForm {

// Adds the ok button
let button = document.createElement('button');
write(button, Translations.get('ok') || 'OK');
write(button, translate('ok') || 'OK');
td.appendChild(button);

InternalEvent.addListener(button, 'click', () => {
Expand All @@ -74,7 +75,7 @@ class MaxForm {

// Adds the cancel button
button = document.createElement('button');
write(button, Translations.get('cancel') || 'Cancel');
write(button, translate('cancel') || 'Cancel');
td.appendChild(button);

InternalEvent.addListener(button, 'click', () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/gui/MaxWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import InternalEvent from '../view/event/InternalEvent';
import Client from '../Client';
import { NODETYPE } from '../util/Constants';
import { br, write } from '../util/domUtils';
import Translations from '../i18n/Translations';
import { getClientX, getClientY } from '../util/EventUtils';
import { htmlEntities } from '../util/StringUtils';
import { utils } from '../util/Utils';

import { translate } from '../internal/i18n-utils';

let activeWindow: MaxWindow | null = null;

/**
Expand Down Expand Up @@ -1057,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,
translate(utils.errorResource) || utils.errorResource,
div,
(w - width) / 2,
h / 4,
Expand All @@ -1079,7 +1080,7 @@ export const error = (
warn.destroy();
});

write(button, Translations.get(utils.closeResource) || utils.closeResource);
write(button, translate(utils.closeResource) || utils.closeResource);

tmp.appendChild(button);
div.appendChild(tmp);
Expand Down
49 changes: 40 additions & 9 deletions packages/core/src/i18n/Translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ limitations under the License.

import Client from '../Client';
import { NONE } from '../util/Constants';
import { get, load } from '../util/MaxXmlRequest';
import type MaxXmlRequest from '../util/MaxXmlRequest';
import { get, load } from '../util/MaxXmlRequest';
import { TranslationsConfig } from './config';
import { isNullish } from '../internal/utils';
import { I18nProvider } from '../types';

// mxGraph source code: https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/util/mxResources.js

Expand Down Expand Up @@ -75,7 +77,7 @@ import { TranslationsConfig } from './config';
*
* @category I18n
*/
class Translations {
export default class Translations {
/*
* Object that maps from keys to values.
*/
Expand Down Expand Up @@ -185,13 +187,13 @@ class Translations {
* @param callback Optional callback for asynchronous loading.
*/
static add = (
basename: string,
basename: string | null = null,
lan: string | null = null,
callback: Function | null = null
): void => {
lan ??= TranslationsConfig.getLanguage()?.toLowerCase() ?? NONE;

if (lan !== NONE) {
if (!isNullish(basename) && lan !== NONE) {
const defaultBundle = Translations.getDefaultBundle(basename, lan);
const specialBundle = Translations.getSpecialBundle(basename, lan);

Expand Down Expand Up @@ -314,19 +316,19 @@ class Translations {
* @param defaultValue Optional string that specifies the default return value.
*/
static get = (
key: string,
key: string | null = null,
params: any[] | null = null,
defaultValue: string | null = null
): string | null => {
let value: string | null = Translations.resources[key];
let value: string | null = key ? Translations.resources[key] : null;

// Applies the default value if no resource was found
if (value == null) {
if (isNullish(value)) {
value = defaultValue;
}

// Replaces the placeholders with the values in the array
if (value != null && params != null) {
if (!isNullish(value) && params) {
value = Translations.replacePlaceholders(value, params);
}
return value;
Expand Down Expand Up @@ -378,4 +380,33 @@ class Translations {
};
}

export default Translations;
/**
* A {@link I18nProvider} that uses {@link Translations} to manage translations.
*
* The configuration is done using {@link TranslationsConfig}.
*
* @experimental subject to change or removal. The I18n system may be modified in the future without prior notice.
* @category I18n
* @since 0.17.0
*/
export class TranslationsAsI18n implements I18nProvider {
isEnabled(): boolean {
return TranslationsConfig.isEnabled();
}

get(
key?: string | null,
params?: any[] | null,
defaultValue?: string | null
): string | null {
return Translations.get(key, params, defaultValue);
}

addResource(
basename?: string | null,
language?: string | null,
callback?: Function | null
): void {
Translations.add(basename, language, callback);
}
}
38 changes: 38 additions & 0 deletions packages/core/src/i18n/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
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.
*/

import { I18nProvider } from '../types';

/**
* A {@link I18nProvider} that does nothing.
*
* @experimental subject to change or removal. The I18n system may be modified in the future without prior notice.
* @since 0.17.0
* @category I18n
*/
export class NoOpI18n implements I18nProvider {
isEnabled() {
return false;
}

get() {
return null;
}

addResource(): void {
// do nothing
}
}
4 changes: 3 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ 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 './i18n/Translations';

export { default as Translations, TranslationsAsI18n } from './i18n/Translations';
export * from './i18n/config';
export * from './i18n/provider';

export * as cellArrayUtils from './util/cellArrayUtils';
export * as cloneUtils from './util/cloneUtils';
Expand Down
Loading