@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.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
resolveLocalizedSchema: () => resolveLocalizedSchema,
|
|
39
39
|
sanitizeSchema: () => sanitizeSchema,
|
|
40
40
|
selectVisibleAnswers: () => selectVisibleAnswers,
|
|
41
|
+
transformFieldType: () => transformFieldType,
|
|
41
42
|
validateAnswers: () => validateAnswers,
|
|
42
43
|
validateFormSchema: () => validateFormSchema,
|
|
43
44
|
validatePageAnswers: () => validatePageAnswers,
|
|
@@ -187,6 +188,52 @@ function isNonEmptyString(value) {
|
|
|
187
188
|
function issue(issues, path, code, message) {
|
|
188
189
|
issues.push({ path, code, message });
|
|
189
190
|
}
|
|
191
|
+
function validateJsonValue(value, path, issues, ancestors = /* @__PURE__ */ new Set()) {
|
|
192
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
193
|
+
if (typeof value === "number") {
|
|
194
|
+
if (!Number.isFinite(value)) issue(issues, path, "invalid_metadata", "Metadata numbers must be finite.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (typeof value !== "object") {
|
|
198
|
+
issue(issues, path, "invalid_metadata", "Expected JSON-serializable metadata.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (ancestors.has(value)) {
|
|
202
|
+
issue(issues, path, "invalid_metadata", "Metadata must not contain cycles.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
value.forEach((item, index) => {
|
|
208
|
+
validateJsonValue(item, `${path}[${index}]`, issues, nextAncestors);
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const prototype = Object.getPrototypeOf(value);
|
|
213
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
214
|
+
issue(issues, path, "invalid_metadata", "Metadata objects must be plain JSON objects.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const [key, item] of Object.entries(value)) {
|
|
218
|
+
validateJsonValue(item, `${path}.${key}`, issues, nextAncestors);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function validateExtensibleNode(value, path, issues) {
|
|
222
|
+
for (const property of ["metadata", "translationMetadata"]) {
|
|
223
|
+
const candidate = value[property];
|
|
224
|
+
if (candidate === void 0) continue;
|
|
225
|
+
if (!isRecord(candidate)) {
|
|
226
|
+
issue(
|
|
227
|
+
issues,
|
|
228
|
+
path.length === 0 ? property : `${path}.${property}`,
|
|
229
|
+
"invalid_metadata",
|
|
230
|
+
"Expected a metadata object."
|
|
231
|
+
);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
validateJsonValue(candidate, path.length === 0 ? property : `${path}.${property}`, issues);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
190
237
|
function validateLocalizedTextMap(value, path, issues) {
|
|
191
238
|
if (!isRecord(value)) {
|
|
192
239
|
issue(issues, path, "invalid_translations", "Expected a locale-to-translation object.");
|
|
@@ -197,7 +244,7 @@ function validateLocalizedTextMap(value, path, issues) {
|
|
|
197
244
|
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a translation object.");
|
|
198
245
|
continue;
|
|
199
246
|
}
|
|
200
|
-
for (const key of ["title", "description"]) {
|
|
247
|
+
for (const key of ["title", "description", "completionMessage"]) {
|
|
201
248
|
if (translation[key] !== void 0 && !isNonEmptyString(translation[key])) {
|
|
202
249
|
issue(issues, `${path}.${locale}.${key}`, "invalid_translation", "Expected non-empty translated text.");
|
|
203
250
|
}
|
|
@@ -248,6 +295,7 @@ function validateOptions(value, path, issues) {
|
|
|
248
295
|
return;
|
|
249
296
|
}
|
|
250
297
|
rejectLegacyProperties(option, optionPath, ["value", "labelKey"], issues);
|
|
298
|
+
validateExtensibleNode(option, optionPath, issues);
|
|
251
299
|
if (!isNonEmptyString(option.id)) {
|
|
252
300
|
issue(issues, `${optionPath}.id`, "invalid_option_id", "Expected a non-empty option ID.");
|
|
253
301
|
} else if (seen.has(option.id)) {
|
|
@@ -293,6 +341,7 @@ function validateField(value, path, issues) {
|
|
|
293
341
|
return false;
|
|
294
342
|
}
|
|
295
343
|
rejectLegacyProperties(value, path, ["titleKey", "labelKey", "helpTextKey", "descriptionKey"], issues);
|
|
344
|
+
validateExtensibleNode(value, path, issues);
|
|
296
345
|
if (!isNonEmptyString(value.id)) issue(issues, `${path}.id`, "invalid_id", "Expected a non-empty ID.");
|
|
297
346
|
if (!isNonEmptyString(value.title)) {
|
|
298
347
|
issue(issues, `${path}.title`, "invalid_title", "Expected a non-empty question title.");
|
|
@@ -372,12 +421,157 @@ function validateField(value, path, issues) {
|
|
|
372
421
|
}
|
|
373
422
|
return true;
|
|
374
423
|
}
|
|
375
|
-
function
|
|
424
|
+
function collectSchemaText(schema) {
|
|
425
|
+
const entries = [{ path: "title", value: schema.title }];
|
|
426
|
+
if (schema.description !== void 0) entries.push({ path: "description", value: schema.description });
|
|
427
|
+
if (schema.completionMessage !== void 0)
|
|
428
|
+
entries.push({ path: "completionMessage", value: schema.completionMessage });
|
|
429
|
+
for (const [locale, translation] of Object.entries(schema.translations ?? {})) {
|
|
430
|
+
for (const property of ["title", "description", "completionMessage"]) {
|
|
431
|
+
const value = translation[property];
|
|
432
|
+
if (value !== void 0) entries.push({ path: `translations.${locale}.${property}`, value });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
436
|
+
entries.push({ path: `fields[${fieldIndex}].title`, value: field.title });
|
|
437
|
+
if (field.description !== void 0)
|
|
438
|
+
entries.push({ path: `fields[${fieldIndex}].description`, value: field.description });
|
|
439
|
+
for (const [locale, translation] of Object.entries(field.translations ?? {})) {
|
|
440
|
+
for (const property of ["title", "description"]) {
|
|
441
|
+
const value = translation[property];
|
|
442
|
+
if (value !== void 0)
|
|
443
|
+
entries.push({ path: `fields[${fieldIndex}].translations.${locale}.${property}`, value });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!("options" in field)) return;
|
|
447
|
+
field.options.forEach((option, optionIndex) => {
|
|
448
|
+
entries.push({ path: `fields[${fieldIndex}].options[${optionIndex}].label`, value: option.label });
|
|
449
|
+
for (const [locale, value] of Object.entries(option.translations ?? {})) {
|
|
450
|
+
entries.push({ path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`, value });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
455
|
+
if (page.title !== void 0) entries.push({ path: `pages[${pageIndex}].title`, value: page.title });
|
|
456
|
+
if (page.description !== void 0)
|
|
457
|
+
entries.push({ path: `pages[${pageIndex}].description`, value: page.description });
|
|
458
|
+
for (const [locale, translation] of Object.entries(page.translations ?? {})) {
|
|
459
|
+
for (const property of ["title", "description"]) {
|
|
460
|
+
const value = translation[property];
|
|
461
|
+
if (value !== void 0)
|
|
462
|
+
entries.push({ path: `pages[${pageIndex}].translations.${locale}.${property}`, value });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return entries;
|
|
467
|
+
}
|
|
468
|
+
function addRequiredTranslationIssues(schema, locale, issues) {
|
|
469
|
+
if (!(schema.supportedLocales ?? []).includes(locale)) {
|
|
470
|
+
issue(issues, "supportedLocales", "required_locale_missing", `Required locale ${locale} is missing.`);
|
|
471
|
+
}
|
|
472
|
+
if (locale === schema.defaultLocale) return;
|
|
473
|
+
const required = [
|
|
474
|
+
{ path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title }
|
|
475
|
+
];
|
|
476
|
+
if (schema.description !== void 0)
|
|
477
|
+
required.push({ path: `translations.${locale}.description`, value: schema.translations?.[locale]?.description });
|
|
478
|
+
if (schema.completionMessage !== void 0) {
|
|
479
|
+
required.push({
|
|
480
|
+
path: `translations.${locale}.completionMessage`,
|
|
481
|
+
value: schema.translations?.[locale]?.completionMessage
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
485
|
+
required.push({
|
|
486
|
+
path: `fields[${fieldIndex}].translations.${locale}.title`,
|
|
487
|
+
value: field.translations?.[locale]?.title
|
|
488
|
+
});
|
|
489
|
+
if (field.description !== void 0) {
|
|
490
|
+
required.push({
|
|
491
|
+
path: `fields[${fieldIndex}].translations.${locale}.description`,
|
|
492
|
+
value: field.translations?.[locale]?.description
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (!("options" in field)) return;
|
|
496
|
+
field.options.forEach((option, optionIndex) => {
|
|
497
|
+
required.push({
|
|
498
|
+
path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
|
|
499
|
+
value: option.translations?.[locale]
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
504
|
+
if (page.title !== void 0) {
|
|
505
|
+
required.push({
|
|
506
|
+
path: `pages[${pageIndex}].translations.${locale}.title`,
|
|
507
|
+
value: page.translations?.[locale]?.title
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (page.description !== void 0) {
|
|
511
|
+
required.push({
|
|
512
|
+
path: `pages[${pageIndex}].translations.${locale}.description`,
|
|
513
|
+
value: page.translations?.[locale]?.description
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
for (const translation of required) {
|
|
518
|
+
if (translation.value === void 0 || translation.value.trim().length === 0) {
|
|
519
|
+
issue(
|
|
520
|
+
issues,
|
|
521
|
+
translation.path,
|
|
522
|
+
"required_translation_missing",
|
|
523
|
+
`A translation for required locale ${locale} is missing.`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function validatePolicy(schema, policy, issues) {
|
|
529
|
+
if (policy.maxFields !== void 0 && schema.fields.length > policy.maxFields) {
|
|
530
|
+
issue(issues, "fields", "max_fields_exceeded", `At most ${policy.maxFields} fields are allowed.`);
|
|
531
|
+
}
|
|
532
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
533
|
+
if (policy.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(field.type)) {
|
|
534
|
+
issue(issues, `fields[${fieldIndex}].type`, "disallowed_field_type", `Field type ${field.type} is not allowed.`);
|
|
535
|
+
}
|
|
536
|
+
if (policy.maxOptionsPerField !== void 0 && "options" in field && field.options.length > policy.maxOptionsPerField) {
|
|
537
|
+
issue(
|
|
538
|
+
issues,
|
|
539
|
+
`fields[${fieldIndex}].options`,
|
|
540
|
+
"max_options_exceeded",
|
|
541
|
+
`At most ${policy.maxOptionsPerField} options are allowed.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
if (policy.maxTextLength !== void 0) {
|
|
546
|
+
for (const entry of collectSchemaText(schema)) {
|
|
547
|
+
if (entry.value.length > policy.maxTextLength) {
|
|
548
|
+
issue(
|
|
549
|
+
issues,
|
|
550
|
+
entry.path,
|
|
551
|
+
"max_text_length_exceeded",
|
|
552
|
+
`Text must be at most ${policy.maxTextLength} characters.`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
|
|
558
|
+
if (policy.maxSchemaBytes !== void 0) {
|
|
559
|
+
try {
|
|
560
|
+
const byteLength = new TextEncoder().encode(JSON.stringify(schema)).byteLength;
|
|
561
|
+
if (byteLength > policy.maxSchemaBytes) {
|
|
562
|
+
issue(issues, "$", "max_schema_bytes_exceeded", `Schema must be at most ${policy.maxSchemaBytes} bytes.`);
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function validateFormSchema(input, options = {}) {
|
|
376
569
|
const issues = [];
|
|
377
570
|
if (!isRecord(input)) {
|
|
378
571
|
return { valid: false, issues: [{ path: "$", code: "invalid_schema", message: "Expected a schema object." }] };
|
|
379
572
|
}
|
|
380
573
|
rejectLegacyProperties(input, "", ["titleKey", "descriptionKey"], issues);
|
|
574
|
+
validateExtensibleNode(input, "", issues);
|
|
381
575
|
if (!isNonEmptyString(input.id)) issue(issues, "id", "invalid_id", "Expected a non-empty ID.");
|
|
382
576
|
if (!Number.isInteger(input.version) || input.version < 1) {
|
|
383
577
|
issue(issues, "version", "invalid_version", "Expected a positive integer version.");
|
|
@@ -385,13 +579,13 @@ function validateFormSchema(input) {
|
|
|
385
579
|
if (!isNonEmptyString(input.title)) {
|
|
386
580
|
issue(issues, "title", "invalid_title", "Expected a non-empty form title.");
|
|
387
581
|
}
|
|
388
|
-
for (const key of ["description", "submitLabelKey"]) {
|
|
582
|
+
for (const key of ["description", "completionMessage", "submitLabelKey"]) {
|
|
389
583
|
if (input[key] !== void 0 && !isNonEmptyString(input[key])) {
|
|
390
584
|
issue(
|
|
391
585
|
issues,
|
|
392
586
|
key,
|
|
393
|
-
key === "
|
|
394
|
-
key === "
|
|
587
|
+
key === "submitLabelKey" ? "invalid_translation_key" : "invalid_description",
|
|
588
|
+
key === "submitLabelKey" ? "Expected a translation key." : "Expected non-empty form text."
|
|
395
589
|
);
|
|
396
590
|
}
|
|
397
591
|
}
|
|
@@ -453,6 +647,7 @@ function validateFormSchema(input) {
|
|
|
453
647
|
issue(issues, pagePath, "invalid_page", "Expected a page object.");
|
|
454
648
|
return;
|
|
455
649
|
}
|
|
650
|
+
validateExtensibleNode(page, pagePath, issues);
|
|
456
651
|
if (!isNonEmptyString(page.id)) {
|
|
457
652
|
issue(issues, `${pagePath}.id`, "invalid_page_id", "Expected a non-empty page ID.");
|
|
458
653
|
} else if (pageIds.has(page.id)) {
|
|
@@ -516,6 +711,14 @@ function validateFormSchema(input) {
|
|
|
516
711
|
}
|
|
517
712
|
}
|
|
518
713
|
}
|
|
714
|
+
if (Array.isArray(input.fields)) {
|
|
715
|
+
const policyIssues = [];
|
|
716
|
+
try {
|
|
717
|
+
validatePolicy(input, options.policy ?? {}, policyIssues);
|
|
718
|
+
issues.push(...policyIssues);
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
519
722
|
return issues.length === 0 ? { valid: true, value: input, issues: [] } : { valid: false, issues };
|
|
520
723
|
}
|
|
521
724
|
function assertValidFormSchema(input) {
|
|
@@ -744,15 +947,20 @@ function aggregateResponses(schema, submissions) {
|
|
|
744
947
|
questions: schema.fields.map((field) => aggregateField(schema, field, submissions))
|
|
745
948
|
};
|
|
746
949
|
}
|
|
747
|
-
function escapeCsvCell(value) {
|
|
950
|
+
function escapeCsvCell(value, neutralizeFormulas = true) {
|
|
748
951
|
if (value === null || value === void 0) return "";
|
|
749
|
-
|
|
952
|
+
let stringValue = String(value);
|
|
953
|
+
if (neutralizeFormulas && typeof value === "string") {
|
|
954
|
+
const trimmed = stringValue.trimStart();
|
|
955
|
+
if (trimmed.length > 0 && ["=", "+", "-", "@"].includes(trimmed[0] ?? "")) {
|
|
956
|
+
stringValue = `'${stringValue}`;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
750
959
|
return /[",\r\n]/.test(stringValue) ? `"${stringValue.replaceAll('"', '""')}"` : stringValue;
|
|
751
960
|
}
|
|
752
961
|
function serializeValue(value) {
|
|
753
962
|
if (value === void 0) return "";
|
|
754
|
-
|
|
755
|
-
return String(value);
|
|
963
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
|
|
756
964
|
}
|
|
757
965
|
function exportResponsesToCsv(schema, responses, options = {}) {
|
|
758
966
|
assertValidFormSchema(schema);
|
|
@@ -773,7 +981,8 @@ function exportResponsesToCsv(schema, responses, options = {}) {
|
|
|
773
981
|
];
|
|
774
982
|
})
|
|
775
983
|
];
|
|
776
|
-
const
|
|
984
|
+
const neutralizeFormulas = options.neutralizeFormulas ?? true;
|
|
985
|
+
const csv = rows.map((row) => row.map((cell) => escapeCsvCell(cell, neutralizeFormulas)).join(",")).join("\r\n");
|
|
777
986
|
return options.withBom ?? true ? `\uFEFF${csv}` : csv;
|
|
778
987
|
}
|
|
779
988
|
|
|
@@ -818,6 +1027,58 @@ async function dispatchWebhook(event, config, fetchImpl = globalThis.fetch) {
|
|
|
818
1027
|
}
|
|
819
1028
|
}
|
|
820
1029
|
|
|
1030
|
+
// src/field.ts
|
|
1031
|
+
var DEFAULT_OPTION = { id: "option-1", label: "Option 1" };
|
|
1032
|
+
function transformFieldType(field, nextType) {
|
|
1033
|
+
const common = {
|
|
1034
|
+
id: field.id,
|
|
1035
|
+
title: field.title,
|
|
1036
|
+
required: field.required,
|
|
1037
|
+
...field.description === void 0 ? {} : { description: field.description },
|
|
1038
|
+
...field.translationKey === void 0 ? {} : { translationKey: field.translationKey },
|
|
1039
|
+
...field.messages === void 0 ? {} : { messages: field.messages },
|
|
1040
|
+
...field.displayCondition === void 0 ? {} : { displayCondition: field.displayCondition },
|
|
1041
|
+
...field.translations === void 0 ? {} : { translations: field.translations },
|
|
1042
|
+
...field.metadata === void 0 ? {} : { metadata: field.metadata },
|
|
1043
|
+
...field.translationMetadata === void 0 ? {} : { translationMetadata: field.translationMetadata }
|
|
1044
|
+
};
|
|
1045
|
+
if (nextType === "text" || nextType === "textarea") {
|
|
1046
|
+
const textProperties = field.type === "text" || field.type === "textarea" ? {
|
|
1047
|
+
...field.placeholderKey === void 0 ? {} : { placeholderKey: field.placeholderKey },
|
|
1048
|
+
...field.minLength === void 0 ? {} : { minLength: field.minLength },
|
|
1049
|
+
...field.maxLength === void 0 ? {} : { maxLength: field.maxLength },
|
|
1050
|
+
...field.pattern === void 0 ? {} : { pattern: field.pattern }
|
|
1051
|
+
} : {};
|
|
1052
|
+
return { ...common, ...textProperties, type: nextType };
|
|
1053
|
+
}
|
|
1054
|
+
if (nextType === "number") {
|
|
1055
|
+
const numberProperties = field.type === "number" ? {
|
|
1056
|
+
...field.placeholderKey === void 0 ? {} : { placeholderKey: field.placeholderKey },
|
|
1057
|
+
...field.min === void 0 ? {} : { min: field.min },
|
|
1058
|
+
...field.max === void 0 ? {} : { max: field.max },
|
|
1059
|
+
...field.step === void 0 ? {} : { step: field.step }
|
|
1060
|
+
} : {};
|
|
1061
|
+
return { ...common, ...numberProperties, type: nextType };
|
|
1062
|
+
}
|
|
1063
|
+
if (nextType === "rating") {
|
|
1064
|
+
const ratingProperties = field.type === "rating" ? {
|
|
1065
|
+
...field.min === void 0 ? {} : { min: field.min },
|
|
1066
|
+
...field.max === void 0 ? {} : { max: field.max }
|
|
1067
|
+
} : { min: 1, max: 5 };
|
|
1068
|
+
return { ...common, ...ratingProperties, type: nextType };
|
|
1069
|
+
}
|
|
1070
|
+
if (nextType === "checkbox") return { ...common, type: nextType };
|
|
1071
|
+
const options = "options" in field && field.options.length > 0 ? field.options : [DEFAULT_OPTION];
|
|
1072
|
+
if (nextType === "multi-select") {
|
|
1073
|
+
const selectionProperties = field.type === "multi-select" ? {
|
|
1074
|
+
...field.minSelections === void 0 ? {} : { minSelections: field.minSelections },
|
|
1075
|
+
...field.maxSelections === void 0 ? {} : { maxSelections: field.maxSelections }
|
|
1076
|
+
} : {};
|
|
1077
|
+
return { ...common, ...selectionProperties, type: nextType, options };
|
|
1078
|
+
}
|
|
1079
|
+
return { ...common, type: nextType, options };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
821
1082
|
// src/validation.ts
|
|
822
1083
|
var DEFAULT_MESSAGES = {
|
|
823
1084
|
required: "validation.required",
|
|
@@ -983,69 +1244,129 @@ function createSubmission(schema, values, options) {
|
|
|
983
1244
|
formVersion: schema.version,
|
|
984
1245
|
locale: options.locale,
|
|
985
1246
|
values: Object.freeze(cloneValues(visibleValues)),
|
|
986
|
-
submittedAt: options.submittedAt
|
|
1247
|
+
submittedAt: options.submittedAt,
|
|
1248
|
+
...options.metadata === void 0 ? {} : { metadata: Object.freeze({ ...options.metadata }) },
|
|
1249
|
+
...options.translationMetadata === void 0 ? {} : { translationMetadata: Object.freeze({ ...options.translationMetadata }) }
|
|
987
1250
|
});
|
|
988
1251
|
}
|
|
989
1252
|
|
|
990
1253
|
// src/translation.ts
|
|
991
|
-
function mergeLocalizedText(translations, locale,
|
|
992
|
-
return { ...translations, [locale]: { ...translations?.[locale], [
|
|
1254
|
+
function mergeLocalizedText(translations, locale, property, value) {
|
|
1255
|
+
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
993
1256
|
}
|
|
994
|
-
function
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1257
|
+
function withTranslationMetadata(node, locale, property, metadata) {
|
|
1258
|
+
if (metadata === void 0) return node;
|
|
1259
|
+
return {
|
|
1260
|
+
...node,
|
|
1261
|
+
translationMetadata: {
|
|
1262
|
+
...node.translationMetadata,
|
|
1263
|
+
[locale]: {
|
|
1264
|
+
...node.translationMetadata?.[locale],
|
|
1265
|
+
[property]: metadata
|
|
1266
|
+
}
|
|
1002
1267
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
|
|
1271
|
+
return {
|
|
1272
|
+
kind,
|
|
1273
|
+
nodeId,
|
|
1274
|
+
property,
|
|
1275
|
+
locale,
|
|
1276
|
+
sourceText,
|
|
1277
|
+
...existingText === void 0 ? {} : { existingText },
|
|
1278
|
+
...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
|
|
1279
|
+
...existingTranslationMetadata === void 0 ? {} : { existingTranslationMetadata }
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function translationSlots(schema, locale) {
|
|
1283
|
+
const descriptors = [];
|
|
1284
|
+
const addFormSlot = (property, sourceText) => {
|
|
1285
|
+
const slot = createSlot(
|
|
1286
|
+
"form",
|
|
1287
|
+
schema.id,
|
|
1288
|
+
property,
|
|
1289
|
+
locale,
|
|
1290
|
+
sourceText,
|
|
1291
|
+
schema.translations?.[locale]?.[property],
|
|
1292
|
+
schema.metadata,
|
|
1293
|
+
schema.translationMetadata?.[locale]?.[property]
|
|
1294
|
+
);
|
|
1295
|
+
descriptors.push({
|
|
1296
|
+
slot,
|
|
1297
|
+
apply: (current, value, metadata) => withTranslationMetadata(
|
|
1298
|
+
{ ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
|
|
1299
|
+
locale,
|
|
1300
|
+
property,
|
|
1301
|
+
metadata
|
|
1302
|
+
)
|
|
1011
1303
|
});
|
|
1012
|
-
}
|
|
1304
|
+
};
|
|
1305
|
+
addFormSlot("title", schema.title);
|
|
1306
|
+
if (schema.description !== void 0) addFormSlot("description", schema.description);
|
|
1307
|
+
if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
|
|
1013
1308
|
schema.fields.forEach((field, fieldIndex) => {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1309
|
+
const addFieldSlot = (property, sourceText) => {
|
|
1310
|
+
const slot = createSlot(
|
|
1311
|
+
"field",
|
|
1312
|
+
field.id,
|
|
1313
|
+
property,
|
|
1314
|
+
locale,
|
|
1315
|
+
sourceText,
|
|
1316
|
+
field.translations?.[locale]?.[property],
|
|
1317
|
+
field.metadata,
|
|
1318
|
+
field.translationMetadata?.[locale]?.[property]
|
|
1319
|
+
);
|
|
1320
|
+
descriptors.push({
|
|
1321
|
+
slot,
|
|
1322
|
+
apply: (current, value, metadata) => ({
|
|
1027
1323
|
...current,
|
|
1028
1324
|
fields: current.fields.map(
|
|
1029
|
-
(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1325
|
+
(candidate, index) => index === fieldIndex ? withTranslationMetadata(
|
|
1326
|
+
{
|
|
1327
|
+
...candidate,
|
|
1328
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1329
|
+
},
|
|
1330
|
+
locale,
|
|
1331
|
+
property,
|
|
1332
|
+
metadata
|
|
1333
|
+
) : candidate
|
|
1033
1334
|
)
|
|
1034
1335
|
})
|
|
1035
1336
|
});
|
|
1036
|
-
}
|
|
1337
|
+
};
|
|
1338
|
+
addFieldSlot("title", field.title);
|
|
1339
|
+
if (field.description !== void 0) addFieldSlot("description", field.description);
|
|
1037
1340
|
if ("options" in field) {
|
|
1038
1341
|
field.options.forEach((option, optionIndex) => {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1342
|
+
const slot = createSlot(
|
|
1343
|
+
"option",
|
|
1344
|
+
option.id,
|
|
1345
|
+
"label",
|
|
1346
|
+
locale,
|
|
1347
|
+
option.label,
|
|
1348
|
+
option.translations?.[locale],
|
|
1349
|
+
option.metadata,
|
|
1350
|
+
option.translationMetadata?.[locale]?.label
|
|
1351
|
+
);
|
|
1352
|
+
descriptors.push({
|
|
1353
|
+
slot,
|
|
1354
|
+
apply: (current, value, metadata) => ({
|
|
1042
1355
|
...current,
|
|
1043
|
-
fields: current.fields.map((
|
|
1044
|
-
if (
|
|
1356
|
+
fields: current.fields.map((candidate, candidateIndex) => {
|
|
1357
|
+
if (candidateIndex !== fieldIndex || !("options" in candidate)) return candidate;
|
|
1045
1358
|
return {
|
|
1046
|
-
...
|
|
1047
|
-
options:
|
|
1048
|
-
(
|
|
1359
|
+
...candidate,
|
|
1360
|
+
options: candidate.options.map(
|
|
1361
|
+
(candidateOption, candidateOptionIndex) => candidateOptionIndex === optionIndex ? withTranslationMetadata(
|
|
1362
|
+
{
|
|
1363
|
+
...candidateOption,
|
|
1364
|
+
translations: { ...candidateOption.translations, [locale]: value }
|
|
1365
|
+
},
|
|
1366
|
+
locale,
|
|
1367
|
+
"label",
|
|
1368
|
+
metadata
|
|
1369
|
+
) : candidateOption
|
|
1049
1370
|
)
|
|
1050
1371
|
};
|
|
1051
1372
|
})
|
|
@@ -1055,42 +1376,51 @@ function translationSlots(schema) {
|
|
|
1055
1376
|
}
|
|
1056
1377
|
});
|
|
1057
1378
|
schema.pages?.forEach((page, pageIndex) => {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
slots.push({
|
|
1073
|
-
text: page.description,
|
|
1074
|
-
apply: (current, value, locale) => ({
|
|
1379
|
+
const addPageSlot = (property, sourceText) => {
|
|
1380
|
+
const slot = createSlot(
|
|
1381
|
+
"page",
|
|
1382
|
+
page.id,
|
|
1383
|
+
property,
|
|
1384
|
+
locale,
|
|
1385
|
+
sourceText,
|
|
1386
|
+
page.translations?.[locale]?.[property],
|
|
1387
|
+
page.metadata,
|
|
1388
|
+
page.translationMetadata?.[locale]?.[property]
|
|
1389
|
+
);
|
|
1390
|
+
descriptors.push({
|
|
1391
|
+
slot,
|
|
1392
|
+
apply: (current, value, metadata) => ({
|
|
1075
1393
|
...current,
|
|
1076
1394
|
...current.pages === void 0 ? {} : {
|
|
1077
1395
|
pages: current.pages.map(
|
|
1078
|
-
(
|
|
1396
|
+
(candidate, index) => index === pageIndex ? withTranslationMetadata(
|
|
1397
|
+
{
|
|
1398
|
+
...candidate,
|
|
1399
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1400
|
+
},
|
|
1401
|
+
locale,
|
|
1402
|
+
property,
|
|
1403
|
+
metadata
|
|
1404
|
+
) : candidate
|
|
1079
1405
|
)
|
|
1080
1406
|
}
|
|
1081
1407
|
})
|
|
1082
1408
|
});
|
|
1083
|
-
}
|
|
1409
|
+
};
|
|
1410
|
+
if (page.title !== void 0) addPageSlot("title", page.title);
|
|
1411
|
+
if (page.description !== void 0) addPageSlot("description", page.description);
|
|
1084
1412
|
});
|
|
1085
|
-
return
|
|
1413
|
+
return descriptors;
|
|
1086
1414
|
}
|
|
1087
1415
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
1088
1416
|
if (targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
1089
1417
|
const formTranslation = schema.translations?.[targetLocale];
|
|
1418
|
+
const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
|
|
1090
1419
|
return {
|
|
1091
1420
|
...schema,
|
|
1092
1421
|
title: formTranslation?.title ?? schema.title,
|
|
1093
1422
|
...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
|
|
1423
|
+
...completionMessage === void 0 ? {} : { completionMessage },
|
|
1094
1424
|
fields: schema.fields.map((field) => {
|
|
1095
1425
|
const translation = field.translations?.[targetLocale];
|
|
1096
1426
|
const localized = {
|
|
@@ -1121,24 +1451,35 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1121
1451
|
}
|
|
1122
1452
|
};
|
|
1123
1453
|
}
|
|
1124
|
-
async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
1454
|
+
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1125
1455
|
assertValidFormSchema(schema);
|
|
1126
|
-
const slots = translationSlots(schema);
|
|
1127
1456
|
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1457
|
+
const updatedSlots = [];
|
|
1458
|
+
const skippedSlots = [];
|
|
1128
1459
|
let result = schema;
|
|
1129
1460
|
for (const locale of locales) {
|
|
1461
|
+
const descriptors = translationSlots(schema, locale);
|
|
1462
|
+
const selected = [];
|
|
1463
|
+
for (const descriptor of descriptors) {
|
|
1464
|
+
const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? (options.overwrite === "all" || descriptor.slot.existingText === void 0);
|
|
1465
|
+
if (shouldTranslate) selected.push(descriptor);
|
|
1466
|
+
else skippedSlots.push(descriptor.slot);
|
|
1467
|
+
}
|
|
1468
|
+
if (selected.length === 0) continue;
|
|
1130
1469
|
const translated = await adapter.translateBatch(
|
|
1131
|
-
|
|
1470
|
+
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
1132
1471
|
locale,
|
|
1133
1472
|
schema.defaultLocale
|
|
1134
1473
|
);
|
|
1135
|
-
if (translated.length !==
|
|
1136
|
-
throw new Error(`Translation adapter returned ${translated.length} texts for ${
|
|
1474
|
+
if (translated.length !== selected.length) {
|
|
1475
|
+
throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
|
|
1137
1476
|
}
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1477
|
+
selected.forEach((descriptor, index) => {
|
|
1478
|
+
const translatedText = translated[index];
|
|
1479
|
+
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
1480
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText);
|
|
1481
|
+
result = descriptor.apply(result, translatedText, metadata);
|
|
1482
|
+
updatedSlots.push(descriptor.slot);
|
|
1142
1483
|
});
|
|
1143
1484
|
}
|
|
1144
1485
|
const supportedLocales = [
|
|
@@ -1148,17 +1489,18 @@ async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
|
1148
1489
|
...locales
|
|
1149
1490
|
])
|
|
1150
1491
|
];
|
|
1151
|
-
|
|
1492
|
+
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
1152
1493
|
assertValidFormSchema(result);
|
|
1153
|
-
return result;
|
|
1494
|
+
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
1154
1495
|
}
|
|
1155
1496
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
1156
1497
|
const populated = await populateSchemaTranslations(
|
|
1157
1498
|
sourceLocale === void 0 ? schema : { ...schema, defaultLocale: sourceLocale },
|
|
1158
1499
|
[targetLocale],
|
|
1159
|
-
adapter
|
|
1500
|
+
adapter,
|
|
1501
|
+
{ overwrite: "all" }
|
|
1160
1502
|
);
|
|
1161
|
-
return resolveLocalizedSchema(populated, targetLocale);
|
|
1503
|
+
return resolveLocalizedSchema(populated.schema, targetLocale);
|
|
1162
1504
|
}
|
|
1163
1505
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1164
1506
|
0 && (module.exports = {
|
|
@@ -1180,6 +1522,7 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
|
|
|
1180
1522
|
resolveLocalizedSchema,
|
|
1181
1523
|
sanitizeSchema,
|
|
1182
1524
|
selectVisibleAnswers,
|
|
1525
|
+
transformFieldType,
|
|
1183
1526
|
validateAnswers,
|
|
1184
1527
|
validateFormSchema,
|
|
1185
1528
|
validatePageAnswers,
|