@bpmn-io/form-js-viewer 1.25.0 → 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,
@@ -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;
@@ -6271,7 +6338,7 @@ function PdfRenderer(props) {
6271
6338
  class: `fjs-${type}-pdf-viewer`
6272
6339
  }) : null, hasError ? jsx(Errors, {
6273
6340
  id: errorMessageId,
6274
- errors: ['Unable to download document']
6341
+ errors: [translate('Unable to download document')]
6275
6342
  }) : null]
6276
6343
  });
6277
6344
  }
@@ -6351,9 +6418,10 @@ function DocumentRenderer(props) {
6351
6418
  const isInViewport = useInViewport(ref);
6352
6419
  const onError = useCallback(() => setHasError(true), []);
6353
6420
  const requestInit = useMemo(() => getDocumentRequestInit(documentEndpointBuilder, documentMetadata), [documentEndpointBuilder, documentMetadata]);
6421
+ const translate = useService('translate');
6354
6422
  const singleDocumentContainerClassName = `fjs-${type}-single-document-container`;
6355
6423
  const errorMessageId = `${domId}-error-message`;
6356
- const errorMessage = 'Unable to download document';
6424
+ const errorMessage = translate('Unable to download document');
6357
6425
  const isContentTypePresent = typeof metadata.contentType === 'string';
6358
6426
  if (isContentTypePresent && metadata.contentType.toLowerCase().startsWith('image/') && isInViewport) {
6359
6427
  return jsxs("div", {
@@ -6431,6 +6499,7 @@ function DownloadButton(props) {
6431
6499
  onDownloadError,
6432
6500
  requestInit
6433
6501
  } = props;
6502
+ const translate = useService('translate');
6434
6503
  const handleDownload = async () => {
6435
6504
  try {
6436
6505
  const response = await fetch(endpoint, requestInit);
@@ -6453,7 +6522,9 @@ function DownloadButton(props) {
6453
6522
  type: "button",
6454
6523
  onClick: handleDownload,
6455
6524
  class: classNames(`fjs-${type}-download-button`),
6456
- "aria-label": `Download ${fileName}`,
6525
+ "aria-label": translate('Download {fileName}', {
6526
+ fileName
6527
+ }),
6457
6528
  children: jsx(SvgDownload, {})
6458
6529
  });
6459
6530
  }
@@ -7799,6 +7870,7 @@ class RepeatRenderManager {
7799
7870
  const {
7800
7871
  data
7801
7872
  } = this._form._getState();
7873
+ const translate = useService('translate');
7802
7874
  const dataPath = this._pathRegistry.getValuePath(repeaterField, {
7803
7875
  indexes
7804
7876
  });
@@ -7855,16 +7927,18 @@ class RepeatRenderManager {
7855
7927
  ref: addButtonRef,
7856
7928
  onClick: onAddItem,
7857
7929
  children: jsxs(Fragment, {
7858
- children: [jsx(SvgAdd, {}), " ", 'Add new']
7930
+ children: [jsx(SvgAdd, {}), " ", translate('Add new')]
7859
7931
  })
7860
7932
  }) : null, collapseEnabled ? jsx("button", {
7861
7933
  type: "button",
7862
7934
  class: "fjs-repeat-render-collapse",
7863
7935
  onClick: toggle,
7864
7936
  children: isCollapsed ? jsxs(Fragment, {
7865
- children: [jsx(SvgExpand, {}), " ", `Expand all (${values.length - 1})`]
7937
+ children: [jsx(SvgExpand, {}), " ", translate('Expand all ({value})', {
7938
+ value: values.length - 1
7939
+ })]
7866
7940
  }) : jsxs(Fragment, {
7867
- children: [jsx(SvgCollapse, {}), " ", 'Collapse']
7941
+ children: [jsx(SvgCollapse, {}), " ", translate('Collapse')]
7868
7942
  })
7869
7943
  }) : null]
7870
7944
  });
@@ -8497,11 +8571,12 @@ const EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-
8497
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}$/;
8498
8572
  const VALIDATE_FEEL_PROPERTIES = ['min', 'max', 'minLength', 'maxLength'];
8499
8573
  class Validator {
8500
- constructor(expressionLanguage, conditionChecker, form, formFieldRegistry) {
8574
+ constructor(expressionLanguage, conditionChecker, form, formFieldRegistry, translate) {
8501
8575
  this._expressionLanguage = expressionLanguage;
8502
8576
  this._conditionChecker = conditionChecker;
8503
8577
  this._form = form;
8504
8578
  this._formFieldRegistry = formFieldRegistry;
8579
+ this._translate = translate;
8505
8580
  }
8506
8581
 
8507
8582
  /**
@@ -8516,13 +8591,13 @@ class Validator {
8516
8591
  } = field;
8517
8592
  let errors = [];
8518
8593
  if (type === 'number') {
8519
- errors = [...errors, ...runNumberValidation(field, value)];
8594
+ errors = [...errors, ...runNumberValidation(field, value, this._translate)];
8520
8595
  }
8521
8596
  if (!validate) {
8522
8597
  return errors;
8523
8598
  }
8524
8599
  const evaluatedValidation = oldEvaluateFEELValues(validate, this._expressionLanguage, this._conditionChecker, this._form);
8525
- errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)];
8600
+ errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value, this._translate)];
8526
8601
  return errors;
8527
8602
  }
8528
8603
 
@@ -8549,31 +8624,39 @@ class Validator {
8549
8624
  } = field;
8550
8625
  let errors = [];
8551
8626
  if (type === 'number') {
8552
- errors = [...errors, ...runNumberValidation(field, value)];
8627
+ errors = [...errors, ...runNumberValidation(field, value, this._translate)];
8553
8628
  }
8554
8629
  if (!validate) {
8555
8630
  return errors;
8556
8631
  }
8557
8632
  const evaluatedValidation = evaluateFEELValues(validate, this._expressionLanguage, expressionContextInfo);
8558
- errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value)];
8633
+ errors = [...errors, ...runPresetValidation(field, evaluatedValidation, value, this._translate)];
8559
8634
  return errors;
8560
8635
  }
8561
8636
  }
8562
- Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry'];
8637
+ Validator.$inject = ['expressionLanguage', 'conditionChecker', 'form', 'formFieldRegistry', 'translate'];
8563
8638
 
8564
8639
  // helpers //////////
8565
8640
 
8566
- function runNumberValidation(field, value) {
8641
+ function runNumberValidation(field, value, translate) {
8567
8642
  const {
8568
8643
  decimalDigits,
8569
8644
  increment
8570
8645
  } = field;
8571
8646
  const errors = [];
8572
8647
  if (value === 'NaN') {
8573
- errors.push('Value is not a number.');
8648
+ errors.push(translate('Value is not a number.'));
8574
8649
  } else if (value) {
8575
8650
  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' : ''}`) + '.');
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
+ }
8577
8660
  }
8578
8661
  if (increment) {
8579
8662
  const bigValue = Big(value);
@@ -8582,54 +8665,69 @@ function runNumberValidation(field, value) {
8582
8665
  if (offset.cmp(0) !== 0) {
8583
8666
  const previousValue = bigValue.minus(offset);
8584
8667
  const nextValue = previousValue.plus(bigIncrement);
8585
- 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
+ }));
8586
8672
  }
8587
8673
  }
8588
8674
  }
8589
8675
  return errors;
8590
8676
  }
8591
- function runPresetValidation(field, validation, value) {
8677
+ function runPresetValidation(field, validation, value, translate) {
8592
8678
  const errors = [];
8593
8679
  if (validation.pattern && value && !new RegExp(validation.pattern).test(value)) {
8594
- 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
+ }));
8595
8683
  }
8596
8684
  if (validation.required) {
8597
8685
  const isUncheckedCheckbox = field.type === 'checkbox' && value === false;
8598
8686
  const isUnsetValue = isNil(value) || value === '';
8599
8687
  const isEmptyMultiselect = Array.isArray(value) && value.length === 0;
8600
8688
  if (isUncheckedCheckbox || isUnsetValue || isEmptyMultiselect) {
8601
- errors.push('Field is required.');
8689
+ errors.push(translate('Field is required.'));
8602
8690
  }
8603
8691
  }
8692
+
8693
+ // stringify: `translate` falls back to the raw placeholder for a falsy replacement, so 0 must not be passed as a number
8604
8694
  if ('min' in validation && (value || value === 0)) {
8605
8695
  try {
8606
8696
  if (Big(value).lt(Big(validation.min))) {
8607
- 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
+ }));
8608
8700
  }
8609
8701
  } catch {
8610
- errors.push('Min validation value is not a valid number.');
8702
+ errors.push(translate('Min validation value is not a valid number.'));
8611
8703
  }
8612
8704
  }
8613
8705
  if ('max' in validation && (value || value === 0)) {
8614
8706
  try {
8615
8707
  if (Big(value).gt(Big(validation.max))) {
8616
- 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
+ }));
8617
8711
  }
8618
8712
  } catch {
8619
- errors.push('Max validation value is not a valid number.');
8713
+ errors.push(translate('Max validation value is not a valid number.'));
8620
8714
  }
8621
8715
  }
8622
8716
  if ('minLength' in validation && value && value.trim().length < validation.minLength) {
8623
- 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
+ }));
8624
8720
  }
8625
8721
  if ('maxLength' in validation && value && value.trim().length > validation.maxLength) {
8626
- 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
+ }));
8627
8725
  }
8628
8726
  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)');
8727
+ errors.push(translate('Field must be a valid international phone number. (e.g. +4930664040900)'));
8630
8728
  }
8631
8729
  if ('validationType' in validation && value && validation.validationType === 'email' && !EMAIL_PATTERN.test(value)) {
8632
- errors.push('Field must be a valid email.');
8730
+ errors.push(translate('Field must be a valid email.'));
8633
8731
  }
8634
8732
  return errors;
8635
8733
  }
@@ -8787,17 +8885,24 @@ class Importer {
8787
8885
  }
8788
8886
  Importer.$inject = ['formFieldRegistry', 'pathRegistry', 'fieldFactory', 'formLayouter'];
8789
8887
 
8888
+ /**
8889
+ * @typedef { import('diagram-js/lib/i18n/translate/translate').default } Translate
8890
+ */
8891
+
8790
8892
  class FieldFactory {
8791
8893
  /**
8792
8894
  * @constructor
8793
8895
  *
8794
- * @param formFieldRegistry
8795
- * @param formFields
8896
+ * @param formFieldRegistry
8897
+ * @param pathRegistry
8898
+ * @param formFields
8899
+ * @param {Translate} translate
8796
8900
  */
8797
- constructor(formFieldRegistry, pathRegistry, formFields) {
8901
+ constructor(formFieldRegistry, pathRegistry, formFields, translate) {
8798
8902
  this._formFieldRegistry = formFieldRegistry;
8799
8903
  this._pathRegistry = pathRegistry;
8800
8904
  this._formFields = formFields;
8905
+ this._translate = translate;
8801
8906
  }
8802
8907
  create(attrs, isNewField = true) {
8803
8908
  const {
@@ -8830,13 +8935,17 @@ class FieldFactory {
8830
8935
  isClosed: true,
8831
8936
  knownAncestorIds
8832
8937
  })) {
8833
- 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
+ }));
8834
8941
  }
8835
8942
  if (config.pathed && path && !this._pathRegistry.canClaimPath([...parentPath, ...path.split('.')], {
8836
8943
  isRepeatable: config.repeatable,
8837
8944
  knownAncestorIds
8838
8945
  })) {
8839
- 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
+ }));
8840
8949
  }
8841
8950
  const field = config.create({
8842
8951
  ...(config.label ? {
@@ -8897,7 +9006,7 @@ class FieldFactory {
8897
9006
  return `${field.type}_${random}`;
8898
9007
  }
8899
9008
  }
8900
- FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields'];
9009
+ FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields', 'translate'];
8901
9010
 
8902
9011
  /**
8903
9012
  * The PathRegistry class manages a hierarchical structure of paths associated with form fields.
@@ -9907,7 +10016,7 @@ class Form {
9907
10016
  config: ['value', enrichedConfig]
9908
10017
  }, {
9909
10018
  form: ['value', this]
9910
- }, CoreModule, ...modules, ...additionalModules]);
10019
+ }, TranslateModule, CoreModule, ...modules, ...additionalModules]);
9911
10020
  }
9912
10021
 
9913
10022
  /**