@bpmn-io/form-js-viewer 1.25.0 → 1.26.1

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,
@@ -3047,6 +3087,7 @@ function Datetime(props) {
3047
3087
  const {
3048
3088
  formId
3049
3089
  } = useContext(FormContext);
3090
+ const translate = useService('translate');
3050
3091
 
3051
3092
  /** @type {import("preact").RefObject<HTMLDivElement>} */
3052
3093
  const dateTimeGroupRef = useRef();
@@ -3137,8 +3178,8 @@ function Datetime(props) {
3137
3178
  const allErrors = useMemo(() => {
3138
3179
  if (required || subtype !== DATETIME_SUBTYPES.DATETIME) return errors;
3139
3180
  const isOnlyOneFieldSet = isValidDate(dateTime.date) && !isValidTime(dateTime.time) || !isValidDate(dateTime.date) && isValidTime(dateTime.time);
3140
- return isOnlyOneFieldSet ? ['Date and time must both be entered.', ...errors] : errors;
3141
- }, [required, subtype, dateTime, errors]);
3181
+ return isOnlyOneFieldSet ? [translate('Date and time must both be entered.'), ...errors] : errors;
3182
+ }, [required, subtype, dateTime, errors, translate]);
3142
3183
  const setDate = useCallback(date => {
3143
3184
  setDateTimeUpdateRequest(prev => prev ? {
3144
3185
  ...prev,
@@ -3300,6 +3341,7 @@ function IFrame(props) {
3300
3341
  const evaluatedLabel = useSingleLineTemplateEvaluation(label, {
3301
3342
  debug: true
3302
3343
  });
3344
+ const translate = useService('translate');
3303
3345
  const [sandbox, allow] = useSecurityAttributesMap(security);
3304
3346
  const [iframeRefresh, setIframeRefresh] = useState(0);
3305
3347
 
@@ -3316,7 +3358,7 @@ function IFrame(props) {
3316
3358
  htmlFor: domId,
3317
3359
  label: evaluatedLabel
3318
3360
  }), !evaluatedUrl && jsx(IFramePlaceholder, {
3319
- text: "No content to show."
3361
+ text: translate('No content to show.')
3320
3362
  }), evaluatedUrl && safeUrl && jsx("iframe", {
3321
3363
  src: safeUrl,
3322
3364
  title: evaluatedLabel,
@@ -3326,7 +3368,7 @@ function IFrame(props) {
3326
3368
  sandbox: sandbox,
3327
3369
  allow
3328
3370
  }, 'iframe-' + iframeRefresh), evaluatedUrl && !safeUrl && jsx(IFramePlaceholder, {
3329
- text: "External content couldn't be loaded."
3371
+ text: translate("External content couldn't be loaded.")
3330
3372
  })]
3331
3373
  });
3332
3374
  }
@@ -3914,6 +3956,7 @@ function Numberfield(props) {
3914
3956
  const {
3915
3957
  required
3916
3958
  } = validate;
3959
+ const translate = useService('translate');
3917
3960
 
3918
3961
  /** @type {import("preact").RefObject<HTMLInputElement>} */
3919
3962
  const inputRef = useRef();
@@ -4087,7 +4130,7 @@ function Numberfield(props) {
4087
4130
  children: [jsx("button", {
4088
4131
  type: "button",
4089
4132
  class: "fjs-number-arrow-up",
4090
- "aria-label": "Increment",
4133
+ "aria-label": translate('Increment'),
4091
4134
  onClick: () => increment(),
4092
4135
  tabIndex: -1,
4093
4136
  children: jsx(SvgAngelUp, {})
@@ -4096,7 +4139,7 @@ function Numberfield(props) {
4096
4139
  }), jsx("button", {
4097
4140
  type: "button",
4098
4141
  class: "fjs-number-arrow-down",
4099
- "aria-label": "Decrement",
4142
+ "aria-label": translate('Decrement'),
4100
4143
  onClick: () => decrement(),
4101
4144
  tabIndex: -1,
4102
4145
  children: jsx(SvgAngelDown, {})
@@ -4779,6 +4822,7 @@ function Taglist(props) {
4779
4822
  /** @type {import("preact").RefObject<HTMLInputElement>} */
4780
4823
  const inputRef = useRef();
4781
4824
  const eventBus = useService('eventBus');
4825
+ const translate = useService('translate');
4782
4826
  const {
4783
4827
  loadState,
4784
4828
  options
@@ -4912,7 +4956,7 @@ function Taglist(props) {
4912
4956
  htmlFor: domId
4913
4957
  }), !disabled && !readonly && !!values.length && jsx(SkipLink, {
4914
4958
  className: "fjs-taglist-skip-link",
4915
- label: "Skip to search",
4959
+ label: translate('Skip to search'),
4916
4960
  onSkip: onSkipToSearch
4917
4961
  }), jsxs("div", {
4918
4962
  class: classNames('fjs-taglist', {
@@ -4933,7 +4977,7 @@ function Taglist(props) {
4933
4977
  children: getLabelCorrelation(entry)
4934
4978
  }), !disabled && !readonly && jsx("button", {
4935
4979
  type: "button",
4936
- title: "Remove tag",
4980
+ title: translate('Remove tag'),
4937
4981
  class: "fjs-taglist-tag-remove",
4938
4982
  onFocus: onElementFocus,
4939
4983
  onBlur: onElementBlur,
@@ -4951,7 +4995,7 @@ function Taglist(props) {
4951
4995
  onChange: onInputChange,
4952
4996
  type: "text",
4953
4997
  value: filter,
4954
- placeholder: disabled || readonly ? undefined : 'Search',
4998
+ placeholder: disabled || readonly ? undefined : translate('Search'),
4955
4999
  autoComplete: "off",
4956
5000
  onKeyDown: onInputKeyDown,
4957
5001
  onMouseDown: () => setIsEscapeClose(false),
@@ -4967,7 +5011,7 @@ function Taglist(props) {
4967
5011
  values: filteredOptions,
4968
5012
  getLabel: option => option.label,
4969
5013
  onValueSelected: option => selectValue(option.value),
4970
- emptyListMessage: hasOptionsLeft ? 'No results' : 'All values selected',
5014
+ emptyListMessage: hasOptionsLeft ? translate('No results') : translate('All values selected'),
4971
5015
  listenerElement: inputRef.current
4972
5016
  })
4973
5017
  }), jsx(Description, {
@@ -5570,6 +5614,8 @@ var SvgCaretRight = function SvgCaretRight(props) {
5570
5614
  const type$2 = 'table';
5571
5615
 
5572
5616
  /**
5617
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
5618
+ *
5573
5619
  * @typedef {('asc'|'desc')} Direction
5574
5620
  *
5575
5621
  * @typedef Sorting
@@ -5619,6 +5665,7 @@ function Table(props) {
5619
5665
  const chunkedData = isNumber(rowCount) ? chunk(sortedData, rowCount) : [sortedData];
5620
5666
  const [currentPage, setCurrentPage] = useState(0);
5621
5667
  const currentChunk = chunkedData[currentPage] || [];
5668
+ const translate = useService('translate');
5622
5669
  useEffect(() => {
5623
5670
  setCurrentPage(0);
5624
5671
  }, [rowCount, sortBy]);
@@ -5650,7 +5697,7 @@ function Table(props) {
5650
5697
  class: classNames('fjs-table-middle-container', {
5651
5698
  'fjs-table-empty': evaluatedColumns.length === 0
5652
5699
  }),
5653
- children: [evaluatedColumns.length === 0 ? 'Nothing to show.' : jsx("div", {
5700
+ children: [evaluatedColumns.length === 0 ? translate('Nothing to show.') : jsx("div", {
5654
5701
  class: "fjs-table-inner-container",
5655
5702
  children: jsxs("table", {
5656
5703
  class: "fjs-table",
@@ -5675,7 +5722,7 @@ function Table(props) {
5675
5722
  toggleSortBy(key);
5676
5723
  }
5677
5724
  },
5678
- "aria-label": getHeaderAriaLabel(sortBy, key, displayLabel),
5725
+ "aria-label": getHeaderAriaLabel(sortBy, key, displayLabel, translate),
5679
5726
  children: jsxs("span", {
5680
5727
  class: "fjs-table-th-label",
5681
5728
  children: [displayLabel, sortBy !== null && sortBy.key === key ? jsx(Fragment, {
@@ -5696,7 +5743,7 @@ function Table(props) {
5696
5743
  children: jsx("td", {
5697
5744
  class: "fjs-table-td",
5698
5745
  colSpan: evaluatedColumns.length,
5699
- children: "Nothing to show."
5746
+ children: translate('Nothing to show.')
5700
5747
  })
5701
5748
  })
5702
5749
  }) : jsx("tbody", {
@@ -5712,9 +5759,13 @@ function Table(props) {
5712
5759
  })
5713
5760
  }), isNumber(rowCount) && chunkedData.length > 1 && evaluatedColumns.length > 0 ? jsxs("nav", {
5714
5761
  class: "fjs-table-nav",
5715
- children: [jsxs("span", {
5762
+ children: [jsx("span", {
5716
5763
  class: "fjs-table-nav-label",
5717
- 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
+ })
5718
5769
  }), jsx("button", {
5719
5770
  type: "button",
5720
5771
  class: "fjs-table-nav-button",
@@ -5722,7 +5773,7 @@ function Table(props) {
5722
5773
  setCurrentPage(page => Math.max(page - 1, 0));
5723
5774
  },
5724
5775
  disabled: currentPage === 0,
5725
- "aria-label": "Previous page",
5776
+ "aria-label": translate('Previous page'),
5726
5777
  children: jsx(SvgCaretLeft, {})
5727
5778
  }), jsx("button", {
5728
5779
  type: "button",
@@ -5731,7 +5782,7 @@ function Table(props) {
5731
5782
  setCurrentPage(page => Math.min(page + 1, chunkedData.length - 1));
5732
5783
  },
5733
5784
  disabled: currentPage >= chunkedData.length - 1,
5734
- "aria-label": "Next page",
5785
+ "aria-label": translate('Next page'),
5735
5786
  children: jsx(SvgCaretRight, {})
5736
5787
  })]
5737
5788
  }) : null]
@@ -5881,15 +5932,20 @@ function sortByColumn(array, key, direction) {
5881
5932
  * @param {null|Sorting} sortBy
5882
5933
  * @param {string} key
5883
5934
  * @param {string} label
5935
+ * @param {Translate} translate
5884
5936
  */
5885
- function getHeaderAriaLabel(sortBy, key, label) {
5937
+ function getHeaderAriaLabel(sortBy, key, label, translate) {
5886
5938
  if (sortBy === null || sortBy.key !== key) {
5887
- return `Click to sort by ${label} descending`;
5939
+ return translate('Click to sort by {value} descending', {
5940
+ value: label
5941
+ });
5888
5942
  }
5889
5943
  if (sortBy.direction === 'asc') {
5890
- return 'Click to remove sorting';
5944
+ return translate('Click to remove sorting');
5891
5945
  }
5892
- return `Click to sort by ${label} ascending`;
5946
+ return translate('Click to sort by {value} ascending', {
5947
+ value: label
5948
+ });
5893
5949
  }
5894
5950
 
5895
5951
  /**
@@ -5960,6 +6016,7 @@ function FilePicker(props) {
5960
6016
  const errorMessageId = `${domId}-error-message`;
5961
6017
  /** @type {File[]} */
5962
6018
  const selectedFiles = fileRegistry === null ? EMPTY_ARRAY$1 : fileRegistry.getFiles(filesKey);
6019
+ const translate = useService('translate');
5963
6020
  useEffect(() => {
5964
6021
  if (filesKey && fileRegistry !== null && !fileRegistry.hasKey(filesKey)) {
5965
6022
  onChange({
@@ -6030,7 +6087,7 @@ function FilePicker(props) {
6030
6087
  children: "Browse"
6031
6088
  }), jsx("span", {
6032
6089
  className: "fjs-form-field-label",
6033
- children: getSelectedFilesLabel(selectedFiles)
6090
+ children: getSelectedFilesLabel(selectedFiles, translate)
6034
6091
  })]
6035
6092
  }), jsx(Errors, {
6036
6093
  id: errorMessageId,
@@ -6053,16 +6110,19 @@ FilePicker.config = {
6053
6110
 
6054
6111
  /**
6055
6112
  * @param {File[]} files
6113
+ * @param {Translate} translate
6056
6114
  * @returns {string}
6057
6115
  */
6058
- function getSelectedFilesLabel(files) {
6116
+ function getSelectedFilesLabel(files, translate) {
6059
6117
  if (files.length === 0) {
6060
- return 'No files selected';
6118
+ return translate('No files selected');
6061
6119
  }
6062
6120
  if (files.length === 1) {
6063
6121
  return files[0].name;
6064
6122
  }
6065
- return `${files.length} files selected`;
6123
+ return translate('{value} files selected', {
6124
+ value: files.length.toString()
6125
+ });
6066
6126
  }
6067
6127
 
6068
6128
  var _path$4;
@@ -6085,6 +6145,10 @@ var SvgDownload = function SvgDownload(props) {
6085
6145
 
6086
6146
  const type = 'documentPreview';
6087
6147
 
6148
+ /**
6149
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
6150
+ */
6151
+
6088
6152
  /**
6089
6153
  * @typedef DocumentEndpointBuilder
6090
6154
  * @property {(document: DocumentMetadata) => string} [buildUrl]
@@ -6130,6 +6194,7 @@ function DocumentPreview(props) {
6130
6194
  const evaluatedLabel = useSingleLineTemplateEvaluation(label, {
6131
6195
  debug: true
6132
6196
  });
6197
+ const translate = useService('translate');
6133
6198
  return jsxs("div", {
6134
6199
  class: formFieldClasses(type),
6135
6200
  children: [jsx(Label, {
@@ -6155,7 +6220,7 @@ function DocumentPreview(props) {
6155
6220
  id: errorMessageId,
6156
6221
  errors: getErrors({
6157
6222
  dataSource
6158
- })
6223
+ }, translate)
6159
6224
  })]
6160
6225
  });
6161
6226
  }
@@ -6177,15 +6242,16 @@ DocumentPreview.config = {
6177
6242
  * @property {string|undefined} dataSource
6178
6243
  *
6179
6244
  * @param {GetErrorOptions} options
6245
+ * @param {Translate} translate
6180
6246
  * @returns {string[]}
6181
6247
  */
6182
- function getErrors(options) {
6248
+ function getErrors(options, translate) {
6183
6249
  const {
6184
6250
  dataSource
6185
6251
  } = options;
6186
6252
  let errors = [];
6187
6253
  if (!isString(dataSource) || dataSource.length < 1) {
6188
- errors.push('Document reference is not defined.');
6254
+ errors.push(translate('Document reference is not defined.'));
6189
6255
  }
6190
6256
  return errors;
6191
6257
  }
@@ -6238,6 +6304,7 @@ function PdfRenderer(props) {
6238
6304
  /** @type {ReturnType<typeof import("preact/hooks").useState<null | string>>} */
6239
6305
  const [pdfObjectUrl, setPdfObjectUrl] = useState(null);
6240
6306
  const [hasError, setHasError] = useState(false);
6307
+ const translate = useService('translate');
6241
6308
  useEffect(() => {
6242
6309
  /** @type {null | string} */
6243
6310
  let objectUrl = null;
@@ -6249,7 +6316,13 @@ function PdfRenderer(props) {
6249
6316
  onError();
6250
6317
  return;
6251
6318
  }
6252
- const blob = await response.blob();
6319
+
6320
+ // Re-type the response instead of trusting its content type. An object URL
6321
+ // inherits the form's origin and <embed> renders it by the blob's own type,
6322
+ // so a response served as HTML would execute script in that origin.
6323
+ const blob = new Blob([await response.arrayBuffer()], {
6324
+ type: 'application/pdf'
6325
+ });
6253
6326
  objectUrl = URL.createObjectURL(blob);
6254
6327
  setPdfObjectUrl(objectUrl);
6255
6328
  } catch {
@@ -6271,7 +6344,7 @@ function PdfRenderer(props) {
6271
6344
  class: `fjs-${type}-pdf-viewer`
6272
6345
  }) : null, hasError ? jsx(Errors, {
6273
6346
  id: errorMessageId,
6274
- errors: ['Unable to download document']
6347
+ errors: [translate('Unable to download document')]
6275
6348
  }) : null]
6276
6349
  });
6277
6350
  }
@@ -6351,9 +6424,10 @@ function DocumentRenderer(props) {
6351
6424
  const isInViewport = useInViewport(ref);
6352
6425
  const onError = useCallback(() => setHasError(true), []);
6353
6426
  const requestInit = useMemo(() => getDocumentRequestInit(documentEndpointBuilder, documentMetadata), [documentEndpointBuilder, documentMetadata]);
6427
+ const translate = useService('translate');
6354
6428
  const singleDocumentContainerClassName = `fjs-${type}-single-document-container`;
6355
6429
  const errorMessageId = `${domId}-error-message`;
6356
- const errorMessage = 'Unable to download document';
6430
+ const errorMessage = translate('Unable to download document');
6357
6431
  const isContentTypePresent = typeof metadata.contentType === 'string';
6358
6432
  if (isContentTypePresent && metadata.contentType.toLowerCase().startsWith('image/') && isInViewport) {
6359
6433
  return jsxs("div", {
@@ -6431,6 +6505,7 @@ function DownloadButton(props) {
6431
6505
  onDownloadError,
6432
6506
  requestInit
6433
6507
  } = props;
6508
+ const translate = useService('translate');
6434
6509
  const handleDownload = async () => {
6435
6510
  try {
6436
6511
  const response = await fetch(endpoint, requestInit);
@@ -6453,7 +6528,9 @@ function DownloadButton(props) {
6453
6528
  type: "button",
6454
6529
  onClick: handleDownload,
6455
6530
  class: classNames(`fjs-${type}-download-button`),
6456
- "aria-label": `Download ${fileName}`,
6531
+ "aria-label": translate('Download {fileName}', {
6532
+ fileName
6533
+ }),
6457
6534
  children: jsx(SvgDownload, {})
6458
6535
  });
6459
6536
  }
@@ -7799,6 +7876,7 @@ class RepeatRenderManager {
7799
7876
  const {
7800
7877
  data
7801
7878
  } = this._form._getState();
7879
+ const translate = useService('translate');
7802
7880
  const dataPath = this._pathRegistry.getValuePath(repeaterField, {
7803
7881
  indexes
7804
7882
  });
@@ -7855,16 +7933,18 @@ class RepeatRenderManager {
7855
7933
  ref: addButtonRef,
7856
7934
  onClick: onAddItem,
7857
7935
  children: jsxs(Fragment, {
7858
- children: [jsx(SvgAdd, {}), " ", 'Add new']
7936
+ children: [jsx(SvgAdd, {}), " ", translate('Add new')]
7859
7937
  })
7860
7938
  }) : null, collapseEnabled ? jsx("button", {
7861
7939
  type: "button",
7862
7940
  class: "fjs-repeat-render-collapse",
7863
7941
  onClick: toggle,
7864
7942
  children: isCollapsed ? jsxs(Fragment, {
7865
- children: [jsx(SvgExpand, {}), " ", `Expand all (${values.length - 1})`]
7943
+ children: [jsx(SvgExpand, {}), " ", translate('Expand all ({value})', {
7944
+ value: values.length - 1
7945
+ })]
7866
7946
  }) : jsxs(Fragment, {
7867
- children: [jsx(SvgCollapse, {}), " ", 'Collapse']
7947
+ children: [jsx(SvgCollapse, {}), " ", translate('Collapse')]
7868
7948
  })
7869
7949
  }) : null]
7870
7950
  });
@@ -8497,11 +8577,12 @@ const EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-
8497
8577
  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}$/;
8498
8578
  const VALIDATE_FEEL_PROPERTIES = ['min', 'max', 'minLength', 'maxLength'];
8499
8579
  class Validator {
8500
- constructor(expressionLanguage, conditionChecker, form, formFieldRegistry) {
8580
+ constructor(expressionLanguage, conditionChecker, form, formFieldRegistry, translate) {
8501
8581
  this._expressionLanguage = expressionLanguage;
8502
8582
  this._conditionChecker = conditionChecker;
8503
8583
  this._form = form;
8504
8584
  this._formFieldRegistry = formFieldRegistry;
8585
+ this._translate = translate;
8505
8586
  }
8506
8587
 
8507
8588
  /**
@@ -8516,13 +8597,13 @@ class Validator {
8516
8597
  } = field;
8517
8598
  let errors = [];
8518
8599
  if (type === 'number') {
8519
- errors = [...errors, ...runNumberValidation(field, value)];
8600
+ errors = [...errors, ...runNumberValidation(field, value, this._translate)];
8520
8601
  }
8521
8602
  if (!validate) {
8522
8603
  return errors;
8523
8604
  }
8524
8605
  const evaluatedValidation = oldEvaluateFEELValues(validate, this._expressionLanguage, this._conditionChecker, this._form);
8525
- errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)];
8606
+ errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value, this._translate)];
8526
8607
  return errors;
8527
8608
  }
8528
8609
 
@@ -8549,31 +8630,39 @@ class Validator {
8549
8630
  } = field;
8550
8631
  let errors = [];
8551
8632
  if (type === 'number') {
8552
- errors = [...errors, ...runNumberValidation(field, value)];
8633
+ errors = [...errors, ...runNumberValidation(field, value, this._translate)];
8553
8634
  }
8554
8635
  if (!validate) {
8555
8636
  return errors;
8556
8637
  }
8557
8638
  const evaluatedValidation = evaluateFEELValues(validate, this._expressionLanguage, expressionContextInfo);
8558
- errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)];
8639
+ errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value, this._translate)];
8559
8640
  return errors;
8560
8641
  }
8561
8642
  }
8562
- Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry'];
8643
+ Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry', 'translate'];
8563
8644
 
8564
8645
  // helpers //////////
8565
8646
 
8566
- function runNumberValidation(field, value) {
8647
+ function runNumberValidation(field, value, translate) {
8567
8648
  const {
8568
8649
  decimalDigits,
8569
8650
  increment
8570
8651
  } = field;
8571
8652
  const errors = [];
8572
8653
  if (value === 'NaN') {
8573
- errors.push('Value is not a number.');
8654
+ errors.push(translate('Value is not a number.'));
8574
8655
  } else if (value) {
8575
8656
  if (decimalDigits >= 0 && countDecimals(value) > decimalDigits) {
8576
- errors.push('Value is expected to ' + (decimalDigits === 0 ? 'be an integer' : `have at most ${decimalDigits} decimal digit${decimalDigits > 1 ? 's' : ''}`) + '.');
8657
+ if (decimalDigits === 0) {
8658
+ errors.push(translate('Value is expected to be an integer.'));
8659
+ } else if (decimalDigits === 1) {
8660
+ errors.push(translate('Value is expected to have at most 1 decimal digit.'));
8661
+ } else {
8662
+ errors.push(translate('Value is expected to have at most {value} decimal digits.', {
8663
+ value: decimalDigits.toString()
8664
+ }));
8665
+ }
8577
8666
  }
8578
8667
  if (increment) {
8579
8668
  const bigValue = Big(value);
@@ -8582,54 +8671,69 @@ function runNumberValidation(field, value) {
8582
8671
  if (offset.cmp(0) !== 0) {
8583
8672
  const previousValue = bigValue.minus(offset);
8584
8673
  const nextValue = previousValue.plus(bigIncrement);
8585
- errors.push(`Please select a valid value, the two nearest valid values are ${previousValue} and ${nextValue}.`);
8674
+ errors.push(translate('Please select a valid value, the two nearest valid values are {previousValue} and {nextValue}.', {
8675
+ previousValue: previousValue,
8676
+ nextValue: nextValue
8677
+ }));
8586
8678
  }
8587
8679
  }
8588
8680
  }
8589
8681
  return errors;
8590
8682
  }
8591
- function runPresetValidation(field, validation, value) {
8683
+ function runPresetValidation(field, validation, value, translate) {
8592
8684
  const errors = [];
8593
8685
  if (validation.pattern && value && !new RegExp(validation.pattern).test(value)) {
8594
- errors.push(validation.patternErrorMessage || `Field must match pattern ${validation.pattern}.`);
8686
+ errors.push(validation.patternErrorMessage || translate('Field must match pattern {value}.', {
8687
+ value: validation.pattern
8688
+ }));
8595
8689
  }
8596
8690
  if (validation.required) {
8597
8691
  const isUncheckedCheckbox = field.type === 'checkbox' && value === false;
8598
8692
  const isUnsetValue = isNil(value) || value === '';
8599
8693
  const isEmptyMultiselect = Array.isArray(value) && value.length === 0;
8600
8694
  if (isUncheckedCheckbox || isUnsetValue || isEmptyMultiselect) {
8601
- errors.push('Field is required.');
8695
+ errors.push(translate('Field is required.'));
8602
8696
  }
8603
8697
  }
8698
+
8699
+ // stringify: `translate` falls back to the raw placeholder for a falsy replacement, so 0 must not be passed as a number
8604
8700
  if ('min' in validation && (value || value === 0)) {
8605
8701
  try {
8606
8702
  if (Big(value).lt(Big(validation.min))) {
8607
- errors.push(`Field must have minimum value of ${validation.min}.`);
8703
+ errors.push(translate('Field must have minimum value of {value}.', {
8704
+ value: validation.min.toString()
8705
+ }));
8608
8706
  }
8609
8707
  } catch {
8610
- errors.push('Min validation value is not a valid number.');
8708
+ errors.push(translate('Min validation value is not a valid number.'));
8611
8709
  }
8612
8710
  }
8613
8711
  if ('max' in validation && (value || value === 0)) {
8614
8712
  try {
8615
8713
  if (Big(value).gt(Big(validation.max))) {
8616
- errors.push(`Field must have maximum value of ${validation.max}.`);
8714
+ errors.push(translate('Field must have maximum value of {value}.', {
8715
+ value: validation.max.toString()
8716
+ }));
8617
8717
  }
8618
8718
  } catch {
8619
- errors.push('Max validation value is not a valid number.');
8719
+ errors.push(translate('Max validation value is not a valid number.'));
8620
8720
  }
8621
8721
  }
8622
8722
  if ('minLength' in validation && value && value.trim().length < validation.minLength) {
8623
- errors.push(`Field must have minimum length of ${validation.minLength}.`);
8723
+ errors.push(translate('Field must have minimum length of {value}.', {
8724
+ value: validation.minLength.toString()
8725
+ }));
8624
8726
  }
8625
8727
  if ('maxLength' in validation && value && value.trim().length > validation.maxLength) {
8626
- errors.push(`Field must have maximum length of ${validation.maxLength}.`);
8728
+ errors.push(translate('Field must have maximum length of {value}.', {
8729
+ value: validation.maxLength.toString()
8730
+ }));
8627
8731
  }
8628
8732
  if ('validationType' in validation && value && validation.validationType === 'phone' && !PHONE_PATTERN.test(value)) {
8629
- errors.push('Field must be a valid international phone number. (e.g. +4930664040900)');
8733
+ errors.push(translate('Field must be a valid international phone number. (e.g. +4930664040900)'));
8630
8734
  }
8631
8735
  if ('validationType' in validation && value && validation.validationType === 'email' && !EMAIL_PATTERN.test(value)) {
8632
- errors.push('Field must be a valid email.');
8736
+ errors.push(translate('Field must be a valid email.'));
8633
8737
  }
8634
8738
  return errors;
8635
8739
  }
@@ -8787,17 +8891,24 @@ class Importer {
8787
8891
  }
8788
8892
  Importer.$inject = ['formFieldRegistry', 'pathRegistry', 'fieldFactory', 'formLayouter'];
8789
8893
 
8894
+ /**
8895
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
8896
+ */
8897
+
8790
8898
  class FieldFactory {
8791
8899
  /**
8792
8900
  * @constructor
8793
8901
  *
8794
- * @param formFieldRegistry
8795
- * @param formFields
8902
+ * @param formFieldRegistry
8903
+ * @param pathRegistry
8904
+ * @param formFields
8905
+ * @param {Translate} translate
8796
8906
  */
8797
- constructor(formFieldRegistry, pathRegistry, formFields) {
8907
+ constructor(formFieldRegistry, pathRegistry, formFields, translate) {
8798
8908
  this._formFieldRegistry = formFieldRegistry;
8799
8909
  this._pathRegistry = pathRegistry;
8800
8910
  this._formFields = formFields;
8911
+ this._translate = translate;
8801
8912
  }
8802
8913
  create(attrs, isNewField = true) {
8803
8914
  const {
@@ -8830,13 +8941,17 @@ class FieldFactory {
8830
8941
  isClosed: true,
8831
8942
  knownAncestorIds
8832
8943
  })) {
8833
- throw new Error(`binding path '${[...parentPath, key].join('.')}' is already claimed`);
8944
+ throw new Error(this._translate("binding path '{value}' is already claimed", {
8945
+ value: [...parentPath, key].join('.')
8946
+ }));
8834
8947
  }
8835
8948
  if (config.pathed && path && !this._pathRegistry.canClaimPath([...parentPath, ...path.split('.')], {
8836
8949
  isRepeatable: config.repeatable,
8837
8950
  knownAncestorIds
8838
8951
  })) {
8839
- throw new Error(`binding path '${[...parentPath, ...path.split('.')].join('.')}' is already claimed`);
8952
+ throw new Error(this._translate("binding path '{value}' is already claimed", {
8953
+ value: [...parentPath, ...path.split('.')].join('.')
8954
+ }));
8840
8955
  }
8841
8956
  const field = config.create({
8842
8957
  ...(config.label ? {
@@ -8897,7 +9012,7 @@ class FieldFactory {
8897
9012
  return `${field.type}_${random}`;
8898
9013
  }
8899
9014
  }
8900
- FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields'];
9015
+ FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields', 'translate'];
8901
9016
 
8902
9017
  /**
8903
9018
  * The PathRegistry class manages a hierarchical structure of paths associated with form fields.
@@ -9907,7 +10022,7 @@ class Form {
9907
10022
  config: ['value', enrichedConfig]
9908
10023
  }, {
9909
10024
  form: ['value', this]
9910
- }, CoreModule, ...modules, ...additionalModules]);
10025
+ }, TranslateModule, CoreModule, ...modules, ...additionalModules]);
9911
10026
  }
9912
10027
 
9913
10028
  /**