@@ -117,14 +119,14 @@ export class GridModel extends FlowModel {
/>
- }>{'Add block'}
+ }>{t('Add block')}
{
this.openStepSettingsDialog('defaultFlow', 'grid');
}}
>
- Configure rows
+ {t('Configure rows')}
@@ -145,23 +147,24 @@ GridModel.registerFlow({
grid: {
uiSchema: {
rows: {
- title: 'Rows',
+ title: tval('Rows'),
'x-decorator': 'FormItem',
'x-component': JsonEditor,
'x-component-props': {
autoSize: { minRows: 10, maxRows: 20 },
- description: 'Configure the rows and columns of the grid.',
+ description: tval('Configure the rows and columns of the grid.'),
},
},
sizes: {
- title: 'Sizes',
+ title: tval('Sizes'),
'x-decorator': 'FormItem',
'x-component': JsonEditor,
'x-component-props': {
rows: 5,
},
- description:
+ description: tval(
'Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.',
+ ),
},
},
async handler(ctx, params) {
diff --git a/packages/core/client/src/flow/models/base/PageModel.tsx b/packages/core/client/src/flow/models/base/PageModel.tsx
index 2076a315fe..1f96e390d0 100644
--- a/packages/core/client/src/flow/models/base/PageModel.tsx
+++ b/packages/core/client/src/flow/models/base/PageModel.tsx
@@ -14,6 +14,7 @@ import { FlowModel, FlowModelRenderer, FlowSettingsButton } from '@nocobase/flow
import { Tabs } from 'antd';
import _ from 'lodash';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
type PageModelStructure = {
subModels: {
@@ -63,7 +64,7 @@ export class PageModel extends FlowModel
{
});
}}
>
- Add tab
+ {this.flowEngine.translate('Add tab')}
}
/>
@@ -82,24 +83,24 @@ export class PageModel extends FlowModel {
PageModel.registerFlow({
key: 'default',
- title: '基础配置',
+ title: tval('Basic configuration'),
auto: true,
steps: {
settings: {
- title: '配置页面',
+ title: tval('Configure page'),
uiSchema: {
title: {
type: 'string',
- title: 'Page Title',
+ title: tval('Page Title'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter page title',
+ placeholder: tval('Enter page title'),
},
},
enableTabs: {
type: 'boolean',
- title: 'Enable tabs',
+ title: tval('Enable tabs'),
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
@@ -111,7 +112,7 @@ PageModel.registerFlow({
};
},
async handler(ctx, params) {
- ctx.model.setProps('title', params.title);
+ ctx.model.setProps('title', ctx.globals.flowEngine.translate(params.title));
ctx.model.setProps('enableTabs', params.enableTabs);
if (ctx.shared.currentDrawer) {
diff --git a/packages/core/client/src/flow/models/data-blocks/calendar/CalendarBlockModel.tsx b/packages/core/client/src/flow/models/data-blocks/calendar/CalendarBlockModel.tsx
index 6a76a16868..98f21ca3b7 100644
--- a/packages/core/client/src/flow/models/data-blocks/calendar/CalendarBlockModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/calendar/CalendarBlockModel.tsx
@@ -12,6 +12,7 @@ import { Card, Modal } from 'antd';
import moment from 'moment';
import React from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
+import { tval } from '@nocobase/utils/client';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { DataBlockModel } from '../../base/BlockModel';
@@ -47,7 +48,7 @@ export class CalendarBlockModel extends DataBlockModel {
}
CalendarBlockModel.define({
- title: 'Calendar',
+ title: tval('Calendar'),
group: 'Content',
hide: true,
defaultOptions: {
@@ -64,13 +65,20 @@ CalendarBlockModel.registerFlow({
step1: {
handler(ctx, params) {
console.log('ctx.extra.event', ctx.extra.event);
+ const t = ctx.model.translate;
Modal.info({
- title: 'Event Selected',
+ title: t('Event selected'),
content: (
-
Title: {ctx.extra.event.nickname}
-
Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
-
End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
+ {t('Title')}: {ctx.extra.event.nickname}
+
+
+ {t('Start')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
+
+ {t('End')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
),
});
@@ -88,13 +96,20 @@ CalendarBlockModel.registerFlow({
step1: {
handler(ctx, params) {
console.log('ctx.extra.event', ctx.extra.event);
+ const t = ctx.model.translate;
Modal.info({
- title: 'Double Click',
+ title: t('Double click'),
content: (
-
Title: {ctx.extra.event.nickname}
-
Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
-
End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
+ {t('Title')}: {ctx.extra.event.nickname}
+
+
+ {t('Start')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
+
+ {t('End')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
+
),
});
@@ -113,20 +128,20 @@ CalendarBlockModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
- title: 'Data Source Key',
+ title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter data source key',
+ placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
- title: 'Collection Name',
+ title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter collection name',
+ placeholder: tval('Enter collection name'),
},
},
},
@@ -149,29 +164,29 @@ CalendarBlockModel.registerFlow({
uiSchema: {
titleAccessor: {
type: 'string',
- title: 'Title accessor',
+ title: tval('Title accessor'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter title accessor',
+ placeholder: tval('Enter title accessor'),
},
},
startAccessor: {
type: 'string',
- title: 'Start accessor',
+ title: tval('Start accessor'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter start accessor',
+ placeholder: tval('Enter start accessor'),
},
},
endAccessor: {
type: 'string',
- title: 'End accessor',
+ title: tval('End accessor'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter end accessor',
+ placeholder: tval('Enter end accessor'),
},
},
},
diff --git a/packages/core/client/src/flow/models/data-blocks/form/FormActionModel.tsx b/packages/core/client/src/flow/models/data-blocks/form/FormActionModel.tsx
index eb09f84754..0cb51df89a 100644
--- a/packages/core/client/src/flow/models/data-blocks/form/FormActionModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/form/FormActionModel.tsx
@@ -8,6 +8,7 @@
*/
import { ButtonProps } from 'antd';
+import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { FormModel } from './FormModel';
@@ -16,14 +17,14 @@ export class FormActionModel extends ActionModel {}
export class FormSubmitActionModel extends FormActionModel {
defaultProps: ButtonProps = {
- children: 'Submit',
+ title: tval('Submit'),
type: 'primary',
htmlType: 'submit',
};
}
FormSubmitActionModel.define({
- title: 'Submit',
+ title: tval('Submit'),
});
FormSubmitActionModel.registerFlow({
@@ -35,7 +36,7 @@ FormSubmitActionModel.registerFlow({
step1: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
- ctx.globals.message.error('No resource selected for submission.');
+ ctx.globals.message.error(ctx.model.flowEngine.translate('No resource selected for submission.'));
return;
}
const currentBlockModel = ctx.shared.currentBlockModel as FormModel;
diff --git a/packages/core/client/src/flow/models/data-blocks/form/FormModel.tsx b/packages/core/client/src/flow/models/data-blocks/form/FormModel.tsx
index 7fc2d2e62b..42363ecaa6 100644
--- a/packages/core/client/src/flow/models/data-blocks/form/FormModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/form/FormModel.tsx
@@ -13,6 +13,7 @@ import { FormProvider } from '@formily/react';
import { AddActionButton, AddFieldButton, FlowModelRenderer, SingleRecordResource } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
import { DataBlockModel } from '../../base/BlockModel';
import { EditableFieldModel } from '../../fields/EditableField/EditableFieldModel';
@@ -80,20 +81,20 @@ FormModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
- title: 'Data Source Key',
+ title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter data source key',
+ placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
- title: 'Collection Name',
+ title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter collection name',
+ placeholder: tval('Enter collection name'),
},
},
},
@@ -126,7 +127,7 @@ FormModel.registerFlow({
});
FormModel.define({
- title: 'Form',
+ title: tval('Form'),
group: 'Content',
defaultOptions: {
use: 'FormModel',
diff --git a/packages/core/client/src/flow/models/data-blocks/form/QuickEditForm.tsx b/packages/core/client/src/flow/models/data-blocks/form/QuickEditForm.tsx
index 41fc8b9fa4..05a7dd732b 100644
--- a/packages/core/client/src/flow/models/data-blocks/form/QuickEditForm.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/form/QuickEditForm.tsx
@@ -116,7 +116,7 @@ export class QuickEditForm extends FlowModel {
resolve(this.form.values); // 在 close 之后 resolve
}}
>
- Submit
+ {this.ctx.globals.flowEngine.translate('Submit')}
diff --git a/packages/core/client/src/flow/models/data-blocks/table/TableActionsColumnModel.tsx b/packages/core/client/src/flow/models/data-blocks/table/TableActionsColumnModel.tsx
index 6579581d85..44734ea2f1 100644
--- a/packages/core/client/src/flow/models/data-blocks/table/TableActionsColumnModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/table/TableActionsColumnModel.tsx
@@ -12,6 +12,7 @@ import { observer } from '@formily/reactive-react';
import { AddActionButton, FlowModel, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine';
import { Skeleton, Space } from 'antd';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { SupportedFieldInterfaces } from '../../base/FieldModel';
@@ -63,7 +64,7 @@ export class TableActionsColumnModel extends FlowModel {
},
]}
>
- {this.props.title || 'Actions'}
+ {this.props.title || this.flowEngine.translate('Actions')}
),
render: this.render(),
diff --git a/packages/core/client/src/flow/models/data-blocks/table/TableColumnModel.tsx b/packages/core/client/src/flow/models/data-blocks/table/TableColumnModel.tsx
index 1add2c9a57..14e4179ad7 100644
--- a/packages/core/client/src/flow/models/data-blocks/table/TableColumnModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/table/TableColumnModel.tsx
@@ -11,6 +11,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import { FlowsFloatContextMenu } from '@nocobase/flow-engine';
import { TableColumnProps, Tooltip } from 'antd';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel';
import { ReadPrettyFieldModel } from '../../fields/ReadPrettyField/ReadPrettyFieldModel';
@@ -65,7 +66,7 @@ export class TableColumnModel extends FieldModel {
}
TableColumnModel.define({
- title: 'Table Column',
+ title: tval('Table column'),
icon: 'TableColumn',
defaultOptions: {
use: 'TableColumnModel',
@@ -103,13 +104,13 @@ TableColumnModel.registerFlow({
},
},
editColumTitle: {
- title: 'Column title',
+ title: tval('Column title'),
uiSchema: {
title: {
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-component-props': {
- placeholder: 'Column title',
+ placeholder: tval('Column title'),
},
},
},
@@ -119,17 +120,18 @@ TableColumnModel.registerFlow({
};
},
handler(ctx, params) {
- ctx.model.setProps('title', params.title || ctx.model.collectionField?.title);
+ const title = ctx.globals.flowEngine.translate(params.title || ctx.model.collectionField?.title);
+ ctx.model.setProps('title', title);
},
},
editTooltip: {
- title: 'Edit tooltip',
+ title: tval('Edit tooltip'),
uiSchema: {
tooltip: {
'x-component': 'Input.TextArea',
'x-decorator': 'FormItem',
'x-component-props': {
- placeholder: 'Edit tooltip',
+ placeholder: tval('Edit tooltip'),
},
},
},
@@ -138,7 +140,7 @@ TableColumnModel.registerFlow({
},
},
editColumnWidth: {
- title: 'Column width',
+ title: tval('Column width'),
uiSchema: {
width: {
'x-component': 'NumberPicker',
@@ -153,7 +155,7 @@ TableColumnModel.registerFlow({
},
},
enableEditable: {
- title: 'Editable',
+ title: tval('Editable'),
uiSchema: {
editable: {
'x-component': 'Switch',
diff --git a/packages/core/client/src/flow/models/data-blocks/table/TableModel.tsx b/packages/core/client/src/flow/models/data-blocks/table/TableModel.tsx
index 6050bf172d..4999631895 100644
--- a/packages/core/client/src/flow/models/data-blocks/table/TableModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/table/TableModel.tsx
@@ -22,6 +22,7 @@ import { Card, Space, Spin, Table } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useRef } from 'react';
+import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { QuickEditForm } from '../form/QuickEditForm';
@@ -101,7 +102,7 @@ export class TableModel extends DataBlockModel {
appendItems={[
{
key: 'actions',
- label: 'Actions column',
+ label: tval('Actions column'),
createModelOptions: {
use: 'TableActionsColumnModel',
},
@@ -263,20 +264,20 @@ TableModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
- title: 'Data Source Key',
+ title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter data source key',
+ placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
- title: 'Collection Name',
+ title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter collection name',
+ placeholder: tval('Enter collection name'),
},
},
},
@@ -299,7 +300,7 @@ TableModel.registerFlow({
},
},
editPageSize: {
- title: 'Edit page size',
+ title: tval('Edit page size'),
uiSchema: {
pageSize: {
'x-component': 'Select',
@@ -328,14 +329,14 @@ TableModel.registerFlow({
},
dataScope: {
use: 'dataScope',
- title: '设置数据范围',
+ title: tval('Set data scope'),
},
},
});
TableModel.define({
- title: 'Table',
- group: 'Content',
+ title: tval('Table'),
+ group: tval('Content'),
defaultOptions: {
use: 'TableModel',
subModels: {
diff --git a/packages/core/client/src/flow/models/data-blocks/tabulator/TabulatorModel.tsx b/packages/core/client/src/flow/models/data-blocks/tabulator/TabulatorModel.tsx
index 8ab7b0c19a..224eee78a1 100644
--- a/packages/core/client/src/flow/models/data-blocks/tabulator/TabulatorModel.tsx
+++ b/packages/core/client/src/flow/models/data-blocks/tabulator/TabulatorModel.tsx
@@ -21,6 +21,7 @@ import {
import { Button, Card, Pagination, Skeleton, Space } from 'antd';
import _ from 'lodash';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
import { ColumnDefinition, TabulatorFull as Tabulator } from 'tabulator-tables';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
@@ -41,7 +42,7 @@ export class TabulatorColumnModel extends FlowModel {
getColumnProps(): ColumnDefinition {
return {
- title: 'abcd',
+ title: tval('abcd'),
width: 100,
headerSort: false,
editable: true,
@@ -126,7 +127,7 @@ export class TabulatorTableActionsColumnModel extends TabulatorColumnModel {
containerStyle={{ display: 'block', padding: '11px 8px', margin: '-11px -8px' }}
>
- {this.props.title || 'Actions'}
+ {this.props.title || tval('Actions')}
@@ -194,7 +195,7 @@ export class TabulatorModel extends DataBlockModel {
appendItems={[
{
key: 'actions',
- label: 'Actions column',
+ label: tval('Actions column'),
createModelOptions: {
use: 'TabulatorTableActionsColumnModel',
},
@@ -260,7 +261,7 @@ export class TabulatorModel extends DataBlockModel {
))}
- }>Configure actions
+ }>{this.translate('Configure actions')}
@@ -292,20 +293,20 @@ TabulatorModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
- title: 'Data Source Key',
+ title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter data source key',
+ placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
- title: 'Collection Name',
+ title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter collection name',
+ placeholder: tval('Enter collection name'),
},
},
},
@@ -342,8 +343,8 @@ TabulatorModel.registerFlow({
});
TabulatorModel.define({
- title: 'Tabulator',
- group: 'Content',
+ title: tval('Tabulator'),
+ group: tval('Content'),
requiresDataSource: true,
hide: true,
defaultOptions: {
diff --git a/packages/core/client/src/flow/models/fields/EditableField/AssociationFieldModel/AssociationSelectEditableFieldModel.tsx b/packages/core/client/src/flow/models/fields/EditableField/AssociationFieldModel/AssociationSelectEditableFieldModel.tsx
index 931ccff02e..5dca643652 100644
--- a/packages/core/client/src/flow/models/fields/EditableField/AssociationFieldModel/AssociationSelectEditableFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/EditableField/AssociationFieldModel/AssociationSelectEditableFieldModel.tsx
@@ -11,6 +11,7 @@ import { Select } from 'antd';
import React from 'react';
import { FlowModelRenderer, useFlowEngine, useFlowModel, reactive } from '@nocobase/flow-engine';
import { useCompile } from '../../../../../schema-component';
+import { tval } from '@nocobase/utils/client';
import { AssociationFieldEditableFieldModel } from './AssociationFieldEditableFieldModel';
function toValue(record: any | any[], fieldNames, multiple = false) {
@@ -41,7 +42,7 @@ function LabelByField(props) {
const currentModel: any = useFlowModel();
const flowEngine = useFlowEngine();
if (modelCache.has(cacheKey)) {
- return option[fieldNames.label] ? : 'N/A';
+ return option[fieldNames.label] ? : tval('N/A');
}
const collectionManager = currentModel.collectionField.collection.collectionManager;
const target = currentModel.collectionField?.options?.target;
@@ -76,7 +77,7 @@ function LabelByField(props) {
return (
- {option[fieldNames.label] ? : 'N/A'}
+ {option[fieldNames.label] ? : tval('N/A')}
);
}
@@ -258,7 +259,7 @@ AssociationSelectEditableFieldModel.registerFlow({
paginationState.page++;
}
} catch (error) {
- console.error('滚动分页请求失败:', error);
+ console.error('Scroll pagination request failed:', error);
} finally {
paginationState.loading = false;
}
@@ -316,13 +317,13 @@ AssociationSelectEditableFieldModel.registerFlow({
AssociationSelectEditableFieldModel.registerFlow({
key: 'fieldNames',
- title: 'Specific properties',
+ title: tval('Specific properties'),
auto: true,
sort: 200,
steps: {
fieldNames: {
use: 'titleField',
- title: 'Title field',
+ title: tval('Title field'),
},
},
});
diff --git a/packages/core/client/src/flow/models/fields/EditableField/ColorEditableFieldModel.tsx b/packages/core/client/src/flow/models/fields/EditableField/ColorEditableFieldModel.tsx
index e4ad77d27a..61d4d93fef 100644
--- a/packages/core/client/src/flow/models/fields/EditableField/ColorEditableFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/EditableField/ColorEditableFieldModel.tsx
@@ -9,6 +9,7 @@
import { connect, mapProps } from '@formily/react';
import { ColorPicker as AntdColorPicker } from 'antd';
+import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from './EditableFieldModel';
const ColorPicker = connect(
@@ -23,7 +24,7 @@ const ColorPicker = connect(
},
presets: [
{
- label: 'Recommended',
+ label: tval('Recommended'),
colors: [
'#8BBB11',
'#52C41A',
diff --git a/packages/core/client/src/flow/models/fields/EditableField/DateTimeFieldModel/DateTimeFieldModel.tsx b/packages/core/client/src/flow/models/fields/EditableField/DateTimeFieldModel/DateTimeFieldModel.tsx
index 52d5573639..f612dd583d 100644
--- a/packages/core/client/src/flow/models/fields/EditableField/DateTimeFieldModel/DateTimeFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/EditableField/DateTimeFieldModel/DateTimeFieldModel.tsx
@@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { DatePicker } from '@formily/antd-v5';
+import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from '../EditableFieldModel';
export class DateTimeFieldModel extends EditableFieldModel {
@@ -35,11 +36,11 @@ DateTimeFieldModel.registerFlow({
key: 'key3',
auto: true,
sort: 1000,
- title: 'Specific properties',
+ title: tval('Specific properties'),
steps: {
dateFormat: {
use: 'dateDisplayFormat',
- title: 'Date display format',
+ title: tval('Date display format'),
},
},
});
diff --git a/packages/core/client/src/flow/models/fields/EditableField/EditableFieldModel.tsx b/packages/core/client/src/flow/models/fields/EditableField/EditableFieldModel.tsx
index e1a047fc95..3f58e1c21c 100644
--- a/packages/core/client/src/flow/models/fields/EditableField/EditableFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/EditableField/EditableFieldModel.tsx
@@ -12,6 +12,7 @@ import type { FieldPatternTypes, FieldValidator } from '@formily/core';
import { Field, Form } from '@formily/core';
import { FieldContext } from '@formily/react';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel';
import { ReactiveField } from '../../../formily/ReactiveField';
import { FormModel } from '../..';
@@ -107,7 +108,7 @@ export class EditableFieldModel extends FieldModel {
EditableFieldModel.registerFlow({
key: 'init',
auto: true,
- title: 'Basic',
+ title: tval('Basic'),
sort: 150,
steps: {
createField: {
@@ -125,13 +126,13 @@ EditableFieldModel.registerFlow({
},
},
editTitle: {
- title: 'Edit Title',
+ title: tval('Edit Title'),
uiSchema: {
title: {
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-component-props': {
- placeholder: 'Enter field title',
+ placeholder: tval('Enter field title'),
},
},
},
@@ -145,7 +146,7 @@ EditableFieldModel.registerFlow({
},
},
initialValue: {
- title: 'Set default value',
+ title: tval('Set default value'),
uiSchema: {
defaultValue: {
'x-component': 'Input',
@@ -158,14 +159,14 @@ EditableFieldModel.registerFlow({
},
},
required: {
- title: 'Required',
+ title: tval('Required'),
uiSchema: {
required: {
'x-component': 'Switch',
'x-decorator': 'FormItem',
'x-component-props': {
- checkedChildren: 'Yes',
- unCheckedChildren: 'No',
+ checkedChildren: tval('Yes'),
+ unCheckedChildren: tval('No'),
},
},
},
@@ -174,14 +175,14 @@ EditableFieldModel.registerFlow({
},
},
displayLabel: {
- title: 'Display label',
+ title: tval('Display label'),
uiSchema: {
displayLabel: {
'x-component': 'Switch',
'x-decorator': 'FormItem',
'x-component-props': {
- checkedChildren: 'Yes',
- unCheckedChildren: 'No',
+ checkedChildren: tval('Yes'),
+ unCheckedChildren: tval('No'),
},
},
},
@@ -193,7 +194,7 @@ EditableFieldModel.registerFlow({
},
},
editDescription: {
- title: 'Edit description',
+ title: tval('Edit description'),
uiSchema: {
description: {
'x-component': 'Input.TextArea',
@@ -205,7 +206,7 @@ EditableFieldModel.registerFlow({
},
},
editTooltip: {
- title: 'Edit tooltip',
+ title: tval('Edit tooltip'),
uiSchema: {
tooltip: {
'x-component': 'Input.TextArea',
@@ -217,7 +218,7 @@ EditableFieldModel.registerFlow({
},
},
pattern: {
- title: 'Pattern',
+ title: tval('Pattern'),
uiSchema: {
pattern: {
'x-component': 'Select',
@@ -225,19 +226,19 @@ EditableFieldModel.registerFlow({
enum: [
{
value: 'editable',
- label: 'Editable',
+ label: tval('Editable'),
},
{
value: 'disabled',
- label: 'Disabled',
+ label: tval('Disabled'),
},
{
value: 'readOnly',
- label: 'ReadOnly',
+ label: tval('ReadOnly'),
},
{
value: 'readPretty',
- label: 'ReadPretty',
+ label: tval('ReadPretty'),
},
],
},
diff --git a/packages/core/client/src/flow/models/fields/EditableField/NanoIDEditableFieldModel.tsx b/packages/core/client/src/flow/models/fields/EditableField/NanoIDEditableFieldModel.tsx
index 15961b7099..d2d726a737 100644
--- a/packages/core/client/src/flow/models/fields/EditableField/NanoIDEditableFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/EditableField/NanoIDEditableFieldModel.tsx
@@ -29,11 +29,11 @@ NanoIDEditableFieldModel.registerFlow({
const { size, customAlphabet } = ctx.model.collectionField.options || { size: 21 };
function isValidNanoid(value) {
if (value?.length !== size) {
- return 'Field value size is' + ` ${size || 21}`;
+ return ctx.globals.flowEngine.translate('Field value size is') + ` ${size || 21}`;
}
for (let i = 0; i < value.length; i++) {
if (customAlphabet?.indexOf(value[i]) === -1) {
- return `Field value do not meet the requirements`;
+ return ctx.globals.flowEngine.translate('Field value do not meet the requirements');
}
}
}
diff --git a/packages/core/client/src/flow/models/fields/EditableField/PasswordEditableFieldModel.tsx b/packages/core/client/src/flow/models/fields/EditableField/PasswordEditableFieldModel.tsx
index c210e36644..079c2bfc7c 100644
--- a/packages/core/client/src/flow/models/fields/EditableField/PasswordEditableFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/EditableField/PasswordEditableFieldModel.tsx
@@ -8,6 +8,7 @@
*/
import { Password } from '@formily/antd-v5';
+import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from './EditableFieldModel';
export class PasswordEditableFieldModel extends EditableFieldModel {
@@ -21,14 +22,17 @@ PasswordEditableFieldModel.registerFlow({
key: 'key3',
auto: true,
sort: 1000,
- title: 'Group3',
+ title: tval('Password Options'),
steps: {
placeholder: {
- title: 'Placeholder',
+ title: tval('Placeholder'),
uiSchema: {
- checkStrength: {
+ placeholder: {
'x-component': 'Input',
'x-decorator': 'FormItem',
+ 'x-component-props': {
+ placeholder: tval('Enter placeholder text'),
+ },
},
},
handler(ctx, params) {
@@ -36,14 +40,14 @@ PasswordEditableFieldModel.registerFlow({
},
},
checkStrength: {
- title: 'Check strength',
+ title: tval('Check strength'),
uiSchema: {
checkStrength: {
'x-component': 'Switch',
'x-decorator': 'FormItem',
'x-component-props': {
- checkedChildren: 'Yes',
- unCheckedChildren: 'No',
+ checkedChildren: tval('Yes'),
+ unCheckedChildren: tval('No'),
},
},
},
diff --git a/packages/core/client/src/flow/models/fields/ReadPrettyField/AssociationFieldModel/AssociationSelectReadPrettyFieldModel.tsx b/packages/core/client/src/flow/models/fields/ReadPrettyField/AssociationFieldModel/AssociationSelectReadPrettyFieldModel.tsx
index 7d4e30fa13..54480cea0c 100644
--- a/packages/core/client/src/flow/models/fields/ReadPrettyField/AssociationFieldModel/AssociationSelectReadPrettyFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/ReadPrettyField/AssociationFieldModel/AssociationSelectReadPrettyFieldModel.tsx
@@ -11,8 +11,7 @@ import React from 'react';
import { AssociationReadPrettyFieldModel } from './AssociationReadPrettyFieldModel';
import { FlowEngineProvider, reactive } from '@nocobase/flow-engine';
import { getUniqueKeyFromCollection } from '../../../../../collection-manager/interfaces/utils';
-import { useCompile } from '../../../../../schema-component';
-import { isTitleField } from '../../../../../data-source';
+import { tval } from '@nocobase/utils/client';
export class AssociationSelectReadPrettyFieldModel extends AssociationReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = [
@@ -71,7 +70,7 @@ export class AssociationSelectReadPrettyFieldModel extends AssociationReadPretty
{idx > 0 && ,}
- {v?.[fieldNames.label] ? mol.render() : 'N/A'}
+ {v?.[fieldNames.label] ? mol.render() : this.flowEngine.translate('N/A')}
);
@@ -85,13 +84,13 @@ export class AssociationSelectReadPrettyFieldModel extends AssociationReadPretty
AssociationSelectReadPrettyFieldModel.registerFlow({
key: 'fieldNames',
- title: 'Specific properties',
+ title: tval('Specific properties'),
auto: true,
sort: 200,
steps: {
fieldNames: {
use: 'titleField',
- title: 'Title field',
+ title: tval('Title field'),
handler(ctx, params) {
const { target } = ctx.model.collectionField.options;
const collectionManager = ctx.model.collectionField.collection.collectionManager;
diff --git a/packages/core/client/src/flow/models/fields/ReadPrettyField/DateTimeReadPrettyFieldModel.tsx b/packages/core/client/src/flow/models/fields/ReadPrettyField/DateTimeReadPrettyFieldModel.tsx
index 56f2570d44..2ccb997a9d 100644
--- a/packages/core/client/src/flow/models/fields/ReadPrettyField/DateTimeReadPrettyFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/ReadPrettyField/DateTimeReadPrettyFieldModel.tsx
@@ -10,6 +10,7 @@
import React from 'react';
import dayjs from 'dayjs';
import { reactive } from '@nocobase/flow-engine';
+import { tval } from '@nocobase/utils/client';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
export class DateTimeReadPrettyFieldModel extends ReadPrettyFieldModel {
@@ -54,11 +55,11 @@ DateTimeReadPrettyFieldModel.registerFlow({
key: 'key3',
auto: true,
sort: 1000,
- title: 'Specific properties',
+ title: tval('Specific properties'),
steps: {
dateFormat: {
use: 'dateDisplayFormat',
- title: 'Date display format',
+ title: tval('Date display format'),
defaultParams: (ctx) => {
const { showTime, dateFormat, timeFormat, picker } = ctx.model.props;
return {
diff --git a/packages/core/client/src/flow/models/fields/ReadPrettyField/JsonReadPrettyFieldModel.tsx b/packages/core/client/src/flow/models/fields/ReadPrettyField/JsonReadPrettyFieldModel.tsx
index 4085a51b9e..aa0b34d9b7 100644
--- a/packages/core/client/src/flow/models/fields/ReadPrettyField/JsonReadPrettyFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/ReadPrettyField/JsonReadPrettyFieldModel.tsx
@@ -28,7 +28,7 @@ export class JsonReadPrettyFieldModel extends ReadPrettyFieldModel {
try {
content = JSON.stringify(value, null, space ?? 2);
} catch (error) {
- content = '[Invalid JSON]';
+ content = this.flowEngine.translate('Invalid JSON format');
}
}
diff --git a/packages/core/client/src/flow/models/fields/ReadPrettyField/ReadPrettyFieldModel.tsx b/packages/core/client/src/flow/models/fields/ReadPrettyField/ReadPrettyFieldModel.tsx
index 5da7808088..354ef6d0bf 100644
--- a/packages/core/client/src/flow/models/fields/ReadPrettyField/ReadPrettyFieldModel.tsx
+++ b/packages/core/client/src/flow/models/fields/ReadPrettyField/ReadPrettyFieldModel.tsx
@@ -9,6 +9,7 @@
import React from 'react';
import { reactive } from '@nocobase/flow-engine';
+import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel';
export class ReadPrettyFieldModel extends FieldModel {
@@ -29,7 +30,7 @@ export class ReadPrettyFieldModel extends FieldModel {
ReadPrettyFieldModel.registerFlow({
key: 'ReadPrettyFieldDefault',
auto: true,
- title: 'Basic',
+ title: tval('Basic'),
sort: 100,
steps: {
step1: {
diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.tsx
index 5a6cd82a3c..984ad33f62 100644
--- a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.tsx
+++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.tsx
@@ -13,6 +13,7 @@ import { FormProvider } from '@formily/react';
import { AddActionButton, AddFieldButton, Collection, FlowModelRenderer } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
+import { tval } from '@nocobase/utils/client';
import { FilterBlockModel } from '../../base/BlockModel';
import { FilterFormFieldModel } from './FilterFormFieldModel';
@@ -69,7 +70,7 @@ export class FilterFormModel extends FilterBlockModel {
FilterFormModel.define({
hide: true,
- title: 'Form',
+ title: tval('Form'),
});
FilterFormModel.registerFlow({
@@ -82,20 +83,20 @@ FilterFormModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
- title: 'Data Source Key',
+ title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter data source key',
+ placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
- title: 'Collection Name',
+ title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
- placeholder: 'Enter collection name',
+ placeholder: tval('Enter collection name'),
},
},
},
diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx
index d4993dd021..4a8f0f8201 100644
--- a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx
+++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx
@@ -9,12 +9,13 @@
import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
+import { tval } from '@nocobase/utils/client';
import { DataBlockModel } from '../../base/BlockModel';
import { FilterFormActionModel } from './FilterFormActionModel';
export class FilterFormResetActionModel extends FilterFormActionModel {
defaultProps: ButtonProps = {
- children: 'Reset',
+ children: tval('Reset'),
};
}
@@ -27,7 +28,7 @@ FilterFormResetActionModel.registerFlow({
step1: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.form) {
- ctx.globals.message.error('No form available for reset.');
+ ctx.globals.message.error(ctx.globals.flowEngine.translate('No form available for reset.'));
return;
}
const currentBlockModel = ctx.shared.currentBlockModel;
diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx
index 793ba0f4cc..9bb35c94d1 100644
--- a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx
+++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx
@@ -9,13 +9,14 @@
import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
import type { ButtonProps, ButtonType } from 'antd/es/button';
+import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { FilterFormActionModel } from './FilterFormActionModel';
export class FilterFormSubmitActionModel extends FilterFormActionModel {
defaultProps: ButtonProps = {
- children: 'Filter',
+ title: tval('Filter'),
type: 'primary',
};
}
@@ -29,7 +30,7 @@ FilterFormSubmitActionModel.registerFlow({
step1: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.form) {
- ctx.globals.message.error('No form available for submission.');
+ ctx.globals.message.error(ctx.globals.flowEngine.translate('No form available for submission.'));
return;
}
const currentBlockModel = ctx.shared.currentBlockModel;
diff --git a/packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx b/packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx
index af52471ba7..0220ef3c8b 100644
--- a/packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx
+++ b/packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx
@@ -9,6 +9,7 @@
import { Card } from 'antd';
import React, { createRef } from 'react';
+import { tval } from '@nocobase/utils/client';
import { BlockModel } from '../../base/BlockModel';
function waitForRefCallback(ref: React.RefObject, cb: (el: T) => void, timeout = 3000) {
@@ -34,8 +35,8 @@ export class HtmlBlockModel extends BlockModel {
}
HtmlBlockModel.define({
- title: 'HTML',
- group: 'Content',
+ title: tval('HTML'),
+ group: tval('Content'),
hide: true,
defaultOptions: {
use: 'HtmlBlockModel',
@@ -58,7 +59,7 @@ HtmlBlockModel.registerFlow({
uiSchema: {
html: {
type: 'string',
- title: 'HTML 内容',
+ title: tval('HTML content'),
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: true,
diff --git a/packages/core/client/src/locale/en-US.json b/packages/core/client/src/locale/en-US.json
index 4529a266f8..1607714011 100644
--- a/packages/core/client/src/locale/en-US.json
+++ b/packages/core/client/src/locale/en-US.json
@@ -895,5 +895,87 @@
"Refresh data blocks": "Refresh data blocks",
"Select data blocks to refresh": "Select data blocks to refresh",
"After successful submission, the selected data blocks will be automatically refreshed.": "After successful submission, the selected data blocks will be automatically refreshed.",
- "Reset link expiration": "Reset link expiration"
+ "Reset link expiration": "Reset link expiration",
+ "Imperative Drawer": "Imperative Drawer",
+ "Click event": "Click event",
+ "Form duplicate": "Form duplicate",
+ "Filter configuration": "Filter configuration",
+ "Default filter conditions": "Default filter conditions",
+ "No resource selected for deletion": "No resource selected for deletion",
+ "No records selected for deletion": "No records selected for deletion",
+ "Selected records deleted successfully": "Selected records deleted successfully",
+ "No resource selected for bulk edit": "No resource selected for bulk edit",
+ "No records selected for bulk edit": "No records selected for bulk edit",
+ "Successfully": "Successfully",
+ "No resource selected for refresh": "No resource selected for refresh",
+ "No resource or record selected for deletion": "No resource or record selected for deletion",
+ "Record deleted successfully": "Record deleted successfully",
+ "Please select non-filterable fields": "Please select non-filterable fields",
+ "Update record action": "Update record",
+ "Basic configuration": "Basic configuration",
+ "Configure page": "Configure page",
+ "Page Title": "Page Title",
+ "Enter page title": "Enter page title",
+ "Enable tabs": "Enable tabs",
+ "HTML content": "HTML content",
+ "Action": "Action",
+ "General configuration": "General configuration",
+ "Open mode configuration": "Open mode configuration",
+ "Medium": "Medium",
+ "Refresh data after execution": "Refresh data after execution",
+ "Enable refresh": "Enable refresh",
+ "Data refreshed successfully": "Data refreshed successfully",
+ "Secondary confirmation": "Secondary confirmation",
+ "Content": "Content",
+ "Edit link": "Edit link",
+ "URL": "URL",
+ "Do not concatenate search params in the URL": "Do not concatenate search params in the URL",
+ "Search parameters": "Search parameters",
+ "Add parameter": "Add parameter",
+ "When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data": "When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data",
+ "\"Content-Type\" only support \"application/json\", and no need to specify": "\"Content-Type\" only support \"application/json\", and no need to specify",
+ "Add request header": "Add request header",
+ "Input request data": "Input request data",
+ "Only support standard JSON data": "Only support standard JSON data",
+ "Timeout config": "Timeout config",
+ "Response type": "Response type",
+ "Action after successful submission": "Action after successful submission",
+ "Stay on the current popup or page": "Stay on the current popup or page",
+ "Return to the previous popup or page": "Return to the previous popup or page",
+ "Actions column": "Actions column",
+ "Data Source Key": "Data Source Key",
+ "Enter data source key": "Enter data source key",
+ "Collection Name": "Collection Name",
+ "Enter collection name": "Enter collection name",
+ "Edit page size": "Edit page size",
+ "Set data scope": "Set data scope",
+ "Edit Title": "Edit Title",
+ "Enter field title": "Enter field title",
+ "Display label": "Display label",
+ "ReadOnly": "ReadOnly",
+ "ReadPretty": "ReadPretty",
+ "Modal": "Modal",
+ "Rows": "Rows",
+ "Configure rows": "Configure rows",
+ "Sizes": "Sizes",
+ "Configure the rows and columns of the grid.": "Configure the rows and columns of the grid.",
+ "Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.": "Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.",
+ "Event selected": "Event selected",
+ "Double click": "Double click",
+ "Start": "Start",
+ "End": "End",
+ "Title accessor": "Title accessor",
+ "Enter title accessor": "Enter title accessor",
+ "Start accessor": "Start accessor",
+ "Enter start accessor": "Enter start accessor",
+ "End accessor": "End accessor",
+ "Enter end accessor": "Enter end accessor",
+ "Recommended": "Recommended",
+ "Password Options": "Password Options",
+ "Placeholder": "Placeholder",
+ "Enter placeholder text": "Enter placeholder text",
+ "Check strength": "Check strength",
+ "N/A": "N/A",
+ "No form available for reset.": "No form available for reset.",
+ "No form available for submission.": "No form available for submission."
}
diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json
index 95c4b45390..50da8c010e 100644
--- a/packages/core/client/src/locale/zh-CN.json
+++ b/packages/core/client/src/locale/zh-CN.json
@@ -1139,5 +1139,76 @@
"Calendar Month":"日历月",
"Calendar Year":"日历年",
"Scan to input":"扫码录入",
- "Disable manual input":"禁止手动输入"
+ "Disable manual input":"禁止手动输入",
+ "Imperative Drawer": "命令式抽屉",
+ "Click event": "点击事件",
+ "Form duplicate": "表单复制",
+ "Filter configuration": "筛选配置",
+ "Default filter conditions": "默认筛选条件",
+ "No resource selected for deletion": "未选择要删除的资源",
+ "No records selected for deletion": "未选择要删除的记录",
+ "Selected records deleted successfully": "选中的记录删除成功",
+ "No resource selected for bulk edit": "未选择要批量编辑的资源",
+ "No records selected for bulk edit": "未选择要批量编辑的记录",
+ "Successfully": "成功",
+ "No resource selected for refresh": "未选择要刷新的资源",
+ "No resource or record selected for deletion": "未选择要删除的资源或记录",
+ "Record deleted successfully": "记录删除成功",
+ "Please select non-filterable fields": "请选择不可筛选的字段",
+ "Update record action": "更新记录",
+ "Basic configuration": "基础配置",
+ "Configure page": "配置页面",
+ "Page Title": "页面标题",
+ "Enter page title": "输入页面标题",
+ "Enable tabs": "启用标签页",
+ "HTML content": "HTML内容",
+ "General configuration": "通用配置",
+ "Open mode configuration": "打开方式配置",
+ "Medium": "中",
+ "Refresh data after execution": "执行后刷新数据",
+ "Enable refresh": "启用刷新",
+ "Data refreshed successfully": "数据刷新成功",
+ "When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data": "当 HTTP 方法为 Post、Put 或 Patch,且此自定义请求在表单内时,请求体将自动填充表单数据",
+ "\"Content-Type\" only support \"application/json\", and no need to specify": "\"Content-Type\" 仅支持 \"application/json\",无需指定",
+ "Add request header": "添加请求头",
+ "Input request data": "输入请求数据",
+ "Only support standard JSON data": "仅支持标准 JSON 数据",
+ "Timeout config": "超时配置",
+ "Response type": "响应类型",
+ "Actions column": "操作列",
+ "Data Source Key": "数据源键",
+ "Enter data source key": "输入数据源键",
+ "Collection Name": "数据表名称",
+ "Enter collection name": "输入数据表名称",
+ "Edit page size": "编辑页面大小",
+ "Set data scope": "设置数据范围",
+ "Edit Title": "编辑标题",
+ "Enter field title": "输入字段标题",
+ "Display label": "显示标签",
+ "ReadOnly": "只读",
+ "ReadPretty": "美观只读",
+ "Modal": "对话框",
+ "Rows": "行",
+ "Configure rows": "配置行",
+ "Sizes": "尺寸",
+ "Configure the rows and columns of the grid.": "配置网格的行和列。",
+ "Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.": "配置每行的尺寸。该值是一个数字数组,表示行中每列的宽度。",
+ "Event selected": "已选择事件",
+ "Double click": "双击",
+ "Start": "开始",
+ "End": "结束",
+ "Title accessor": "标题访问器",
+ "Enter title accessor": "输入标题访问器",
+ "Start accessor": "开始访问器",
+ "Enter start accessor": "输入开始访问器",
+ "End accessor": "结束访问器",
+ "Enter end accessor": "输入结束访问器",
+ "Recommended": "推荐",
+ "Password Options": "密码选项",
+ "Placeholder": "占位符",
+ "Enter placeholder text": "输入占位符文本",
+ "Check strength": "检查强度",
+ "N/A": "不适用",
+ "No form available for reset.": "没有可用的表单进行重置。",
+ "No form available for submission.": "没有可用的表单进行提交。"
}
diff --git a/packages/core/flow-engine/src/__tests__/utils.test.ts b/packages/core/flow-engine/src/__tests__/utils.test.ts
deleted file mode 100644
index 3ae58da357..0000000000
--- a/packages/core/flow-engine/src/__tests__/utils.test.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * This file is part of the NocoBase (R) project.
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
- * Authors: NocoBase Team.
- *
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
- * For more information, please refer to: https://www.nocobase.com/agreement.
- */
-
-import { describe, it, expect, beforeEach } from 'vitest';
-import { TranslationUtil } from '../utils';
-
-describe('TranslationUtil', () => {
- let translationUtil: TranslationUtil;
- let mockTranslator: (key: string, options?: any) => string;
-
- beforeEach(() => {
- translationUtil = new TranslationUtil();
- mockTranslator = (key: string, options?: any) => {
- // 简单的模拟翻译函数
- if (options?.name) {
- return key.replace('{name}', options.name);
- }
- return `translated_${key}`;
- };
- });
-
- describe('translate', () => {
- it('should handle simple translation', () => {
- const result = translationUtil.translate('Hello World', mockTranslator);
- expect(result).toBe('translated_Hello World');
- });
-
- it('should handle translation with options', () => {
- const result = translationUtil.translate('Hello {name}', mockTranslator, { name: 'John' });
- expect(result).toBe('Hello John');
- });
-
- it('should handle template compilation with single quotes', () => {
- const template = "{{t('Hello World')}}";
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('translated_Hello World');
- });
-
- it('should handle template compilation with double quotes', () => {
- const template = '{{t("User Name")}}';
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('translated_User Name');
- });
-
- it('should handle template compilation with backticks', () => {
- const template = '{{t(`Email`)}}';
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('translated_Email');
- });
-
- it('should handle template with options', () => {
- const template = '{{t(\'Hello {name}\', {"name": "John"})}}';
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('Hello John');
- });
-
- it('should handle template with whitespace', () => {
- const template = "{{ t ( 'User Name' ) }}";
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('translated_User Name');
- });
-
- it('should handle mixed content', () => {
- const template = "前缀 {{t('User Name')}} 后缀";
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('前缀 translated_User Name 后缀');
- });
-
- it('should handle multiple templates', () => {
- const template = "{{t('Hello')}} {{t('World')}}";
- const result = translationUtil.translate(template, mockTranslator);
- expect(result).toBe('translated_Hello translated_World');
- });
-
- it('should handle invalid template gracefully', () => {
- const template = "{{t('unclosed quote)}}";
- const result = translationUtil.translate(template, mockTranslator);
- // 由于模板格式不匹配,会被当作普通字符串翻译
- expect(result).toBe("translated_{{t('unclosed quote)}}");
- });
-
- it('should return original value for non-string input', () => {
- const result = translationUtil.translate(null as any, mockTranslator);
- expect(result).toBe(null);
- });
-
- it('should return empty string for empty input', () => {
- const result = translationUtil.translate('', mockTranslator);
- expect(result).toBe('');
- });
- });
-
- describe('caching', () => {
- it('should cache template results', () => {
- const template = "{{t('Hello World')}}";
-
- // 第一次调用
- const result1 = translationUtil.translate(template, mockTranslator);
- expect(result1).toBe('translated_Hello World');
- expect(translationUtil.getCacheSize()).toBe(1);
-
- // 第二次调用应该使用缓存
- const result2 = translationUtil.translate(template, mockTranslator);
- expect(result2).toBe('translated_Hello World');
- expect(translationUtil.getCacheSize()).toBe(1);
- });
-
- it('should not cache simple translations', () => {
- const result1 = translationUtil.translate('Hello World', mockTranslator);
- expect(result1).toBe('translated_Hello World');
- expect(translationUtil.getCacheSize()).toBe(0);
- });
-
- it('should clear cache', () => {
- const template = "{{t('Hello World')}}";
- translationUtil.translate(template, mockTranslator);
- expect(translationUtil.getCacheSize()).toBe(1);
-
- translationUtil.clearCache();
- expect(translationUtil.getCacheSize()).toBe(0);
- });
- });
-
- describe('error handling', () => {
- it('should handle translator function errors', () => {
- const errorTranslator = () => {
- throw new Error('Translation error');
- };
-
- const template = "{{t('Hello World')}}";
- const result = translationUtil.translate(template, errorTranslator);
- // 应该返回原始模板而不是抛出错误
- expect(result).toBe("{{t('Hello World')}}");
- });
-
- it('should handle invalid JSON options', () => {
- const template = "{{t('Hello', invalid json)}}";
- const result = translationUtil.translate(template, mockTranslator);
- // JSON 解析失败,但仍会调用翻译器,只是没有 options
- expect(result).toBe('translated_Hello');
- });
- });
-});
diff --git a/packages/core/flow-engine/src/components/FlowErrorFallback.tsx b/packages/core/flow-engine/src/components/FlowErrorFallback.tsx
index c4f01a135c..4af56f880b 100644
--- a/packages/core/flow-engine/src/components/FlowErrorFallback.tsx
+++ b/packages/core/flow-engine/src/components/FlowErrorFallback.tsx
@@ -11,6 +11,7 @@ import { Button, Result, Typography } from 'antd';
import React, { FC, useState } from 'react';
import { FallbackProps } from 'react-error-boundary';
import { useFlowModel } from '../hooks/useFlowModel';
+import { getT } from '../utils';
const { Paragraph, Text } = Typography;
@@ -20,6 +21,7 @@ const { Paragraph, Text } = Typography;
const FlowErrorFallbackInner: FC = ({ error, resetErrorBoundary }) => {
const [loading, setLoading] = useState(false);
const model = useFlowModel(); // 在这里安全地使用 Hook
+ const t = getT(model);
const handleCopyError = async () => {
setLoading(true);
@@ -104,9 +106,9 @@ const FlowErrorFallbackInner: FC = ({ error, resetErrorBoundary }
const subTitle = (
- {'This is likely a NocoBase internals bug. Please open an issue at '}
+ {t('This is likely a NocoBase internals bug. Please open an issue at')}{' '}
- here
+ {t('here')}
{model && (
@@ -121,23 +123,23 @@ const FlowErrorFallbackInner: FC
= ({ error, resetErrorBoundary }
- Feedback
+ {t('Feedback')}
,
canDownloadLogs && (
),
,
resetErrorBoundary && (
),
].filter(Boolean)}
@@ -197,18 +199,18 @@ export const FlowErrorFallback: FC & {
Feedback
,
,
resetErrorBoundary && (
),
].filter(Boolean)}
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx
index 8f12102bc0..90f6ac2d36 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx
@@ -20,6 +20,7 @@ import {
import { FlowModel } from '../../../../models';
import { StepDefinition } from '../../../../types';
import { openStepSettings } from './StepSettings';
+import { getT } from '../../../../utils';
// Type definitions for better type safety
interface StepInfo {
@@ -110,34 +111,35 @@ export const DefaultSettingsIcon: React.FC = ({
flattenSubMenus = true,
}) => {
const { message } = App.useApp();
+ const t = getT(model);
// 分离处理函数以便更好的代码组织
const handleCopyUid = useCallback(async () => {
try {
await navigator.clipboard.writeText(model.uid);
- message.success('UID 已复制到剪贴板');
+ message.success(t('UID copied to clipboard'));
} catch (error) {
- console.error('复制失败:', error);
- message.error('复制失败,请重试');
+ console.error(t('Copy failed'), ':', error);
+ message.error(t('Copy failed, please try again'));
}
}, [model.uid, message]);
const handleDelete = useCallback(() => {
Modal.confirm({
- title: '确认删除',
+ title: t('Confirm delete'),
icon: ,
- content: '确定要删除此项吗?此操作不可撤销。',
- okText: '确认删除',
+ content: t('Are you sure you want to delete this item? This action cannot be undone.'),
+ okText: t('Confirm'),
okType: 'primary',
- cancelText: '取消',
+ cancelText: t('Cancel'),
async onOk() {
try {
await model.destroy();
} catch (error) {
- console.error('删除操作失败:', error);
+ console.error(t('Delete operation failed'), ':', error);
Modal.error({
- title: '删除失败',
- content: '删除操作失败,请检查控制台获取详细信息。',
+ title: t('Delete failed'),
+ content: t('Delete operation failed, please check the console for details.'),
});
}
},
@@ -181,7 +183,7 @@ export const DefaultSettingsIcon: React.FC = ({
stepKey,
});
} catch (error) {
- console.log('配置弹窗已取消或出错:', error);
+ console.log(t('Configuration popup cancelled or error'), ':', error);
}
},
[model],
@@ -231,12 +233,14 @@ export const DefaultSettingsIcon: React.FC = ({
// 如果step使用了action,检查action是否有uiSchema
let hasActionUiSchema = false;
+ let stepTitle = actionStep.title;
if (actionStep.use) {
try {
const action = targetModel.flowEngine?.getAction?.(actionStep.use);
hasActionUiSchema = action && action.uiSchema != null;
+ stepTitle = stepTitle || action.title;
} catch (error) {
- console.warn(`获取action '${actionStep.use}' 失败:`, error);
+ console.warn(t('Failed to get action {{action}}', { action: actionStep.use }), ':', error);
}
}
@@ -253,7 +257,7 @@ export const DefaultSettingsIcon: React.FC = ({
stepKey,
step: actionStep,
uiSchema: mergedUiSchema,
- title: actionStep.title || stepKey,
+ title: t(stepTitle) || stepKey,
modelKey, // 添加模型标识
};
})
@@ -263,7 +267,11 @@ export const DefaultSettingsIcon: React.FC = ({
})
.filter(Boolean);
} catch (error) {
- console.error(`获取模型 '${targetModel?.uid || 'unknown'}' 的可配置flows失败:`, error);
+ console.error(
+ t('Failed to get configurable flows for model {{model}}', { model: targetModel?.uid || 'unknown' }),
+ ':',
+ error,
+ );
return [];
}
}, []);
@@ -338,7 +346,7 @@ export const DefaultSettingsIcon: React.FC = ({
// 在平铺模式下始终按流程分组
items.push({
key: groupKey,
- label: flow.title || flow.key,
+ label: t(flow.title) || flow.key,
type: 'group',
});
@@ -353,7 +361,7 @@ export const DefaultSettingsIcon: React.FC = ({
items.push({
key: uniqueKey,
icon: ,
- label: stepInfo.title,
+ label: t(stepInfo.title),
});
});
});
@@ -382,7 +390,7 @@ export const DefaultSettingsIcon: React.FC = ({
items.push({
key: groupKey,
- label: flow.title || flow.key,
+ label: t(flow.title) || flow.key,
type: 'group',
});
@@ -392,7 +400,7 @@ export const DefaultSettingsIcon: React.FC = ({
items.push({
key: uniqueKey,
icon: ,
- label: stepInfo.title,
+ label: t(stepInfo.title),
});
});
});
@@ -408,7 +416,7 @@ export const DefaultSettingsIcon: React.FC = ({
subMenuChildren.push({
key: uniqueKey,
icon: ,
- label: stepInfo.title,
+ label: t(stepInfo.title),
});
});
});
@@ -443,7 +451,7 @@ export const DefaultSettingsIcon: React.FC = ({
items.push({
key: 'copy-uid',
icon: ,
- label: '复制 UID',
+ label: t('Copy UID'),
});
}
@@ -452,7 +460,7 @@ export const DefaultSettingsIcon: React.FC = ({
items.push({
key: 'delete',
icon: ,
- label: '删除',
+ label: t('Delete'),
});
}
}
@@ -467,7 +475,7 @@ export const DefaultSettingsIcon: React.FC = ({
// 渲染前验证模型
if (!model || !model.uid) {
- console.warn('提供的模型无效');
+ console.warn(t('Invalid model provided'));
return null;
}
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx
index 9bab5e49bd..31207526ca 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx
@@ -15,6 +15,7 @@ import { observer } from '@formily/react';
import { FlowModel } from '../../../../models';
import { useFlowModelById } from '../../../../hooks';
import { openStepSettingsDialog } from './StepSettingsDialog';
+import { getT } from '../../../../utils';
// 右键菜单组件接口
interface ModelProvidedProps {
@@ -70,25 +71,26 @@ const FlowsContextMenu: React.FC = (props) => {
// 使用传入的model
const FlowsContextMenuWithModel: React.FC = observer(
({ model, children, enabled = true, position = 'right', showDeleteButton = true }) => {
+ const t = getT(model);
const handleMenuClick = useCallback(
({ key }: { key: string }) => {
if (key === 'delete') {
// 处理删除操作
Modal.confirm({
- title: '确认删除',
+ title: t('Confirm delete'),
icon: ,
- content: '确定要删除此项吗?此操作不可撤销。',
- okText: '确认删除',
+ content: t('Are you sure you want to delete this item? This action cannot be undone.'),
+ okText: t('Confirm Delete'),
okType: 'danger',
- cancelText: '取消',
+ cancelText: t('Cancel'),
onOk() {
try {
model.dispatchEvent('remove');
} catch (error) {
- console.error('删除操作失败:', error);
+ console.error(t('Delete operation failed'), ':', error);
Modal.error({
- title: '删除失败',
- content: '删除操作失败,请检查控制台获取详细信息。',
+ title: t('Delete failed'),
+ content: t('Delete operation failed, please check the console for details.'),
});
}
},
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx
index 9a3a20e249..b7f1a1b652 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx
@@ -16,6 +16,7 @@ import { ToolbarItemConfig } from '../../../../types';
import { useFlowModelById } from '../../../../hooks';
import { useFlowEngine } from '../../../../provider';
import { FlowEngine } from '../../../../flowEngine';
+import { getT } from '../../../../utils';
import { Droppable } from '../../../dnd';
// 检测DOM中直接子元素是否包含button元素的辅助函数
@@ -300,7 +301,8 @@ const FlowsFloatContextMenuWithModel: React.FC = observer(
}, []);
if (!model) {
- return ;
+ const t = getT(model || ({} as FlowModel));
+ return ;
}
// 如果未启用或没有children,直接返回children
@@ -357,9 +359,10 @@ const FlowsFloatContextMenuWithModelById: React.FC = observer(
extraToolbarItems: extraToolbarItems,
}) => {
const model = useFlowModelById(uid, modelClassName);
+ const flowEngine = useFlowEngine();
if (!model) {
- return ;
+ return ;
}
return (
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx
index 50a7e8b9aa..95f48b7a0b 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx
@@ -12,8 +12,9 @@ import { message, Button } from 'antd';
import React from 'react';
import { FlowModel } from '../../../../models';
import { StepDefinition } from '../../../../types';
-import { resolveDefaultParams, resolveUiSchema } from '../../../../utils';
+import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
+import { toJS } from '@formily/reactive';
/**
* 检查步骤是否已经有了所需的配置值
@@ -116,11 +117,14 @@ export interface StepFormDialogProps {
const openRequiredParamsStepFormDialog = async ({
model,
dialogWidth = 800,
- dialogTitle = '步骤参数配置',
+ dialogTitle,
}: StepFormDialogProps): Promise => {
+ const t = getT(model);
+ const defaultTitle = dialogTitle || t('Step parameter configuration');
+
if (!model) {
- message.error('提供的模型无效');
- throw new Error('提供的模型无效');
+ message.error(t('Invalid model provided'));
+ throw new Error(t('Invalid model provided'));
}
// 创建一个Promise, 并最终返回,当此弹窗关闭时此promise resolve或者reject
@@ -171,8 +175,8 @@ const openRequiredParamsStepFormDialog = async ({
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema,确保step的uiSchema优先级更高
- const mergedUiSchema = { ...resolvedActionUiSchema };
- Object.entries(resolvedStepUiSchema).forEach(([fieldKey, schema]) => {
+ const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
+ Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
@@ -224,7 +228,11 @@ const openRequiredParamsStepFormDialog = async ({
// 解析 defaultParams
const resolvedActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
- const mergedParams = { ...resolvedActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
+ const mergedParams = {
+ ...toJS(resolvedActionDefaultParams),
+ ...toJS(resolvedDefaultParams),
+ ...toJS(stepParams),
+ };
if (Object.keys(mergedParams).length > 0) {
if (!initialValues[flowKey]) {
@@ -300,17 +308,24 @@ const openRequiredParamsStepFormDialog = async ({
// 创建分步表单实例(只有多个步骤时才需要)
const formStep = requiredSteps.length > 1 ? FormStep.createFormStep(0) : null;
+ const flowEngine = model.flowEngine || {};
+ const scopes = {
+ formStep,
+ totalSteps: requiredSteps.length,
+ requiredSteps,
+ useStepSettingContext,
+ ...flowEngine.flowSettings?.scopes,
+ };
+
// 创建FormDialog
const formDialog = FormDialog(
{
- title: dialogTitle,
+ title: dialogTitle || t('Step parameter configuration'),
width: dialogWidth,
footer: null, // 移除默认的底部按钮,使用自定义的导航按钮
destroyOnClose: true,
},
(form) => {
- const flowEngine = model.flowEngine || {};
-
const handleSubmit = async () => {
try {
await form.submit();
@@ -329,7 +344,7 @@ const openRequiredParamsStepFormDialog = async ({
resolve(currentValues);
formDialog.close();
} catch (error) {
- console.error('提交表单时出错:', error);
+ console.error(t('Error submitting form'), ':', error);
// reject(error);
// 这里不需要reject,因为forConfirm会处理
}
@@ -340,37 +355,38 @@ const openRequiredParamsStepFormDialog = async ({
resolve({});
};
+ const dialogScopes = {
+ ...scopes,
+ closeDialog: handleClose,
+ handleNext: () => {
+ // 验证当前步骤的表单
+ form
+ .validate()
+ .then(() => {
+ if (formStep) {
+ formStep.next();
+ }
+ })
+ .catch((errors: any) => {
+ console.log(t('Form validation failed'), ':', errors);
+ // 可以在这里添加更详细的错误处理
+ });
+ },
+ };
+
+ // 编译 formSchema 中的表达式
+ const compiledFormSchema = compileUiSchema(dialogScopes, formSchema);
+
return (
<>
{
- // 验证当前步骤的表单
- form
- .validate()
- .then(() => {
- if (formStep) {
- formStep.next();
- }
- })
- .catch((errors: any) => {
- console.log('表单验证失败:', errors);
- // 可以在这里添加更详细的错误处理
- });
- },
- ...flowEngine.flowSettings?.scopes,
- }}
+ scope={dialogScopes}
/>
@@ -388,7 +404,7 @@ const openRequiredParamsStepFormDialog = async ({
{/* 只有一个步骤时,只显示完成按钮 */}
{requiredSteps.length === 1 ? (
) : (
<>
@@ -400,7 +416,7 @@ const openRequiredParamsStepFormDialog = async ({
}
}}
>
- 上一步
+ {t('Previous step')}
>
)}
@@ -447,10 +463,10 @@ const openRequiredParamsStepFormDialog = async ({
// 打开对话框
formDialog.open({
- initialValues,
+ initialValues: compileUiSchema(scopes, initialValues),
});
} catch (error) {
- reject(new Error(`导入 FormDialog 或 FormStep 失败: ${error.message}`));
+ reject(new Error(`${t('Failed to import FormDialog or FormStep')}: ${error.message}`));
}
})();
}).catch((e) => {
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettings.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettings.tsx
index 8fd167a435..b4579d6077 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettings.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettings.tsx
@@ -12,6 +12,7 @@ import { StepSettingsProps } from '../../../../types';
import { openStepSettingsDialog } from './StepSettingsDialog';
import { openStepSettingsDrawer } from './StepSettingsDrawer';
import { FlowModel } from '../../../../models';
+import { getT } from '../../../../utils';
/**
* 统一的步骤设置入口函数
@@ -24,9 +25,11 @@ import { FlowModel } from '../../../../models';
* @returns Promise 返回表单提交的值
*/
const openStepSettings = async ({ model, flowKey, stepKey, width = 600, title }: StepSettingsProps): Promise => {
+ const t = getT(model);
+
if (!model) {
- message.error('提供的模型无效');
- throw new Error('提供的模型无效');
+ message.error(t('Invalid model provided'));
+ throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
@@ -34,13 +37,13 @@ const openStepSettings = async ({ model, flowKey, stepKey, width = 600, title }:
const step = flow?.steps?.[stepKey];
if (!flow) {
- message.error(`未找到Key为 ${flowKey} 的流程`);
- throw new Error(`未找到Key为 ${flowKey} 的流程`);
+ message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
+ throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
- message.error(`未找到Key为 ${stepKey} 的步骤`);
- throw new Error(`未找到Key为 ${stepKey} 的步骤`);
+ message.error(t('Step with key {{stepKey}} not found', { stepKey }));
+ throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
// 检查步骤的 settingMode 配置,默认为 'dialog'
@@ -84,7 +87,7 @@ const isStepUsingDrawerMode = (model: FlowModel, flowKey: string, stepKey: strin
return step.settingMode === 'drawer';
} catch (error) {
- console.warn('检查步骤设置模式时出错:', error);
+ console.warn('Error checking step setting mode:', error);
return false;
}
};
@@ -107,7 +110,7 @@ const getStepSettingMode = (model: FlowModel, flowKey: string, stepKey: string):
return step.settingMode || 'dialog';
} catch (error) {
- console.warn('获取步骤设置模式时出错:', error);
+ console.warn('Error getting step setting mode:', error);
return null;
}
};
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx
index 301fe9f317..b8c10acd05 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx
@@ -11,8 +11,9 @@ import { createSchemaField, ISchema } from '@formily/react';
import { message } from 'antd';
import React from 'react';
import { StepSettingsDialogProps } from '../../../../types';
-import { resolveDefaultParams, resolveUiSchema } from '../../../../utils';
+import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
+import { toJS } from '@formily/reactive';
const SchemaField = createSchemaField();
@@ -32,9 +33,11 @@ const openStepSettingsDialog = async ({
dialogWidth = 600,
dialogTitle,
}: StepSettingsDialogProps): Promise => {
+ const t = getT(model);
+
if (!model) {
- message.error('提供的模型无效');
- throw new Error('提供的模型无效');
+ message.error(t('Invalid model provided'));
+ throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
@@ -42,16 +45,16 @@ const openStepSettingsDialog = async ({
const step = flow?.steps?.[stepKey];
if (!flow) {
- message.error(`未找到Key为 ${flowKey} 的流程`);
- throw new Error(`未找到Key为 ${flowKey} 的流程`);
+ message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
+ throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
- message.error(`未找到Key为 ${stepKey} 的步骤`);
- throw new Error(`未找到Key为 ${stepKey} 的步骤`);
+ message.error(t('Step with key {{stepKey}} not found', { stepKey }));
+ throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
- const title = dialogTitle || (step ? `${step.title || stepKey} - 配置` : `步骤配置 - ${stepKey}`);
+ let title = step.title;
// 创建参数解析上下文
const paramsContext = {
@@ -72,6 +75,7 @@ const openStepSettingsDialog = async ({
actionUiSchema = action.uiSchema;
}
actionDefaultParams = action.defaultParams || {};
+ title = title || action.title;
}
// 解析动态 uiSchema
@@ -79,8 +83,8 @@ const openStepSettingsDialog = async ({
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema,确保step的uiSchema优先级更高
- const mergedUiSchema = { ...resolvedActionUiSchema };
- Object.entries(resolvedStepUiSchema).forEach(([fieldKey, schema]) => {
+ const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
+ Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
@@ -90,17 +94,23 @@ const openStepSettingsDialog = async ({
// 如果没有可配置的UI Schema,显示提示
if (Object.keys(mergedUiSchema).length === 0) {
- message.info('此步骤没有可配置的参数');
+ message.info(t('This step has no configurable parameters'));
return {};
}
+ const flowEngine = model.flowEngine;
+ const scopes = {
+ useStepSettingContext,
+ ...flowEngine.flowSettings?.scopes,
+ };
+
// 获取初始值
const stepParams = model.getStepParams(flowKey, stepKey) || {};
// 解析 defaultParams
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
- const initialValues = { ...resolveActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
+ const initialValues = { ...toJS(resolveActionDefaultParams), ...toJS(resolvedDefaultParams), ...toJS(stepParams) };
// 构建表单Schema
const formSchema: ISchema = {
@@ -122,21 +132,19 @@ const openStepSettingsDialog = async ({
try {
({ FormDialog } = await import('@formily/antd-v5'));
} catch (error) {
- throw new Error(`导入 FormDialog 失败: ${error.message}`);
+ throw new Error(`${t('Failed to import FormDialog')}: ${error.message}`);
}
// 创建FormDialog
const formDialog = FormDialog(
{
- title,
+ title: dialogTitle || `${t(title)} - ${t('Configuration')}`,
width: dialogWidth,
- okText: '确认',
- cancelText: '取消',
+ okText: t('OK'),
+ cancelText: t('Cancel'),
destroyOnClose: true,
},
(form) => {
- const flowEngine = model.flowEngine;
-
// 创建上下文值
const contextValue: StepSettingContextType = {
model,
@@ -148,17 +156,17 @@ const openStepSettingsDialog = async ({
stepKey,
};
+ // 编译 formSchema 中的表达式
+ const compiledFormSchema = compileUiSchema(scopes, formSchema);
+
return (
);
@@ -172,11 +180,11 @@ const openStepSettingsDialog = async ({
const currentValues = payload.values;
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
- message.success('配置已保存');
+ message.success(t('Configuration saved'));
next(payload);
} catch (error) {
- console.error('保存配置时出错:', error);
- message.error('保存配置时出错,请检查控制台');
+ console.error(t('Error saving configuration'), ':', error);
+ message.error(t('Error saving configuration, please check console'));
throw error;
}
});
@@ -185,7 +193,7 @@ const openStepSettingsDialog = async ({
// 打开对话框
return formDialog.open({
- initialValues,
+ initialValues: compileUiSchema(scopes, initialValues),
});
};
diff --git a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx
index 69f301bd32..1d0811379e 100644
--- a/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx
+++ b/packages/core/flow-engine/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx
@@ -10,9 +10,11 @@
import { createSchemaField, ISchema } from '@formily/react';
import { message, Button, Space } from 'antd';
import React, { useState } from 'react';
-import { StepDefinition, StepSettingsDrawerProps } from '../../../../types';
-import { resolveDefaultParams, resolveUiSchema } from '../../../../utils';
+import { useTranslation } from 'react-i18next';
+import { StepSettingsDrawerProps } from '../../../../types';
+import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
+import { toJS } from '@formily/reactive';
const SchemaField = createSchemaField();
@@ -32,9 +34,11 @@ const openStepSettingsDrawer = async ({
drawerWidth = 600,
drawerTitle,
}: StepSettingsDrawerProps): Promise => {
+ const t = getT(model);
+
if (!model) {
- message.error('提供的模型无效');
- throw new Error('提供的模型无效');
+ message.error(t('Invalid model provided'));
+ throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
@@ -42,16 +46,16 @@ const openStepSettingsDrawer = async ({
const step = flow?.steps?.[stepKey];
if (!flow) {
- message.error(`未找到Key为 ${flowKey} 的流程`);
- throw new Error(`未找到Key为 ${flowKey} 的流程`);
+ message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
+ throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
- message.error(`未找到Key为 ${stepKey} 的步骤`);
- throw new Error(`未找到Key为 ${stepKey} 的步骤`);
+ message.error(t('Step with key {{stepKey}} not found', { stepKey }));
+ throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
- const title = drawerTitle || (step ? `${step.title || stepKey} - 配置` : `步骤配置 - ${stepKey}`);
+ let title = step.title;
// 创建参数解析上下文
const paramsContext = {
@@ -71,6 +75,7 @@ const openStepSettingsDrawer = async ({
actionUiSchema = action.uiSchema;
}
actionDefaultParams = action.defaultParams || {};
+ title = title || action.title;
}
// 解析动态 uiSchema
@@ -78,8 +83,8 @@ const openStepSettingsDrawer = async ({
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema,确保step的uiSchema优先级更高
- const mergedUiSchema = { ...resolvedActionUiSchema };
- Object.entries(resolvedStepUiSchema).forEach(([fieldKey, schema]) => {
+ const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
+ Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
@@ -89,17 +94,23 @@ const openStepSettingsDrawer = async ({
// 如果没有可配置的UI Schema,显示提示
if (Object.keys(mergedUiSchema).length === 0) {
- message.info('此步骤没有可配置的参数');
+ message.info(t('This step has no configurable parameters'));
return {};
}
// 获取初始值
const stepParams = model.getStepParams(flowKey, stepKey) || {};
+ const flowEngine = model.flowEngine;
+ const scopes = {
+ useStepSettingContext,
+ ...flowEngine.flowSettings?.scopes,
+ };
+
// 解析 defaultParams
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
- const initialValues = { ...resolveActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
+ const initialValues = { ...toJS(resolveActionDefaultParams), ...toJS(resolvedDefaultParams), ...toJS(stepParams) };
// 构建表单Schema
const formSchema: ISchema = {
@@ -122,13 +133,13 @@ const openStepSettingsDrawer = async ({
({ Form } = await import('@formily/antd-v5'));
({ createForm } = await import('@formily/core'));
} catch (error) {
- throw new Error(`导入 Formily 组件失败: ${error.message}`);
+ throw new Error(`${t('Failed to import Formily components')}: ${error.message}`);
}
// 获取drawer API
const drawer = model.flowEngine?.context?.drawer;
if (!drawer) {
- throw new Error('Drawer API 不可用,请确保在 FlowEngineGlobalsContextProvider 内使用');
+ throw new Error(t('Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider'));
}
return new Promise((resolve) => {
@@ -137,11 +148,12 @@ const openStepSettingsDrawer = async ({
// 创建表单实例
const form = createForm({
- initialValues,
+ initialValues: compileUiSchema(scopes, initialValues),
});
// 创建抽屉内容组件
const DrawerContent: React.FC = () => {
+ const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
@@ -156,13 +168,13 @@ const openStepSettingsDrawer = async ({
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
- message.success('配置已保存');
+ message.success(t('Configuration saved'));
isResolved = true;
drawerRef.destroy();
resolve(currentValues);
} catch (error) {
- console.error('保存配置时出错:', error);
- message.error('保存配置时出错,请检查控制台');
+ console.error(t('Error saving configuration'), ':', error);
+ message.error(t('Error saving configuration, please check console'));
} finally {
setLoading(false);
}
@@ -176,8 +188,6 @@ const openStepSettingsDrawer = async ({
drawerRef.destroy();
};
- const flowEngine = model.flowEngine;
-
// 创建上下文值
const contextValue: StepSettingContextType = {
model,
@@ -189,20 +199,20 @@ const openStepSettingsDrawer = async ({
stepKey,
};
+ // 编译 formSchema 中的表达式
+ const compiledFormSchema = compileUiSchema(scopes, formSchema);
+
return (
@@ -217,9 +227,9 @@ const openStepSettingsDrawer = async ({
}}
>
-
+
@@ -229,7 +239,7 @@ const openStepSettingsDrawer = async ({
// 打开抽屉
const drawerRef = drawer.open({
- title,
+ title: drawerTitle || `${t(title)} - ${t('Configuration')}`,
width: drawerWidth,
content:
,
onClose: () => {
diff --git a/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx b/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx
index f4b56d0b53..3b0073c25c 100644
--- a/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx
+++ b/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx
@@ -14,6 +14,7 @@ import { ModelConstructor } from '../../types';
import { FlowSettingsButton } from '../common/FlowSettingsButton';
import { withFlowDesignMode } from '../common/withFlowDesignMode';
import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton';
+import { useTranslation } from 'react-i18next';
interface AddActionButtonProps {
/**
@@ -48,6 +49,11 @@ interface AddActionButtonProps {
items?: SubModelItemsType;
}
+const DefaultBtn = () => {
+ const { t } = useTranslation();
+ return
}>{t('Configure actions')};
+};
+
/**
* 专门用于添加动作模型的按钮组件
*
@@ -63,7 +69,7 @@ const AddActionButtonCore: React.FC
= ({
model,
subModelBaseClass = 'ActionFlowModel',
subModelKey = 'actions',
- children = }>{'Configure actions'},
+ children = ,
subModelType = 'array',
items,
filter,
diff --git a/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx b/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx
index c20ece0eeb..16cfc5143c 100644
--- a/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx
+++ b/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx
@@ -15,6 +15,7 @@ import { FlowModelOptions, ModelConstructor } from '../../types';
import { FlowSettingsButton } from '../common/FlowSettingsButton';
import { withFlowDesignMode } from '../common/withFlowDesignMode';
import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton';
+import { useTranslation } from 'react-i18next';
export type BuildCreateModelOptionsType = {
defaultOptions: FlowModelOptions;
@@ -65,6 +66,11 @@ function defaultBuildCreateModelOptions({ defaultOptions }: BuildCreateModelOpti
return defaultOptions;
}
+const DefaultBtn = () => {
+ const { t } = useTranslation();
+ return }>{t('Configure fields')};
+};
+
/**
* 专门用于添加字段模型的按钮组件
*
@@ -80,7 +86,7 @@ const AddFieldButtonCore: React.FC = ({
model,
subModelBaseClass = 'FieldFlowModel',
subModelKey = 'fields',
- children = }>{'Configure fields'},
+ children = ,
subModelType = 'array',
collection,
buildCreateModelOptions = defaultBuildCreateModelOptions,
diff --git a/packages/core/flow-engine/src/components/subModel/LazyDropdown.tsx b/packages/core/flow-engine/src/components/subModel/LazyDropdown.tsx
index ec2a0b90d6..49fb6e0510 100644
--- a/packages/core/flow-engine/src/components/subModel/LazyDropdown.tsx
+++ b/packages/core/flow-engine/src/components/subModel/LazyDropdown.tsx
@@ -9,6 +9,7 @@
import { Dropdown, DropdownProps, Input, Menu, Spin, Empty, InputProps } from 'antd';
import React, { useEffect, useState, useMemo, useRef, FC } from 'react';
+import { useFlowModel } from '../../hooks';
/**
* 通过鼠标的位置计算出最佳的 dropdown 的高度,以尽量避免出现滚动条
@@ -72,6 +73,7 @@ interface LazyDropdownMenuProps extends Omit {
}
const LazyDropdown: React.FC & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
+ const model = useFlowModel();
const [loadedChildren, setLoadedChildren] = useState>({});
const [loadingKeys, setLoadingKeys] = useState>(new Set());
const [menuVisible, setMenuVisible] = useState(false);
@@ -82,6 +84,7 @@ const LazyDropdown: React.FC & { menu: LazyDropdownM
const dropdownMaxHeight = useNiceDropdownMaxHeight([menuVisible]);
const [isSearching, setIsSearching] = useState(false);
const searchTimeoutRef = useRef(null);
+ const t = model.translate;
// 清理定时器,避免内存泄露
useEffect(() => {
@@ -223,7 +226,7 @@ const LazyDropdown: React.FC & { menu: LazyDropdownM
visible={menuVisible}
variant="borderless"
allowClear
- placeholder={item.searchPlaceholder || 'search'}
+ placeholder={t(item.searchPlaceholder || 'Search')}
value={currentSearchValue}
onChange={(e) => {
e.stopPropagation();
@@ -264,7 +267,7 @@ const LazyDropdown: React.FC & { menu: LazyDropdownM
key: `${item.key}-empty`,
label: (
-
+
),
disabled: true,
@@ -278,7 +281,7 @@ const LazyDropdown: React.FC & { menu: LazyDropdownM
return {
type: 'group',
key: item.key,
- label: item.label,
+ label: typeof item.label === 'string' ? t(item.label) : item.label,
children: groupChildren,
};
}
@@ -289,7 +292,7 @@ const LazyDropdown: React.FC & { menu: LazyDropdownM
return {
key: item.key,
- label: item.label,
+ label: typeof item.label === 'string' ? t(item.label) : item.label,
onClick: (info) => {
if (children) {
return;
@@ -316,7 +319,7 @@ const LazyDropdown: React.FC & { menu: LazyDropdownM
key: `${keyPath}-empty`,
label: (
-
+
),
disabled: true,
diff --git a/packages/core/flow-engine/src/data-source/index.ts b/packages/core/flow-engine/src/data-source/index.ts
index 580887ea1b..4a9efd0ccd 100644
--- a/packages/core/flow-engine/src/data-source/index.ts
+++ b/packages/core/flow-engine/src/data-source/index.ts
@@ -96,7 +96,7 @@ export class DataSource {
}
get displayName() {
- return this.options.displayName ? this.flowEngine?.t(this.options.displayName) : this.key;
+ return this.options.displayName ? this.flowEngine?.translate(this.options.displayName) : this.key;
}
get key() {
@@ -272,7 +272,7 @@ export class Collection {
}
get title() {
- return this.options.title ? this.flowEngine?.t(this.options.title) : this.name;
+ return this.options.title ? this.flowEngine?.translate(this.options.title) : this.name;
}
initInherits() {
@@ -413,7 +413,7 @@ export class CollectionField {
get title() {
const titleValue = this.options?.title || this.options?.uiSchema?.title || this.options.name;
- return this.flowEngine?.t(titleValue);
+ return this.flowEngine?.translate(titleValue);
}
set title(value: string) {
diff --git a/packages/core/flow-engine/src/flowEngine.ts b/packages/core/flow-engine/src/flowEngine.ts
index 760dec6861..25993af49e 100644
--- a/packages/core/flow-engine/src/flowEngine.ts
+++ b/packages/core/flow-engine/src/flowEngine.ts
@@ -14,11 +14,13 @@ import {
ActionDefinition,
ActionOptions,
CreateModelOptions,
+ FlowContext,
FlowDefinition,
IFlowModelRepository,
ModelConstructor,
} from './types';
-import { isInheritedFrom, TranslationUtil } from './utils';
+import { isInheritedFrom } from './utils';
+import { initFlowEngineLocale } from './locale';
interface ApplyFlowCacheEntry {
status: 'pending' | 'resolved' | 'rejected';
@@ -36,16 +38,15 @@ export class FlowEngine {
private modelInstances: Map = new Map();
/** @public Stores flow settings including components and scopes for formily settings. */
public flowSettings: FlowSettings = new FlowSettings();
- context: Record = {};
+ context: FlowContext['globals'] = {} as FlowContext['globals'];
private modelRepository: IFlowModelRepository | null = null;
private _applyFlowCache = new Map();
- /** @private Translation utility for template compilation and caching */
- private _translationUtil = new TranslationUtil();
reactView: ReactView;
constructor() {
this.reactView = new ReactView(this);
+ this.flowSettings.registerScopes({ t: this.translate.bind(this) });
}
// 注册默认的 FlowModel
@@ -58,6 +59,9 @@ export class FlowEngine {
setContext(context: any) {
this.context = { ...this.context, ...context };
+ if (this.context.i18n) {
+ initFlowEngineLocale(this.context.i18n);
+ }
}
getContext() {
@@ -82,12 +86,20 @@ export class FlowEngine {
* flowEngine.t("前缀 {{ t('User Name') }} 后缀")
* flowEngine.t("{{t('Hello {name}', {name: 'John'})}}")
*/
- public t(keyOrTemplate: string, options?: any): string {
- return this._translationUtil.translate(
- keyOrTemplate,
- (key: string, opts?: any) => this.translateKey(key, opts),
- options,
- );
+ public translate(keyOrTemplate: string, options?: any): string {
+ if (!keyOrTemplate || typeof keyOrTemplate !== 'string') {
+ return keyOrTemplate;
+ }
+
+ // 先尝试一次翻译
+ let result = this.translateKey(keyOrTemplate, options);
+
+ // 检查翻译结果是否包含模板语法,如果有则进行模板编译
+ if (this.isTemplate(result)) {
+ result = this.compileTemplate(result);
+ }
+
+ return result;
}
/**
@@ -102,6 +114,44 @@ export class FlowEngine {
return key;
}
+ /**
+ * 检查字符串是否包含模板语法
+ * @private
+ */
+ private isTemplate(str: string): boolean {
+ return /\{\{\s*t\s*\(\s*["'`].*?["'`]\s*(?:,\s*.*?)?\s*\)\s*\}\}/g.test(str);
+ }
+
+ /**
+ * 编译模板字符串
+ * @private
+ */
+ private compileTemplate(template: string): string {
+ return template.replace(
+ /\{\{\s*t\s*\(\s*["'`](.*?)["'`]\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
+ (match, key, optionsStr) => {
+ try {
+ let templateOptions = {};
+ if (optionsStr) {
+ optionsStr = optionsStr.trim();
+ if (optionsStr.startsWith('{') && optionsStr.endsWith('}')) {
+ // 使用受限的 Function 构造器解析
+ try {
+ templateOptions = new Function('$root', `with($root) { return (${optionsStr}); }`)({});
+ } catch (parseError) {
+ return match;
+ }
+ }
+ }
+ return this.translateKey(key, templateOptions);
+ } catch (error) {
+ console.warn(`FlowEngine: Failed to compile template "${match}":`, error);
+ return match;
+ }
+ },
+ );
+ }
+
get applyFlowCache() {
return this._applyFlowCache;
}
diff --git a/packages/core/flow-engine/src/locale/en-US.json b/packages/core/flow-engine/src/locale/en-US.json
new file mode 100644
index 0000000000..3f470cd725
--- /dev/null
+++ b/packages/core/flow-engine/src/locale/en-US.json
@@ -0,0 +1,59 @@
+{
+ "Invalid model provided": "Invalid model provided",
+ "Flow with key {{flowKey}} not found": "Flow with key {{flowKey}} not found",
+ "Step with key {{stepKey}} not found": "Step with key {{stepKey}} not found",
+ "Configuration": "Configuration",
+ "Step configuration": "Step configuration",
+ "This step has no configurable parameters": "This step has no configurable parameters",
+ "Failed to import Formily components": "Failed to import Formily components",
+ "Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider": "Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider",
+ "Configuration saved": "Configuration saved",
+ "Error saving configuration": "Error saving configuration",
+ "Error saving configuration, please check console": "Error saving configuration, please check console",
+ "Failed to import FormDialog": "Failed to import FormDialog",
+ "OK": "OK",
+ "Cancel": "Cancel",
+ "Step parameter configuration": "Step parameter configuration",
+ "Error submitting form": "Error submitting form",
+ "Complete configuration": "Complete configuration",
+ "Previous step": "Previous step",
+ "Form validation failed": "Form validation failed",
+ "Next step": "Next step",
+ "Failed to import FormDialog or FormStep": "Failed to import FormDialog or FormStep",
+ "UID copied to clipboard": "UID copied to clipboard",
+ "Copy failed": "Copy failed",
+ "Copy failed, please try again": "Copy failed, please try again",
+ "Confirm delete": "Confirm delete",
+ "Are you sure you want to delete this item? This action cannot be undone.": "Are you sure you want to delete this item? This action cannot be undone.",
+ "Delete operation failed": "Delete operation failed",
+ "Delete failed": "Delete failed",
+ "Delete operation failed, please check the console for details.": "Delete operation failed, please check the console for details.",
+ "Configuration popup cancelled or error": "Configuration popup cancelled or error",
+ "Failed to get action {{action}}": "Failed to get action {{action}}",
+ "Failed to get configurable flows for model {{model}}": "Failed to get configurable flows for model {{model}}",
+ "Copy UID": "Copy UID",
+ "Delete": "Delete",
+ "This is likely a NocoBase internals bug. Please open an issue at": "This is likely a NocoBase internals bug. Please open an issue at",
+ "here": "here",
+ "Render failed": "Render failed",
+ "Feedback": "Feedback",
+ "Download logs": "Download logs",
+ "Copy error info": "Copy error info",
+ "Try again": "Try again",
+ "Data blocks": "Data blocks",
+ "Filter blocks": "Filter blocks",
+ "Other blocks": "Other blocks",
+ "Invalid input parameters": "Invalid input parameters",
+ "Invalid subModelKey format": "Invalid subModelKey format: {{subModelKey}}",
+ "Submodel not found": "Submodel '{{subKey}}' not found",
+ "Expected array for subModel": "Expected array for '{{subKey}}', got {{type}}",
+ "Array index out of bounds": "Array index {{index}} out of bounds for '{{subKey}}'",
+ "Expected object for subModel": "Expected object for '{{subKey}}', got array",
+ "No createModelOptions found for item": "No createModelOptions found for item",
+ "createModelOptions must specify use property": "createModelOptions must specify \"use\" property",
+ "Failed to add sub model": "Failed to add sub model",
+ "Failed to destroy model after creation error": "Failed to destroy model after creation error",
+ "Add": "Add",
+ "Name": "Name",
+ "Model with ID {{uid}} not found": "Model with ID {{uid}} not found"
+}
\ No newline at end of file
diff --git a/packages/core/flow-engine/src/locale/index.ts b/packages/core/flow-engine/src/locale/index.ts
new file mode 100644
index 0000000000..0ea7dc2bfd
--- /dev/null
+++ b/packages/core/flow-engine/src/locale/index.ts
@@ -0,0 +1,38 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import enUS from './en-US.json';
+import zhCN from './zh-CN.json';
+import { FLOW_ENGINE_NAMESPACE } from '../utils';
+
+export const locales = {
+ 'en-US': enUS,
+ 'zh-CN': zhCN,
+};
+
+/**
+ * Get translation for a key with fallback
+ */
+export function getFlowEngineTranslation(key: string, locale = 'en-US'): string {
+ const translations = locales[locale] || locales['en-US'];
+ return translations[key] || key;
+}
+
+/**
+ * Initialize flow-engine locale resources
+ * This should be called when the flow-engine is initialized
+ */
+export function initFlowEngineLocale(i18nInstance?: any) {
+ if (i18nInstance && typeof i18nInstance.addResourceBundle === 'function') {
+ // Register flow-engine translations to the flow-engine namespace
+ Object.entries(locales).forEach(([locale, resources]) => {
+ i18nInstance.addResourceBundle(locale, FLOW_ENGINE_NAMESPACE, resources, true, false);
+ });
+ }
+}
diff --git a/packages/core/flow-engine/src/locale/zh-CN.json b/packages/core/flow-engine/src/locale/zh-CN.json
new file mode 100644
index 0000000000..d4044d60e3
--- /dev/null
+++ b/packages/core/flow-engine/src/locale/zh-CN.json
@@ -0,0 +1,59 @@
+{
+ "Invalid model provided": "提供的模型无效",
+ "Flow with key {{flowKey}} not found": "未找到key为 {{flowKey}} 的流程",
+ "Step with key {{stepKey}} not found": "未找到key为 {{stepKey}} 的步骤",
+ "Configuration": "配置",
+ "Step configuration": "步骤配置",
+ "This step has no configurable parameters": "此步骤没有可配置的参数",
+ "Failed to import Formily components": "导入 Formily 组件失败",
+ "Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider": "抽屉 API 不可用,请确保在 FlowEngineGlobalsContextProvider 中使用",
+ "Configuration saved": "配置已保存",
+ "Error saving configuration": "保存配置时出错",
+ "Error saving configuration, please check console": "保存配置时出错,请检查控制台",
+ "Failed to import FormDialog": "导入 FormDialog 失败",
+ "OK": "确定",
+ "Cancel": "取消",
+ "Step parameter configuration": "步骤参数配置",
+ "Error submitting form": "提交表单时出错",
+ "Complete configuration": "完成配置",
+ "Previous step": "上一步",
+ "Form validation failed": "表单验证失败",
+ "Next step": "下一步",
+ "Failed to import FormDialog or FormStep": "导入 FormDialog 或 FormStep 失败",
+ "UID copied to clipboard": "UID 已复制到剪贴板",
+ "Copy failed": "复制失败",
+ "Copy failed, please try again": "复制失败,请重试",
+ "Confirm delete": "确认删除",
+ "Are you sure you want to delete this item? This action cannot be undone.": "确定要删除此项吗?此操作不可撤销。",
+ "Delete operation failed": "删除操作失败",
+ "Delete failed": "删除失败",
+ "Delete operation failed, please check the console for details.": "删除操作失败,请检查控制台获取详细信息。",
+ "Configuration popup cancelled or error": "配置弹窗已取消或出错",
+ "Failed to get action {{action}}": "获取 action '{{action}}' 失败",
+ "Failed to get configurable flows for model {{model}}": "获取模型 '{{model}}' 的可配置 flows 失败",
+ "Copy UID": "复制 UID",
+ "Delete": "删除",
+ "This is likely a NocoBase internals bug. Please open an issue at": "这可能是 NocoBase 内部错误。请在以下地址提交问题",
+ "here": "这里",
+ "Render failed": "渲染失败",
+ "Feedback": "反馈",
+ "Download logs": "下载日志",
+ "Copy error info": "复制错误信息",
+ "Try again": "重试",
+ "Data blocks": "数据区块",
+ "Filter blocks": "筛选区块",
+ "Other blocks": "其他区块",
+ "Invalid input parameters": "输入参数无效",
+ "Invalid subModelKey format": "无效的 subModelKey 格式: {{subModelKey}}",
+ "Submodel not found": "未找到 Submodel '{{subKey}}'",
+ "Expected array for subModel": "期望 '{{subKey}}' 为数组,实际为 {{type}}",
+ "Array index out of bounds": "数组索引 {{index}} 超出 '{{subKey}}' 的边界",
+ "Expected object for subModel": "期望 '{{subKey}}' 为对象,实际为数组",
+ "No createModelOptions found for item": "未找到该项的 createModelOptions",
+ "createModelOptions must specify use property": "createModelOptions 必须指定 \"use\" 属性",
+ "Failed to add sub model": "添加子模型失败",
+ "Failed to destroy model after creation error": "创建错误后销毁模型失败",
+ "Add": "添加",
+ "Name": "名称",
+ "Model with ID {{uid}} not found": "未找到 ID 为 {{uid}} 的模型"
+}
\ No newline at end of file
diff --git a/packages/core/flow-engine/src/models/flowModel.tsx b/packages/core/flow-engine/src/models/flowModel.tsx
index 5332ffa1be..1918f2af7a 100644
--- a/packages/core/flow-engine/src/models/flowModel.tsx
+++ b/packages/core/flow-engine/src/models/flowModel.tsx
@@ -401,7 +401,7 @@ export class FlowModel = {
exit: () => {
throw new FlowExitException(flowKey, this.uid);
@@ -875,6 +875,10 @@ export class FlowModel) {
this._sharedContext = { ...this._sharedContext, ...ctx };
}
diff --git a/packages/core/flow-engine/src/types.ts b/packages/core/flow-engine/src/types.ts
index b8a59b2f9c..b49299ea0b 100644
--- a/packages/core/flow-engine/src/types.ts
+++ b/packages/core/flow-engine/src/types.ts
@@ -11,6 +11,7 @@ import { ISchema } from '@formily/json-schema';
import type { FlowEngine } from './flowEngine';
import type { FlowModel } from './models';
import { ReactView } from './ReactView';
+import { APIClient } from '@nocobase/sdk';
/**
* 工具类型:如果 T 是数组类型,则提取数组元素类型;否则返回 T 本身
@@ -99,7 +100,11 @@ export interface FlowContext {
reactView: ReactView;
stepResults: Record; // Results from previous steps
shared: Record; // Shared data within the flow (read/write)
- globals: Record; // Global context data (read-only)
+ globals: Record & {
+ flowEngine: FlowEngine;
+ app: any;
+ api: APIClient;
+ };
extra: Record; // Extra context passed to applyFlow (read-only)
model: TModel; // Current model instance with specific type
app: any; // Application instance (required)
diff --git a/packages/core/flow-engine/src/utils.ts b/packages/core/flow-engine/src/utils.ts
index e63e564019..7eb98fd82c 100644
--- a/packages/core/flow-engine/src/utils.ts
+++ b/packages/core/flow-engine/src/utils.ts
@@ -9,9 +9,29 @@
import _ from 'lodash';
import type { ISchema } from '@formily/json-schema';
+import { Schema } from '@formily/json-schema';
import type { FlowModel } from './models';
import { ActionDefinition, DeepPartial, FlowContext, FlowDefinition, ModelConstructor, ParamsContext } from './types';
+// Flow Engine 命名空间常量
+export const FLOW_ENGINE_NAMESPACE = 'flow-engine';
+
+/**
+ * 获取带有 flow-engine 命名空间的翻译函数
+ * @param model FlowModel 实例
+ * @returns 翻译函数,自动使用 flow-engine 命名空间
+ */
+export function getT(model: FlowModel): (key: string, options?: any) => string {
+ if (model.flowEngine?.translate) {
+ return (key: string, options?: any) => {
+ // 自动添加 flow-engine 命名空间
+ return model.flowEngine.translate(key, { ns: [FLOW_ENGINE_NAMESPACE, 'client'], nsMode: 'fallback', ...options });
+ };
+ }
+ // 回退到原始键值
+ return (key: string) => key;
+}
+
export function generateUid(): string {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
@@ -155,95 +175,87 @@ export function defineAction(options: ActionDefinition) {
return options;
}
+// 模块级全局缓存,与 useCompile 保持一致
+const compileCache = {};
+
/**
- * 翻译工具类,负责模板编译和缓存管理
+ * 编译 UI Schema 中的表达式
+ *
+ * @param scope 编译作用域,包含可用的变量和函数(如 t, randomString 等)
+ * @param uiSchema 待编译的 UI Schema
+ * @param options 编译选项
+ * @returns 编译后的 UI Schema
*/
-export class TranslationUtil {
- /** @private Simple template cache - template string -> compiled result */
- private _templateCache = new Map();
+export function compileUiSchema(scope: Record, uiSchema: any, options: { noCache?: boolean } = {}): any {
+ const { noCache = false } = options;
- /**
- * 翻译函数,支持简单翻译和模板编译
- * @param keyOrTemplate 翻译键或包含 {{t('key', options)}} 的模板字符串
- * @param translator 翻译函数,通常是 i18n.t
- * @param options 翻译选项(如命名空间、参数等)
- * @returns 翻译后的文本
- */
- public translate(keyOrTemplate: string, translator: (key: string, options?: any) => string, options?: any): string {
- if (!keyOrTemplate || typeof keyOrTemplate !== 'string') {
- return keyOrTemplate;
+ const hasVariable = (source: string): boolean => {
+ const reg = /\{\{.*?\}\}/g;
+ return reg.test(source);
+ };
+
+ const compile = (source: any): any => {
+ let shouldCompile = false;
+ let cacheKey: string;
+
+ // source is expression, for example: {{ t('Add new') }}
+ if (typeof source === 'string' && source.startsWith('{{')) {
+ shouldCompile = true;
+ cacheKey = source;
}
- // 检查是否包含模板语法 {{t('...')}}
- const hasTemplate = this.isTemplate(keyOrTemplate);
-
- if (hasTemplate) {
- // 模板编译模式 - 简单缓存
- if (this._templateCache.has(keyOrTemplate)) {
- return this._templateCache.get(keyOrTemplate)!;
+ // source is Component Object, for example: { 'x-component': "Cascader", type: "array", title: "所属地区(行政区划)" }
+ if (source && typeof source === 'object' && !Array.isArray(source)) {
+ try {
+ cacheKey = JSON.stringify(source);
+ } catch (e) {
+ console.warn('Failed to stringify:', e);
+ return source;
}
-
- // 解析模板
- const result = this.compileTemplate(keyOrTemplate, translator);
-
- // 简单缓存:直接存储
- this._templateCache.set(keyOrTemplate, result);
- return result;
- } else {
- // 简单翻译模式 - 直接调用翻译函数
- return translator(keyOrTemplate, options);
+ if (compileCache[cacheKey]) return compileCache[cacheKey];
+ shouldCompile = hasVariable(cacheKey);
}
- }
- /**
- * 检查字符串是否包含模板语法
- * @private
- */
- private isTemplate(str: string): boolean {
- return /\{\{\s*t\s*\(\s*["'`].*?["'`]\s*(?:,\s*.*?)?\s*\)\s*\}\}/g.test(str);
- }
+ // source is Array, for example: [{ 'title': "{{ t('Admin') }}", name: 'admin' }, { 'title': "{{ t('Root') }}", name: 'root' }]
+ if (Array.isArray(source)) {
+ try {
+ cacheKey = JSON.stringify(source);
+ } catch (e) {
+ console.warn('Failed to stringify:', e);
+ return source;
+ }
+ if (compileCache[cacheKey]) return compileCache[cacheKey];
+ shouldCompile = hasVariable(cacheKey);
+ }
- /**
- * 编译模板字符串
- * @private
- */
- private compileTemplate(template: string, translator: (key: string, options?: any) => string): string {
- return template.replace(
- /\{\{\s*t\s*\(\s*["'`](.*?)["'`]\s*(?:,\s*((?:[^{}]|\{[^}]*\})*))?\s*\)\s*\}\}/g,
- (match, key, optionsStr) => {
+ if (shouldCompile) {
+ if (!cacheKey) {
try {
- let templateOptions = {};
- if (optionsStr) {
- optionsStr = optionsStr.trim();
- if (optionsStr.startsWith('{') && optionsStr.endsWith('}')) {
- try {
- templateOptions = JSON.parse(optionsStr);
- } catch (jsonError) {
- // JSON 解析失败,返回原始匹配字符串
- return match;
- }
- }
- }
- return translator(key, templateOptions);
+ return Schema.compile(source, scope);
} catch (error) {
- console.warn(`TranslationUtil: Failed to compile template "${match}":`, error);
- return match;
+ console.warn('Failed to compile with Formily Schema.compile:', error);
+ return source;
}
- },
- );
- }
+ }
+ try {
+ if (noCache) {
+ return Schema.compile(source, scope);
+ }
+ compileCache[cacheKey] = compileCache[cacheKey] || Schema.compile(source, scope);
+ return compileCache[cacheKey];
+ } catch (e) {
+ console.log('compileUiSchema error', source, e);
+ try {
+ return Schema.compile(source, scope);
+ } catch (error) {
+ return source;
+ }
+ }
+ }
- /**
- * 清空模板缓存
- */
- public clearCache(): void {
- this._templateCache.clear();
- }
+ // source is: plain object、string、number、boolean、undefined、null
+ return source;
+ };
- /**
- * 获取缓存大小
- */
- public getCacheSize(): number {
- return this._templateCache.size;
- }
+ return compile(uiSchema);
}
diff --git a/packages/plugins/@nocobase/plugin-block-lowcode/src/client/LowcodeBlockModel.tsx b/packages/plugins/@nocobase/plugin-block-lowcode/src/client/LowcodeBlockModel.tsx
index ef28d88478..b60a5b5849 100644
--- a/packages/plugins/@nocobase/plugin-block-lowcode/src/client/LowcodeBlockModel.tsx
+++ b/packages/plugins/@nocobase/plugin-block-lowcode/src/client/LowcodeBlockModel.tsx
@@ -19,7 +19,7 @@ import * as antd from 'antd';
import { Card, Spin } from 'antd';
import React, { createRef } from 'react';
import ReactDOM from 'react-dom/client';
-import { NAMESPACE } from './locale';
+import { NAMESPACE, tStr } from './locale';
export class LowcodeBlockModel extends BlockModel {
ref = createRef();
declare resource: APIResource;
@@ -125,15 +125,15 @@ ctx.element.innerHTML = \`
LowcodeBlockModel.registerFlow({
key: 'default',
- title: 'Configuration',
+ title: tStr('Configuration'),
auto: true,
steps: {
executionStep: {
- title: 'Code',
+ title: tStr('Code'),
uiSchema: {
code: {
type: 'string',
- title: 'Execution Code',
+ title: tStr('Execution Code'),
'x-component': 'CodeEditor',
'x-component-props': {
minHeight: '400px',