@form-engine-ts/core 1.1.0 → 2.0.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 +10 -1
- package/dist/index.cjs +220 -88
- package/dist/index.d.cts +50 -10
- package/dist/index.d.ts +50 -10
- package/dist/index.js +220 -88
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,11 +30,20 @@ Add `pages` to partition every field into an accessible wizard and use `validate
|
|
|
30
30
|
for step-scoped validation. Schemas without `pages` remain single-page forms.
|
|
31
31
|
|
|
32
32
|
Store authoring-time translations on forms, fields, options, and pages. `resolveLocalizedSchema` applies them synchronously,
|
|
33
|
-
while `populateSchemaTranslations` fills them through an injected `AsyncTranslationAdapter`.
|
|
33
|
+
while `populateSchemaTranslations` fills them through an injected `AsyncTranslationAdapter`. Population defaults to
|
|
34
|
+
`overwrite: "missing-only"`, accepts per-slot `shouldOverwrite` and `createMetadata` callbacks, and returns
|
|
35
|
+
`{ schema, report }` with updated and skipped translation slots.
|
|
36
|
+
|
|
37
|
+
Forms, pages, fields, options, and submissions accept JSON-only `metadata` and per-locale/property
|
|
38
|
+
`translationMetadata`. These extension values survive sanitization, localization, submission creation, and storage
|
|
39
|
+
round-trips. `completionMessage` is localized with the rest of the form text.
|
|
34
40
|
|
|
35
41
|
`calculateCrossTabulation` builds a two-question frequency matrix from submissions. `dispatchWebhook` posts typed
|
|
36
42
|
`response.submitted` or `schema.updated` events with timeout handling, custom headers, and optional HMAC-SHA256 signing.
|
|
37
43
|
|
|
44
|
+
CSV export neutralizes string cells whose first non-whitespace character is `=`, `+`, `-`, or `@`. This is enabled by
|
|
45
|
+
default; trusted callers can pass `{ neutralizeFormulas: false }`. RFC 4180 quoting and the UTF-8 BOM remain unchanged.
|
|
46
|
+
|
|
38
47
|
Storage adapters share inclusive ISO 8601 submission-range filtering:
|
|
39
48
|
|
|
40
49
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -187,6 +187,52 @@ function isNonEmptyString(value) {
|
|
|
187
187
|
function issue(issues, path, code, message) {
|
|
188
188
|
issues.push({ path, code, message });
|
|
189
189
|
}
|
|
190
|
+
function validateJsonValue(value, path, issues, ancestors = /* @__PURE__ */ new Set()) {
|
|
191
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
192
|
+
if (typeof value === "number") {
|
|
193
|
+
if (!Number.isFinite(value)) issue(issues, path, "invalid_metadata", "Metadata numbers must be finite.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (typeof value !== "object") {
|
|
197
|
+
issue(issues, path, "invalid_metadata", "Expected JSON-serializable metadata.");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (ancestors.has(value)) {
|
|
201
|
+
issue(issues, path, "invalid_metadata", "Metadata must not contain cycles.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
value.forEach((item, index) => {
|
|
207
|
+
validateJsonValue(item, `${path}[${index}]`, issues, nextAncestors);
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const prototype = Object.getPrototypeOf(value);
|
|
212
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
213
|
+
issue(issues, path, "invalid_metadata", "Metadata objects must be plain JSON objects.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
for (const [key, item] of Object.entries(value)) {
|
|
217
|
+
validateJsonValue(item, `${path}.${key}`, issues, nextAncestors);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function validateExtensibleNode(value, path, issues) {
|
|
221
|
+
for (const property of ["metadata", "translationMetadata"]) {
|
|
222
|
+
const candidate = value[property];
|
|
223
|
+
if (candidate === void 0) continue;
|
|
224
|
+
if (!isRecord(candidate)) {
|
|
225
|
+
issue(
|
|
226
|
+
issues,
|
|
227
|
+
path.length === 0 ? property : `${path}.${property}`,
|
|
228
|
+
"invalid_metadata",
|
|
229
|
+
"Expected a metadata object."
|
|
230
|
+
);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
validateJsonValue(candidate, path.length === 0 ? property : `${path}.${property}`, issues);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
190
236
|
function validateLocalizedTextMap(value, path, issues) {
|
|
191
237
|
if (!isRecord(value)) {
|
|
192
238
|
issue(issues, path, "invalid_translations", "Expected a locale-to-translation object.");
|
|
@@ -197,7 +243,7 @@ function validateLocalizedTextMap(value, path, issues) {
|
|
|
197
243
|
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a translation object.");
|
|
198
244
|
continue;
|
|
199
245
|
}
|
|
200
|
-
for (const key of ["title", "description"]) {
|
|
246
|
+
for (const key of ["title", "description", "completionMessage"]) {
|
|
201
247
|
if (translation[key] !== void 0 && !isNonEmptyString(translation[key])) {
|
|
202
248
|
issue(issues, `${path}.${locale}.${key}`, "invalid_translation", "Expected non-empty translated text.");
|
|
203
249
|
}
|
|
@@ -248,6 +294,7 @@ function validateOptions(value, path, issues) {
|
|
|
248
294
|
return;
|
|
249
295
|
}
|
|
250
296
|
rejectLegacyProperties(option, optionPath, ["value", "labelKey"], issues);
|
|
297
|
+
validateExtensibleNode(option, optionPath, issues);
|
|
251
298
|
if (!isNonEmptyString(option.id)) {
|
|
252
299
|
issue(issues, `${optionPath}.id`, "invalid_option_id", "Expected a non-empty option ID.");
|
|
253
300
|
} else if (seen.has(option.id)) {
|
|
@@ -293,6 +340,7 @@ function validateField(value, path, issues) {
|
|
|
293
340
|
return false;
|
|
294
341
|
}
|
|
295
342
|
rejectLegacyProperties(value, path, ["titleKey", "labelKey", "helpTextKey", "descriptionKey"], issues);
|
|
343
|
+
validateExtensibleNode(value, path, issues);
|
|
296
344
|
if (!isNonEmptyString(value.id)) issue(issues, `${path}.id`, "invalid_id", "Expected a non-empty ID.");
|
|
297
345
|
if (!isNonEmptyString(value.title)) {
|
|
298
346
|
issue(issues, `${path}.title`, "invalid_title", "Expected a non-empty question title.");
|
|
@@ -378,6 +426,7 @@ function validateFormSchema(input) {
|
|
|
378
426
|
return { valid: false, issues: [{ path: "$", code: "invalid_schema", message: "Expected a schema object." }] };
|
|
379
427
|
}
|
|
380
428
|
rejectLegacyProperties(input, "", ["titleKey", "descriptionKey"], issues);
|
|
429
|
+
validateExtensibleNode(input, "", issues);
|
|
381
430
|
if (!isNonEmptyString(input.id)) issue(issues, "id", "invalid_id", "Expected a non-empty ID.");
|
|
382
431
|
if (!Number.isInteger(input.version) || input.version < 1) {
|
|
383
432
|
issue(issues, "version", "invalid_version", "Expected a positive integer version.");
|
|
@@ -385,13 +434,13 @@ function validateFormSchema(input) {
|
|
|
385
434
|
if (!isNonEmptyString(input.title)) {
|
|
386
435
|
issue(issues, "title", "invalid_title", "Expected a non-empty form title.");
|
|
387
436
|
}
|
|
388
|
-
for (const key of ["description", "submitLabelKey"]) {
|
|
437
|
+
for (const key of ["description", "completionMessage", "submitLabelKey"]) {
|
|
389
438
|
if (input[key] !== void 0 && !isNonEmptyString(input[key])) {
|
|
390
439
|
issue(
|
|
391
440
|
issues,
|
|
392
441
|
key,
|
|
393
|
-
key === "
|
|
394
|
-
key === "
|
|
442
|
+
key === "submitLabelKey" ? "invalid_translation_key" : "invalid_description",
|
|
443
|
+
key === "submitLabelKey" ? "Expected a translation key." : "Expected non-empty form text."
|
|
395
444
|
);
|
|
396
445
|
}
|
|
397
446
|
}
|
|
@@ -453,6 +502,7 @@ function validateFormSchema(input) {
|
|
|
453
502
|
issue(issues, pagePath, "invalid_page", "Expected a page object.");
|
|
454
503
|
return;
|
|
455
504
|
}
|
|
505
|
+
validateExtensibleNode(page, pagePath, issues);
|
|
456
506
|
if (!isNonEmptyString(page.id)) {
|
|
457
507
|
issue(issues, `${pagePath}.id`, "invalid_page_id", "Expected a non-empty page ID.");
|
|
458
508
|
} else if (pageIds.has(page.id)) {
|
|
@@ -744,15 +794,20 @@ function aggregateResponses(schema, submissions) {
|
|
|
744
794
|
questions: schema.fields.map((field) => aggregateField(schema, field, submissions))
|
|
745
795
|
};
|
|
746
796
|
}
|
|
747
|
-
function escapeCsvCell(value) {
|
|
797
|
+
function escapeCsvCell(value, neutralizeFormulas = true) {
|
|
748
798
|
if (value === null || value === void 0) return "";
|
|
749
|
-
|
|
799
|
+
let stringValue = String(value);
|
|
800
|
+
if (neutralizeFormulas && typeof value === "string") {
|
|
801
|
+
const trimmed = stringValue.trimStart();
|
|
802
|
+
if (trimmed.length > 0 && ["=", "+", "-", "@"].includes(trimmed[0] ?? "")) {
|
|
803
|
+
stringValue = `'${stringValue}`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
750
806
|
return /[",\r\n]/.test(stringValue) ? `"${stringValue.replaceAll('"', '""')}"` : stringValue;
|
|
751
807
|
}
|
|
752
808
|
function serializeValue(value) {
|
|
753
809
|
if (value === void 0) return "";
|
|
754
|
-
|
|
755
|
-
return String(value);
|
|
810
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
|
|
756
811
|
}
|
|
757
812
|
function exportResponsesToCsv(schema, responses, options = {}) {
|
|
758
813
|
assertValidFormSchema(schema);
|
|
@@ -773,7 +828,8 @@ function exportResponsesToCsv(schema, responses, options = {}) {
|
|
|
773
828
|
];
|
|
774
829
|
})
|
|
775
830
|
];
|
|
776
|
-
const
|
|
831
|
+
const neutralizeFormulas = options.neutralizeFormulas ?? true;
|
|
832
|
+
const csv = rows.map((row) => row.map((cell) => escapeCsvCell(cell, neutralizeFormulas)).join(",")).join("\r\n");
|
|
777
833
|
return options.withBom ?? true ? `\uFEFF${csv}` : csv;
|
|
778
834
|
}
|
|
779
835
|
|
|
@@ -983,69 +1039,125 @@ function createSubmission(schema, values, options) {
|
|
|
983
1039
|
formVersion: schema.version,
|
|
984
1040
|
locale: options.locale,
|
|
985
1041
|
values: Object.freeze(cloneValues(visibleValues)),
|
|
986
|
-
submittedAt: options.submittedAt
|
|
1042
|
+
submittedAt: options.submittedAt,
|
|
1043
|
+
...options.metadata === void 0 ? {} : { metadata: Object.freeze({ ...options.metadata }) },
|
|
1044
|
+
...options.translationMetadata === void 0 ? {} : { translationMetadata: Object.freeze({ ...options.translationMetadata }) }
|
|
987
1045
|
});
|
|
988
1046
|
}
|
|
989
1047
|
|
|
990
1048
|
// src/translation.ts
|
|
991
|
-
function mergeLocalizedText(translations, locale,
|
|
992
|
-
return { ...translations, [locale]: { ...translations?.[locale], [
|
|
1049
|
+
function mergeLocalizedText(translations, locale, property, value) {
|
|
1050
|
+
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
993
1051
|
}
|
|
994
|
-
function
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1052
|
+
function withTranslationMetadata(node, locale, property, metadata) {
|
|
1053
|
+
if (metadata === void 0) return node;
|
|
1054
|
+
return {
|
|
1055
|
+
...node,
|
|
1056
|
+
translationMetadata: {
|
|
1057
|
+
...node.translationMetadata,
|
|
1058
|
+
[locale]: {
|
|
1059
|
+
...node.translationMetadata?.[locale],
|
|
1060
|
+
[property]: metadata
|
|
1061
|
+
}
|
|
1002
1062
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
function createSlot(kind, nodeId, property, locale, sourceText, existingText, metadata) {
|
|
1066
|
+
return {
|
|
1067
|
+
kind,
|
|
1068
|
+
nodeId,
|
|
1069
|
+
property,
|
|
1070
|
+
locale,
|
|
1071
|
+
sourceText,
|
|
1072
|
+
...existingText === void 0 ? {} : { existingText },
|
|
1073
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function translationSlots(schema, locale) {
|
|
1077
|
+
const descriptors = [];
|
|
1078
|
+
const addFormSlot = (property, sourceText) => {
|
|
1079
|
+
const slot = createSlot(
|
|
1080
|
+
"form",
|
|
1081
|
+
schema.id,
|
|
1082
|
+
property,
|
|
1083
|
+
locale,
|
|
1084
|
+
sourceText,
|
|
1085
|
+
schema.translations?.[locale]?.[property],
|
|
1086
|
+
schema.metadata
|
|
1087
|
+
);
|
|
1088
|
+
descriptors.push({
|
|
1089
|
+
slot,
|
|
1090
|
+
apply: (current, value, metadata) => withTranslationMetadata(
|
|
1091
|
+
{ ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
|
|
1092
|
+
locale,
|
|
1093
|
+
property,
|
|
1094
|
+
metadata
|
|
1095
|
+
)
|
|
1011
1096
|
});
|
|
1012
|
-
}
|
|
1097
|
+
};
|
|
1098
|
+
addFormSlot("title", schema.title);
|
|
1099
|
+
if (schema.description !== void 0) addFormSlot("description", schema.description);
|
|
1100
|
+
if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
|
|
1013
1101
|
schema.fields.forEach((field, fieldIndex) => {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
apply: (current, value,
|
|
1102
|
+
const addFieldSlot = (property, sourceText) => {
|
|
1103
|
+
const slot = createSlot(
|
|
1104
|
+
"field",
|
|
1105
|
+
field.id,
|
|
1106
|
+
property,
|
|
1107
|
+
locale,
|
|
1108
|
+
sourceText,
|
|
1109
|
+
field.translations?.[locale]?.[property],
|
|
1110
|
+
field.metadata
|
|
1111
|
+
);
|
|
1112
|
+
descriptors.push({
|
|
1113
|
+
slot,
|
|
1114
|
+
apply: (current, value, metadata) => ({
|
|
1027
1115
|
...current,
|
|
1028
1116
|
fields: current.fields.map(
|
|
1029
|
-
(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1117
|
+
(candidate, index) => index === fieldIndex ? withTranslationMetadata(
|
|
1118
|
+
{
|
|
1119
|
+
...candidate,
|
|
1120
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1121
|
+
},
|
|
1122
|
+
locale,
|
|
1123
|
+
property,
|
|
1124
|
+
metadata
|
|
1125
|
+
) : candidate
|
|
1033
1126
|
)
|
|
1034
1127
|
})
|
|
1035
1128
|
});
|
|
1036
|
-
}
|
|
1129
|
+
};
|
|
1130
|
+
addFieldSlot("title", field.title);
|
|
1131
|
+
if (field.description !== void 0) addFieldSlot("description", field.description);
|
|
1037
1132
|
if ("options" in field) {
|
|
1038
1133
|
field.options.forEach((option, optionIndex) => {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1134
|
+
const slot = createSlot(
|
|
1135
|
+
"option",
|
|
1136
|
+
option.id,
|
|
1137
|
+
"label",
|
|
1138
|
+
locale,
|
|
1139
|
+
option.label,
|
|
1140
|
+
option.translations?.[locale],
|
|
1141
|
+
option.metadata
|
|
1142
|
+
);
|
|
1143
|
+
descriptors.push({
|
|
1144
|
+
slot,
|
|
1145
|
+
apply: (current, value, metadata) => ({
|
|
1042
1146
|
...current,
|
|
1043
|
-
fields: current.fields.map((
|
|
1044
|
-
if (
|
|
1147
|
+
fields: current.fields.map((candidate, candidateIndex) => {
|
|
1148
|
+
if (candidateIndex !== fieldIndex || !("options" in candidate)) return candidate;
|
|
1045
1149
|
return {
|
|
1046
|
-
...
|
|
1047
|
-
options:
|
|
1048
|
-
(
|
|
1150
|
+
...candidate,
|
|
1151
|
+
options: candidate.options.map(
|
|
1152
|
+
(candidateOption, candidateOptionIndex) => candidateOptionIndex === optionIndex ? withTranslationMetadata(
|
|
1153
|
+
{
|
|
1154
|
+
...candidateOption,
|
|
1155
|
+
translations: { ...candidateOption.translations, [locale]: value }
|
|
1156
|
+
},
|
|
1157
|
+
locale,
|
|
1158
|
+
"label",
|
|
1159
|
+
metadata
|
|
1160
|
+
) : candidateOption
|
|
1049
1161
|
)
|
|
1050
1162
|
};
|
|
1051
1163
|
})
|
|
@@ -1055,42 +1167,50 @@ function translationSlots(schema) {
|
|
|
1055
1167
|
}
|
|
1056
1168
|
});
|
|
1057
1169
|
schema.pages?.forEach((page, pageIndex) => {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (page.description !== void 0) {
|
|
1072
|
-
slots.push({
|
|
1073
|
-
text: page.description,
|
|
1074
|
-
apply: (current, value, locale) => ({
|
|
1170
|
+
const addPageSlot = (property, sourceText) => {
|
|
1171
|
+
const slot = createSlot(
|
|
1172
|
+
"page",
|
|
1173
|
+
page.id,
|
|
1174
|
+
property,
|
|
1175
|
+
locale,
|
|
1176
|
+
sourceText,
|
|
1177
|
+
page.translations?.[locale]?.[property],
|
|
1178
|
+
page.metadata
|
|
1179
|
+
);
|
|
1180
|
+
descriptors.push({
|
|
1181
|
+
slot,
|
|
1182
|
+
apply: (current, value, metadata) => ({
|
|
1075
1183
|
...current,
|
|
1076
1184
|
...current.pages === void 0 ? {} : {
|
|
1077
1185
|
pages: current.pages.map(
|
|
1078
|
-
(
|
|
1186
|
+
(candidate, index) => index === pageIndex ? withTranslationMetadata(
|
|
1187
|
+
{
|
|
1188
|
+
...candidate,
|
|
1189
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1190
|
+
},
|
|
1191
|
+
locale,
|
|
1192
|
+
property,
|
|
1193
|
+
metadata
|
|
1194
|
+
) : candidate
|
|
1079
1195
|
)
|
|
1080
1196
|
}
|
|
1081
1197
|
})
|
|
1082
1198
|
});
|
|
1083
|
-
}
|
|
1199
|
+
};
|
|
1200
|
+
if (page.title !== void 0) addPageSlot("title", page.title);
|
|
1201
|
+
if (page.description !== void 0) addPageSlot("description", page.description);
|
|
1084
1202
|
});
|
|
1085
|
-
return
|
|
1203
|
+
return descriptors;
|
|
1086
1204
|
}
|
|
1087
1205
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
1088
1206
|
if (targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
1089
1207
|
const formTranslation = schema.translations?.[targetLocale];
|
|
1208
|
+
const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
|
|
1090
1209
|
return {
|
|
1091
1210
|
...schema,
|
|
1092
1211
|
title: formTranslation?.title ?? schema.title,
|
|
1093
1212
|
...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
|
|
1213
|
+
...completionMessage === void 0 ? {} : { completionMessage },
|
|
1094
1214
|
fields: schema.fields.map((field) => {
|
|
1095
1215
|
const translation = field.translations?.[targetLocale];
|
|
1096
1216
|
const localized = {
|
|
@@ -1121,24 +1241,35 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1121
1241
|
}
|
|
1122
1242
|
};
|
|
1123
1243
|
}
|
|
1124
|
-
async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
1244
|
+
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1125
1245
|
assertValidFormSchema(schema);
|
|
1126
|
-
const slots = translationSlots(schema);
|
|
1127
1246
|
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1247
|
+
const updatedSlots = [];
|
|
1248
|
+
const skippedSlots = [];
|
|
1128
1249
|
let result = schema;
|
|
1129
1250
|
for (const locale of locales) {
|
|
1251
|
+
const descriptors = translationSlots(schema, locale);
|
|
1252
|
+
const selected = [];
|
|
1253
|
+
for (const descriptor of descriptors) {
|
|
1254
|
+
const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? (options.overwrite === "all" || descriptor.slot.existingText === void 0);
|
|
1255
|
+
if (shouldTranslate) selected.push(descriptor);
|
|
1256
|
+
else skippedSlots.push(descriptor.slot);
|
|
1257
|
+
}
|
|
1258
|
+
if (selected.length === 0) continue;
|
|
1130
1259
|
const translated = await adapter.translateBatch(
|
|
1131
|
-
|
|
1260
|
+
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
1132
1261
|
locale,
|
|
1133
1262
|
schema.defaultLocale
|
|
1134
1263
|
);
|
|
1135
|
-
if (translated.length !==
|
|
1136
|
-
throw new Error(`Translation adapter returned ${translated.length} texts for ${
|
|
1264
|
+
if (translated.length !== selected.length) {
|
|
1265
|
+
throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
|
|
1137
1266
|
}
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1267
|
+
selected.forEach((descriptor, index) => {
|
|
1268
|
+
const translatedText = translated[index];
|
|
1269
|
+
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
1270
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText);
|
|
1271
|
+
result = descriptor.apply(result, translatedText, metadata);
|
|
1272
|
+
updatedSlots.push(descriptor.slot);
|
|
1142
1273
|
});
|
|
1143
1274
|
}
|
|
1144
1275
|
const supportedLocales = [
|
|
@@ -1148,17 +1279,18 @@ async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
|
1148
1279
|
...locales
|
|
1149
1280
|
])
|
|
1150
1281
|
];
|
|
1151
|
-
|
|
1282
|
+
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
1152
1283
|
assertValidFormSchema(result);
|
|
1153
|
-
return result;
|
|
1284
|
+
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
1154
1285
|
}
|
|
1155
1286
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
1156
1287
|
const populated = await populateSchemaTranslations(
|
|
1157
1288
|
sourceLocale === void 0 ? schema : { ...schema, defaultLocale: sourceLocale },
|
|
1158
1289
|
[targetLocale],
|
|
1159
|
-
adapter
|
|
1290
|
+
adapter,
|
|
1291
|
+
{ overwrite: "all" }
|
|
1160
1292
|
);
|
|
1161
|
-
return resolveLocalizedSchema(populated, targetLocale);
|
|
1293
|
+
return resolveLocalizedSchema(populated.schema, targetLocale);
|
|
1162
1294
|
}
|
|
1163
1295
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1164
1296
|
0 && (module.exports = {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
2
2
|
type ConditionOperator = "equals" | "not_equals" | "contains" | "not_empty";
|
|
3
3
|
type ConditionValue = string | number | boolean;
|
|
4
|
+
type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
|
|
5
|
+
readonly [key: string]: JsonValue;
|
|
6
|
+
};
|
|
7
|
+
/** Arbitrary, JSON-serializable data preserved by every form-engine operation. */
|
|
8
|
+
interface ExtensibleNode {
|
|
9
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
10
|
+
/** Locale -> translated property -> metadata created for that translation. */
|
|
11
|
+
readonly translationMetadata?: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, JsonValue>>>>>>;
|
|
12
|
+
}
|
|
4
13
|
interface DisplayCondition {
|
|
5
14
|
readonly questionId: string;
|
|
6
15
|
readonly operator: ConditionOperator;
|
|
@@ -9,15 +18,16 @@ interface DisplayCondition {
|
|
|
9
18
|
interface LocalizedText {
|
|
10
19
|
readonly title?: string;
|
|
11
20
|
readonly description?: string;
|
|
21
|
+
readonly completionMessage?: string;
|
|
12
22
|
}
|
|
13
23
|
type SchemaTranslations = Readonly<Record<string, LocalizedText>>;
|
|
14
24
|
type ValidationCode = "required" | "invalid_type" | "min_length" | "max_length" | "pattern" | "min" | "max" | "step" | "invalid_option" | "min_selections" | "max_selections" | "unknown_field";
|
|
15
|
-
interface FieldOption {
|
|
25
|
+
interface FieldOption extends ExtensibleNode {
|
|
16
26
|
readonly id: string;
|
|
17
27
|
readonly label: string;
|
|
18
28
|
readonly translations?: Readonly<Record<string, string>>;
|
|
19
29
|
}
|
|
20
|
-
interface BaseField {
|
|
30
|
+
interface BaseField extends ExtensibleNode {
|
|
21
31
|
readonly id: string;
|
|
22
32
|
readonly type: FieldType;
|
|
23
33
|
readonly title: string;
|
|
@@ -61,7 +71,7 @@ interface CheckboxField extends BaseField {
|
|
|
61
71
|
readonly type: "checkbox";
|
|
62
72
|
}
|
|
63
73
|
type FormField = TextField | NumberField | RatingField | SelectField | MultiSelectField | CheckboxField;
|
|
64
|
-
interface FormPage {
|
|
74
|
+
interface FormPage extends ExtensibleNode {
|
|
65
75
|
readonly id: string;
|
|
66
76
|
readonly title?: string;
|
|
67
77
|
readonly description?: string;
|
|
@@ -69,11 +79,12 @@ interface FormPage {
|
|
|
69
79
|
readonly displayCondition?: DisplayCondition;
|
|
70
80
|
readonly translations?: SchemaTranslations;
|
|
71
81
|
}
|
|
72
|
-
interface FormSchema {
|
|
82
|
+
interface FormSchema extends ExtensibleNode {
|
|
73
83
|
readonly id: string;
|
|
74
84
|
readonly version: number;
|
|
75
85
|
readonly title: string;
|
|
76
86
|
readonly description?: string;
|
|
87
|
+
readonly completionMessage?: string;
|
|
77
88
|
readonly submitLabelKey?: string;
|
|
78
89
|
readonly defaultLocale?: string;
|
|
79
90
|
readonly supportedLocales?: readonly string[];
|
|
@@ -102,6 +113,7 @@ interface ValidationIssue {
|
|
|
102
113
|
readonly messageKey: string;
|
|
103
114
|
readonly params: Readonly<Record<string, string | number>>;
|
|
104
115
|
}
|
|
116
|
+
type ValidationError = ValidationIssue;
|
|
105
117
|
type AnswerValidationResult = {
|
|
106
118
|
readonly valid: true;
|
|
107
119
|
readonly issues: readonly [];
|
|
@@ -109,7 +121,7 @@ type AnswerValidationResult = {
|
|
|
109
121
|
readonly valid: false;
|
|
110
122
|
readonly issues: readonly ValidationIssue[];
|
|
111
123
|
};
|
|
112
|
-
interface FormSubmission {
|
|
124
|
+
interface FormSubmission extends ExtensibleNode {
|
|
113
125
|
readonly id: string;
|
|
114
126
|
readonly formId: string;
|
|
115
127
|
readonly formVersion: number;
|
|
@@ -182,7 +194,13 @@ interface FormAnalytics {
|
|
|
182
194
|
type Question = FormField;
|
|
183
195
|
type QuestionType = FieldType;
|
|
184
196
|
type ChoiceOption = FieldOption;
|
|
185
|
-
|
|
197
|
+
interface FormResponse extends ExtensibleNode {
|
|
198
|
+
readonly responseId: string;
|
|
199
|
+
readonly formId: string;
|
|
200
|
+
readonly sourceLocale?: string;
|
|
201
|
+
readonly answers: Readonly<Record<string, unknown>>;
|
|
202
|
+
readonly submittedAt: string;
|
|
203
|
+
}
|
|
186
204
|
interface CrossTabulationResult {
|
|
187
205
|
readonly rowQuestionId: string;
|
|
188
206
|
readonly colQuestionId: string;
|
|
@@ -206,9 +224,10 @@ declare function calculateChoiceDistribution(responses: readonly FormSubmission[
|
|
|
206
224
|
declare function calculateNumericSummary(responses: readonly FormSubmission[], questionId: string): NumericSummary;
|
|
207
225
|
declare function calculateCrossTabulation(responses: readonly FormSubmission[], rowQuestionId: string, colQuestionId: string): CrossTabulationResult;
|
|
208
226
|
declare function aggregateResponses(schema: FormSchema, submissions: readonly FormSubmission[]): FormAnalytics;
|
|
209
|
-
declare function escapeCsvCell(value: string | number | null | undefined): string;
|
|
227
|
+
declare function escapeCsvCell(value: string | number | boolean | null | undefined, neutralizeFormulas?: boolean): string;
|
|
210
228
|
interface CsvExportOptions {
|
|
211
229
|
readonly withBom?: boolean;
|
|
230
|
+
readonly neutralizeFormulas?: boolean;
|
|
212
231
|
}
|
|
213
232
|
declare function exportResponsesToCsv(schema: FormSchema, responses: readonly FormSubmission[], options?: CsvExportOptions): string;
|
|
214
233
|
|
|
@@ -246,15 +265,36 @@ declare function sanitizeSchema(schema: FormSchema): FormSchema;
|
|
|
246
265
|
declare function validateFormSchema(input: unknown): SchemaValidationResult;
|
|
247
266
|
declare function assertValidFormSchema(input: unknown): asserts input is FormSchema;
|
|
248
267
|
|
|
249
|
-
interface CreateSubmissionOptions {
|
|
268
|
+
interface CreateSubmissionOptions extends ExtensibleNode {
|
|
250
269
|
readonly id: string;
|
|
251
270
|
readonly locale: string;
|
|
252
271
|
readonly submittedAt: string;
|
|
253
272
|
}
|
|
254
273
|
declare function createSubmission(schema: FormSchema, values: FormValues, options: CreateSubmissionOptions): FormSubmission;
|
|
255
274
|
|
|
275
|
+
interface TranslationSlot {
|
|
276
|
+
readonly kind: "form" | "page" | "field" | "option";
|
|
277
|
+
readonly nodeId: string;
|
|
278
|
+
readonly property: "title" | "description" | "label" | "completionMessage";
|
|
279
|
+
readonly locale: string;
|
|
280
|
+
readonly sourceText: string;
|
|
281
|
+
readonly existingText?: string;
|
|
282
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
283
|
+
}
|
|
284
|
+
interface PopulateTranslationOptions {
|
|
285
|
+
readonly overwrite?: "missing-only" | "all";
|
|
286
|
+
readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
|
|
287
|
+
readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
|
|
288
|
+
}
|
|
289
|
+
interface TranslationReport {
|
|
290
|
+
readonly updatedSlots: readonly TranslationSlot[];
|
|
291
|
+
readonly skippedSlots: readonly TranslationSlot[];
|
|
292
|
+
}
|
|
256
293
|
declare function resolveLocalizedSchema(schema: FormSchema, targetLocale: string): FormSchema;
|
|
257
|
-
declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter): Promise<
|
|
294
|
+
declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter, options?: PopulateTranslationOptions): Promise<{
|
|
295
|
+
readonly schema: FormSchema;
|
|
296
|
+
readonly report: TranslationReport;
|
|
297
|
+
}>;
|
|
258
298
|
declare function resolveFormTranslation(schema: FormSchema, adapter: AsyncTranslationAdapter, targetLocale: string, sourceLocale?: string): Promise<FormSchema>;
|
|
259
299
|
|
|
260
300
|
declare function validateAnswers(schema: FormSchema, values: FormValues): AnswerValidationResult;
|
|
@@ -266,4 +306,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
266
306
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
267
307
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
268
308
|
|
|
269
|
-
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type ValidationCode, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
309
|
+
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type JsonValue, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PopulateTranslationOptions, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidationCode, type ValidationError, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
2
2
|
type ConditionOperator = "equals" | "not_equals" | "contains" | "not_empty";
|
|
3
3
|
type ConditionValue = string | number | boolean;
|
|
4
|
+
type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
|
|
5
|
+
readonly [key: string]: JsonValue;
|
|
6
|
+
};
|
|
7
|
+
/** Arbitrary, JSON-serializable data preserved by every form-engine operation. */
|
|
8
|
+
interface ExtensibleNode {
|
|
9
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
10
|
+
/** Locale -> translated property -> metadata created for that translation. */
|
|
11
|
+
readonly translationMetadata?: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, JsonValue>>>>>>;
|
|
12
|
+
}
|
|
4
13
|
interface DisplayCondition {
|
|
5
14
|
readonly questionId: string;
|
|
6
15
|
readonly operator: ConditionOperator;
|
|
@@ -9,15 +18,16 @@ interface DisplayCondition {
|
|
|
9
18
|
interface LocalizedText {
|
|
10
19
|
readonly title?: string;
|
|
11
20
|
readonly description?: string;
|
|
21
|
+
readonly completionMessage?: string;
|
|
12
22
|
}
|
|
13
23
|
type SchemaTranslations = Readonly<Record<string, LocalizedText>>;
|
|
14
24
|
type ValidationCode = "required" | "invalid_type" | "min_length" | "max_length" | "pattern" | "min" | "max" | "step" | "invalid_option" | "min_selections" | "max_selections" | "unknown_field";
|
|
15
|
-
interface FieldOption {
|
|
25
|
+
interface FieldOption extends ExtensibleNode {
|
|
16
26
|
readonly id: string;
|
|
17
27
|
readonly label: string;
|
|
18
28
|
readonly translations?: Readonly<Record<string, string>>;
|
|
19
29
|
}
|
|
20
|
-
interface BaseField {
|
|
30
|
+
interface BaseField extends ExtensibleNode {
|
|
21
31
|
readonly id: string;
|
|
22
32
|
readonly type: FieldType;
|
|
23
33
|
readonly title: string;
|
|
@@ -61,7 +71,7 @@ interface CheckboxField extends BaseField {
|
|
|
61
71
|
readonly type: "checkbox";
|
|
62
72
|
}
|
|
63
73
|
type FormField = TextField | NumberField | RatingField | SelectField | MultiSelectField | CheckboxField;
|
|
64
|
-
interface FormPage {
|
|
74
|
+
interface FormPage extends ExtensibleNode {
|
|
65
75
|
readonly id: string;
|
|
66
76
|
readonly title?: string;
|
|
67
77
|
readonly description?: string;
|
|
@@ -69,11 +79,12 @@ interface FormPage {
|
|
|
69
79
|
readonly displayCondition?: DisplayCondition;
|
|
70
80
|
readonly translations?: SchemaTranslations;
|
|
71
81
|
}
|
|
72
|
-
interface FormSchema {
|
|
82
|
+
interface FormSchema extends ExtensibleNode {
|
|
73
83
|
readonly id: string;
|
|
74
84
|
readonly version: number;
|
|
75
85
|
readonly title: string;
|
|
76
86
|
readonly description?: string;
|
|
87
|
+
readonly completionMessage?: string;
|
|
77
88
|
readonly submitLabelKey?: string;
|
|
78
89
|
readonly defaultLocale?: string;
|
|
79
90
|
readonly supportedLocales?: readonly string[];
|
|
@@ -102,6 +113,7 @@ interface ValidationIssue {
|
|
|
102
113
|
readonly messageKey: string;
|
|
103
114
|
readonly params: Readonly<Record<string, string | number>>;
|
|
104
115
|
}
|
|
116
|
+
type ValidationError = ValidationIssue;
|
|
105
117
|
type AnswerValidationResult = {
|
|
106
118
|
readonly valid: true;
|
|
107
119
|
readonly issues: readonly [];
|
|
@@ -109,7 +121,7 @@ type AnswerValidationResult = {
|
|
|
109
121
|
readonly valid: false;
|
|
110
122
|
readonly issues: readonly ValidationIssue[];
|
|
111
123
|
};
|
|
112
|
-
interface FormSubmission {
|
|
124
|
+
interface FormSubmission extends ExtensibleNode {
|
|
113
125
|
readonly id: string;
|
|
114
126
|
readonly formId: string;
|
|
115
127
|
readonly formVersion: number;
|
|
@@ -182,7 +194,13 @@ interface FormAnalytics {
|
|
|
182
194
|
type Question = FormField;
|
|
183
195
|
type QuestionType = FieldType;
|
|
184
196
|
type ChoiceOption = FieldOption;
|
|
185
|
-
|
|
197
|
+
interface FormResponse extends ExtensibleNode {
|
|
198
|
+
readonly responseId: string;
|
|
199
|
+
readonly formId: string;
|
|
200
|
+
readonly sourceLocale?: string;
|
|
201
|
+
readonly answers: Readonly<Record<string, unknown>>;
|
|
202
|
+
readonly submittedAt: string;
|
|
203
|
+
}
|
|
186
204
|
interface CrossTabulationResult {
|
|
187
205
|
readonly rowQuestionId: string;
|
|
188
206
|
readonly colQuestionId: string;
|
|
@@ -206,9 +224,10 @@ declare function calculateChoiceDistribution(responses: readonly FormSubmission[
|
|
|
206
224
|
declare function calculateNumericSummary(responses: readonly FormSubmission[], questionId: string): NumericSummary;
|
|
207
225
|
declare function calculateCrossTabulation(responses: readonly FormSubmission[], rowQuestionId: string, colQuestionId: string): CrossTabulationResult;
|
|
208
226
|
declare function aggregateResponses(schema: FormSchema, submissions: readonly FormSubmission[]): FormAnalytics;
|
|
209
|
-
declare function escapeCsvCell(value: string | number | null | undefined): string;
|
|
227
|
+
declare function escapeCsvCell(value: string | number | boolean | null | undefined, neutralizeFormulas?: boolean): string;
|
|
210
228
|
interface CsvExportOptions {
|
|
211
229
|
readonly withBom?: boolean;
|
|
230
|
+
readonly neutralizeFormulas?: boolean;
|
|
212
231
|
}
|
|
213
232
|
declare function exportResponsesToCsv(schema: FormSchema, responses: readonly FormSubmission[], options?: CsvExportOptions): string;
|
|
214
233
|
|
|
@@ -246,15 +265,36 @@ declare function sanitizeSchema(schema: FormSchema): FormSchema;
|
|
|
246
265
|
declare function validateFormSchema(input: unknown): SchemaValidationResult;
|
|
247
266
|
declare function assertValidFormSchema(input: unknown): asserts input is FormSchema;
|
|
248
267
|
|
|
249
|
-
interface CreateSubmissionOptions {
|
|
268
|
+
interface CreateSubmissionOptions extends ExtensibleNode {
|
|
250
269
|
readonly id: string;
|
|
251
270
|
readonly locale: string;
|
|
252
271
|
readonly submittedAt: string;
|
|
253
272
|
}
|
|
254
273
|
declare function createSubmission(schema: FormSchema, values: FormValues, options: CreateSubmissionOptions): FormSubmission;
|
|
255
274
|
|
|
275
|
+
interface TranslationSlot {
|
|
276
|
+
readonly kind: "form" | "page" | "field" | "option";
|
|
277
|
+
readonly nodeId: string;
|
|
278
|
+
readonly property: "title" | "description" | "label" | "completionMessage";
|
|
279
|
+
readonly locale: string;
|
|
280
|
+
readonly sourceText: string;
|
|
281
|
+
readonly existingText?: string;
|
|
282
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
283
|
+
}
|
|
284
|
+
interface PopulateTranslationOptions {
|
|
285
|
+
readonly overwrite?: "missing-only" | "all";
|
|
286
|
+
readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
|
|
287
|
+
readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
|
|
288
|
+
}
|
|
289
|
+
interface TranslationReport {
|
|
290
|
+
readonly updatedSlots: readonly TranslationSlot[];
|
|
291
|
+
readonly skippedSlots: readonly TranslationSlot[];
|
|
292
|
+
}
|
|
256
293
|
declare function resolveLocalizedSchema(schema: FormSchema, targetLocale: string): FormSchema;
|
|
257
|
-
declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter): Promise<
|
|
294
|
+
declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter, options?: PopulateTranslationOptions): Promise<{
|
|
295
|
+
readonly schema: FormSchema;
|
|
296
|
+
readonly report: TranslationReport;
|
|
297
|
+
}>;
|
|
258
298
|
declare function resolveFormTranslation(schema: FormSchema, adapter: AsyncTranslationAdapter, targetLocale: string, sourceLocale?: string): Promise<FormSchema>;
|
|
259
299
|
|
|
260
300
|
declare function validateAnswers(schema: FormSchema, values: FormValues): AnswerValidationResult;
|
|
@@ -266,4 +306,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
266
306
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
267
307
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
268
308
|
|
|
269
|
-
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type ValidationCode, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
309
|
+
export { type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvExportOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type JsonValue, type LocalizedText, type MultiSelectField, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PopulateTranslationOptions, type Question, type QuestionAggregate, type QuestionType, type RatingField, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidationCode, type ValidationError, type ValidationIssue, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, createSubmission, dispatchWebhook, escapeCsvCell, exportResponsesToCsv, isDisplayConditionSatisfied, isQuestionVisible, populateSchemaTranslations, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
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.");
|
|
@@ -331,6 +379,7 @@ function validateFormSchema(input) {
|
|
|
331
379
|
return { valid: false, issues: [{ path: "$", code: "invalid_schema", message: "Expected a schema object." }] };
|
|
332
380
|
}
|
|
333
381
|
rejectLegacyProperties(input, "", ["titleKey", "descriptionKey"], issues);
|
|
382
|
+
validateExtensibleNode(input, "", issues);
|
|
334
383
|
if (!isNonEmptyString(input.id)) issue(issues, "id", "invalid_id", "Expected a non-empty ID.");
|
|
335
384
|
if (!Number.isInteger(input.version) || input.version < 1) {
|
|
336
385
|
issue(issues, "version", "invalid_version", "Expected a positive integer version.");
|
|
@@ -338,13 +387,13 @@ function validateFormSchema(input) {
|
|
|
338
387
|
if (!isNonEmptyString(input.title)) {
|
|
339
388
|
issue(issues, "title", "invalid_title", "Expected a non-empty form title.");
|
|
340
389
|
}
|
|
341
|
-
for (const key of ["description", "submitLabelKey"]) {
|
|
390
|
+
for (const key of ["description", "completionMessage", "submitLabelKey"]) {
|
|
342
391
|
if (input[key] !== void 0 && !isNonEmptyString(input[key])) {
|
|
343
392
|
issue(
|
|
344
393
|
issues,
|
|
345
394
|
key,
|
|
346
|
-
key === "
|
|
347
|
-
key === "
|
|
395
|
+
key === "submitLabelKey" ? "invalid_translation_key" : "invalid_description",
|
|
396
|
+
key === "submitLabelKey" ? "Expected a translation key." : "Expected non-empty form text."
|
|
348
397
|
);
|
|
349
398
|
}
|
|
350
399
|
}
|
|
@@ -406,6 +455,7 @@ function validateFormSchema(input) {
|
|
|
406
455
|
issue(issues, pagePath, "invalid_page", "Expected a page object.");
|
|
407
456
|
return;
|
|
408
457
|
}
|
|
458
|
+
validateExtensibleNode(page, pagePath, issues);
|
|
409
459
|
if (!isNonEmptyString(page.id)) {
|
|
410
460
|
issue(issues, `${pagePath}.id`, "invalid_page_id", "Expected a non-empty page ID.");
|
|
411
461
|
} else if (pageIds.has(page.id)) {
|
|
@@ -697,15 +747,20 @@ function aggregateResponses(schema, submissions) {
|
|
|
697
747
|
questions: schema.fields.map((field) => aggregateField(schema, field, submissions))
|
|
698
748
|
};
|
|
699
749
|
}
|
|
700
|
-
function escapeCsvCell(value) {
|
|
750
|
+
function escapeCsvCell(value, neutralizeFormulas = true) {
|
|
701
751
|
if (value === null || value === void 0) return "";
|
|
702
|
-
|
|
752
|
+
let stringValue = String(value);
|
|
753
|
+
if (neutralizeFormulas && typeof value === "string") {
|
|
754
|
+
const trimmed = stringValue.trimStart();
|
|
755
|
+
if (trimmed.length > 0 && ["=", "+", "-", "@"].includes(trimmed[0] ?? "")) {
|
|
756
|
+
stringValue = `'${stringValue}`;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
703
759
|
return /[",\r\n]/.test(stringValue) ? `"${stringValue.replaceAll('"', '""')}"` : stringValue;
|
|
704
760
|
}
|
|
705
761
|
function serializeValue(value) {
|
|
706
762
|
if (value === void 0) return "";
|
|
707
|
-
|
|
708
|
-
return String(value);
|
|
763
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
|
|
709
764
|
}
|
|
710
765
|
function exportResponsesToCsv(schema, responses, options = {}) {
|
|
711
766
|
assertValidFormSchema(schema);
|
|
@@ -726,7 +781,8 @@ function exportResponsesToCsv(schema, responses, options = {}) {
|
|
|
726
781
|
];
|
|
727
782
|
})
|
|
728
783
|
];
|
|
729
|
-
const
|
|
784
|
+
const neutralizeFormulas = options.neutralizeFormulas ?? true;
|
|
785
|
+
const csv = rows.map((row) => row.map((cell) => escapeCsvCell(cell, neutralizeFormulas)).join(",")).join("\r\n");
|
|
730
786
|
return options.withBom ?? true ? `\uFEFF${csv}` : csv;
|
|
731
787
|
}
|
|
732
788
|
|
|
@@ -936,69 +992,125 @@ function createSubmission(schema, values, options) {
|
|
|
936
992
|
formVersion: schema.version,
|
|
937
993
|
locale: options.locale,
|
|
938
994
|
values: Object.freeze(cloneValues(visibleValues)),
|
|
939
|
-
submittedAt: options.submittedAt
|
|
995
|
+
submittedAt: options.submittedAt,
|
|
996
|
+
...options.metadata === void 0 ? {} : { metadata: Object.freeze({ ...options.metadata }) },
|
|
997
|
+
...options.translationMetadata === void 0 ? {} : { translationMetadata: Object.freeze({ ...options.translationMetadata }) }
|
|
940
998
|
});
|
|
941
999
|
}
|
|
942
1000
|
|
|
943
1001
|
// src/translation.ts
|
|
944
|
-
function mergeLocalizedText(translations, locale,
|
|
945
|
-
return { ...translations, [locale]: { ...translations?.[locale], [
|
|
1002
|
+
function mergeLocalizedText(translations, locale, property, value) {
|
|
1003
|
+
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
946
1004
|
}
|
|
947
|
-
function
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1005
|
+
function withTranslationMetadata(node, locale, property, metadata) {
|
|
1006
|
+
if (metadata === void 0) return node;
|
|
1007
|
+
return {
|
|
1008
|
+
...node,
|
|
1009
|
+
translationMetadata: {
|
|
1010
|
+
...node.translationMetadata,
|
|
1011
|
+
[locale]: {
|
|
1012
|
+
...node.translationMetadata?.[locale],
|
|
1013
|
+
[property]: metadata
|
|
1014
|
+
}
|
|
955
1015
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function createSlot(kind, nodeId, property, locale, sourceText, existingText, metadata) {
|
|
1019
|
+
return {
|
|
1020
|
+
kind,
|
|
1021
|
+
nodeId,
|
|
1022
|
+
property,
|
|
1023
|
+
locale,
|
|
1024
|
+
sourceText,
|
|
1025
|
+
...existingText === void 0 ? {} : { existingText },
|
|
1026
|
+
...metadata === void 0 ? {} : { metadata }
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function translationSlots(schema, locale) {
|
|
1030
|
+
const descriptors = [];
|
|
1031
|
+
const addFormSlot = (property, sourceText) => {
|
|
1032
|
+
const slot = createSlot(
|
|
1033
|
+
"form",
|
|
1034
|
+
schema.id,
|
|
1035
|
+
property,
|
|
1036
|
+
locale,
|
|
1037
|
+
sourceText,
|
|
1038
|
+
schema.translations?.[locale]?.[property],
|
|
1039
|
+
schema.metadata
|
|
1040
|
+
);
|
|
1041
|
+
descriptors.push({
|
|
1042
|
+
slot,
|
|
1043
|
+
apply: (current, value, metadata) => withTranslationMetadata(
|
|
1044
|
+
{ ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
|
|
1045
|
+
locale,
|
|
1046
|
+
property,
|
|
1047
|
+
metadata
|
|
1048
|
+
)
|
|
964
1049
|
});
|
|
965
|
-
}
|
|
1050
|
+
};
|
|
1051
|
+
addFormSlot("title", schema.title);
|
|
1052
|
+
if (schema.description !== void 0) addFormSlot("description", schema.description);
|
|
1053
|
+
if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
|
|
966
1054
|
schema.fields.forEach((field, fieldIndex) => {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
apply: (current, value,
|
|
1055
|
+
const addFieldSlot = (property, sourceText) => {
|
|
1056
|
+
const slot = createSlot(
|
|
1057
|
+
"field",
|
|
1058
|
+
field.id,
|
|
1059
|
+
property,
|
|
1060
|
+
locale,
|
|
1061
|
+
sourceText,
|
|
1062
|
+
field.translations?.[locale]?.[property],
|
|
1063
|
+
field.metadata
|
|
1064
|
+
);
|
|
1065
|
+
descriptors.push({
|
|
1066
|
+
slot,
|
|
1067
|
+
apply: (current, value, metadata) => ({
|
|
980
1068
|
...current,
|
|
981
1069
|
fields: current.fields.map(
|
|
982
|
-
(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1070
|
+
(candidate, index) => index === fieldIndex ? withTranslationMetadata(
|
|
1071
|
+
{
|
|
1072
|
+
...candidate,
|
|
1073
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1074
|
+
},
|
|
1075
|
+
locale,
|
|
1076
|
+
property,
|
|
1077
|
+
metadata
|
|
1078
|
+
) : candidate
|
|
986
1079
|
)
|
|
987
1080
|
})
|
|
988
1081
|
});
|
|
989
|
-
}
|
|
1082
|
+
};
|
|
1083
|
+
addFieldSlot("title", field.title);
|
|
1084
|
+
if (field.description !== void 0) addFieldSlot("description", field.description);
|
|
990
1085
|
if ("options" in field) {
|
|
991
1086
|
field.options.forEach((option, optionIndex) => {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1087
|
+
const slot = createSlot(
|
|
1088
|
+
"option",
|
|
1089
|
+
option.id,
|
|
1090
|
+
"label",
|
|
1091
|
+
locale,
|
|
1092
|
+
option.label,
|
|
1093
|
+
option.translations?.[locale],
|
|
1094
|
+
option.metadata
|
|
1095
|
+
);
|
|
1096
|
+
descriptors.push({
|
|
1097
|
+
slot,
|
|
1098
|
+
apply: (current, value, metadata) => ({
|
|
995
1099
|
...current,
|
|
996
|
-
fields: current.fields.map((
|
|
997
|
-
if (
|
|
1100
|
+
fields: current.fields.map((candidate, candidateIndex) => {
|
|
1101
|
+
if (candidateIndex !== fieldIndex || !("options" in candidate)) return candidate;
|
|
998
1102
|
return {
|
|
999
|
-
...
|
|
1000
|
-
options:
|
|
1001
|
-
(
|
|
1103
|
+
...candidate,
|
|
1104
|
+
options: candidate.options.map(
|
|
1105
|
+
(candidateOption, candidateOptionIndex) => candidateOptionIndex === optionIndex ? withTranslationMetadata(
|
|
1106
|
+
{
|
|
1107
|
+
...candidateOption,
|
|
1108
|
+
translations: { ...candidateOption.translations, [locale]: value }
|
|
1109
|
+
},
|
|
1110
|
+
locale,
|
|
1111
|
+
"label",
|
|
1112
|
+
metadata
|
|
1113
|
+
) : candidateOption
|
|
1002
1114
|
)
|
|
1003
1115
|
};
|
|
1004
1116
|
})
|
|
@@ -1008,42 +1120,50 @@ function translationSlots(schema) {
|
|
|
1008
1120
|
}
|
|
1009
1121
|
});
|
|
1010
1122
|
schema.pages?.forEach((page, pageIndex) => {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (page.description !== void 0) {
|
|
1025
|
-
slots.push({
|
|
1026
|
-
text: page.description,
|
|
1027
|
-
apply: (current, value, locale) => ({
|
|
1123
|
+
const addPageSlot = (property, sourceText) => {
|
|
1124
|
+
const slot = createSlot(
|
|
1125
|
+
"page",
|
|
1126
|
+
page.id,
|
|
1127
|
+
property,
|
|
1128
|
+
locale,
|
|
1129
|
+
sourceText,
|
|
1130
|
+
page.translations?.[locale]?.[property],
|
|
1131
|
+
page.metadata
|
|
1132
|
+
);
|
|
1133
|
+
descriptors.push({
|
|
1134
|
+
slot,
|
|
1135
|
+
apply: (current, value, metadata) => ({
|
|
1028
1136
|
...current,
|
|
1029
1137
|
...current.pages === void 0 ? {} : {
|
|
1030
1138
|
pages: current.pages.map(
|
|
1031
|
-
(
|
|
1139
|
+
(candidate, index) => index === pageIndex ? withTranslationMetadata(
|
|
1140
|
+
{
|
|
1141
|
+
...candidate,
|
|
1142
|
+
translations: mergeLocalizedText(candidate.translations, locale, property, value)
|
|
1143
|
+
},
|
|
1144
|
+
locale,
|
|
1145
|
+
property,
|
|
1146
|
+
metadata
|
|
1147
|
+
) : candidate
|
|
1032
1148
|
)
|
|
1033
1149
|
}
|
|
1034
1150
|
})
|
|
1035
1151
|
});
|
|
1036
|
-
}
|
|
1152
|
+
};
|
|
1153
|
+
if (page.title !== void 0) addPageSlot("title", page.title);
|
|
1154
|
+
if (page.description !== void 0) addPageSlot("description", page.description);
|
|
1037
1155
|
});
|
|
1038
|
-
return
|
|
1156
|
+
return descriptors;
|
|
1039
1157
|
}
|
|
1040
1158
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
1041
1159
|
if (targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
1042
1160
|
const formTranslation = schema.translations?.[targetLocale];
|
|
1161
|
+
const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
|
|
1043
1162
|
return {
|
|
1044
1163
|
...schema,
|
|
1045
1164
|
title: formTranslation?.title ?? schema.title,
|
|
1046
1165
|
...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
|
|
1166
|
+
...completionMessage === void 0 ? {} : { completionMessage },
|
|
1047
1167
|
fields: schema.fields.map((field) => {
|
|
1048
1168
|
const translation = field.translations?.[targetLocale];
|
|
1049
1169
|
const localized = {
|
|
@@ -1074,24 +1194,35 @@ function resolveLocalizedSchema(schema, targetLocale) {
|
|
|
1074
1194
|
}
|
|
1075
1195
|
};
|
|
1076
1196
|
}
|
|
1077
|
-
async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
1197
|
+
async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
|
|
1078
1198
|
assertValidFormSchema(schema);
|
|
1079
|
-
const slots = translationSlots(schema);
|
|
1080
1199
|
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1200
|
+
const updatedSlots = [];
|
|
1201
|
+
const skippedSlots = [];
|
|
1081
1202
|
let result = schema;
|
|
1082
1203
|
for (const locale of locales) {
|
|
1204
|
+
const descriptors = translationSlots(schema, locale);
|
|
1205
|
+
const selected = [];
|
|
1206
|
+
for (const descriptor of descriptors) {
|
|
1207
|
+
const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? (options.overwrite === "all" || descriptor.slot.existingText === void 0);
|
|
1208
|
+
if (shouldTranslate) selected.push(descriptor);
|
|
1209
|
+
else skippedSlots.push(descriptor.slot);
|
|
1210
|
+
}
|
|
1211
|
+
if (selected.length === 0) continue;
|
|
1083
1212
|
const translated = await adapter.translateBatch(
|
|
1084
|
-
|
|
1213
|
+
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
1085
1214
|
locale,
|
|
1086
1215
|
schema.defaultLocale
|
|
1087
1216
|
);
|
|
1088
|
-
if (translated.length !==
|
|
1089
|
-
throw new Error(`Translation adapter returned ${translated.length} texts for ${
|
|
1217
|
+
if (translated.length !== selected.length) {
|
|
1218
|
+
throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
|
|
1090
1219
|
}
|
|
1091
|
-
|
|
1092
|
-
const
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1220
|
+
selected.forEach((descriptor, index) => {
|
|
1221
|
+
const translatedText = translated[index];
|
|
1222
|
+
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
1223
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText);
|
|
1224
|
+
result = descriptor.apply(result, translatedText, metadata);
|
|
1225
|
+
updatedSlots.push(descriptor.slot);
|
|
1095
1226
|
});
|
|
1096
1227
|
}
|
|
1097
1228
|
const supportedLocales = [
|
|
@@ -1101,17 +1232,18 @@ async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
|
1101
1232
|
...locales
|
|
1102
1233
|
])
|
|
1103
1234
|
];
|
|
1104
|
-
|
|
1235
|
+
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
1105
1236
|
assertValidFormSchema(result);
|
|
1106
|
-
return result;
|
|
1237
|
+
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
1107
1238
|
}
|
|
1108
1239
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
1109
1240
|
const populated = await populateSchemaTranslations(
|
|
1110
1241
|
sourceLocale === void 0 ? schema : { ...schema, defaultLocale: sourceLocale },
|
|
1111
1242
|
[targetLocale],
|
|
1112
|
-
adapter
|
|
1243
|
+
adapter,
|
|
1244
|
+
{ overwrite: "all" }
|
|
1113
1245
|
);
|
|
1114
|
-
return resolveLocalizedSchema(populated, targetLocale);
|
|
1246
|
+
return resolveLocalizedSchema(populated.schema, targetLocale);
|
|
1115
1247
|
}
|
|
1116
1248
|
export {
|
|
1117
1249
|
aggregateResponses,
|