@form-engine-ts/core 1.1.0 → 2.1.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/README.md +19 -1
- package/dist/index.cjs +432 -89
- package/dist/index.d.cts +71 -11
- package/dist/index.d.ts +71 -11
- package/dist/index.js +431 -89
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -140,6 +140,52 @@ function isNonEmptyString(value) {
|
|
|
140
140
|
function issue(issues, path, code, message) {
|
|
141
141
|
issues.push({ path, code, message });
|
|
142
142
|
}
|
|
143
|
+
function validateJsonValue(value, path, issues, ancestors = /* @__PURE__ */ new Set()) {
|
|
144
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
145
|
+
if (typeof value === "number") {
|
|
146
|
+
if (!Number.isFinite(value)) issue(issues, path, "invalid_metadata", "Metadata numbers must be finite.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value !== "object") {
|
|
150
|
+
issue(issues, path, "invalid_metadata", "Expected JSON-serializable metadata.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (ancestors.has(value)) {
|
|
154
|
+
issue(issues, path, "invalid_metadata", "Metadata must not contain cycles.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
value.forEach((item, index) => {
|
|
160
|
+
validateJsonValue(item, `${path}[${index}]`, issues, nextAncestors);
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const prototype = Object.getPrototypeOf(value);
|
|
165
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
166
|
+
issue(issues, path, "invalid_metadata", "Metadata objects must be plain JSON objects.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const [key, item] of Object.entries(value)) {
|
|
170
|
+
validateJsonValue(item, `${path}.${key}`, issues, nextAncestors);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function validateExtensibleNode(value, path, issues) {
|
|
174
|
+
for (const property of ["metadata", "translationMetadata"]) {
|
|
175
|
+
const candidate = value[property];
|
|
176
|
+
if (candidate === void 0) continue;
|
|
177
|
+
if (!isRecord(candidate)) {
|
|
178
|
+
issue(
|
|
179
|
+
issues,
|
|
180
|
+
path.length === 0 ? property : `${path}.${property}`,
|
|
181
|
+
"invalid_metadata",
|
|
182
|
+
"Expected a metadata object."
|
|
183
|
+
);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
validateJsonValue(candidate, path.length === 0 ? property : `${path}.${property}`, issues);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
143
189
|
function validateLocalizedTextMap(value, path, issues) {
|
|
144
190
|
if (!isRecord(value)) {
|
|
145
191
|
issue(issues, path, "invalid_translations", "Expected a locale-to-translation object.");
|
|
@@ -150,7 +196,7 @@ function validateLocalizedTextMap(value, path, issues) {
|
|
|
150
196
|
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a translation object.");
|
|
151
197
|
continue;
|
|
152
198
|
}
|
|
153
|
-
for (const key of ["title", "description"]) {
|
|
199
|
+
for (const key of ["title", "description", "completionMessage"]) {
|
|
154
200
|
if (translation[key] !== void 0 && !isNonEmptyString(translation[key])) {
|
|
155
201
|
issue(issues, `${path}.${locale}.${key}`, "invalid_translation", "Expected non-empty translated text.");
|
|
156
202
|
}
|
|
@@ -201,6 +247,7 @@ function validateOptions(value, path, issues) {
|
|
|
201
247
|
return;
|
|
202
248
|
}
|
|
203
249
|
rejectLegacyProperties(option, optionPath, ["value", "labelKey"], issues);
|
|
250
|
+
validateExtensibleNode(option, optionPath, issues);
|
|
204
251
|
if (!isNonEmptyString(option.id)) {
|
|
205
252
|
issue(issues, `${optionPath}.id`, "invalid_option_id", "Expected a non-empty option ID.");
|
|
206
253
|
} else if (seen.has(option.id)) {
|
|
@@ -246,6 +293,7 @@ function validateField(value, path, issues) {
|
|
|
246
293
|
return false;
|
|
247
294
|
}
|
|
248
295
|
rejectLegacyProperties(value, path, ["titleKey", "labelKey", "helpTextKey", "descriptionKey"], issues);
|
|
296
|
+
validateExtensibleNode(value, path, issues);
|
|
249
297
|
if (!isNonEmptyString(value.id)) issue(issues, `${path}.id`, "invalid_id", "Expected a non-empty ID.");
|
|
250
298
|
if (!isNonEmptyString(value.title)) {
|
|
251
299
|
issue(issues, `${path}.title`, "invalid_title", "Expected a non-empty question title.");
|
|
@@ -325,12 +373,157 @@ function validateField(value, path, issues) {
|
|
|
325
373
|
}
|
|
326
374
|
return true;
|
|
327
375
|
}
|
|
328
|
-
function
|
|
376
|
+
function collectSchemaText(schema) {
|
|
377
|
+
const entries = [{ path: "title", value: schema.title }];
|
|
378
|
+
if (schema.description !== void 0) entries.push({ path: "description", value: schema.description });
|
|
379
|
+
if (schema.completionMessage !== void 0)
|
|
380
|
+
entries.push({ path: "completionMessage", value: schema.completionMessage });
|
|
381
|
+
for (const [locale, translation] of Object.entries(schema.translations ?? {})) {
|
|
382
|
+
for (const property of ["title", "description", "completionMessage"]) {
|
|
383
|
+
const value = translation[property];
|
|
384
|
+
if (value !== void 0) entries.push({ path: `translations.${locale}.${property}`, value });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
388
|
+
entries.push({ path: `fields[${fieldIndex}].title`, value: field.title });
|
|
389
|
+
if (field.description !== void 0)
|
|
390
|
+
entries.push({ path: `fields[${fieldIndex}].description`, value: field.description });
|
|
391
|
+
for (const [locale, translation] of Object.entries(field.translations ?? {})) {
|
|
392
|
+
for (const property of ["title", "description"]) {
|
|
393
|
+
const value = translation[property];
|
|
394
|
+
if (value !== void 0)
|
|
395
|
+
entries.push({ path: `fields[${fieldIndex}].translations.${locale}.${property}`, value });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!("options" in field)) return;
|
|
399
|
+
field.options.forEach((option, optionIndex) => {
|
|
400
|
+
entries.push({ path: `fields[${fieldIndex}].options[${optionIndex}].label`, value: option.label });
|
|
401
|
+
for (const [locale, value] of Object.entries(option.translations ?? {})) {
|
|
402
|
+
entries.push({ path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`, value });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
407
|
+
if (page.title !== void 0) entries.push({ path: `pages[${pageIndex}].title`, value: page.title });
|
|
408
|
+
if (page.description !== void 0)
|
|
409
|
+
entries.push({ path: `pages[${pageIndex}].description`, value: page.description });
|
|
410
|
+
for (const [locale, translation] of Object.entries(page.translations ?? {})) {
|
|
411
|
+
for (const property of ["title", "description"]) {
|
|
412
|
+
const value = translation[property];
|
|
413
|
+
if (value !== void 0)
|
|
414
|
+
entries.push({ path: `pages[${pageIndex}].translations.${locale}.${property}`, value });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
return entries;
|
|
419
|
+
}
|
|
420
|
+
function addRequiredTranslationIssues(schema, locale, issues) {
|
|
421
|
+
if (!(schema.supportedLocales ?? []).includes(locale)) {
|
|
422
|
+
issue(issues, "supportedLocales", "required_locale_missing", `Required locale ${locale} is missing.`);
|
|
423
|
+
}
|
|
424
|
+
if (locale === schema.defaultLocale) return;
|
|
425
|
+
const required = [
|
|
426
|
+
{ path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title }
|
|
427
|
+
];
|
|
428
|
+
if (schema.description !== void 0)
|
|
429
|
+
required.push({ path: `translations.${locale}.description`, value: schema.translations?.[locale]?.description });
|
|
430
|
+
if (schema.completionMessage !== void 0) {
|
|
431
|
+
required.push({
|
|
432
|
+
path: `translations.${locale}.completionMessage`,
|
|
433
|
+
value: schema.translations?.[locale]?.completionMessage
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
437
|
+
required.push({
|
|
438
|
+
path: `fields[${fieldIndex}].translations.${locale}.title`,
|
|
439
|
+
value: field.translations?.[locale]?.title
|
|
440
|
+
});
|
|
441
|
+
if (field.description !== void 0) {
|
|
442
|
+
required.push({
|
|
443
|
+
path: `fields[${fieldIndex}].translations.${locale}.description`,
|
|
444
|
+
value: field.translations?.[locale]?.description
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (!("options" in field)) return;
|
|
448
|
+
field.options.forEach((option, optionIndex) => {
|
|
449
|
+
required.push({
|
|
450
|
+
path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
|
|
451
|
+
value: option.translations?.[locale]
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
456
|
+
if (page.title !== void 0) {
|
|
457
|
+
required.push({
|
|
458
|
+
path: `pages[${pageIndex}].translations.${locale}.title`,
|
|
459
|
+
value: page.translations?.[locale]?.title
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (page.description !== void 0) {
|
|
463
|
+
required.push({
|
|
464
|
+
path: `pages[${pageIndex}].translations.${locale}.description`,
|
|
465
|
+
value: page.translations?.[locale]?.description
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
for (const translation of required) {
|
|
470
|
+
if (translation.value === void 0 || translation.value.trim().length === 0) {
|
|
471
|
+
issue(
|
|
472
|
+
issues,
|
|
473
|
+
translation.path,
|
|
474
|
+
"required_translation_missing",
|
|
475
|
+
`A translation for required locale ${locale} is missing.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function validatePolicy(schema, policy, issues) {
|
|
481
|
+
if (policy.maxFields !== void 0 && schema.fields.length > policy.maxFields) {
|
|
482
|
+
issue(issues, "fields", "max_fields_exceeded", `At most ${policy.maxFields} fields are allowed.`);
|
|
483
|
+
}
|
|
484
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
485
|
+
if (policy.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(field.type)) {
|
|
486
|
+
issue(issues, `fields[${fieldIndex}].type`, "disallowed_field_type", `Field type ${field.type} is not allowed.`);
|
|
487
|
+
}
|
|
488
|
+
if (policy.maxOptionsPerField !== void 0 && "options" in field && field.options.length > policy.maxOptionsPerField) {
|
|
489
|
+
issue(
|
|
490
|
+
issues,
|
|
491
|
+
`fields[${fieldIndex}].options`,
|
|
492
|
+
"max_options_exceeded",
|
|
493
|
+
`At most ${policy.maxOptionsPerField} options are allowed.`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
if (policy.maxTextLength !== void 0) {
|
|
498
|
+
for (const entry of collectSchemaText(schema)) {
|
|
499
|
+
if (entry.value.length > policy.maxTextLength) {
|
|
500
|
+
issue(
|
|
501
|
+
issues,
|
|
502
|
+
entry.path,
|
|
503
|
+
"max_text_length_exceeded",
|
|
504
|
+
`Text must be at most ${policy.maxTextLength} characters.`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
|
|
510
|
+
if (policy.maxSchemaBytes !== void 0) {
|
|
511
|
+
try {
|
|
512
|
+
const byteLength = new TextEncoder().encode(JSON.stringify(schema)).byteLength;
|
|
513
|
+
if (byteLength > policy.maxSchemaBytes) {
|
|
514
|
+
issue(issues, "$", "max_schema_bytes_exceeded", `Schema must be at most ${policy.maxSchemaBytes} bytes.`);
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function validateFormSchema(input, options = {}) {
|
|
329
521
|
const issues = [];
|
|
330
522
|
if (!isRecord(input)) {
|
|
331
523
|
return { valid: false, issues: [{ path: "$", code: "invalid_schema", message: "Expected a schema object." }] };
|
|
332
524
|
}
|
|
333
525
|
rejectLegacyProperties(input, "", ["titleKey", "descriptionKey"], issues);
|
|
526
|
+
validateExtensibleNode(input, "", issues);
|
|
334
527
|
if (!isNonEmptyString(input.id)) issue(issues, "id", "invalid_id", "Expected a non-empty ID.");
|
|
335
528
|
if (!Number.isInteger(input.version) || input.version < 1) {
|
|
336
529
|
issue(issues, "version", "invalid_version", "Expected a positive integer version.");
|
|
@@ -338,13 +531,13 @@ function validateFormSchema(input) {
|
|
|
338
531
|
if (!isNonEmptyString(input.title)) {
|
|
339
532
|
issue(issues, "title", "invalid_title", "Expected a non-empty form title.");
|
|
340
533
|
}
|
|
341
|
-
for (const key of ["description", "submitLabelKey"]) {
|
|
534
|
+
for (const key of ["description", "completionMessage", "submitLabelKey"]) {
|
|
342
535
|
if (input[key] !== void 0 && !isNonEmptyString(input[key])) {
|
|
343
536
|
issue(
|
|
344
537
|
issues,
|
|
345
538
|
key,
|
|
346
|
-
key === "
|
|
347
|
-
key === "
|
|
539
|
+
key === "submitLabelKey" ? "invalid_translation_key" : "invalid_description",
|
|
540
|
+
key === "submitLabelKey" ? "Expected a translation key." : "Expected non-empty form text."
|
|
348
541
|
);
|
|
349
542
|
}
|
|
350
543
|
}
|
|
@@ -406,6 +599,7 @@ function validateFormSchema(input) {
|
|
|
406
599
|
issue(issues, pagePath, "invalid_page", "Expected a page object.");
|
|
407
600
|
return;
|
|
408
601
|
}
|
|
602
|
+
validateExtensibleNode(page, pagePath, issues);
|
|
409
603
|
if (!isNonEmptyString(page.id)) {
|
|
410
604
|
issue(issues, `${pagePath}.id`, "invalid_page_id", "Expected a non-empty page ID.");
|
|
411
605
|
} else if (pageIds.has(page.id)) {
|
|
@@ -469,6 +663,14 @@ function validateFormSchema(input) {
|
|
|
469
663
|
}
|
|
470
664
|
}
|
|
471
665
|
}
|
|
666
|
+
if (Array.isArray(input.fields)) {
|
|
667
|
+
const policyIssues = [];
|
|
668
|
+
try {
|
|
669
|
+
validatePolicy(input, options.policy ?? {}, policyIssues);
|
|
670
|
+
issues.push(...policyIssues);
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
472
674
|
return issues.length === 0 ? { valid: true, value: input, issues: [] } : { valid: false, issues };
|
|
473
675
|
}
|
|
474
676
|
function assertValidFormSchema(input) {
|
|
@@ -697,15 +899,20 @@ function aggregateResponses(schema, submissions) {
|
|
|
697
899
|
questions: schema.fields.map((field) => aggregateField(schema, field, submissions))
|
|
698
900
|
};
|
|
699
901
|
}
|
|
700
|
-
function escapeCsvCell(value) {
|
|
902
|
+
function escapeCsvCell(value, neutralizeFormulas = true) {
|
|
701
903
|
if (value === null || value === void 0) return "";
|
|
702
|
-
|
|
904
|
+
let stringValue = String(value);
|
|
905
|
+
if (neutralizeFormulas && typeof value === "string") {
|
|
906
|
+
const trimmed = stringValue.trimStart();
|
|
907
|
+
if (trimmed.length > 0 && ["=", "+", "-", "@"].includes(trimmed[0] ?? "")) {
|
|
908
|
+
stringValue = `'${stringValue}`;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
703
911
|
return /[",\r\n]/.test(stringValue) ? `"${stringValue.replaceAll('"', '""')}"` : stringValue;
|
|
704
912
|
}
|
|
705
913
|
function serializeValue(value) {
|
|
706
914
|
if (value === void 0) return "";
|
|
707
|
-
|
|
708
|
-
return String(value);
|
|
915
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
|
|
709
916
|
}
|
|
710
917
|
function exportResponsesToCsv(schema, responses, options = {}) {
|
|
711
918
|
assertValidFormSchema(schema);
|
|
@@ -726,7 +933,8 @@ function exportResponsesToCsv(schema, responses, options = {}) {
|
|
|
726
933
|
];
|
|
727
934
|
})
|
|
728
935
|
];
|
|
729
|
-
const
|
|
936
|
+
const neutralizeFormulas = options.neutralizeFormulas ?? true;
|
|
937
|
+
const csv = rows.map((row) => row.map((cell) => escapeCsvCell(cell, neutralizeFormulas)).join(",")).join("\r\n");
|
|
730
938
|
return options.withBom ?? true ? `\uFEFF${csv}` : csv;
|
|
731
939
|
}
|
|
732
940
|
|
|
@@ -771,6 +979,58 @@ async function dispatchWebhook(event, config, fetchImpl = globalThis.fetch) {
|
|
|
771
979
|
}
|
|
772
980
|
}
|
|
773
981
|
|
|
982
|
+
// src/field.ts
|
|
983
|
+
var DEFAULT_OPTION = { id: "option-1", label: "Option 1" };
|
|
984
|
+
function transformFieldType(field, nextType) {
|
|
985
|
+
const common = {
|
|
986
|
+
id: field.id,
|
|
987
|
+
title: field.title,
|
|
988
|
+
required: field.required,
|
|
989
|
+
...field.description === void 0 ? {} : { description: field.description },
|
|
990
|
+
...field.translationKey === void 0 ? {} : { translationKey: field.translationKey },
|
|
991
|
+
...field.messages === void 0 ? {} : { messages: field.messages },
|
|
992
|
+
...field.displayCondition === void 0 ? {} : { displayCondition: field.displayCondition },
|
|
993
|
+
...field.translations === void 0 ? {} : { translations: field.translations },
|
|
994
|
+
...field.metadata === void 0 ? {} : { metadata: field.metadata },
|
|
995
|
+
...field.translationMetadata === void 0 ? {} : { translationMetadata: field.translationMetadata }
|
|
996
|
+
};
|
|
997
|
+
if (nextType === "text" || nextType === "textarea") {
|
|
998
|
+
const textProperties = field.type === "text" || field.type === "textarea" ? {
|
|
999
|
+
...field.placeholderKey === void 0 ? {} : { placeholderKey: field.placeholderKey },
|
|
1000
|
+
...field.minLength === void 0 ? {} : { minLength: field.minLength },
|
|
1001
|
+
...field.maxLength === void 0 ? {} : { maxLength: field.maxLength },
|
|
1002
|
+
...field.pattern === void 0 ? {} : { pattern: field.pattern }
|
|
1003
|
+
} : {};
|
|
1004
|
+
return { ...common, ...textProperties, type: nextType };
|
|
1005
|
+
}
|
|
1006
|
+
if (nextType === "number") {
|
|
1007
|
+
const numberProperties = field.type === "number" ? {
|
|
1008
|
+
...field.placeholderKey === void 0 ? {} : { placeholderKey: field.placeholderKey },
|
|
1009
|
+
...field.min === void 0 ? {} : { min: field.min },
|
|
1010
|
+
...field.max === void 0 ? {} : { max: field.max },
|
|
1011
|
+
...field.step === void 0 ? {} : { step: field.step }
|
|
1012
|
+
} : {};
|
|
1013
|
+
return { ...common, ...numberProperties, type: nextType };
|
|
1014
|
+
}
|
|
1015
|
+
if (nextType === "rating") {
|
|
1016
|
+
const ratingProperties = field.type === "rating" ? {
|
|
1017
|
+
...field.min === void 0 ? {} : { min: field.min },
|
|
1018
|
+
...field.max === void 0 ? {} : { max: field.max }
|
|
1019
|
+
} : { min: 1, max: 5 };
|
|
1020
|
+
return { ...common, ...ratingProperties, type: nextType };
|
|
1021
|
+
}
|
|
1022
|
+
if (nextType === "checkbox") return { ...common, type: nextType };
|
|
1023
|
+
const options = "options" in field && field.options.length > 0 ? field.options : [DEFAULT_OPTION];
|
|
1024
|
+
if (nextType === "multi-select") {
|
|
1025
|
+
const selectionProperties = field.type === "multi-select" ? {
|
|
1026
|
+
...field.minSelections === void 0 ? {} : { minSelections: field.minSelections },
|
|
1027
|
+
...field.maxSelections === void 0 ? {} : { maxSelections: field.maxSelections }
|
|
1028
|
+
} : {};
|
|
1029
|
+
return { ...common, ...selectionProperties, type: nextType, options };
|
|
1030
|
+
}
|
|
1031
|
+
return { ...common, type: nextType, options };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
774
1034
|
// src/validation.ts
|
|
775
1035
|
var DEFAULT_MESSAGES = {
|
|
776
1036
|
required: "validation.required",
|
|
@@ -936,69 +1196,129 @@ function createSubmission(schema, values, options) {
|
|
|
936
1196
|
formVersion: schema.version,
|
|
937
1197
|
locale: options.locale,
|
|
938
1198
|
values: Object.freeze(cloneValues(visibleValues)),
|
|
939
|
-
submittedAt: options.submittedAt
|
|
1199
|
+
submittedAt: options.submittedAt,
|
|
1200
|
+
...options.metadata === void 0 ? {} : { metadata: Object.freeze({ ...options.metadata }) },
|
|
1201
|
+
...options.translationMetadata === void 0 ? {} : { translationMetadata: Object.freeze({ ...options.translationMetadata }) }
|
|
940
1202
|
});
|
|
941
1203
|
}
|
|
942
1204
|
|
|
943
1205
|
// src/translation.ts
|
|
944
|
-
function mergeLocalizedText(translations, locale,
|
|
945
|
-
return { ...translations, [locale]: { ...translations?.[locale], [
|
|
1206
|
+
function mergeLocalizedText(translations, locale, property, value) {
|
|
1207
|
+
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
946
1208
|
}
|
|
947
|
-
function
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1209
|
+
function withTranslationMetadata(node, locale, property, metadata) {
|
|
1210
|
+
if (metadata === void 0) return node;
|
|
1211
|
+
return {
|
|
1212
|
+
...node,
|
|
1213
|
+
translationMetadata: {
|
|
1214
|
+
...node.translationMetadata,
|
|
1215
|
+
[locale]: {
|
|
1216
|
+
...node.translationMetadata?.[locale],
|
|
1217
|
+
[property]: metadata
|
|
1218
|
+
}
|
|
955
1219
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
|
|
1223
|
+
return {
|
|
1224
|
+
kind,
|
|
1225
|
+
nodeId,
|
|
1226
|
+
property,
|
|
1227
|
+
locale,
|
|
1228
|
+
sourceText,
|
|
1229
|
+
...existingText === void 0 ? {} : { existingText },
|
|
1230
|
+
...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
|
|
1231
|
+
...existingTranslationMetadata === void 0 ? {} : { existingTranslationMetadata }
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function translationSlots(schema, locale) {
|
|
1235
|
+
const descriptors = [];
|
|
1236
|
+
const addFormSlot = (property, sourceText) => {
|
|
1237
|
+
const slot = createSlot(
|
|
1238
|
+
"form",
|
|
1239
|
+
schema.id,
|
|
1240
|
+
property,
|
|
1241
|
+
locale,
|
|
1242
|
+
sourceText,
|
|
1243
|
+
schema.translations?.[locale]?.[property],
|
|
1244
|
+
schema.metadata,
|
|
1245
|
+
schema.translationMetadata?.[locale]?.[property]
|
|
1246
|
+
);
|
|
1247
|
+
descriptors.push({
|
|
1248
|
+
slot,
|
|
1249
|
+
apply: (current, value, metadata) => withTranslationMetadata(
|
|
1250
|
+
{ ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
|
|
1251
|
+
locale,
|
|
1252
|
+
property,
|
|
1253
|
+
metadata
|
|
1254
|
+
)
|
|
964
1255
|
});
|
|
965
|
-
}
|
|
1256
|
+
};
|
|
1257
|
+
addFormSlot("title", schema.title);
|
|
1258
|
+
if (schema.description !== void 0) addFormSlot("description", schema.description);
|
|
1259
|
+
if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
|
|
966
1260
|
schema.fields.forEach((field, fieldIndex) => {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1261
|
+
const addFieldSlot = (property, sourceText) => {
|
|
1262
|
+
const slot = createSlot(
|
|
1263
|
+
"field",
|
|
1264
|
+
field.id,
|
|
1265
|
+
property,
|
|
1266
|
+
locale,
|
|
1267
|
+
sourceText,
|
|
1268
|
+
field.translations?.[locale]?.[property],
|
|
1269
|
+
field.metadata,
|
|
1270
|
+
field.translationMetadata?.[locale]?.[property]
|
|
1271
|
+
);
|
|
1272
|
+
descriptors.push({
|
|
1273
|
+
slot,
|
|
1274
|
+
apply: (current, value, metadata) => ({
|
|
980
1275
|
...current,
|
|
981
1276
|
fields: current.fields.map(
|
|
982
|
-
(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1277
|
+
(candidate, index) => index === fieldIndex ? withTranslationMetadata(
|
|
1278
|
+
{
|
|
1279
|
+
...candidate,
|
|
1280
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1281
|
+
},
|
|
1282
|
+
locale,
|
|
1283
|
+
property,
|
|
1284
|
+
metadata
|
|
1285
|
+
) : candidate
|
|
986
1286
|
)
|
|
987
1287
|
})
|
|
988
1288
|
});
|
|
989
|
-
}
|
|
1289
|
+
};
|
|
1290
|
+
addFieldSlot("title", field.title);
|
|
1291
|
+
if (field.description !== void 0) addFieldSlot("description", field.description);
|
|
990
1292
|
if ("options" in field) {
|
|
991
1293
|
field.options.forEach((option, optionIndex) => {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1294
|
+
const slot = createSlot(
|
|
1295
|
+
"option",
|
|
1296
|
+
option.id,
|
|
1297
|
+
"label",
|
|
1298
|
+
locale,
|
|
1299
|
+
option.label,
|
|
1300
|
+
option.translations?.[locale],
|
|
1301
|
+
option.metadata,
|
|
1302
|
+
option.translationMetadata?.[locale]?.label
|
|
1303
|
+
);
|
|
1304
|
+
descriptors.push({
|
|
1305
|
+
slot,
|
|
1306
|
+
apply: (current, value, metadata) => ({
|
|
995
1307
|
...current,
|
|
996
|
-
fields: current.fields.map((
|
|
997
|
-
if (
|
|
1308
|
+
fields: current.fields.map((candidate, candidateIndex) => {
|
|
1309
|
+
if (candidateIndex !== fieldIndex || !("options" in candidate)) return candidate;
|
|
998
1310
|
return {
|
|
999
|
-
...
|
|
1000
|
-
options:
|
|
1001
|
-
(
|
|
1311
|
+
...candidate,
|
|
1312
|
+
options: candidate.options.map(
|
|
1313
|
+
(candidateOption, candidateOptionIndex) => candidateOptionIndex === optionIndex ? withTranslationMetadata(
|
|
1314
|
+
{
|
|
1315
|
+
...candidateOption,
|
|
1316
|
+
translations: { ...candidateOption.translations, [locale]: value }
|
|
1317
|
+
},
|
|
1318
|
+
locale,
|
|
1319
|
+
"label",
|
|
1320
|
+
metadata
|
|
1321
|
+
) : candidateOption
|
|
1002
1322
|
)
|
|
1003
1323
|
};
|
|
1004
1324
|
})
|
|
@@ -1008,42 +1328,51 @@ function translationSlots(schema) {
|
|
|
1008
1328
|
}
|
|
1009
1329
|
});
|
|
1010
1330
|
schema.pages?.forEach((page, pageIndex) => {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
slots.push({
|
|
1026
|
-
text: page.description,
|
|
1027
|
-
apply: (current, value, locale) => ({
|
|
1331
|
+
const addPageSlot = (property, sourceText) => {
|
|
1332
|
+
const slot = createSlot(
|
|
1333
|
+
"page",
|
|
1334
|
+
page.id,
|
|
1335
|
+
property,
|
|
1336
|
+
locale,
|
|
1337
|
+
sourceText,
|
|
1338
|
+
page.translations?.[locale]?.[property],
|
|
1339
|
+
page.metadata,
|
|
1340
|
+
page.translationMetadata?.[locale]?.[property]
|
|
1341
|
+
);
|
|
1342
|
+
descriptors.push({
|
|
1343
|
+
slot,
|
|
1344
|
+
apply: (current, value, metadata) => ({
|
|
1028
1345
|
...current,
|
|
1029
1346
|
...current.pages === void 0 ? {} : {
|
|
1030
1347
|
pages: current.pages.map(
|
|
1031
|
-
(
|
|
1348
|
+
(candidate, index) => index === pageIndex ? withTranslationMetadata(
|
|
1349
|
+
{
|
|
1350
|
+
...candidate,
|
|
1351
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1352
|
+
},
|
|
1353
|
+
locale,
|
|
1354
|
+
property,
|
|
1355
|
+
metadata
|
|
1356
|
+
) : candidate
|
|
1032
1357
|
)
|
|
1033
1358
|
}
|
|
1034
1359
|
})
|
|
1035
1360
|
});
|
|
1036
|
-
}
|
|
1361
|
+
};
|
|
1362
|
+
if (page.title !== void 0) addPageSlot("title", page.title);
|
|
1363
|
+
if (page.description !== void 0) addPageSlot("description", page.description);
|
|
1037
1364
|
});
|
|
1038
|
-
return
|
|
1365
|
+
return descriptors;
|
|
1039
1366
|
}
|
|
1040
1367
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
1041
1368
|
if (targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
1042
1369
|
const formTranslation = schema.translations?.[targetLocale];
|
|
1370
|
+
const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
|
|
1043
1371
|
return {
|
|
1044
1372
|
...schema,
|
|
1045
1373
|
title: formTranslation?.title ?? schema.title,
|
|
1046
1374
|
...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
|
|
1375
|
+
...completionMessage === void 0 ? {} : { completionMessage },
|
|
1047
1376
|
fields: schema.fields.map((field) => {
|
|
1048
1377
|
const translation = field.translations?.[targetLocale];
|
|
1049
1378
|
const localized = {
|
|
@@ -1074,24 +1403,35 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1074
1403
|
}
|
|
1075
1404
|
};
|
|
1076
1405
|
}
|
|
1077
|
-
async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
1406
|
+
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1078
1407
|
assertValidFormSchema(schema);
|
|
1079
|
-
const slots = translationSlots(schema);
|
|
1080
1408
|
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1409
|
+
const updatedSlots = [];
|
|
1410
|
+
const skippedSlots = [];
|
|
1081
1411
|
let result = schema;
|
|
1082
1412
|
for (const locale of locales) {
|
|
1413
|
+
const descriptors = translationSlots(schema, locale);
|
|
1414
|
+
const selected = [];
|
|
1415
|
+
for (const descriptor of descriptors) {
|
|
1416
|
+
const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? (options.overwrite === "all" || descriptor.slot.existingText === void 0);
|
|
1417
|
+
if (shouldTranslate) selected.push(descriptor);
|
|
1418
|
+
else skippedSlots.push(descriptor.slot);
|
|
1419
|
+
}
|
|
1420
|
+
if (selected.length === 0) continue;
|
|
1083
1421
|
const translated = await adapter.translateBatch(
|
|
1084
|
-
|
|
1422
|
+
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
1085
1423
|
locale,
|
|
1086
1424
|
schema.defaultLocale
|
|
1087
1425
|
);
|
|
1088
|
-
if (translated.length !==
|
|
1089
|
-
throw new Error(`Translation adapter returned ${translated.length} texts for ${
|
|
1426
|
+
if (translated.length !== selected.length) {
|
|
1427
|
+
throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
|
|
1090
1428
|
}
|
|
1091
|
-
|
|
1092
|
-
const
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1429
|
+
selected.forEach((descriptor, index) => {
|
|
1430
|
+
const translatedText = translated[index];
|
|
1431
|
+
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
1432
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText);
|
|
1433
|
+
result = descriptor.apply(result, translatedText, metadata);
|
|
1434
|
+
updatedSlots.push(descriptor.slot);
|
|
1095
1435
|
});
|
|
1096
1436
|
}
|
|
1097
1437
|
const supportedLocales = [
|
|
@@ -1101,17 +1441,18 @@ async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
|
1101
1441
|
...locales
|
|
1102
1442
|
])
|
|
1103
1443
|
];
|
|
1104
|
-
|
|
1444
|
+
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
1105
1445
|
assertValidFormSchema(result);
|
|
1106
|
-
return result;
|
|
1446
|
+
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
1107
1447
|
}
|
|
1108
1448
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
1109
1449
|
const populated = await populateSchemaTranslations(
|
|
1110
1450
|
sourceLocale === void 0 ? schema : { ...schema, defaultLocale: sourceLocale },
|
|
1111
1451
|
[targetLocale],
|
|
1112
|
-
adapter
|
|
1452
|
+
adapter,
|
|
1453
|
+
{ overwrite: "all" }
|
|
1113
1454
|
);
|
|
1114
|
-
return resolveLocalizedSchema(populated, targetLocale);
|
|
1455
|
+
return resolveLocalizedSchema(populated.schema, targetLocale);
|
|
1115
1456
|
}
|
|
1116
1457
|
export {
|
|
1117
1458
|
aggregateResponses,
|
|
@@ -1132,6 +1473,7 @@ export {
|
|
|
1132
1473
|
resolveLocalizedSchema,
|
|
1133
1474
|
sanitizeSchema,
|
|
1134
1475
|
selectVisibleAnswers,
|
|
1476
|
+
transformFieldType,
|
|
1135
1477
|
validateAnswers,
|
|
1136
1478
|
validateFormSchema,
|
|
1137
1479
|
validatePageAnswers,
|