diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7a554e..7bab1b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.12.0
+
+This version introduces the localization feature. Now you can localize the editor to any language you want.
+
## 0.11.3
This version improves the behavior of the `Dynamic` value editor, when the sub editor contains a control visible in the property header.
diff --git a/README.md b/README.md
index c8a7b21..ffd4f0b 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ Powerful workflow editor builder for sequential workflows. Written in TypeScript
* [🛠Playground](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/playground.html)
* [📖 Editors](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/editors.html)
* [🎯 Placement Restrictions](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/placement-restrictions.html)
+* [🚩 Internationalization](https://nocode-js.github.io/sequential-workflow-editor/webpack-app/public/i18n.html)
* [🚢 Vanilla JS](https://nocode-js.github.io/sequential-workflow-editor/vanilla-js-app/vanilla-js.html)
Pro:
diff --git a/demos/vanilla-js-app/vanilla-js.html b/demos/vanilla-js-app/vanilla-js.html
index 2d430fb..474359e 100644
--- a/demos/vanilla-js-app/vanilla-js.html
+++ b/demos/vanilla-js-app/vanilla-js.html
@@ -23,9 +23,9 @@
}
-
-
-
+
+
+
diff --git a/demos/webpack-app/package.json b/demos/webpack-app/package.json
index 73eaa17..9e92ecd 100644
--- a/demos/webpack-app/package.json
+++ b/demos/webpack-app/package.json
@@ -16,10 +16,10 @@
"dependencies": {
"xstate": "^4.38.2",
"sequential-workflow-model": "^0.2.0",
- "sequential-workflow-designer": "^0.17.0",
+ "sequential-workflow-designer": "^0.21.1",
"sequential-workflow-machine": "^0.4.0",
- "sequential-workflow-editor-model": "^0.11.3",
- "sequential-workflow-editor": "^0.11.3"
+ "sequential-workflow-editor-model": "^0.12.0",
+ "sequential-workflow-editor": "^0.12.0"
},
"devDependencies": {
"ts-loader": "^9.4.2",
diff --git a/demos/webpack-app/public/assets/i18n.css b/demos/webpack-app/public/assets/i18n.css
new file mode 100644
index 0000000..76cccaa
--- /dev/null
+++ b/demos/webpack-app/public/assets/i18n.css
@@ -0,0 +1,59 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
+body {
+ display: flex;
+ flex-direction: column;
+}
+body,
+input,
+h1,
+textarea {
+ font: 14px/1.3em Arial, Verdana, sans-serif;
+}
+.header {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ background: #203fd2;
+ color: #fff;
+}
+.header h1 {
+ margin: 0;
+ padding: 0;
+}
+.header a {
+ color: #fff;
+}
+.header .column {
+ padding: 10px;
+}
+.header .column.flex-1 {
+ flex: 1;
+}
+.header .text-center {
+ text-align: center;
+}
+.header .column.text-end {
+ text-align: right;
+}
+@media only screen and (max-width: 700px) {
+ .header .column.hidden-mobile {
+ display: none;
+ }
+}
+a {
+ color: #000;
+ text-decoration: underline;
+}
+a:hover {
+ text-decoration: none;
+}
+#designer {
+ flex: 1;
+}
diff --git a/demos/webpack-app/public/i18n.html b/demos/webpack-app/public/i18n.html
new file mode 100644
index 0000000..bf8f0e5
--- /dev/null
+++ b/demos/webpack-app/public/i18n.html
@@ -0,0 +1,29 @@
+
+
+
+
+ 🚩 I18n Example - Sequential Workflow Editor
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/webpack-app/src/i18n/app.ts b/demos/webpack-app/src/i18n/app.ts
new file mode 100644
index 0000000..5da379f
--- /dev/null
+++ b/demos/webpack-app/src/i18n/app.ts
@@ -0,0 +1,156 @@
+import { EditorProvider } from 'sequential-workflow-editor';
+import { ChownStep, I18nDefinition, definitionModel } from './definition-model';
+import { Designer, Uid } from 'sequential-workflow-designer';
+import { defaultI18n } from 'sequential-workflow-editor-model';
+
+import 'sequential-workflow-designer/css/designer.css';
+import 'sequential-workflow-designer/css/designer-light.css';
+import 'sequential-workflow-editor/css/editor.css';
+
+const designerDict: Record> = {
+ pl: {
+ 'controlBar.resetView': 'Resetuj widok',
+ 'controlBar.zoomIn': 'Przybliż',
+ 'controlBar.zoomOut': 'Oddal',
+ 'controlBar.turnOnOffDragAndDrop': 'Włącz/wyłącz przeciąganie i upuszczanie',
+ 'controlBar.deleteSelectedStep': 'Usuń wybrany krok',
+ 'controlBar.undo': 'Cofnij',
+ 'controlBar.redo': 'Dalej',
+ 'smartEditor.toggle': 'Zwiń/rozwiń',
+ 'toolbox.title': 'Przybornik',
+ 'toolbox.search': 'Szukaj',
+ 'contextMenu.select': 'Zaznacz',
+ 'contextMenu.unselect': 'Odznacz',
+ 'contextMenu.delete': 'Usuń',
+ 'contextMenu.resetView': 'Resetuj widok',
+ 'contextMenu.duplicate': 'Duplikuj',
+
+ // steps
+ 'toolbox.item.chown.label': 'Uprawnienia'
+ }
+};
+
+const editorDict: Record> = {
+ pl: {
+ 'toolbox.defaultGroupName': 'Inne',
+ 'stringDictionary.noItems': 'Brak elementów',
+ 'stringDictionary.addItem': 'Dodaj element',
+ 'stringDictionary.key': 'Klucz',
+ 'stringDictionary.value': 'Wartość',
+ 'stringDictionary.delete': 'Usuń',
+ 'stringDictionary.valueTooShort': 'Wartość musi mieć conajmniej :min znaków',
+ 'stringDictionary.duplicatedKey': 'Klucz jest zduplikowany',
+ 'stringDictionary.keyIsRequired': 'Klucz jest wymagany',
+
+ 'number.valueMustBeNumber': 'Wartość musi być liczbą',
+ 'number.valueTooLow': 'Wartość musi być minimum :min.',
+ 'number.valueTooHigh': 'Wartość musi być maximum :max.',
+
+ 'boolean.false': 'Fałsz',
+ 'boolean.true': 'Prawda',
+
+ 'string.valueTooShort': 'Wartość musi mieć minimum :min znaków.',
+ 'string.valueDoesNotMatchPattern': 'Wartość nie pasuje do oczekiwanego wzorca.',
+
+ 'dynamic.string.label': 'Tekst',
+ 'dynamic.number.label': 'Liczba',
+
+ // root
+ 'root.property:properties/timeout': 'Przekroczenie czasu',
+ 'root.property:properties/debug': 'Tryb debug',
+
+ // steps
+ 'step.chown.name': 'Uprawnienia',
+ 'step.chown.property:name': 'Nazwa',
+ 'step.chown.property:properties/stringOrNumber': 'Tekst lub liczba',
+ 'step.chown.property:properties/users': 'Użytkownik'
+ }
+};
+
+export class App {
+ public static create() {
+ const placeholder = document.getElementById('designer') as HTMLElement;
+ const langInput = document.getElementById('lang') as HTMLInputElement;
+ const app = new App(placeholder, langInput.value);
+ app.reload();
+ langInput.addEventListener('change', () => {
+ app.setLang(langInput.value);
+ app.reload();
+ });
+ return app;
+ }
+
+ private designer: Designer | null = null;
+ private definition: I18nDefinition | null = null;
+
+ public constructor(private readonly placeholder: HTMLElement, private lang: string) {}
+
+ private readonly designerI18n = (key: string, defaultValue: string) => {
+ const dict = designerDict[this.lang];
+ if (dict) {
+ const translation = dict[key];
+ if (translation) {
+ return translation;
+ }
+ }
+ console.log(``, key, defaultValue);
+ return defaultValue;
+ };
+
+ private readonly editorI18n = (key: string, defaultValue: string, replacements?: { [key: string]: string }) => {
+ const dict = editorDict[this.lang];
+ if (dict) {
+ const translation = dict[key];
+ if (translation) {
+ defaultValue = translation;
+ } else {
+ console.log(``, key, defaultValue);
+ }
+ }
+ return defaultI18n(key, defaultValue, replacements);
+ };
+
+ public setLang(lang: string) {
+ this.lang = lang;
+ }
+
+ public reload() {
+ if (this.designer) {
+ this.designer.destroy();
+ }
+
+ const editorProvider = EditorProvider.create(definitionModel, {
+ uidGenerator: Uid.next,
+ i18n: this.editorI18n
+ });
+
+ if (!this.definition) {
+ this.definition = editorProvider.activateDefinition();
+ const step = editorProvider.activateStep('chown') as ChownStep;
+ this.definition.sequence.push(step);
+ }
+
+ this.designer = Designer.create(this.placeholder, this.definition, {
+ controlBar: true,
+ editors: {
+ rootEditorProvider: editorProvider.createRootEditorProvider(),
+ stepEditorProvider: editorProvider.createStepEditorProvider()
+ },
+ validator: {
+ step: editorProvider.createStepValidator(),
+ root: editorProvider.createRootValidator()
+ },
+ steps: {
+ iconUrlProvider: () => './assets/icon-task.svg'
+ },
+ toolbox: {
+ groups: editorProvider.getToolboxGroups(),
+ labelProvider: editorProvider.createStepLabelProvider()
+ },
+ i18n: this.designerI18n
+ });
+ this.designer.onDefinitionChanged.subscribe(d => (this.definition = d));
+ }
+}
+
+document.addEventListener('DOMContentLoaded', App.create, false);
diff --git a/demos/webpack-app/src/i18n/definition-model.ts b/demos/webpack-app/src/i18n/definition-model.ts
new file mode 100644
index 0000000..709955d
--- /dev/null
+++ b/demos/webpack-app/src/i18n/definition-model.ts
@@ -0,0 +1,69 @@
+import {
+ Dynamic,
+ StringDictionary,
+ createBooleanValueModel,
+ createDefinitionModel,
+ createDynamicValueModel,
+ createNumberValueModel,
+ createStepModel,
+ createStringDictionaryValueModel,
+ createStringValueModel
+} from 'sequential-workflow-editor-model';
+import { Definition, Step } from 'sequential-workflow-model';
+
+export interface I18nDefinition extends Definition {
+ properties: {
+ timeout: number;
+ debug: boolean;
+ };
+}
+
+export interface ChownStep extends Step {
+ type: 'chown';
+ componentType: 'task';
+ properties: {
+ stringOrNumber: Dynamic;
+ users: StringDictionary;
+ };
+}
+
+export const definitionModel = createDefinitionModel(model => {
+ model.root(root => {
+ root.property('timeout').value(
+ createNumberValueModel({
+ min: 100,
+ max: 200,
+ defaultValue: 150
+ })
+ );
+ root.property('debug').value(
+ createBooleanValueModel({
+ defaultValue: false
+ })
+ );
+ });
+ model.steps([
+ createStepModel('chown', 'task', step => {
+ step.property('stringOrNumber').value(
+ createDynamicValueModel({
+ models: [
+ createStringValueModel({
+ pattern: /^[a-zA-Z0-9]+$/
+ }),
+ createNumberValueModel({
+ min: 1,
+ max: 100,
+ defaultValue: 50
+ })
+ ]
+ })
+ );
+ step.property('users').value(
+ createStringDictionaryValueModel({
+ valueMinLength: 1,
+ uniqueKeys: true
+ })
+ );
+ })
+ ]);
+});
diff --git a/demos/webpack-app/webpack.config.js b/demos/webpack-app/webpack.config.js
index 174c1ee..f7e41d5 100644
--- a/demos/webpack-app/webpack.config.js
+++ b/demos/webpack-app/webpack.config.js
@@ -28,7 +28,8 @@ function bundle(name) {
}
module.exports = [
- bundle('playground'),
bundle('editors'),
- bundle('placement-restrictions')
+ bundle('i18n'),
+ bundle('placement-restrictions'),
+ bundle('playground'),
];
diff --git a/docs/I18N-KEYS.md b/docs/I18N-KEYS.md
new file mode 100644
index 0000000..1ce22ac
--- /dev/null
+++ b/docs/I18N-KEYS.md
@@ -0,0 +1,52 @@
+# I18N Keys
+
+This document lists all the I18N keys used in the Sequential Workflow Editor.
+
+```json
+{
+ "anyVariable.addVariable": "Add variable",
+ "anyVariable.delete": "Delete",
+ "anyVariable.select": "- Select -",
+ "anyVariables.invalidVariableType": "Variable :name has invalid type",
+ "anyVariables.noVariablesSelected": "No variables selected",
+ "anyVariables.variableIsLost": "Variable :name is lost",
+ "boolean.false": "False",
+ "boolean.invalidType": "The value must be a boolean.",
+ "boolean.true": "True",
+ "branches.empty": "No branches defined.",
+ "branches.invalidLength": "Invalid number of branches.",
+ "branches.missingBranch": "Missing branch: :name.",
+ "branches.mustBeObject": "The value must be object.",
+ "choice.notSupportedValue": "Value is not supported.",
+ "generatedString.differentValue": "Generator returns different value than the current value",
+ "nullableAnyVariable.invalidVariableType": "The variable :name has invalid type",
+ "nullableAnyVariable.select": "- Select -",
+ "nullableAnyVariable.variableIsLost": "The variable :name is lost",
+ "nullableAnyVariable.variableIsRequired": "The variable is required",
+ "nullableVariable.select": "- Select -",
+ "nullableVariable.variableIsLost": "The variable :name is not found",
+ "nullableVariable.variableIsRequired": "The variable is required",
+ "nullableVariableDefinition.expectedType": "Variable type must be :type",
+ "nullableVariableDefinition.variableIsDuplicated": "Variable name is already used",
+ "nullableVariableDefinition.variableIsRequired": "The variable is required",
+ "number.valueMustBeNumber": "The value must be a number.",
+ "number.valueTooHigh": "The value must be at most :max.",
+ "number.valueTooLow": "The value must be at least :min.",
+ "string.valueDoesNotMatchPattern": "The value does not match the required pattern.",
+ "string.valueMustBeString": "The value must be a string.",
+ "string.valueTooShort": "The value must be at least :min characters long.",
+ "stringDictionary.addItem": "Add item",
+ "stringDictionary.delete": "Delete",
+ "stringDictionary.duplicatedKey": "Key name is duplicated",
+ "stringDictionary.key": "Key",
+ "stringDictionary.keyIsRequired": "Key is required",
+ "stringDictionary.noItems": "No items",
+ "stringDictionary.value": "Value",
+ "stringDictionary.valueTooShort": "Value must be at least :min characters long",
+ "toolbox.defaultGroupName": "Others",
+ "variableDefinitions.delete": "Delete",
+ "variableDefinitions.namePlaceholder": "Variable name",
+ "variableDefinitions.valueTypeIsNotAllowed": "Value type is not allowed",
+ "variableDefinitions.variableNameIsDuplicated": "Variable name is already used"
+}
+```
diff --git a/editor/package.json b/editor/package.json
index 9fbe34a..13996c2 100644
--- a/editor/package.json
+++ b/editor/package.json
@@ -1,6 +1,6 @@
{
"name": "sequential-workflow-editor",
- "version": "0.11.3",
+ "version": "0.12.0",
"type": "module",
"main": "./lib/esm/index.js",
"types": "./lib/index.d.ts",
@@ -46,11 +46,11 @@
"prettier:fix": "prettier --write ./src ./css"
},
"dependencies": {
- "sequential-workflow-editor-model": "^0.11.3",
+ "sequential-workflow-editor-model": "^0.12.0",
"sequential-workflow-model": "^0.2.0"
},
"peerDependencies": {
- "sequential-workflow-editor-model": "^0.11.3",
+ "sequential-workflow-editor-model": "^0.12.0",
"sequential-workflow-model": "^0.2.0"
},
"devDependencies": {
diff --git a/editor/src/components/dynamic-list-component.ts b/editor/src/components/dynamic-list-component.ts
index 073fe65..8ad0c10 100644
--- a/editor/src/components/dynamic-list-component.ts
+++ b/editor/src/components/dynamic-list-component.ts
@@ -1,4 +1,4 @@
-import { SimpleEvent, ValueContext } from 'sequential-workflow-editor-model';
+import { I18n, SimpleEvent, ValueContext } from 'sequential-workflow-editor-model';
import { Html } from '../core/html';
import { Component } from './component';
import { validationErrorComponent } from './validation-error-component';
@@ -23,7 +23,7 @@ export interface DynamicListItemComponent extends Component {
export function dynamicListComponent = DynamicListItemComponent>(
initialItems: TItem[],
- itemComponentFactory: (item: TItem) => TItemComponent,
+ itemComponentFactory: (item: TItem, i18n: I18n) => TItemComponent,
context: ValueContext,
configuration?: DynamicListComponentConfiguration
): DynamicListComponent {
@@ -74,7 +74,7 @@ export function dynamicListComponent 0) {
items.forEach((item, index) => {
- const component = itemComponentFactory(item);
+ const component = itemComponentFactory(item, context.i18n);
component.onItemChanged.subscribe(item => onItemChanged(item, index));
component.onDeleteClicked.subscribe(() => onItemDeleted(index));
view.insertBefore(component.view, validation.view);
diff --git a/editor/src/editor-header.ts b/editor/src/editor-header.ts
index 2786d77..3755f94 100644
--- a/editor/src/editor-header.ts
+++ b/editor/src/editor-header.ts
@@ -1,3 +1,4 @@
+import { I18n } from 'sequential-workflow-editor-model';
import { Component } from './components/component';
import { Html } from './core';
import { appendMultilineText } from './core/append-multiline-text';
@@ -8,17 +9,18 @@ export interface EditorHeaderData {
}
export class EditorHeader implements Component {
- public static create(data: EditorHeaderData): EditorHeader {
+ public static create(data: EditorHeaderData, stepType: string, i18n: I18n): EditorHeader {
const view = Html.element('div', { class: 'swe-editor-header' });
const title = Html.element('h3', { class: 'swe-editor-header-title' });
- title.textContent = data.label;
+ title.textContent = i18n(`step.${stepType}.name`, data.label);
view.appendChild(title);
if (data.description) {
- const description = Html.element('p', { class: 'swe-editor-header-description' });
- appendMultilineText(description, data.description);
- view.appendChild(description);
+ const description = i18n(`step.${stepType}.description`, data.description);
+ const p = Html.element('p', { class: 'swe-editor-header-description' });
+ appendMultilineText(p, description);
+ view.appendChild(p);
}
return new EditorHeader(view);
}
diff --git a/editor/src/editor-provider-configuration.ts b/editor/src/editor-provider-configuration.ts
index 6b177f5..7219e5c 100644
--- a/editor/src/editor-provider-configuration.ts
+++ b/editor/src/editor-provider-configuration.ts
@@ -1,10 +1,11 @@
-import { UidGenerator } from 'sequential-workflow-editor-model';
+import { I18n, UidGenerator } from 'sequential-workflow-editor-model';
import { DefinitionWalker } from 'sequential-workflow-model';
import { EditorExtension } from './editor-extension';
export interface EditorProviderConfiguration {
uidGenerator: UidGenerator;
definitionWalker?: DefinitionWalker;
+ i18n?: I18n;
isHeaderHidden?: boolean;
extensions?: EditorExtension[];
}
diff --git a/editor/src/editor-provider.ts b/editor/src/editor-provider.ts
index becd919..e9677a4 100644
--- a/editor/src/editor-provider.ts
+++ b/editor/src/editor-provider.ts
@@ -6,7 +6,9 @@ import {
ModelActivator,
DefinitionValidator,
Path,
- StepValidatorContext
+ StepValidatorContext,
+ defaultI18n,
+ I18n
} from 'sequential-workflow-editor-model';
import { EditorServices, ValueEditorFactoryResolver } from './value-editors';
import {
@@ -28,15 +30,17 @@ export class EditorProvider {
configuration: EditorProviderConfiguration
): EditorProvider {
const definitionWalker = configuration.definitionWalker ?? new DefinitionWalker();
+ const i18n = configuration.i18n ?? defaultI18n;
const activator = ModelActivator.create(definitionModel, configuration.uidGenerator);
const validator = DefinitionValidator.create(definitionModel, definitionWalker);
const valueEditorFactoryResolver = ValueEditorFactoryResolver.create(configuration.extensions);
- return new EditorProvider(activator, validator, definitionModel, definitionWalker, valueEditorFactoryResolver, configuration);
+ return new EditorProvider(activator, validator, definitionModel, definitionWalker, i18n, valueEditorFactoryResolver, configuration);
}
private readonly services: EditorServices = {
activator: this.activator,
- valueEditorFactoryResolver: this.valueEditorFactoryResolver
+ valueEditorFactoryResolver: this.valueEditorFactoryResolver,
+ i18n: this.i18n
};
private constructor(
@@ -44,15 +48,15 @@ export class EditorProvider {
private readonly validator: DefinitionValidator,
private readonly definitionModel: DefinitionModel,
private readonly definitionWalker: DefinitionWalker,
+ private readonly i18n: I18n,
private readonly valueEditorFactoryResolver: ValueEditorFactoryResolver,
private readonly configuration: EditorProviderConfiguration
) {}
public createRootEditorProvider(): RootEditorProvider {
return (definition: Definition, context: GlobalEditorContext): HTMLElement => {
- const rootContext = DefinitionContext.createForRoot(definition, this.definitionModel, this.definitionWalker);
- const typeClassName = 'root';
- const editor = Editor.create(null, null, this.definitionModel.root.properties, rootContext, this.services, typeClassName);
+ const rootContext = DefinitionContext.createForRoot(definition, this.definitionModel, this.definitionWalker, this.i18n);
+ const editor = Editor.create(null, null, this.definitionModel.root.properties, null, rootContext, this.services);
editor.onValuesChanged.subscribe(() => {
context.notifyPropertiesChanged();
});
@@ -62,9 +66,14 @@ export class EditorProvider {
public createStepEditorProvider(): StepEditorProvider {
return (step: Step, context: StepEditorContext, definition: Definition) => {
- const definitionContext = DefinitionContext.createForStep(step, definition, this.definitionModel, this.definitionWalker);
+ const definitionContext = DefinitionContext.createForStep(
+ step,
+ definition,
+ this.definitionModel,
+ this.definitionWalker,
+ this.i18n
+ );
const stepModel = this.definitionModel.steps[step.type];
- const typeClassName = stepModel.type;
const propertyModels = [stepModel.name, ...stepModel.properties];
const headerData: EditorHeaderData | null = this.configuration.isHeaderHidden
@@ -81,7 +90,7 @@ export class EditorProvider {
validator = () => stepValidator.validate(stepValidatorContext);
}
- const editor = Editor.create(headerData, validator, propertyModels, definitionContext, this.services, typeClassName);
+ const editor = Editor.create(headerData, validator, propertyModels, stepModel.type, definitionContext, this.services);
editor.onValuesChanged.subscribe((paths: Path[]) => {
const isNameChanged = paths.some(path => path.equals(stepModel.name.value.path));
@@ -125,7 +134,7 @@ export class EditorProvider {
const groups: ToolboxGroup[] = [];
const categories = new Set(stepModels.map(step => step.category));
categories.forEach((category: string | undefined) => {
- const name = category ?? 'Others';
+ const name = category ?? this.i18n('toolbox.defaultGroupName', 'Others');
const groupStepModels = stepModels.filter(step => step.category === category);
const groupSteps = groupStepModels.map(step => this.activateStep(step.type));
groupSteps.sort((a, b) => a.name.localeCompare(b.name));
diff --git a/editor/src/editor.ts b/editor/src/editor.ts
index 3aceb41..1f54e33 100644
--- a/editor/src/editor.ts
+++ b/editor/src/editor.ts
@@ -12,15 +12,15 @@ export class Editor {
headerData: EditorHeaderData | null,
validator: EditorValidator | null,
propertyModels: PropertyModels,
+ stepType: string | null,
definitionContext: DefinitionContext,
- editorServices: EditorServices,
- typeClassName: string
+ editorServices: EditorServices
): Editor {
const root = document.createElement('div');
- root.className = `swe-editor swe-type-${typeClassName}`;
+ root.className = `swe-editor swe-type-${stepType ?? 'root'}`;
if (headerData) {
- const header = EditorHeader.create(headerData);
+ const header = EditorHeader.create(headerData, stepType ?? 'root', editorServices.i18n);
root.appendChild(header.view);
}
@@ -36,7 +36,7 @@ export class Editor {
continue;
}
- const propertyEditor = PropertyEditor.create(propertyModel, definitionContext, editorServices);
+ const propertyEditor = PropertyEditor.create(propertyModel, stepType, definitionContext, editorServices);
root.appendChild(propertyEditor.view);
editors.set(propertyModel, propertyEditor);
}
diff --git a/editor/src/property-editor/property-editor.ts b/editor/src/property-editor/property-editor.ts
index 9eca6e9..fa92041 100644
--- a/editor/src/property-editor/property-editor.ts
+++ b/editor/src/property-editor/property-editor.ts
@@ -16,10 +16,16 @@ import { PropertyHintComponent, propertyHint } from './property-hint';
export class PropertyEditor implements Component {
public static create(
propertyModel: PropertyModel,
+ stepType: string | null,
definitionContext: DefinitionContext,
editorServices: EditorServices
): PropertyEditor {
- const valueContext = ValueContext.createFromDefinitionContext(propertyModel.value, propertyModel, definitionContext);
+ const valueContext = ValueContext.createFromDefinitionContext(
+ propertyModel.value,
+ propertyModel,
+ definitionContext,
+ editorServices.i18n
+ );
const valueEditorFactory = editorServices.valueEditorFactoryResolver.resolve(propertyModel.value.id, propertyModel.value.editorId);
const valueEditor = valueEditorFactory(valueContext, editorServices);
let hint: PropertyHintComponent | null = null;
@@ -34,7 +40,8 @@ export class PropertyEditor implements Component {
const label = Html.element('h4', {
class: 'swe-property-header-label'
});
- label.innerText = propertyModel.label;
+ const i18nPrefix = stepType ? `step.${stepType}.property:` : 'root.property:';
+ label.innerText = editorServices.i18n(i18nPrefix + propertyModel.path.toString(), propertyModel.label);
header.appendChild(label);
view.appendChild(header);
@@ -64,7 +71,12 @@ export class PropertyEditor implements Component {
let validationError: PropertyValidationErrorComponent | null = null;
if (propertyModel.validator) {
- const valueContext = ValueContext.createFromDefinitionContext(propertyModel.value, propertyModel, definitionContext);
+ const valueContext = ValueContext.createFromDefinitionContext(
+ propertyModel.value,
+ propertyModel,
+ definitionContext,
+ editorServices.i18n
+ );
const validatorContext = PropertyValidatorContext.create(valueContext);
validationError = propertyValidationErrorComponent(propertyModel.validator, validatorContext);
view.appendChild(validationError.view);
diff --git a/editor/src/value-editors/any-variables/any-variable-item-component.ts b/editor/src/value-editors/any-variables/any-variable-item-component.ts
index a8065b7..ccbf8ec 100644
--- a/editor/src/value-editors/any-variables/any-variable-item-component.ts
+++ b/editor/src/value-editors/any-variables/any-variable-item-component.ts
@@ -1,4 +1,4 @@
-import { AnyVariable, SimpleEvent } from 'sequential-workflow-editor-model';
+import { AnyVariable, I18n, SimpleEvent } from 'sequential-workflow-editor-model';
import { Html } from '../../core/html';
import { validationErrorComponent } from '../../components/validation-error-component';
import { buttonComponent } from '../../components/button-component';
@@ -9,7 +9,7 @@ import { Icons } from '../../core/icons';
export type AnyVariableItemComponent = DynamicListItemComponent;
-export function anyVariableItemComponent(variable: AnyVariable): AnyVariableItemComponent {
+export function anyVariableItemComponent(variable: AnyVariable, i18n: I18n): AnyVariableItemComponent {
function validate(error: string | null) {
validation.setError(error);
}
@@ -21,7 +21,7 @@ export function anyVariableItemComponent(variable: AnyVariable): AnyVariableItem
const name = Html.element('span');
name.innerText = formatVariableNameWithType(variable.name, variable.type);
- const deleteButton = buttonComponent('Delete', {
+ const deleteButton = buttonComponent(i18n('anyVariable.delete', 'Delete'), {
size: 'small',
theme: 'secondary',
icon: Icons.close
diff --git a/editor/src/value-editors/any-variables/any-variable-selector-component.ts b/editor/src/value-editors/any-variables/any-variable-selector-component.ts
index dd6336c..7f6bc7c 100644
--- a/editor/src/value-editors/any-variables/any-variable-selector-component.ts
+++ b/editor/src/value-editors/any-variables/any-variable-selector-component.ts
@@ -31,7 +31,7 @@ export function anyVariableSelectorComponent(context: ValueContext formatVariableName(variable.name));
- variableSelect.setValues(['- Select -', ...variableNames]);
+ variableSelect.setValues([context.i18n('anyVariable.select', '- Select -'), ...variableNames]);
}
function onAddClicked() {
@@ -57,7 +57,7 @@ export function anyVariableSelectorComponent(context: ValueContext(context.getValue().variables, anyVariableItemComponent, context, {
- emptyMessage: 'No variables selected'
+ emptyMessage: context.i18n('anyVariables.noVariablesSelected', 'No variables selected')
});
list.onChanged.subscribe(onChanged);
diff --git a/editor/src/value-editors/boolean/boolean-value-editor.ts b/editor/src/value-editors/boolean/boolean-value-editor.ts
index 2739d42..57845a9 100644
--- a/editor/src/value-editors/boolean/boolean-value-editor.ts
+++ b/editor/src/value-editors/boolean/boolean-value-editor.ts
@@ -20,7 +20,7 @@ export function booleanValueEditor(context: ValueContext): Va
const select = selectComponent({
stretched: true
});
- select.setValues(['False', 'True']);
+ select.setValues([context.i18n('boolean.false', 'False'), context.i18n('boolean.true', 'True')]);
select.selectIndex(context.getValue() ? 1 : 0);
select.onSelected.subscribe(onSelected);
diff --git a/editor/src/value-editors/dynamic/dynamic-value-editor.ts b/editor/src/value-editors/dynamic/dynamic-value-editor.ts
index a0c6da0..e916215 100644
--- a/editor/src/value-editors/dynamic/dynamic-value-editor.ts
+++ b/editor/src/value-editors/dynamic/dynamic-value-editor.ts
@@ -65,7 +65,11 @@ export function dynamicValueEditor(context: ValueContext, ser
const subModelSelect = selectComponent({
size: 'small'
});
- subModelSelect.setValues(context.model.subModels.map(model => model.label));
+ subModelSelect.setValues(
+ context.model.subModels.map(model => {
+ return context.i18n(`dynamic.${model.id}.label`, model.label);
+ })
+ );
subModelSelect.selectIndex(context.model.subModels.findIndex(model => model.id === startValue.modelId));
subModelSelect.onSelected.subscribe(onTypeChanged);
control.appendChild(subModelSelect.view);
diff --git a/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts b/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts
index 38591ea..f70a5b4 100644
--- a/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts
+++ b/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts
@@ -35,7 +35,10 @@ export function nullableAnyVariableValueEditor(
const select = selectComponent({
stretched: true
});
- select.setValues(['- Select -', ...variables.map(variable => formatVariableNameWithType(variable.name, variable.type))]);
+ select.setValues([
+ context.i18n('nullableAnyVariable.select', '- Select -'),
+ ...variables.map(variable => formatVariableNameWithType(variable.name, variable.type))
+ ]);
if (startValue) {
select.selectIndex(variables.findIndex(variable => variable.name === startValue.name) + 1);
} else {
diff --git a/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts b/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts
index ff7952c..8f6817e 100644
--- a/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts
+++ b/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts
@@ -31,7 +31,10 @@ export function nullableVariableValueEditor(context: ValueContext formatVariableNameWithType(variable.name, variable.type))]);
+ select.setValues([
+ context.i18n('nullableVariable.select', '- Select -'),
+ ...variables.map(variable => formatVariableNameWithType(variable.name, variable.type))
+ ]);
if (startValue) {
select.selectIndex(variables.findIndex(variable => variable.name === startValue.name) + 1);
} else {
diff --git a/editor/src/value-editors/string-dictionary/string-dictionary-item-component.ts b/editor/src/value-editors/string-dictionary/string-dictionary-item-component.ts
index 3258a56..6dd92af 100644
--- a/editor/src/value-editors/string-dictionary/string-dictionary-item-component.ts
+++ b/editor/src/value-editors/string-dictionary/string-dictionary-item-component.ts
@@ -1,4 +1,4 @@
-import { SimpleEvent, StringDictionaryItem } from 'sequential-workflow-editor-model';
+import { I18n, SimpleEvent, StringDictionaryItem } from 'sequential-workflow-editor-model';
import { validationErrorComponent } from '../../components/validation-error-component';
import { Html } from '../../core';
import { rowComponent } from '../../components/row-component';
@@ -9,7 +9,7 @@ import { Icons } from '../../core/icons';
export type StringDictionaryItemComponent = DynamicListItemComponent;
-export function stringDictionaryItemComponent(item: StringDictionaryItem): StringDictionaryItemComponent {
+export function stringDictionaryItemComponent(item: StringDictionaryItem, i18n: I18n): StringDictionaryItemComponent {
function validate(error: string | null) {
validation.setError(error);
}
@@ -22,16 +22,16 @@ export function stringDictionaryItemComponent(item: StringDictionaryItem): Strin
const onDeleteClicked = new SimpleEvent();
const keyInput = inputComponent(item.key, {
- placeholder: 'Key'
+ placeholder: i18n('stringDictionary.key', 'Key')
});
keyInput.onChanged.subscribe(onChanged);
const valueInput = inputComponent(item.value, {
- placeholder: 'Value'
+ placeholder: i18n('stringDictionary.value', 'Value')
});
valueInput.onChanged.subscribe(onChanged);
- const deleteButton = buttonComponent('Delete', {
+ const deleteButton = buttonComponent(i18n('stringDictionary.delete', 'Delete'), {
size: 'small',
theme: 'secondary',
icon: Icons.close
diff --git a/editor/src/value-editors/string-dictionary/string-dictionary-value-editor.ts b/editor/src/value-editors/string-dictionary/string-dictionary-value-editor.ts
index 8809392..6bb2127 100644
--- a/editor/src/value-editors/string-dictionary/string-dictionary-value-editor.ts
+++ b/editor/src/value-editors/string-dictionary/string-dictionary-value-editor.ts
@@ -23,13 +23,13 @@ export function stringDictionaryValueEditor(context: ValueContext(context.getValue().items, stringDictionaryItemComponent, context, {
- emptyMessage: 'No items'
+ emptyMessage: context.i18n('stringDictionary.noItems', 'No items')
});
list.onChanged.subscribe(onChanged);
const container = valueEditorContainerComponent([list.view]);
- const addButton = buttonComponent('Add item', {
+ const addButton = buttonComponent(context.i18n('stringDictionary.addItem', 'Add item'), {
size: 'small',
icon: Icons.add
});
diff --git a/editor/src/value-editors/value-editor.ts b/editor/src/value-editors/value-editor.ts
index f6f565c..43a28bd 100644
--- a/editor/src/value-editors/value-editor.ts
+++ b/editor/src/value-editors/value-editor.ts
@@ -1,4 +1,4 @@
-import { ModelActivator, ValueModel, ValueContext } from 'sequential-workflow-editor-model';
+import { ModelActivator, ValueModel, ValueContext, I18n } from 'sequential-workflow-editor-model';
import { ValueEditorFactoryResolver } from './value-editor-factory-resolver';
import { Component } from '../components/component';
@@ -16,4 +16,5 @@ export type ValueEditorFactory = (
export interface EditorServices {
valueEditorFactoryResolver: ValueEditorFactoryResolver;
activator: ModelActivator;
+ i18n: I18n;
}
diff --git a/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts b/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts
index 4188f8b..d1a87dd 100644
--- a/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts
+++ b/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts
@@ -41,7 +41,7 @@ export function variableDefinitionItemComponent(
const input = prependedInputComponent(
'$',
inputComponent(variable.name, {
- placeholder: 'Variable name'
+ placeholder: context.i18n('variableDefinitions.namePlaceholder', 'Variable name')
})
);
input.onChanged.subscribe(onNameChanged);
@@ -55,7 +55,7 @@ export function variableDefinitionItemComponent(
typeSelect.selectIndex(valueTypes.findIndex(type => type === variable.type));
typeSelect.onSelected.subscribe(onTypeChanged);
- const deleteButton = buttonComponent('Delete', {
+ const deleteButton = buttonComponent(context.i18n('variableDefinitions.delete', 'Delete'), {
size: 'small',
theme: 'secondary',
icon: Icons.close
diff --git a/model/package.json b/model/package.json
index 8cef089..a2cbdda 100644
--- a/model/package.json
+++ b/model/package.json
@@ -1,6 +1,6 @@
{
"name": "sequential-workflow-editor-model",
- "version": "0.11.3",
+ "version": "0.12.0",
"homepage": "https://nocode-js.com/",
"author": {
"name": "NoCode JS",
diff --git a/model/src/context/definition-context.ts b/model/src/context/definition-context.ts
index 3e11249..b533ba3 100644
--- a/model/src/context/definition-context.ts
+++ b/model/src/context/definition-context.ts
@@ -1,24 +1,27 @@
import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model';
import { DefinitionModel } from '../model';
import { ParentsProvider } from './variables-provider';
+import { I18n } from '../i18n';
export class DefinitionContext {
public static createForStep(
step: Step,
definition: Definition,
definitionModel: DefinitionModel,
- definitionWalker: DefinitionWalker
+ definitionWalker: DefinitionWalker,
+ i18n: I18n
): DefinitionContext {
- const parentsProvider = ParentsProvider.createForStep(step, definition, definitionModel, definitionWalker);
+ const parentsProvider = ParentsProvider.createForStep(step, definition, definitionModel, definitionWalker, i18n);
return new DefinitionContext(step, definition, definitionModel, parentsProvider);
}
public static createForRoot(
definition: Definition,
definitionModel: DefinitionModel,
- definitionWalker: DefinitionWalker
+ definitionWalker: DefinitionWalker,
+ i18n: I18n
): DefinitionContext {
- const parentsProvider = ParentsProvider.createForRoot(definition, definitionModel, definitionWalker);
+ const parentsProvider = ParentsProvider.createForRoot(definition, definitionModel, definitionWalker, i18n);
return new DefinitionContext(definition, definition, definitionModel, parentsProvider);
}
diff --git a/model/src/context/scoped-property-context.ts b/model/src/context/scoped-property-context.ts
index e1e3c07..2ccf67d 100644
--- a/model/src/context/scoped-property-context.ts
+++ b/model/src/context/scoped-property-context.ts
@@ -3,17 +3,20 @@ import { ContextVariable } from '../model';
import { ParentsProvider } from './variables-provider';
import { PropertyContext } from './property-context';
import { ValueType } from '../types';
+import { I18n } from '../i18n';
export class ScopedPropertyContext {
public static create(
propertyContext: PropertyContext,
- parentsProvider: ParentsProvider
+ parentsProvider: ParentsProvider,
+ i18n: I18n
): ScopedPropertyContext {
- return new ScopedPropertyContext(propertyContext, parentsProvider);
+ return new ScopedPropertyContext(propertyContext, i18n, parentsProvider);
}
private constructor(
public readonly propertyContext: PropertyContext,
+ public readonly i18n: I18n,
private readonly parentsProvider: ParentsProvider
) {}
diff --git a/model/src/context/value-context.ts b/model/src/context/value-context.ts
index db385d4..e930337 100644
--- a/model/src/context/value-context.ts
+++ b/model/src/context/value-context.ts
@@ -4,15 +4,17 @@ import { Path, SimpleEvent } from '../core';
import { ScopedPropertyContext } from './scoped-property-context';
import { PropertyContext } from './property-context';
import { DefinitionContext } from './definition-context';
+import { I18n } from '../i18n';
export class ValueContext {
public static createFromDefinitionContext(
valueModel: TValModel,
propertyModel: PropertyModel,
- definitionContext: DefinitionContext
+ definitionContext: DefinitionContext,
+ i18n: I18n
) {
const propertyContext = PropertyContext.create(definitionContext.object, propertyModel, definitionContext.definitionModel);
- const scopedPropertyContext = ScopedPropertyContext.create(propertyContext, definitionContext.parentsProvider);
+ const scopedPropertyContext = ScopedPropertyContext.create(propertyContext, definitionContext.parentsProvider, i18n);
return new ValueContext(valueModel, scopedPropertyContext);
}
@@ -30,6 +32,7 @@ export class ValueContext => {
return this.model.path.read>(this.scopedPropertyContext.propertyContext.object);
diff --git a/model/src/context/variables-provider.ts b/model/src/context/variables-provider.ts
index 0b136d3..fc49e13 100644
--- a/model/src/context/variables-provider.ts
+++ b/model/src/context/variables-provider.ts
@@ -3,36 +3,40 @@ import { ContextVariable, DefinitionModel } from '../model';
import { DefinitionContext } from './definition-context';
import { PropertyModels } from '../model';
import { ValueContext } from './value-context';
+import { I18n } from '../i18n';
export class ParentsProvider {
public static createForStep(
step: Step,
definition: Definition,
definitionModel: DefinitionModel,
- definitionWalker: DefinitionWalker
+ definitionWalker: DefinitionWalker,
+ i18n: I18n
): ParentsProvider {
- return new ParentsProvider(step, definition, definitionModel, definitionWalker);
+ return new ParentsProvider(step, definition, definitionModel, definitionWalker, i18n);
}
public static createForRoot(
definition: Definition,
definitionModel: DefinitionModel,
- definitionWalker: DefinitionWalker
+ definitionWalker: DefinitionWalker,
+ i18n: I18n
): ParentsProvider {
- return new ParentsProvider(null, definition, definitionModel, definitionWalker);
+ return new ParentsProvider(null, definition, definitionModel, definitionWalker, i18n);
}
private constructor(
private readonly step: Step | null,
private readonly definition: Definition,
private readonly definitionModel: DefinitionModel,
- private readonly definitionWalker: DefinitionWalker
+ private readonly definitionWalker: DefinitionWalker,
+ private readonly i18n: I18n
) {}
public getVariables(): ContextVariable[] {
const result: ContextVariable[] = [];
- const rootContext = DefinitionContext.createForRoot(this.definition, this.definitionModel, this.definitionWalker);
+ const rootContext = DefinitionContext.createForRoot(this.definition, this.definitionModel, this.definitionWalker, this.i18n);
this.appendVariables(result, null, this.definitionModel.root.properties, rootContext);
if (this.step) {
@@ -49,7 +53,13 @@ export class ParentsProvider {
throw new Error(`Unknown step type: ${parent.type}`);
}
- const parentContext = DefinitionContext.createForStep(parent, this.definition, this.definitionModel, this.definitionWalker);
+ const parentContext = DefinitionContext.createForStep(
+ parent,
+ this.definition,
+ this.definitionModel,
+ this.definitionWalker,
+ this.i18n
+ );
this.appendVariables(result, parent.id, parentModel.properties, parentContext);
}
}
@@ -63,7 +73,7 @@ export class ParentsProvider {
definitionContext: DefinitionContext
) {
for (const propertyModel of propertyModels) {
- const valueContext = ValueContext.createFromDefinitionContext(propertyModel.value, propertyModel, definitionContext);
+ const valueContext = ValueContext.createFromDefinitionContext(propertyModel.value, propertyModel, definitionContext, this.i18n);
const definitions = propertyModel.value.getVariableDefinitions(valueContext);
if (!definitions) {
diff --git a/model/src/i18n.spec.ts b/model/src/i18n.spec.ts
new file mode 100644
index 0000000..418fd3a
--- /dev/null
+++ b/model/src/i18n.spec.ts
@@ -0,0 +1,18 @@
+import { defaultI18n } from './i18n';
+
+describe('defaultI18n', () => {
+ it('returns expected value', () => {
+ expect(defaultI18n('key', 'test')).toBe('test');
+ expect(
+ defaultI18n('key', 'We need :min users', {
+ min: '10'
+ })
+ ).toBe('We need 10 users');
+ expect(
+ defaultI18n('key', 'Your name :name should have :n characters', {
+ name: 'Alice',
+ n: '4'
+ })
+ ).toBe('Your name Alice should have 4 characters');
+ });
+});
diff --git a/model/src/i18n.ts b/model/src/i18n.ts
new file mode 100644
index 0000000..1a39a1e
--- /dev/null
+++ b/model/src/i18n.ts
@@ -0,0 +1,12 @@
+export type I18n = (key: string, defaultValue: string, replacements?: Record) => string;
+
+export const defaultI18n: I18n = (_, defaultValue, replacements) => {
+ if (replacements) {
+ let result = defaultValue;
+ Object.keys(replacements).forEach(key => {
+ result = result.replace(':' + key, replacements[key]);
+ });
+ return result;
+ }
+ return defaultValue;
+};
diff --git a/model/src/index.ts b/model/src/index.ts
index 8b3912a..8b79aca 100644
--- a/model/src/index.ts
+++ b/model/src/index.ts
@@ -5,5 +5,6 @@ export * from './core';
export * from './validator';
export * from './value-models';
export * from './external-types';
+export * from './i18n';
export * from './model';
export * from './types';
diff --git a/model/src/model.ts b/model/src/model.ts
index 433d662..c22de6b 100644
--- a/model/src/model.ts
+++ b/model/src/model.ts
@@ -66,6 +66,9 @@ export interface ValueModel<
id: ValueModelId;
editorId?: string;
path: Path;
+ /**
+ * Default translation for the label.
+ */
label: string;
configuration: TConfiguration;
subModels?: ValueModel[];
diff --git a/model/src/test-tools/value-context-stub.ts b/model/src/test-tools/value-context-stub.ts
index 0baa3bc..5353acf 100644
--- a/model/src/test-tools/value-context-stub.ts
+++ b/model/src/test-tools/value-context-stub.ts
@@ -1,4 +1,5 @@
import { ValueContext } from '../context';
+import { defaultI18n } from '../i18n';
import { ValueModel } from '../model';
export function createValueContextStub(
@@ -9,6 +10,7 @@ export function createValueContextStub(
getValue: () => value,
model: {
configuration
- }
+ },
+ i18n: defaultI18n
} as unknown as ValueContext;
}
diff --git a/model/src/validator/definition-validator.spec.ts b/model/src/validator/definition-validator.spec.ts
index 1b0b3c9..c9d31fd 100644
--- a/model/src/validator/definition-validator.spec.ts
+++ b/model/src/validator/definition-validator.spec.ts
@@ -2,6 +2,7 @@ import { Definition, DefinitionWalker, Step } from 'sequential-workflow-model';
import { createDefinitionModel, createRootModel, createStepModel } from '../builders';
import { createNumberValueModel } from '../value-models';
import { DefinitionValidator } from './definition-validator';
+import { defaultI18n } from '../i18n';
interface FooDefinition extends Definition {
properties: {
@@ -38,7 +39,7 @@ describe('DefinitionValidator', () => {
);
});
const walker = new DefinitionWalker();
- const validator = DefinitionValidator.create(model, walker);
+ const validator = DefinitionValidator.create(model, walker, defaultI18n);
it('returns error when root is invalid', () => {
const def: FooDefinition = {
diff --git a/model/src/validator/definition-validator.ts b/model/src/validator/definition-validator.ts
index 66472f3..9a4af97 100644
--- a/model/src/validator/definition-validator.ts
+++ b/model/src/validator/definition-validator.ts
@@ -4,15 +4,17 @@ import { DefinitionContext, ValueContext } from '../context';
import { PropertyValidatorContext } from './property-validator-context';
import { Path } from '../core';
import { StepValidatorContext } from './step-validator-context';
+import { I18n, defaultI18n } from '../i18n';
export class DefinitionValidator {
- public static create(definitionModel: DefinitionModel, definitionWalker: DefinitionWalker): DefinitionValidator {
- return new DefinitionValidator(definitionModel, definitionWalker);
+ public static create(definitionModel: DefinitionModel, definitionWalker: DefinitionWalker, i18n?: I18n): DefinitionValidator {
+ return new DefinitionValidator(definitionModel, definitionWalker, i18n ?? defaultI18n);
}
private constructor(
private readonly model: DefinitionModel,
- private readonly walker: DefinitionWalker
+ private readonly walker: DefinitionWalker,
+ private readonly i18n: I18n
) {}
/**
@@ -44,7 +46,7 @@ export class DefinitionValidator {
}
public validateStep(step: Step, definition: Definition): PropertyValidationError | null {
- const definitionContext = DefinitionContext.createForStep(step, definition, this.model, this.walker);
+ const definitionContext = DefinitionContext.createForStep(step, definition, this.model, this.walker, this.i18n);
const stepModel = this.model.steps[step.type];
if (!stepModel) {
@@ -73,7 +75,7 @@ export class DefinitionValidator {
}
public validateRoot(definition: Definition): PropertyValidationError | null {
- const definitionContext = DefinitionContext.createForRoot(definition, this.model, this.walker);
+ const definitionContext = DefinitionContext.createForRoot(definition, this.model, this.walker, this.i18n);
return this.validateProperties(this.model.root.properties, definitionContext);
}
@@ -91,7 +93,7 @@ export class DefinitionValidator {
}
private validateProperty(propertyModel: PropertyModel, definitionContext: DefinitionContext): ValidationResult {
- const valueContext = ValueContext.createFromDefinitionContext(propertyModel.value, propertyModel, definitionContext);
+ const valueContext = ValueContext.createFromDefinitionContext(propertyModel.value, propertyModel, definitionContext, this.i18n);
const valueError = propertyModel.value.validate(valueContext);
if (valueError) {
return valueError;
diff --git a/model/src/value-models/any-variables/any-variables-value-model.ts b/model/src/value-models/any-variables/any-variables-value-model.ts
index 1b4ed14..4f9d3d8 100644
--- a/model/src/value-models/any-variables/any-variables-value-model.ts
+++ b/model/src/value-models/any-variables/any-variables-value-model.ts
@@ -31,11 +31,15 @@ export const createAnyVariablesValueModel = (
value.variables.forEach((variable, index) => {
if (!context.hasVariable(variable.name, variable.type)) {
- errors[index] = `Variable ${variable.name} is lost`;
+ errors[index] = context.i18n('anyVariables.variableIsLost', 'Variable :name is lost', {
+ name: variable.name
+ });
return;
}
if (configuration.valueTypes && !configuration.valueTypes.includes(variable.type)) {
- errors[index] = `Variable ${variable.name} has invalid type`;
+ errors[index] = context.i18n('anyVariables.invalidVariableType', 'Variable :name has invalid type', {
+ name: variable.name
+ });
return;
}
});
diff --git a/model/src/value-models/boolean/boolean-value-model-validator.ts b/model/src/value-models/boolean/boolean-value-model-validator.ts
index 255e54b..8ce3cdc 100644
--- a/model/src/value-models/boolean/boolean-value-model-validator.ts
+++ b/model/src/value-models/boolean/boolean-value-model-validator.ts
@@ -5,7 +5,7 @@ import { BooleanValueModel } from './boolean-value-model';
export function booleanValueModelValidator(context: ValueContext): ValidationResult {
const value = context.getValue();
if (typeof value !== 'boolean') {
- return createValidationSingleError('The value must be a boolean.');
+ return createValidationSingleError(context.i18n('boolean.invalidType', 'The value must be a boolean.'));
}
return null;
}
diff --git a/model/src/value-models/branches/branches-value-model-validator.ts b/model/src/value-models/branches/branches-value-model-validator.ts
index 840b6f6..aeef989 100644
--- a/model/src/value-models/branches/branches-value-model-validator.ts
+++ b/model/src/value-models/branches/branches-value-model-validator.ts
@@ -10,20 +10,22 @@ export function branchesValueModelValidator(
const branches = context.getValue();
if (typeof branches !== 'object') {
- return createValidationSingleError('The value must be object.');
+ return createValidationSingleError(context.i18n('branches.mustBeObject', 'The value must be object.'));
}
const branchNames = Object.keys(branches);
if (branchNames.length === 0) {
- return createValidationSingleError('No branches defined.');
+ return createValidationSingleError(context.i18n('branches.empty', 'No branches defined.'));
}
if (!configuration.dynamic) {
const configurationBranchNames = Object.keys(configuration.branches);
if (branchNames.length !== configurationBranchNames.length) {
- return createValidationSingleError('Invalid number of branches.');
+ return createValidationSingleError(context.i18n('branches.invalidLength', 'Invalid number of branches.'));
}
const missingBranchName = configurationBranchNames.find(branchName => !branchNames.includes(branchName));
if (missingBranchName) {
- return createValidationSingleError(`Missing branch: ${missingBranchName}.`);
+ return createValidationSingleError(
+ context.i18n('branches.missingBranch', 'Missing branch: :name.', { name: missingBranchName })
+ );
}
}
return null;
diff --git a/model/src/value-models/choice/choice-value-model.ts b/model/src/value-models/choice/choice-value-model.ts
index 0bfcb96..ba73f36 100644
--- a/model/src/value-models/choice/choice-value-model.ts
+++ b/model/src/value-models/choice/choice-value-model.ts
@@ -37,7 +37,7 @@ export function createChoiceValueModel(
validate(context: ValueContext>): ValidationResult {
const value = context.getValue();
if (!configuration.choices.includes(value)) {
- return createValidationSingleError('Choice is not supported.');
+ return createValidationSingleError(context.i18n('choice.notSupportedValue', 'Value is not supported.'));
}
return null;
}
diff --git a/model/src/value-models/generated-string/generated-string-model.ts b/model/src/value-models/generated-string/generated-string-model.ts
index 64e0217..118a04f 100644
--- a/model/src/value-models/generated-string/generated-string-model.ts
+++ b/model/src/value-models/generated-string/generated-string-model.ts
@@ -34,7 +34,9 @@ export function createGeneratedStringValueModel): ValidationResult {
const value = context.getValue();
if (configuration.isRequired && !value) {
- return createValidationSingleError(`The variable is required.`);
+ return createValidationSingleError(context.i18n('nullableAnyVariable.variableIsRequired', 'The variable is required'));
}
if (value) {
if (!context.hasVariable(value.name, value.type)) {
- return createValidationSingleError(`The variable ${value.name} is lost`);
+ return createValidationSingleError(
+ context.i18n('nullableAnyVariable.variableIsLost', 'The variable :name is lost', { name: value.name })
+ );
}
if (configuration.valueTypes && !configuration.valueTypes.includes(value.type)) {
- return createValidationSingleError(`The variable ${value.name} has invalid type`);
+ return createValidationSingleError(
+ context.i18n('nullableAnyVariable.invalidVariableType', 'The variable :name has invalid type', { name: value.name })
+ );
}
}
return null;
diff --git a/model/src/value-models/nullable-variable-definition/nullable-variable-definition-value-model.ts b/model/src/value-models/nullable-variable-definition/nullable-variable-definition-value-model.ts
index 48cbe41..f9a2ea0 100644
--- a/model/src/value-models/nullable-variable-definition/nullable-variable-definition-value-model.ts
+++ b/model/src/value-models/nullable-variable-definition/nullable-variable-definition-value-model.ts
@@ -38,7 +38,9 @@ export const createNullableVariableDefinitionValueModel = (
validate(context: ValueContext): ValidationResult {
const value = context.getValue();
if (configuration.isRequired && !value) {
- return createValidationSingleError('Variable name is required.');
+ return createValidationSingleError(
+ context.i18n('nullableVariableDefinition.variableIsRequired', 'The variable is required')
+ );
}
if (value) {
const nameError = variableNameValidator(value.name);
@@ -46,10 +48,16 @@ export const createNullableVariableDefinitionValueModel = (
return createValidationSingleError(nameError);
}
if (value.type !== configuration.valueType) {
- return createValidationSingleError(`Variable type must be ${configuration.valueType}.`);
+ return createValidationSingleError(
+ context.i18n('nullableVariableDefinition.expectedType', 'Variable type must be :type', {
+ type: configuration.valueType
+ })
+ );
}
if (context.isVariableDuplicated(value.name)) {
- return createValidationSingleError('Variable name is already used.');
+ return createValidationSingleError(
+ context.i18n('nullableVariableDefinition.variableIsDuplicated', 'Variable name is already used')
+ );
}
}
return null;
diff --git a/model/src/value-models/nullable-variable/nullable-variable-value-model.ts b/model/src/value-models/nullable-variable/nullable-variable-value-model.ts
index 3216847..c362fa9 100644
--- a/model/src/value-models/nullable-variable/nullable-variable-value-model.ts
+++ b/model/src/value-models/nullable-variable/nullable-variable-value-model.ts
@@ -28,11 +28,15 @@ export const createNullableVariableValueModel = (
validate(context: ValueContext): ValidationResult {
const value = context.getValue();
if (configuration.isRequired && !value) {
- return createValidationSingleError(`The variable is required.`);
+ return createValidationSingleError(context.i18n('nullableVariable.variableIsRequired', 'The variable is required'));
}
if (value && value.name) {
if (!context.hasVariable(value.name, configuration.valueType)) {
- return createValidationSingleError(`The variable ${value.name} is not found.`);
+ return createValidationSingleError(
+ context.i18n('nullableVariable.variableIsLost', 'The variable :name is not found', {
+ name: value.name
+ })
+ );
}
}
return null;
diff --git a/model/src/value-models/number/number-value-model-validator.ts b/model/src/value-models/number/number-value-model-validator.ts
index e653cb5..89e1ae2 100644
--- a/model/src/value-models/number/number-value-model-validator.ts
+++ b/model/src/value-models/number/number-value-model-validator.ts
@@ -7,13 +7,21 @@ export function numberValueModelValidator(context: ValueContext configuration.max) {
- return createValidationSingleError(`The value must be at most ${configuration.max}.`);
+ return createValidationSingleError(
+ context.i18n('number.valueTooHigh', 'The value must be at most :max.', {
+ max: String(configuration.max)
+ })
+ );
}
return null;
}
diff --git a/model/src/value-models/string-dictionary/string-dictionary-value-model-validator.ts b/model/src/value-models/string-dictionary/string-dictionary-value-model-validator.ts
index 2171d01..0afba29 100644
--- a/model/src/value-models/string-dictionary/string-dictionary-value-model-validator.ts
+++ b/model/src/value-models/string-dictionary/string-dictionary-value-model-validator.ts
@@ -13,7 +13,7 @@ export function stringDictionaryValueModelValidator(context: ValueContext i !== index && item.key === key);
if (duplicate >= 0) {
- errors[index] = 'Key name is duplicated';
+ errors[index] = context.i18n('stringDictionary.duplicatedKey', 'Key name is duplicated');
}
}
}
@@ -21,10 +21,12 @@ export function stringDictionaryValueModelValidator(context: ValueContext i !== index && v.name === variable.name);
if (isDuplicated) {
- errors[index] = 'Variable name is duplicated';
+ errors[index] = context.i18n('variableDefinitions.variableNameIsDuplicated', 'Variable name is duplicated');
return;
}
if (context.isVariableDuplicated(variable.name)) {
- errors[index] = 'Variable name is already used';
+ errors[index] = context.i18n('variableDefinitions.variableNameIsDuplicated', 'Variable name is already used');
return;
}
if (configuration.valueTypes && !configuration.valueTypes.includes(variable.type)) {
- errors[index] = 'Value type is not allowed';
+ errors[index] = context.i18n('variableDefinitions.valueTypeIsNotAllowed', 'Value type is not allowed');
}
});
diff --git a/scripts/generate-i18n-keys.cjs b/scripts/generate-i18n-keys.cjs
new file mode 100644
index 0000000..521d040
--- /dev/null
+++ b/scripts/generate-i18n-keys.cjs
@@ -0,0 +1,57 @@
+// Usage: node generate-i18n-keys.cjs
+
+const fs = require('fs');
+const path = require('path');
+
+function* walkDir(dirPath) {
+ const files = fs.readdirSync(dirPath, { withFileTypes: true });
+ for (const file of files) {
+ if (file.isDirectory()) {
+ yield* walkDir(path.join(dirPath, file.name));
+ } else {
+ yield path.join(dirPath, file.name);
+ }
+ }
+}
+
+function processDir(dirPath) {
+ const dict = {};
+ for (const file of walkDir(path.join(__dirname, dirPath))) {
+ if (file.endsWith('.ts')) {
+ const content = fs.readFileSync(file, 'utf8');
+ const items = content.match(/i18n\s*\([^)]+/g);
+ if (items) {
+ items.forEach(item => {
+ const values = item.match(/'([^']+)'/g);
+ if (values?.length === 2) {
+ dict[values[0].slice(1, -1)] = values[1].slice(1, -1);
+ }
+ });
+ }
+ }
+ }
+ return dict;
+}
+
+function sortDict(dict) {
+ const keys = Object.keys(dict);
+ keys.sort((a, b) => a.localeCompare(b));
+ return keys.reduce((result, key) => {
+ result[key] = dict[key];
+ return result;
+ }, {});
+
+}
+
+const sortedDict = sortDict({
+ ...processDir('../model/src'),
+ ...processDir('../editor/src')
+});
+
+let output = '# I18N Keys\n\n';
+output += 'This document lists all the I18N keys used in the Sequential Workflow Editor.\n\n';
+output += '```json\n' + JSON.stringify(sortedDict, null, 2) + '\n```\n';
+
+fs.writeFileSync(path.join(__dirname, '../docs/I18N-KEYS.md'), output);
+
+console.log(sortedDict);
diff --git a/yarn.lock b/yarn.lock
index d36e17c..19775b6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5269,10 +5269,10 @@ semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
-sequential-workflow-designer@^0.17.0:
- version "0.17.0"
- resolved "https://registry.yarnpkg.com/sequential-workflow-designer/-/sequential-workflow-designer-0.17.0.tgz#fb126b4a45fef107ac54b041e116ddaca607e48f"
- integrity sha512-RzKn99irxTqsLn84RkMp7Q6XnvVjufG3xTKUy89uSC9a4CgG0k70jL36zLXxpW7cGEjjzGK/LgIeQ9/kiUOInQ==
+sequential-workflow-designer@^0.21.1:
+ version "0.21.1"
+ resolved "https://registry.yarnpkg.com/sequential-workflow-designer/-/sequential-workflow-designer-0.21.1.tgz#943a4c7737b5726068e7c973d94aad565c1f85cf"
+ integrity sha512-w20IHGTx/+3SCvwVJBXYGGOjAQJ/YTTR3TKSnqfSa7atwXlAd/NbScmuVtBIjyRubZ7E6isPIfin19eMEv8WKw==
dependencies:
sequential-workflow-model "^0.2.0"