@bpmn-io/form-js-viewer 1.24.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -16,6 +16,46 @@ import { parseExpression, parseUnaryTests, evaluate, unaryTest } from '@bpmn-io/
16
16
  import { evaluate as evaluate$1, parseToSimpleTree } from 'feelers';
17
17
  import { marked } from 'marked';
18
18
 
19
+ /**
20
+ * @typedef { {
21
+ * [key: string]: string;
22
+ * } } TranslateReplacements
23
+ */
24
+
25
+ /**
26
+ * A simple translation stub to be used for multi-language support
27
+ * in diagrams. Can be easily replaced with a more sophisticated
28
+ * solution.
29
+ *
30
+ * @example
31
+ *
32
+ * ```javascript
33
+ * // use it inside any diagram component by injecting `translate`.
34
+ *
35
+ * function MyService(translate) {
36
+ * alert(translate('HELLO {you}', { you: 'You!' }));
37
+ * }
38
+ * ```
39
+ *
40
+ * @param {string} template to interpolate
41
+ * @param {TranslateReplacements} [replacements] a map with substitutes
42
+ *
43
+ * @return {string} the translated string
44
+ */
45
+ function translate(template, replacements) {
46
+ replacements = replacements || {};
47
+ return template.replace(/{([^}]+)}/g, function (_, key) {
48
+ return replacements[key] || '{' + key + '}';
49
+ });
50
+ }
51
+
52
+ /**
53
+ * @type { import('didi').ModuleDeclaration }
54
+ */
55
+ var TranslateModule = {
56
+ translate: ['value', translate]
57
+ };
58
+
19
59
  const getFlavouredFeelVariableNames = (feelString, feelFlavour = 'expression', options = {}) => {
20
60
  const {
21
61
  depth = 0,
@@ -1940,10 +1980,13 @@ function Label(props) {
1940
1980
  class: classNames('fjs-form-field-label', {
1941
1981
  'fjs-incollapsible-label': !collapseOnEmpty
1942
1982
  }, props['class']),
1943
- children: [props.children, evaluatedLabel, required && jsx("span", {
1944
- class: "fjs-asterix",
1945
- "aria-hidden": true,
1946
- children: "*"
1983
+ children: [props.children, jsxs("span", {
1984
+ class: "fjs-label-content",
1985
+ children: [evaluatedLabel, required && jsx("span", {
1986
+ class: "fjs-asterix",
1987
+ "aria-hidden": true,
1988
+ children: "*"
1989
+ })]
1947
1990
  })]
1948
1991
  });
1949
1992
  }
@@ -3044,6 +3087,7 @@ function Datetime(props) {
3044
3087
  const {
3045
3088
  formId
3046
3089
  } = useContext(FormContext);
3090
+ const translate = useService('translate');
3047
3091
 
3048
3092
  /** @type {import("preact").RefObject<HTMLDivElement>} */
3049
3093
  const dateTimeGroupRef = useRef();
@@ -3134,8 +3178,8 @@ function Datetime(props) {
3134
3178
  const allErrors = useMemo(() => {
3135
3179
  if (required || subtype !== DATETIME_SUBTYPES.DATETIME) return errors;
3136
3180
  const isOnlyOneFieldSet = isValidDate(dateTime.date) && !isValidTime(dateTime.time) || !isValidDate(dateTime.date) && isValidTime(dateTime.time);
3137
- return isOnlyOneFieldSet ? ['Date and time must both be entered.', ...errors] : errors;
3138
- }, [required, subtype, dateTime, errors]);
3181
+ return isOnlyOneFieldSet ? [translate('Date and time must both be entered.'), ...errors] : errors;
3182
+ }, [required, subtype, dateTime, errors, translate]);
3139
3183
  const setDate = useCallback(date => {
3140
3184
  setDateTimeUpdateRequest(prev => prev ? {
3141
3185
  ...prev,
@@ -3297,6 +3341,7 @@ function IFrame(props) {
3297
3341
  const evaluatedLabel = useSingleLineTemplateEvaluation(label, {
3298
3342
  debug: true
3299
3343
  });
3344
+ const translate = useService('translate');
3300
3345
  const [sandbox, allow] = useSecurityAttributesMap(security);
3301
3346
  const [iframeRefresh, setIframeRefresh] = useState(0);
3302
3347
 
@@ -3313,7 +3358,7 @@ function IFrame(props) {
3313
3358
  htmlFor: domId,
3314
3359
  label: evaluatedLabel
3315
3360
  }), !evaluatedUrl && jsx(IFramePlaceholder, {
3316
- text: "No content to show."
3361
+ text: translate('No content to show.')
3317
3362
  }), evaluatedUrl && safeUrl && jsx("iframe", {
3318
3363
  src: safeUrl,
3319
3364
  title: evaluatedLabel,
@@ -3323,7 +3368,7 @@ function IFrame(props) {
3323
3368
  sandbox: sandbox,
3324
3369
  allow
3325
3370
  }, 'iframe-' + iframeRefresh), evaluatedUrl && !safeUrl && jsx(IFramePlaceholder, {
3326
- text: "External content couldn't be loaded."
3371
+ text: translate("External content couldn't be loaded.")
3327
3372
  })]
3328
3373
  });
3329
3374
  }
@@ -3911,6 +3956,7 @@ function Numberfield(props) {
3911
3956
  const {
3912
3957
  required
3913
3958
  } = validate;
3959
+ const translate = useService('translate');
3914
3960
 
3915
3961
  /** @type {import("preact").RefObject<HTMLInputElement>} */
3916
3962
  const inputRef = useRef();
@@ -4084,7 +4130,7 @@ function Numberfield(props) {
4084
4130
  children: [jsx("button", {
4085
4131
  type: "button",
4086
4132
  class: "fjs-number-arrow-up",
4087
- "aria-label": "Increment",
4133
+ "aria-label": translate('Increment'),
4088
4134
  onClick: () => increment(),
4089
4135
  tabIndex: -1,
4090
4136
  children: jsx(SvgAngelUp, {})
@@ -4093,7 +4139,7 @@ function Numberfield(props) {
4093
4139
  }), jsx("button", {
4094
4140
  type: "button",
4095
4141
  class: "fjs-number-arrow-down",
4096
- "aria-label": "Decrement",
4142
+ "aria-label": translate('Decrement'),
4097
4143
  onClick: () => decrement(),
4098
4144
  tabIndex: -1,
4099
4145
  children: jsx(SvgAngelDown, {})
@@ -4776,6 +4822,7 @@ function Taglist(props) {
4776
4822
  /** @type {import("preact").RefObject<HTMLInputElement>} */
4777
4823
  const inputRef = useRef();
4778
4824
  const eventBus = useService('eventBus');
4825
+ const translate = useService('translate');
4779
4826
  const {
4780
4827
  loadState,
4781
4828
  options
@@ -4909,7 +4956,7 @@ function Taglist(props) {
4909
4956
  htmlFor: domId
4910
4957
  }), !disabled && !readonly && !!values.length && jsx(SkipLink, {
4911
4958
  className: "fjs-taglist-skip-link",
4912
- label: "Skip to search",
4959
+ label: translate('Skip to search'),
4913
4960
  onSkip: onSkipToSearch
4914
4961
  }), jsxs("div", {
4915
4962
  class: classNames('fjs-taglist', {
@@ -4930,7 +4977,7 @@ function Taglist(props) {
4930
4977
  children: getLabelCorrelation(entry)
4931
4978
  }), !disabled && !readonly && jsx("button", {
4932
4979
  type: "button",
4933
- title: "Remove tag",
4980
+ title: translate('Remove tag'),
4934
4981
  class: "fjs-taglist-tag-remove",
4935
4982
  onFocus: onElementFocus,
4936
4983
  onBlur: onElementBlur,
@@ -4948,7 +4995,7 @@ function Taglist(props) {
4948
4995
  onChange: onInputChange,
4949
4996
  type: "text",
4950
4997
  value: filter,
4951
- placeholder: disabled || readonly ? undefined : 'Search',
4998
+ placeholder: disabled || readonly ? undefined : translate('Search'),
4952
4999
  autoComplete: "off",
4953
5000
  onKeyDown: onInputKeyDown,
4954
5001
  onMouseDown: () => setIsEscapeClose(false),
@@ -4964,7 +5011,7 @@ function Taglist(props) {
4964
5011
  values: filteredOptions,
4965
5012
  getLabel: option => option.label,
4966
5013
  onValueSelected: option => selectValue(option.value),
4967
- emptyListMessage: hasOptionsLeft ? 'No results' : 'All values selected',
5014
+ emptyListMessage: hasOptionsLeft ? translate('No results') : translate('All values selected'),
4968
5015
  listenerElement: inputRef.current
4969
5016
  })
4970
5017
  }), jsx(Description, {
@@ -5567,6 +5614,8 @@ var SvgCaretRight = function SvgCaretRight(props) {
5567
5614
  const type$2 = 'table';
5568
5615
 
5569
5616
  /**
5617
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
5618
+ *
5570
5619
  * @typedef {('asc'|'desc')} Direction
5571
5620
  *
5572
5621
  * @typedef Sorting
@@ -5616,6 +5665,7 @@ function Table(props) {
5616
5665
  const chunkedData = isNumber(rowCount) ? chunk(sortedData, rowCount) : [sortedData];
5617
5666
  const [currentPage, setCurrentPage] = useState(0);
5618
5667
  const currentChunk = chunkedData[currentPage] || [];
5668
+ const translate = useService('translate');
5619
5669
  useEffect(() => {
5620
5670
  setCurrentPage(0);
5621
5671
  }, [rowCount, sortBy]);
@@ -5647,7 +5697,7 @@ function Table(props) {
5647
5697
  class: classNames('fjs-table-middle-container', {
5648
5698
  'fjs-table-empty': evaluatedColumns.length === 0
5649
5699
  }),
5650
- children: [evaluatedColumns.length === 0 ? 'Nothing to show.' : jsx("div", {
5700
+ children: [evaluatedColumns.length === 0 ? translate('Nothing to show.') : jsx("div", {
5651
5701
  class: "fjs-table-inner-container",
5652
5702
  children: jsxs("table", {
5653
5703
  class: "fjs-table",
@@ -5672,7 +5722,7 @@ function Table(props) {
5672
5722
  toggleSortBy(key);
5673
5723
  }
5674
5724
  },
5675
- "aria-label": getHeaderAriaLabel(sortBy, key, displayLabel),
5725
+ "aria-label": getHeaderAriaLabel(sortBy, key, displayLabel, translate),
5676
5726
  children: jsxs("span", {
5677
5727
  class: "fjs-table-th-label",
5678
5728
  children: [displayLabel, sortBy !== null && sortBy.key === key ? jsx(Fragment, {
@@ -5693,7 +5743,7 @@ function Table(props) {
5693
5743
  children: jsx("td", {
5694
5744
  class: "fjs-table-td",
5695
5745
  colSpan: evaluatedColumns.length,
5696
- children: "Nothing to show."
5746
+ children: translate('Nothing to show.')
5697
5747
  })
5698
5748
  })
5699
5749
  }) : jsx("tbody", {
@@ -5709,9 +5759,13 @@ function Table(props) {
5709
5759
  })
5710
5760
  }), isNumber(rowCount) && chunkedData.length > 1 && evaluatedColumns.length > 0 ? jsxs("nav", {
5711
5761
  class: "fjs-table-nav",
5712
- children: [jsxs("span", {
5762
+ children: [jsx("span", {
5713
5763
  class: "fjs-table-nav-label",
5714
- children: [currentPage + 1, " of ", chunkedData.length]
5764
+ children: translate('{page} of {total}', {
5765
+ // stringify: see Validator.js
5766
+ page: (currentPage + 1).toString(),
5767
+ total: chunkedData.length.toString()
5768
+ })
5715
5769
  }), jsx("button", {
5716
5770
  type: "button",
5717
5771
  class: "fjs-table-nav-button",
@@ -5719,7 +5773,7 @@ function Table(props) {
5719
5773
  setCurrentPage(page => Math.max(page - 1, 0));
5720
5774
  },
5721
5775
  disabled: currentPage === 0,
5722
- "aria-label": "Previous page",
5776
+ "aria-label": translate('Previous page'),
5723
5777
  children: jsx(SvgCaretLeft, {})
5724
5778
  }), jsx("button", {
5725
5779
  type: "button",
@@ -5728,7 +5782,7 @@ function Table(props) {
5728
5782
  setCurrentPage(page => Math.min(page + 1, chunkedData.length - 1));
5729
5783
  },
5730
5784
  disabled: currentPage >= chunkedData.length - 1,
5731
- "aria-label": "Next page",
5785
+ "aria-label": translate('Next page'),
5732
5786
  children: jsx(SvgCaretRight, {})
5733
5787
  })]
5734
5788
  }) : null]
@@ -5878,15 +5932,20 @@ function sortByColumn(array, key, direction) {
5878
5932
  * @param {null|Sorting} sortBy
5879
5933
  * @param {string} key
5880
5934
  * @param {string} label
5935
+ * @param {Translate} translate
5881
5936
  */
5882
- function getHeaderAriaLabel(sortBy, key, label) {
5937
+ function getHeaderAriaLabel(sortBy, key, label, translate) {
5883
5938
  if (sortBy === null || sortBy.key !== key) {
5884
- return `Click to sort by ${label} descending`;
5939
+ return translate('Click to sort by {value} descending', {
5940
+ value: label
5941
+ });
5885
5942
  }
5886
5943
  if (sortBy.direction === 'asc') {
5887
- return 'Click to remove sorting';
5944
+ return translate('Click to remove sorting');
5888
5945
  }
5889
- return `Click to sort by ${label} ascending`;
5946
+ return translate('Click to sort by {value} ascending', {
5947
+ value: label
5948
+ });
5890
5949
  }
5891
5950
 
5892
5951
  /**
@@ -5957,6 +6016,7 @@ function FilePicker(props) {
5957
6016
  const errorMessageId = `${domId}-error-message`;
5958
6017
  /** @type {File[]} */
5959
6018
  const selectedFiles = fileRegistry === null ? EMPTY_ARRAY$1 : fileRegistry.getFiles(filesKey);
6019
+ const translate = useService('translate');
5960
6020
  useEffect(() => {
5961
6021
  if (filesKey && fileRegistry !== null && !fileRegistry.hasKey(filesKey)) {
5962
6022
  onChange({
@@ -6027,7 +6087,7 @@ function FilePicker(props) {
6027
6087
  children: "Browse"
6028
6088
  }), jsx("span", {
6029
6089
  className: "fjs-form-field-label",
6030
- children: getSelectedFilesLabel(selectedFiles)
6090
+ children: getSelectedFilesLabel(selectedFiles, translate)
6031
6091
  })]
6032
6092
  }), jsx(Errors, {
6033
6093
  id: errorMessageId,
@@ -6050,16 +6110,19 @@ FilePicker.config = {
6050
6110
 
6051
6111
  /**
6052
6112
  * @param {File[]} files
6113
+ * @param {Translate} translate
6053
6114
  * @returns {string}
6054
6115
  */
6055
- function getSelectedFilesLabel(files) {
6116
+ function getSelectedFilesLabel(files, translate) {
6056
6117
  if (files.length === 0) {
6057
- return 'No files selected';
6118
+ return translate('No files selected');
6058
6119
  }
6059
6120
  if (files.length === 1) {
6060
6121
  return files[0].name;
6061
6122
  }
6062
- return `${files.length} files selected`;
6123
+ return translate('{value} files selected', {
6124
+ value: files.length.toString()
6125
+ });
6063
6126
  }
6064
6127
 
6065
6128
  var _path$4;
@@ -6082,6 +6145,10 @@ var SvgDownload = function SvgDownload(props) {
6082
6145
 
6083
6146
  const type = 'documentPreview';
6084
6147
 
6148
+ /**
6149
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
6150
+ */
6151
+
6085
6152
  /**
6086
6153
  * @typedef DocumentEndpointBuilder
6087
6154
  * @property {(document: DocumentMetadata) => string} [buildUrl]
@@ -6127,6 +6194,7 @@ function DocumentPreview(props) {
6127
6194
  const evaluatedLabel = useSingleLineTemplateEvaluation(label, {
6128
6195
  debug: true
6129
6196
  });
6197
+ const translate = useService('translate');
6130
6198
  return jsxs("div", {
6131
6199
  class: formFieldClasses(type),
6132
6200
  children: [jsx(Label, {
@@ -6152,7 +6220,7 @@ function DocumentPreview(props) {
6152
6220
  id: errorMessageId,
6153
6221
  errors: getErrors({
6154
6222
  dataSource
6155
- })
6223
+ }, translate)
6156
6224
  })]
6157
6225
  });
6158
6226
  }
@@ -6174,15 +6242,16 @@ DocumentPreview.config = {
6174
6242
  * @property {string|undefined} dataSource
6175
6243
  *
6176
6244
  * @param {GetErrorOptions} options
6245
+ * @param {Translate} translate
6177
6246
  * @returns {string[]}
6178
6247
  */
6179
- function getErrors(options) {
6248
+ function getErrors(options, translate) {
6180
6249
  const {
6181
6250
  dataSource
6182
6251
  } = options;
6183
6252
  let errors = [];
6184
6253
  if (!isString(dataSource) || dataSource.length < 1) {
6185
- errors.push('Document reference is not defined.');
6254
+ errors.push(translate('Document reference is not defined.'));
6186
6255
  }
6187
6256
  return errors;
6188
6257
  }
@@ -6235,6 +6304,7 @@ function PdfRenderer(props) {
6235
6304
  /** @type {ReturnType<typeof import("preact/hooks").useState<null | string>>} */
6236
6305
  const [pdfObjectUrl, setPdfObjectUrl] = useState(null);
6237
6306
  const [hasError, setHasError] = useState(false);
6307
+ const translate = useService('translate');
6238
6308
  useEffect(() => {
6239
6309
  /** @type {null | string} */
6240
6310
  let objectUrl = null;
@@ -6268,7 +6338,7 @@ function PdfRenderer(props) {
6268
6338
  class: `fjs-${type}-pdf-viewer`
6269
6339
  }) : null, hasError ? jsx(Errors, {
6270
6340
  id: errorMessageId,
6271
- errors: ['Unable to download document']
6341
+ errors: [translate('Unable to download document')]
6272
6342
  }) : null]
6273
6343
  });
6274
6344
  }
@@ -6348,9 +6418,10 @@ function DocumentRenderer(props) {
6348
6418
  const isInViewport = useInViewport(ref);
6349
6419
  const onError = useCallback(() => setHasError(true), []);
6350
6420
  const requestInit = useMemo(() => getDocumentRequestInit(documentEndpointBuilder, documentMetadata), [documentEndpointBuilder, documentMetadata]);
6421
+ const translate = useService('translate');
6351
6422
  const singleDocumentContainerClassName = `fjs-${type}-single-document-container`;
6352
6423
  const errorMessageId = `${domId}-error-message`;
6353
- const errorMessage = 'Unable to download document';
6424
+ const errorMessage = translate('Unable to download document');
6354
6425
  const isContentTypePresent = typeof metadata.contentType === 'string';
6355
6426
  if (isContentTypePresent && metadata.contentType.toLowerCase().startsWith('image/') && isInViewport) {
6356
6427
  return jsxs("div", {
@@ -6428,6 +6499,7 @@ function DownloadButton(props) {
6428
6499
  onDownloadError,
6429
6500
  requestInit
6430
6501
  } = props;
6502
+ const translate = useService('translate');
6431
6503
  const handleDownload = async () => {
6432
6504
  try {
6433
6505
  const response = await fetch(endpoint, requestInit);
@@ -6450,7 +6522,9 @@ function DownloadButton(props) {
6450
6522
  type: "button",
6451
6523
  onClick: handleDownload,
6452
6524
  class: classNames(`fjs-${type}-download-button`),
6453
- "aria-label": `Download ${fileName}`,
6525
+ "aria-label": translate('Download {fileName}', {
6526
+ fileName
6527
+ }),
6454
6528
  children: jsx(SvgDownload, {})
6455
6529
  });
6456
6530
  }
@@ -7796,6 +7870,7 @@ class RepeatRenderManager {
7796
7870
  const {
7797
7871
  data
7798
7872
  } = this._form._getState();
7873
+ const translate = useService('translate');
7799
7874
  const dataPath = this._pathRegistry.getValuePath(repeaterField, {
7800
7875
  indexes
7801
7876
  });
@@ -7852,16 +7927,18 @@ class RepeatRenderManager {
7852
7927
  ref: addButtonRef,
7853
7928
  onClick: onAddItem,
7854
7929
  children: jsxs(Fragment, {
7855
- children: [jsx(SvgAdd, {}), " ", 'Add new']
7930
+ children: [jsx(SvgAdd, {}), " ", translate('Add new')]
7856
7931
  })
7857
7932
  }) : null, collapseEnabled ? jsx("button", {
7858
7933
  type: "button",
7859
7934
  class: "fjs-repeat-render-collapse",
7860
7935
  onClick: toggle,
7861
7936
  children: isCollapsed ? jsxs(Fragment, {
7862
- children: [jsx(SvgExpand, {}), " ", `Expand all (${values.length - 1})`]
7937
+ children: [jsx(SvgExpand, {}), " ", translate('Expand all ({value})', {
7938
+ value: values.length - 1
7939
+ })]
7863
7940
  }) : jsxs(Fragment, {
7864
- children: [jsx(SvgCollapse, {}), " ", 'Collapse']
7941
+ children: [jsx(SvgCollapse, {}), " ", translate('Collapse')]
7865
7942
  })
7866
7943
  }) : null]
7867
7944
  });
@@ -8494,11 +8571,12 @@ const EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-
8494
8571
  const PHONE_PATTERN = /(\+|00)(297|93|244|1264|358|355|376|971|54|374|1684|1268|61|43|994|257|32|229|226|880|359|973|1242|387|590|375|501|1441|591|55|1246|673|975|267|236|1|61|41|56|86|225|237|243|242|682|57|269|238|506|53|5999|61|1345|357|420|49|253|1767|45|1809|1829|1849|213|593|20|291|212|34|372|251|358|679|500|33|298|691|241|44|995|44|233|350|224|590|220|245|240|30|1473|299|502|594|1671|592|852|504|385|509|36|62|44|91|246|353|98|964|354|972|39|1876|44|962|81|76|77|254|996|855|686|1869|82|383|965|856|961|231|218|1758|423|94|266|370|352|371|853|590|212|377|373|261|960|52|692|389|223|356|95|382|976|1670|258|222|1664|596|230|265|60|262|264|687|227|672|234|505|683|31|47|977|674|64|968|92|507|64|51|63|680|675|48|1787|1939|850|351|595|970|689|974|262|40|7|250|966|249|221|65|500|4779|677|232|503|378|252|508|381|211|239|597|421|386|46|268|1721|248|963|1649|235|228|66|992|690|993|670|676|1868|216|90|688|886|255|256|380|598|1|998|3906698|379|1784|58|1284|1340|84|678|681|685|967|27|260|263)(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{4,20}$/;
8495
8572
  const VALIDATE_FEEL_PROPERTIES = ['min', 'max', 'minLength', 'maxLength'];
8496
8573
  class Validator {
8497
- constructor(expressionLanguage, conditionChecker, form, formFieldRegistry) {
8574
+ constructor(expressionLanguage, conditionChecker, form, formFieldRegistry, translate) {
8498
8575
  this._expressionLanguage = expressionLanguage;
8499
8576
  this._conditionChecker = conditionChecker;
8500
8577
  this._form = form;
8501
8578
  this._formFieldRegistry = formFieldRegistry;
8579
+ this._translate = translate;
8502
8580
  }
8503
8581
 
8504
8582
  /**
@@ -8513,13 +8591,13 @@ class Validator {
8513
8591
  } = field;
8514
8592
  let errors = [];
8515
8593
  if (type === 'number') {
8516
- errors = [...errors, ...runNumberValidation(field, value)];
8594
+ errors = [...errors, ...runNumberValidation(field, value, this._translate)];
8517
8595
  }
8518
8596
  if (!validate) {
8519
8597
  return errors;
8520
8598
  }
8521
8599
  const evaluatedValidation = oldEvaluateFEELValues(validate, this._expressionLanguage, this._conditionChecker, this._form);
8522
- errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)];
8600
+ errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value, this._translate)];
8523
8601
  return errors;
8524
8602
  }
8525
8603
 
@@ -8546,31 +8624,39 @@ class Validator {
8546
8624
  } = field;
8547
8625
  let errors = [];
8548
8626
  if (type === 'number') {
8549
- errors = [...errors, ...runNumberValidation(field, value)];
8627
+ errors = [...errors, ...runNumberValidation(field, value, this._translate)];
8550
8628
  }
8551
8629
  if (!validate) {
8552
8630
  return errors;
8553
8631
  }
8554
8632
  const evaluatedValidation = evaluateFEELValues(validate, this._expressionLanguage, expressionContextInfo);
8555
- errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)];
8633
+ errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value, this._translate)];
8556
8634
  return errors;
8557
8635
  }
8558
8636
  }
8559
- Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry'];
8637
+ Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry', 'translate'];
8560
8638
 
8561
8639
  // helpers //////////
8562
8640
 
8563
- function runNumberValidation(field, value) {
8641
+ function runNumberValidation(field, value, translate) {
8564
8642
  const {
8565
8643
  decimalDigits,
8566
8644
  increment
8567
8645
  } = field;
8568
8646
  const errors = [];
8569
8647
  if (value === 'NaN') {
8570
- errors.push('Value is not a number.');
8648
+ errors.push(translate('Value is not a number.'));
8571
8649
  } else if (value) {
8572
8650
  if (decimalDigits >= 0 && countDecimals(value) > decimalDigits) {
8573
- errors.push('Value is expected to ' + (decimalDigits === 0 ? 'be an integer' : `have at most ${decimalDigits} decimal digit${decimalDigits > 1 ? 's' : ''}`) + '.');
8651
+ if (decimalDigits === 0) {
8652
+ errors.push(translate('Value is expected to be an integer.'));
8653
+ } else if (decimalDigits === 1) {
8654
+ errors.push(translate('Value is expected to have at most 1 decimal digit.'));
8655
+ } else {
8656
+ errors.push(translate('Value is expected to have at most {value} decimal digits.', {
8657
+ value: decimalDigits.toString()
8658
+ }));
8659
+ }
8574
8660
  }
8575
8661
  if (increment) {
8576
8662
  const bigValue = Big(value);
@@ -8579,54 +8665,69 @@ function runNumberValidation(field, value) {
8579
8665
  if (offset.cmp(0) !== 0) {
8580
8666
  const previousValue = bigValue.minus(offset);
8581
8667
  const nextValue = previousValue.plus(bigIncrement);
8582
- errors.push(`Please select a valid value, the two nearest valid values are ${previousValue} and ${nextValue}.`);
8668
+ errors.push(translate('Please select a valid value, the two nearest valid values are {previousValue} and {nextValue}.', {
8669
+ previousValue: previousValue,
8670
+ nextValue: nextValue
8671
+ }));
8583
8672
  }
8584
8673
  }
8585
8674
  }
8586
8675
  return errors;
8587
8676
  }
8588
- function runPresetValidation(field, validation, value) {
8677
+ function runPresetValidation(field, validation, value, translate) {
8589
8678
  const errors = [];
8590
8679
  if (validation.pattern && value && !new RegExp(validation.pattern).test(value)) {
8591
- errors.push(validation.patternErrorMessage || `Field must match pattern ${validation.pattern}.`);
8680
+ errors.push(validation.patternErrorMessage || translate('Field must match pattern {value}.', {
8681
+ value: validation.pattern
8682
+ }));
8592
8683
  }
8593
8684
  if (validation.required) {
8594
8685
  const isUncheckedCheckbox = field.type === 'checkbox' && value === false;
8595
8686
  const isUnsetValue = isNil(value) || value === '';
8596
8687
  const isEmptyMultiselect = Array.isArray(value) && value.length === 0;
8597
8688
  if (isUncheckedCheckbox || isUnsetValue || isEmptyMultiselect) {
8598
- errors.push('Field is required.');
8689
+ errors.push(translate('Field is required.'));
8599
8690
  }
8600
8691
  }
8692
+
8693
+ // stringify: `translate` falls back to the raw placeholder for a falsy replacement, so 0 must not be passed as a number
8601
8694
  if ('min' in validation && (value || value === 0)) {
8602
8695
  try {
8603
8696
  if (Big(value).lt(Big(validation.min))) {
8604
- errors.push(`Field must have minimum value of ${validation.min}.`);
8697
+ errors.push(translate('Field must have minimum value of {value}.', {
8698
+ value: validation.min.toString()
8699
+ }));
8605
8700
  }
8606
8701
  } catch {
8607
- errors.push('Min validation value is not a valid number.');
8702
+ errors.push(translate('Min validation value is not a valid number.'));
8608
8703
  }
8609
8704
  }
8610
8705
  if ('max' in validation && (value || value === 0)) {
8611
8706
  try {
8612
8707
  if (Big(value).gt(Big(validation.max))) {
8613
- errors.push(`Field must have maximum value of ${validation.max}.`);
8708
+ errors.push(translate('Field must have maximum value of {value}.', {
8709
+ value: validation.max.toString()
8710
+ }));
8614
8711
  }
8615
8712
  } catch {
8616
- errors.push('Max validation value is not a valid number.');
8713
+ errors.push(translate('Max validation value is not a valid number.'));
8617
8714
  }
8618
8715
  }
8619
8716
  if ('minLength' in validation && value && value.trim().length < validation.minLength) {
8620
- errors.push(`Field must have minimum length of ${validation.minLength}.`);
8717
+ errors.push(translate('Field must have minimum length of {value}.', {
8718
+ value: validation.minLength.toString()
8719
+ }));
8621
8720
  }
8622
8721
  if ('maxLength' in validation && value && value.trim().length > validation.maxLength) {
8623
- errors.push(`Field must have maximum length of ${validation.maxLength}.`);
8722
+ errors.push(translate('Field must have maximum length of {value}.', {
8723
+ value: validation.maxLength.toString()
8724
+ }));
8624
8725
  }
8625
8726
  if ('validationType' in validation && value && validation.validationType === 'phone' && !PHONE_PATTERN.test(value)) {
8626
- errors.push('Field must be a valid international phone number. (e.g. +4930664040900)');
8727
+ errors.push(translate('Field must be a valid international phone number. (e.g. +4930664040900)'));
8627
8728
  }
8628
8729
  if ('validationType' in validation && value && validation.validationType === 'email' && !EMAIL_PATTERN.test(value)) {
8629
- errors.push('Field must be a valid email.');
8730
+ errors.push(translate('Field must be a valid email.'));
8630
8731
  }
8631
8732
  return errors;
8632
8733
  }
@@ -8784,17 +8885,24 @@ class Importer {
8784
8885
  }
8785
8886
  Importer.$inject = ['formFieldRegistry', 'pathRegistry', 'fieldFactory', 'formLayouter'];
8786
8887
 
8888
+ /**
8889
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
8890
+ */
8891
+
8787
8892
  class FieldFactory {
8788
8893
  /**
8789
8894
  * @constructor
8790
8895
  *
8791
- * @param formFieldRegistry
8792
- * @param formFields
8896
+ * @param formFieldRegistry
8897
+ * @param pathRegistry
8898
+ * @param formFields
8899
+ * @param {Translate} translate
8793
8900
  */
8794
- constructor(formFieldRegistry, pathRegistry, formFields) {
8901
+ constructor(formFieldRegistry, pathRegistry, formFields, translate) {
8795
8902
  this._formFieldRegistry = formFieldRegistry;
8796
8903
  this._pathRegistry = pathRegistry;
8797
8904
  this._formFields = formFields;
8905
+ this._translate = translate;
8798
8906
  }
8799
8907
  create(attrs, isNewField = true) {
8800
8908
  const {
@@ -8827,13 +8935,17 @@ class FieldFactory {
8827
8935
  isClosed: true,
8828
8936
  knownAncestorIds
8829
8937
  })) {
8830
- throw new Error(`binding path '${[...parentPath, key].join('.')}' is already claimed`);
8938
+ throw new Error(this._translate("binding path '{value}' is already claimed", {
8939
+ value: [...parentPath, key].join('.')
8940
+ }));
8831
8941
  }
8832
8942
  if (config.pathed && path && !this._pathRegistry.canClaimPath([...parentPath, ...path.split('.')], {
8833
8943
  isRepeatable: config.repeatable,
8834
8944
  knownAncestorIds
8835
8945
  })) {
8836
- throw new Error(`binding path '${[...parentPath, ...path.split('.')].join('.')}' is already claimed`);
8946
+ throw new Error(this._translate("binding path '{value}' is already claimed", {
8947
+ value: [...parentPath, ...path.split('.')].join('.')
8948
+ }));
8837
8949
  }
8838
8950
  const field = config.create({
8839
8951
  ...(config.label ? {
@@ -8894,7 +9006,7 @@ class FieldFactory {
8894
9006
  return `${field.type}_${random}`;
8895
9007
  }
8896
9008
  }
8897
- FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields'];
9009
+ FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields', 'translate'];
8898
9010
 
8899
9011
  /**
8900
9012
  * The PathRegistry class manages a hierarchical structure of paths associated with form fields.
@@ -9904,7 +10016,7 @@ class Form {
9904
10016
  config: ['value', enrichedConfig]
9905
10017
  }, {
9906
10018
  form: ['value', this]
9907
- }, CoreModule, ...modules, ...additionalModules]);
10019
+ }, TranslateModule, CoreModule, ...modules, ...additionalModules]);
9908
10020
  }
9909
10021
 
9910
10022
  /**