@form-engine-ts/core 4.4.0 → 4.6.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 +16 -0
- package/dist/index.cjs +442 -69
- package/dist/index.d.cts +80 -4
- package/dist/index.d.ts +80 -4
- package/dist/index.js +435 -69
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,6 +47,19 @@ function collectSchemaLocales(schema) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// src/sanitization.ts
|
|
50
|
+
function displayRuleSourceIds(field) {
|
|
51
|
+
if (field.displayRule === void 0)
|
|
52
|
+
return field.displayCondition?.questionId === void 0 ? [] : [field.displayCondition.questionId];
|
|
53
|
+
const ids = [];
|
|
54
|
+
const visit = (group) => {
|
|
55
|
+
for (const condition of group.conditions) {
|
|
56
|
+
if ("logic" in condition) visit(condition);
|
|
57
|
+
else ids.push(condition.fieldId);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
visit(field.displayRule.condition);
|
|
61
|
+
return ids;
|
|
62
|
+
}
|
|
50
63
|
function registeredEntries(value, registeredLocales) {
|
|
51
64
|
if (value === void 0) return void 0;
|
|
52
65
|
const entries = Object.entries(value).filter(([locale]) => registeredLocales.has(locale));
|
|
@@ -114,26 +127,28 @@ function cyclicQuestionIds(fields) {
|
|
|
114
127
|
if (!firstById.has(field.id)) firstById.set(field.id, field);
|
|
115
128
|
}
|
|
116
129
|
const cyclic = /* @__PURE__ */ new Set();
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (existingIndex !== void 0) {
|
|
126
|
-
for (const id of path.slice(existingIndex)) cyclic.add(id);
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
pathIndex.set(currentId, path.length);
|
|
130
|
-
path.push(currentId);
|
|
131
|
-
const field = firstById.get(currentId);
|
|
132
|
-
const sourceId = field?.displayCondition?.questionId;
|
|
133
|
-
currentId = sourceId === currentId ? void 0 : sourceId;
|
|
130
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
131
|
+
const visited = /* @__PURE__ */ new Set();
|
|
132
|
+
const path = [];
|
|
133
|
+
const visit = (id) => {
|
|
134
|
+
if (visiting.has(id)) {
|
|
135
|
+
const start = path.indexOf(id);
|
|
136
|
+
for (const member of path.slice(start < 0 ? 0 : start)) cyclic.add(member);
|
|
137
|
+
return;
|
|
134
138
|
}
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
if (visited.has(id)) return;
|
|
140
|
+
visiting.add(id);
|
|
141
|
+
path.push(id);
|
|
142
|
+
const field = firstById.get(id);
|
|
143
|
+
if (field === void 0) return;
|
|
144
|
+
for (const sourceId of displayRuleSourceIds(field)) {
|
|
145
|
+
if (sourceId !== id && firstById.has(sourceId)) visit(sourceId);
|
|
146
|
+
}
|
|
147
|
+
path.pop();
|
|
148
|
+
visiting.delete(id);
|
|
149
|
+
visited.add(id);
|
|
150
|
+
};
|
|
151
|
+
for (const id of firstById.keys()) visit(id);
|
|
137
152
|
return cyclic;
|
|
138
153
|
}
|
|
139
154
|
function validateSchemaStructure(schema) {
|
|
@@ -165,20 +180,20 @@ function validateSchemaStructure(schema) {
|
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
182
|
for (const field of schema.fields) {
|
|
168
|
-
const sourceId
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
183
|
+
for (const sourceId of displayRuleSourceIds(field)) {
|
|
184
|
+
if (sourceId === field.id) {
|
|
185
|
+
issues.push({
|
|
186
|
+
type: "self_condition_reference",
|
|
187
|
+
questionId: field.id,
|
|
188
|
+
message: `Question "${field.id}" cannot depend on itself.`
|
|
189
|
+
});
|
|
190
|
+
} else if (!questionIds.has(sourceId)) {
|
|
191
|
+
issues.push({
|
|
192
|
+
type: "dangling_condition_reference",
|
|
193
|
+
questionId: field.id,
|
|
194
|
+
message: `Question "${field.id}" references missing question "${sourceId}".`
|
|
195
|
+
});
|
|
196
|
+
}
|
|
182
197
|
}
|
|
183
198
|
}
|
|
184
199
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
@@ -187,6 +202,11 @@ function validateSchemaStructure(schema) {
|
|
|
187
202
|
issues.push({
|
|
188
203
|
type: "cyclic_condition_reference",
|
|
189
204
|
questionId: field.id,
|
|
205
|
+
cycle: [
|
|
206
|
+
field.id,
|
|
207
|
+
...displayRuleSourceIds(field).filter((sourceId) => cyclic.has(sourceId)).slice(0, 1),
|
|
208
|
+
field.id
|
|
209
|
+
],
|
|
190
210
|
message: `Question "${field.id}" participates in a display-condition cycle.`
|
|
191
211
|
});
|
|
192
212
|
}
|
|
@@ -201,11 +221,17 @@ function sanitizeSchema(schema, options = {}) {
|
|
|
201
221
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
202
222
|
const sanitizedFields = schema.fields.map((sourceField) => {
|
|
203
223
|
const field = sanitizeFieldConstraints(sanitizeFieldLocales(sourceField, registeredLocales), options.policy);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
let sanitized = field;
|
|
225
|
+
const ruleSources = displayRuleSourceIds(field);
|
|
226
|
+
if (field.displayRule !== void 0 && (cyclic.has(field.id) || ruleSources.some((sourceId2) => sourceId2 === field.id || !existingQuestionIds.has(sourceId2)))) {
|
|
227
|
+
const { displayRule: _displayRule, ...withoutRule } = sanitized;
|
|
228
|
+
sanitized = withoutRule;
|
|
229
|
+
}
|
|
230
|
+
const sourceId = sanitized.displayCondition?.questionId;
|
|
231
|
+
if (sourceId !== void 0 && (!existingQuestionIds.has(sourceId) || sourceId === sanitized.id || cyclic.has(sanitized.id))) {
|
|
232
|
+
const { displayCondition: _displayCondition, ...withoutCondition } = sanitized;
|
|
233
|
+
return withoutCondition;
|
|
207
234
|
}
|
|
208
|
-
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
209
235
|
return sanitized;
|
|
210
236
|
});
|
|
211
237
|
const localizedSchema = sanitizeNodeLocales(schema, registeredLocales);
|
|
@@ -248,7 +274,17 @@ function sanitizeSchema(schema, options = {}) {
|
|
|
248
274
|
|
|
249
275
|
// src/schema.ts
|
|
250
276
|
var FIELD_TYPES = /* @__PURE__ */ new Set(["text", "textarea", "number", "rating", "select", "multi-select", "checkbox", "radio"]);
|
|
251
|
-
var CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
277
|
+
var CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
278
|
+
"equals",
|
|
279
|
+
"not_equals",
|
|
280
|
+
"contains",
|
|
281
|
+
"not_contains",
|
|
282
|
+
"is_empty",
|
|
283
|
+
"is_not_empty",
|
|
284
|
+
"greater_than",
|
|
285
|
+
"less_than",
|
|
286
|
+
"not_empty"
|
|
287
|
+
]);
|
|
252
288
|
function isRecord(value) {
|
|
253
289
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
290
|
}
|
|
@@ -405,7 +441,7 @@ function validateDisplayCondition(value, path, issues) {
|
|
|
405
441
|
return false;
|
|
406
442
|
}
|
|
407
443
|
const hasValue = Object.hasOwn(value, "value") && value.value !== void 0;
|
|
408
|
-
if (value.operator === "not_empty") {
|
|
444
|
+
if (value.operator === "not_empty" || value.operator === "is_empty" || value.operator === "is_not_empty") {
|
|
409
445
|
if (hasValue) issue(issues, `${path}.value`, "unexpected_condition_value", "not_empty does not accept a value.");
|
|
410
446
|
} else if (!hasValue) {
|
|
411
447
|
issue(issues, `${path}.value`, "missing_condition_value", `${value.operator} requires a value.`);
|
|
@@ -416,6 +452,75 @@ function validateDisplayCondition(value, path, issues) {
|
|
|
416
452
|
}
|
|
417
453
|
return true;
|
|
418
454
|
}
|
|
455
|
+
function validateDisplayConditionGroup(value, path, issues) {
|
|
456
|
+
if (!isRecord(value)) {
|
|
457
|
+
issue(issues, path, "invalid_condition_group", "Expected a display condition group.");
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (value.logic !== "all" && value.logic !== "any") {
|
|
461
|
+
issue(issues, `${path}.logic`, "invalid_condition_logic", "Expected all or any.");
|
|
462
|
+
}
|
|
463
|
+
if (!Array.isArray(value.conditions)) {
|
|
464
|
+
issue(issues, `${path}.conditions`, "invalid_condition_group", "Expected a conditions array.");
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
value.conditions.forEach((condition, index) => {
|
|
468
|
+
const conditionPath = `${path}.conditions[${index}]`;
|
|
469
|
+
if (isRecord(condition) && Object.hasOwn(condition, "logic")) {
|
|
470
|
+
validateDisplayConditionGroup(condition, conditionPath, issues);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!isRecord(condition)) {
|
|
474
|
+
issue(issues, conditionPath, "invalid_condition", "Expected a field display condition.");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!isNonEmptyString(condition.fieldId)) {
|
|
478
|
+
issue(issues, `${conditionPath}.fieldId`, "invalid_condition_source", "Expected a field ID.");
|
|
479
|
+
}
|
|
480
|
+
if (typeof condition.operator !== "string" || !CONDITION_OPERATORS.has(condition.operator)) {
|
|
481
|
+
issue(issues, `${conditionPath}.operator`, "invalid_condition_operator", "Unsupported condition operator.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const needsValue = !["not_empty", "is_empty", "is_not_empty"].includes(condition.operator);
|
|
485
|
+
const hasValue = Object.hasOwn(condition, "value") && condition.value !== void 0;
|
|
486
|
+
if (needsValue && !hasValue)
|
|
487
|
+
issue(issues, `${conditionPath}.value`, "missing_condition_value", "A value is required.");
|
|
488
|
+
});
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
function validateDisplayRule(value, path, issues) {
|
|
492
|
+
if (!isRecord(value)) {
|
|
493
|
+
issue(issues, path, "invalid_display_rule", "Expected a display rule object.");
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
if (value.action !== "show" && value.action !== "hide") {
|
|
497
|
+
issue(issues, `${path}.action`, "invalid_display_action", "Expected show or hide.");
|
|
498
|
+
}
|
|
499
|
+
validateDisplayConditionGroup(value.condition, `${path}.condition`, issues);
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
function validateSubmissionSettings(value, path, issues) {
|
|
503
|
+
if (!isRecord(value)) {
|
|
504
|
+
issue(issues, path, "invalid_submission_settings", "Expected submission settings to be an object.");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
validateExtensibleNode(value, path, issues);
|
|
508
|
+
if (value.showConfirmationBeforeSubmit !== void 0 && typeof value.showConfirmationBeforeSubmit !== "boolean") {
|
|
509
|
+
issue(issues, `${path}.showConfirmationBeforeSubmit`, "invalid_submission_setting", "Expected a boolean.");
|
|
510
|
+
}
|
|
511
|
+
if (value.confirmationRenderMode !== void 0 && !["dialog", "inline", "replace"].includes(String(value.confirmationRenderMode))) {
|
|
512
|
+
issue(
|
|
513
|
+
issues,
|
|
514
|
+
`${path}.confirmationRenderMode`,
|
|
515
|
+
"invalid_submission_setting",
|
|
516
|
+
"Unsupported confirmation render mode."
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
for (const key of ["confirmButtonLabel", "cancelButtonLabel"]) {
|
|
520
|
+
if (value[key] !== void 0 && !isNonEmptyString(value[key]))
|
|
521
|
+
issue(issues, `${path}.${key}`, "invalid_submission_setting", "Expected non-empty text.");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
419
524
|
function validateField(value, path, issues) {
|
|
420
525
|
if (!isRecord(value)) {
|
|
421
526
|
issue(issues, path, "invalid_field", "Expected a field object.");
|
|
@@ -439,6 +544,7 @@ function validateField(value, path, issues) {
|
|
|
439
544
|
if (value.displayCondition !== void 0) {
|
|
440
545
|
validateDisplayCondition(value.displayCondition, `${path}.displayCondition`, issues);
|
|
441
546
|
}
|
|
547
|
+
if (value.displayRule !== void 0) validateDisplayRule(value.displayRule, `${path}.displayRule`, issues);
|
|
442
548
|
if (value.translations !== void 0) validateLocalizedTextMap(value.translations, `${path}.translations`, issues);
|
|
443
549
|
if (typeof value.type !== "string" || !FIELD_TYPES.has(value.type)) {
|
|
444
550
|
issue(issues, `${path}.type`, "invalid_field_type", "Unsupported field type.");
|
|
@@ -823,6 +929,8 @@ function validateFormSchema(input, options = {}) {
|
|
|
823
929
|
}
|
|
824
930
|
}
|
|
825
931
|
if (input.translations !== void 0) validateLocalizedTextMap(input.translations, "translations", issues);
|
|
932
|
+
if (input.submissionSettings !== void 0)
|
|
933
|
+
validateSubmissionSettings(input.submissionSettings, "submissionSettings", issues);
|
|
826
934
|
if (!Array.isArray(input.fields) || input.fields.length === 0) {
|
|
827
935
|
issue(issues, "fields", "invalid_fields", "Expected at least one field.");
|
|
828
936
|
} else {
|
|
@@ -846,8 +954,19 @@ function validateFormSchema(input, options = {}) {
|
|
|
846
954
|
continue;
|
|
847
955
|
}
|
|
848
956
|
const fieldIndex = inputFields.findIndex((field) => isRecord(field) && field.id === structuralIssue.questionId);
|
|
849
|
-
const
|
|
850
|
-
|
|
957
|
+
const currentField = inputFields[fieldIndex];
|
|
958
|
+
const usesDisplayRule = isRecord(currentField) && currentField.displayRule !== void 0;
|
|
959
|
+
const code = structuralIssue.type === "dangling_condition_reference" ? "unknown_condition_source" : structuralIssue.type === "self_condition_reference" ? "self_condition" : usesDisplayRule ? "circular_display_condition" : "condition_cycle";
|
|
960
|
+
const path = usesDisplayRule ? `fields[${fieldIndex}].displayRule` : `fields[${fieldIndex}].displayCondition`;
|
|
961
|
+
if (usesDisplayRule && structuralIssue.type === "cyclic_condition_reference") {
|
|
962
|
+
issues.push({
|
|
963
|
+
path,
|
|
964
|
+
code,
|
|
965
|
+
type: code,
|
|
966
|
+
message: structuralIssue.message,
|
|
967
|
+
...structuralIssue.cycle === void 0 ? {} : { cycle: structuralIssue.cycle }
|
|
968
|
+
});
|
|
969
|
+
} else issue(issues, path, code, structuralIssue.message);
|
|
851
970
|
}
|
|
852
971
|
if (input.pages !== void 0) {
|
|
853
972
|
if (!Array.isArray(input.pages) || input.pages.length === 0) {
|
|
@@ -956,23 +1075,61 @@ function normalizeString(value) {
|
|
|
956
1075
|
return value.trim().normalize("NFKC").toLowerCase();
|
|
957
1076
|
}
|
|
958
1077
|
function valuesEqual(left, right) {
|
|
959
|
-
|
|
1078
|
+
if (typeof left === "string" && typeof right === "string") return normalizeString(left) === normalizeString(right);
|
|
1079
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
1080
|
+
return left.length === right.length && left.every((item, index) => valuesEqual(item, right[index]));
|
|
1081
|
+
}
|
|
1082
|
+
return left === right;
|
|
1083
|
+
}
|
|
1084
|
+
function evaluateFieldCondition(condition, currentAnswers) {
|
|
1085
|
+
const sourceId = "fieldId" in condition ? condition.fieldId : condition.questionId;
|
|
1086
|
+
const answer = currentAnswers[sourceId];
|
|
1087
|
+
if (isEmpty(answer) && ["equals", "not_equals", "contains"].includes(condition.operator)) return false;
|
|
1088
|
+
if (condition.operator === "is_empty") return isEmpty(answer);
|
|
1089
|
+
if (condition.operator === "is_not_empty" || condition.operator === "not_empty") return !isEmpty(answer);
|
|
1090
|
+
if (condition.operator === "contains" || condition.operator === "not_contains") {
|
|
1091
|
+
const contains = typeof answer === "string" && typeof condition.value === "string" ? normalizeString(answer).includes(normalizeString(condition.value)) : Array.isArray(answer) && answer.some((item) => valuesEqual(item, condition.value));
|
|
1092
|
+
return condition.operator === "not_contains" ? !contains : contains;
|
|
1093
|
+
}
|
|
1094
|
+
if (condition.value === void 0) return false;
|
|
1095
|
+
if (condition.operator === "equals") return valuesEqual(answer, condition.value);
|
|
1096
|
+
if (condition.operator === "not_equals") return !valuesEqual(answer, condition.value);
|
|
1097
|
+
if (condition.operator === "greater_than")
|
|
1098
|
+
return typeof answer === "number" && typeof condition.value === "number" && answer > condition.value;
|
|
1099
|
+
if (condition.operator === "less_than")
|
|
1100
|
+
return typeof answer === "number" && typeof condition.value === "number" && answer < condition.value;
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
function isDisplayConditionGroupSatisfied(group, currentAnswers) {
|
|
1104
|
+
const results = group.conditions.map(
|
|
1105
|
+
(condition) => "logic" in condition ? isDisplayConditionGroupSatisfied(condition, currentAnswers) : evaluateFieldCondition(condition, currentAnswers)
|
|
1106
|
+
);
|
|
1107
|
+
return group.logic === "all" ? results.every(Boolean) : results.some(Boolean);
|
|
960
1108
|
}
|
|
961
1109
|
function isQuestionVisible(question, currentAnswers) {
|
|
1110
|
+
if (question.displayRule !== void 0) {
|
|
1111
|
+
const satisfied = isDisplayConditionGroupSatisfied(question.displayRule.condition, currentAnswers);
|
|
1112
|
+
return question.displayRule.action === "hide" ? !satisfied : satisfied;
|
|
1113
|
+
}
|
|
962
1114
|
return isDisplayConditionSatisfied(question.displayCondition, currentAnswers);
|
|
963
1115
|
}
|
|
964
1116
|
function isDisplayConditionSatisfied(condition, currentAnswers) {
|
|
965
1117
|
if (condition === void 0) return true;
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (condition
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1118
|
+
if ("logic" in condition) return isDisplayConditionGroupSatisfied(condition, currentAnswers);
|
|
1119
|
+
return evaluateFieldCondition(condition, currentAnswers);
|
|
1120
|
+
}
|
|
1121
|
+
function conditionSourceIds(condition) {
|
|
1122
|
+
if (condition === void 0) return [];
|
|
1123
|
+
if ("questionId" in condition) return [condition.questionId];
|
|
1124
|
+
const ids = [];
|
|
1125
|
+
const visit = (group) => {
|
|
1126
|
+
for (const item of group.conditions) {
|
|
1127
|
+
if ("logic" in item) visit(item);
|
|
1128
|
+
else ids.push(item.fieldId);
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
visit(condition.condition);
|
|
1132
|
+
return ids;
|
|
976
1133
|
}
|
|
977
1134
|
function calculateBaseFieldVisibility(schema, currentAnswers) {
|
|
978
1135
|
const fields = new Map(schema.fields.map((field) => [field.id, field]));
|
|
@@ -983,10 +1140,12 @@ function calculateBaseFieldVisibility(schema, currentAnswers) {
|
|
|
983
1140
|
if (existing !== void 0) return existing;
|
|
984
1141
|
if (resolving.has(field.id)) return false;
|
|
985
1142
|
resolving.add(field.id);
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
1143
|
+
const sources = field.displayRule === void 0 ? conditionSourceIds(field.displayCondition) : conditionSourceIds(field.displayRule);
|
|
1144
|
+
const sourcesVisible = sources.every((sourceId) => {
|
|
1145
|
+
const source = fields.get(sourceId);
|
|
1146
|
+
return source !== void 0 && source.id !== field.id && resolve(source);
|
|
1147
|
+
});
|
|
1148
|
+
const visible = sourcesVisible && isQuestionVisible(field, currentAnswers);
|
|
990
1149
|
resolving.delete(field.id);
|
|
991
1150
|
resolved.set(field.id, visible);
|
|
992
1151
|
return visible;
|
|
@@ -1984,6 +2143,39 @@ function createSubmission(schema, values, options) {
|
|
|
1984
2143
|
}
|
|
1985
2144
|
|
|
1986
2145
|
// src/translation.ts
|
|
2146
|
+
var isManualTranslationMetadata = (metadata) => {
|
|
2147
|
+
if (metadata === void 0) return false;
|
|
2148
|
+
if ("isManuallyEdited" in metadata && metadata.isManuallyEdited === true || "isManual" in metadata && metadata.isManual === true)
|
|
2149
|
+
return true;
|
|
2150
|
+
return typeof metadata.translationSource === "string" && metadata.translationSource.toLowerCase() === "manual";
|
|
2151
|
+
};
|
|
2152
|
+
var computeSourceTextHash = (text) => {
|
|
2153
|
+
const normalized = text.normalize("NFKC").trim().replace(/\s+/gu, " ");
|
|
2154
|
+
let hash = 2166136261;
|
|
2155
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
2156
|
+
hash ^= normalized.charCodeAt(index);
|
|
2157
|
+
hash = Math.imul(hash, 16777619);
|
|
2158
|
+
}
|
|
2159
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2160
|
+
};
|
|
2161
|
+
function getTranslationStatus(sourceText, translatedText, metadata) {
|
|
2162
|
+
return getTranslationStatusWithManualOverride(sourceText, translatedText, metadata, void 0);
|
|
2163
|
+
}
|
|
2164
|
+
function getTranslationStatusWithManualOverride(sourceText, translatedText, metadata, manualOverride) {
|
|
2165
|
+
if (translatedText === void 0 || translatedText.trim() === "") return "missing";
|
|
2166
|
+
if (metadata === void 0) return "translated";
|
|
2167
|
+
const currentHash = computeSourceTextHash(sourceText);
|
|
2168
|
+
const sourceHash = typeof metadata.sourceTextHash === "string" ? metadata.sourceTextHash : void 0;
|
|
2169
|
+
const isManual = manualOverride ?? isManualTranslationMetadata(metadata);
|
|
2170
|
+
if (isManual) return sourceHash === void 0 || sourceHash === currentHash ? "manual" : "manual-stale";
|
|
2171
|
+
return sourceHash === void 0 || sourceHash === currentHash ? "translated" : "stale";
|
|
2172
|
+
}
|
|
2173
|
+
async function translateBatch(adapter, texts, locale, sourceLocale) {
|
|
2174
|
+
if ("translateBatch" in adapter) return adapter.translateBatch(texts, locale, sourceLocale);
|
|
2175
|
+
return texts.map(
|
|
2176
|
+
(text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
1987
2179
|
function mergeLocalizedText(translations, locale, property, value) {
|
|
1988
2180
|
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
1989
2181
|
}
|
|
@@ -2000,19 +2192,27 @@ function withTranslationMetadata(node, locale, property, metadata) {
|
|
|
2000
2192
|
}
|
|
2001
2193
|
};
|
|
2002
2194
|
}
|
|
2003
|
-
function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
|
|
2195
|
+
function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}) {
|
|
2196
|
+
const path = `${kind}.${nodeId}.${property}`;
|
|
2197
|
+
const manual = options.isManualTranslation?.(existingTranslationMetadata, { path, locale }) ?? isManualTranslationMetadata(existingTranslationMetadata);
|
|
2198
|
+
const normalizedMetadata = existingTranslationMetadata === void 0 ? void 0 : options.normalizeMetadata === void 0 ? existingTranslationMetadata : { ...options.normalizeMetadata(existingTranslationMetadata, sourceText) };
|
|
2199
|
+
const status = getTranslationStatusWithManualOverride(sourceText, existingText, normalizedMetadata, manual);
|
|
2004
2200
|
return {
|
|
2005
2201
|
kind,
|
|
2006
2202
|
nodeId,
|
|
2007
2203
|
property,
|
|
2008
2204
|
locale,
|
|
2009
2205
|
sourceText,
|
|
2206
|
+
target: { kind, ...nodeId.length === 0 ? {} : { id: nodeId }, property },
|
|
2207
|
+
path,
|
|
2208
|
+
sourceTextHash: computeSourceTextHash(sourceText),
|
|
2209
|
+
status,
|
|
2010
2210
|
...existingText === void 0 ? {} : { existingText },
|
|
2011
2211
|
...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
|
|
2012
|
-
...
|
|
2212
|
+
...normalizedMetadata === void 0 ? {} : { existingTranslationMetadata: normalizedMetadata }
|
|
2013
2213
|
};
|
|
2014
2214
|
}
|
|
2015
|
-
function translationSlots(schema, locale) {
|
|
2215
|
+
function translationSlots(schema, locale, options = {}) {
|
|
2016
2216
|
const descriptors = [];
|
|
2017
2217
|
const addFormSlot = (property, sourceText) => {
|
|
2018
2218
|
const slot = createSlot(
|
|
@@ -2023,10 +2223,15 @@ function translationSlots(schema, locale) {
|
|
|
2023
2223
|
sourceText,
|
|
2024
2224
|
schema.translations?.[locale]?.[property],
|
|
2025
2225
|
schema.metadata,
|
|
2026
|
-
schema.translationMetadata?.[locale]?.[property]
|
|
2226
|
+
schema.translationMetadata?.[locale]?.[property],
|
|
2227
|
+
options
|
|
2027
2228
|
);
|
|
2028
2229
|
descriptors.push({
|
|
2029
2230
|
slot,
|
|
2231
|
+
manual: options.isManualTranslation?.(schema.translationMetadata?.[locale]?.[property], {
|
|
2232
|
+
path: slot.path ?? "",
|
|
2233
|
+
locale
|
|
2234
|
+
}) ?? isManualTranslationMetadata(schema.translationMetadata?.[locale]?.[property]),
|
|
2030
2235
|
apply: (current, value, metadata) => withTranslationMetadata(
|
|
2031
2236
|
{ ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
|
|
2032
2237
|
locale,
|
|
@@ -2048,10 +2253,15 @@ function translationSlots(schema, locale) {
|
|
|
2048
2253
|
sourceText,
|
|
2049
2254
|
field.translations?.[locale]?.[property],
|
|
2050
2255
|
field.metadata,
|
|
2051
|
-
field.translationMetadata?.[locale]?.[property]
|
|
2256
|
+
field.translationMetadata?.[locale]?.[property],
|
|
2257
|
+
options
|
|
2052
2258
|
);
|
|
2053
2259
|
descriptors.push({
|
|
2054
2260
|
slot,
|
|
2261
|
+
manual: options.isManualTranslation?.(field.translationMetadata?.[locale]?.[property], {
|
|
2262
|
+
path: slot.path ?? "",
|
|
2263
|
+
locale
|
|
2264
|
+
}) ?? isManualTranslationMetadata(field.translationMetadata?.[locale]?.[property]),
|
|
2055
2265
|
apply: (current, value, metadata) => ({
|
|
2056
2266
|
...current,
|
|
2057
2267
|
fields: current.fields.map(
|
|
@@ -2080,10 +2290,15 @@ function translationSlots(schema, locale) {
|
|
|
2080
2290
|
option.label,
|
|
2081
2291
|
option.translations?.[locale],
|
|
2082
2292
|
option.metadata,
|
|
2083
|
-
option.translationMetadata?.[locale]?.label
|
|
2293
|
+
option.translationMetadata?.[locale]?.label,
|
|
2294
|
+
options
|
|
2084
2295
|
);
|
|
2085
2296
|
descriptors.push({
|
|
2086
2297
|
slot,
|
|
2298
|
+
manual: options.isManualTranslation?.(option.translationMetadata?.[locale]?.label, {
|
|
2299
|
+
path: slot.path ?? "",
|
|
2300
|
+
locale
|
|
2301
|
+
}) ?? isManualTranslationMetadata(option.translationMetadata?.[locale]?.label),
|
|
2087
2302
|
apply: (current, value, metadata) => ({
|
|
2088
2303
|
...current,
|
|
2089
2304
|
fields: current.fields.map((candidate, candidateIndex) => {
|
|
@@ -2118,10 +2333,15 @@ function translationSlots(schema, locale) {
|
|
|
2118
2333
|
sourceText,
|
|
2119
2334
|
page.translations?.[locale]?.[property],
|
|
2120
2335
|
page.metadata,
|
|
2121
|
-
page.translationMetadata?.[locale]?.[property]
|
|
2336
|
+
page.translationMetadata?.[locale]?.[property],
|
|
2337
|
+
options
|
|
2122
2338
|
);
|
|
2123
2339
|
descriptors.push({
|
|
2124
2340
|
slot,
|
|
2341
|
+
manual: options.isManualTranslation?.(page.translationMetadata?.[locale]?.[property], {
|
|
2342
|
+
path: slot.path ?? "",
|
|
2343
|
+
locale
|
|
2344
|
+
}) ?? isManualTranslationMetadata(page.translationMetadata?.[locale]?.[property]),
|
|
2125
2345
|
apply: (current, value, metadata) => ({
|
|
2126
2346
|
...current,
|
|
2127
2347
|
...current.pages === void 0 ? {} : {
|
|
@@ -2145,6 +2365,125 @@ function translationSlots(schema, locale) {
|
|
|
2145
2365
|
});
|
|
2146
2366
|
return descriptors;
|
|
2147
2367
|
}
|
|
2368
|
+
function removeLocaleRecord(record, locale) {
|
|
2369
|
+
if (record === void 0) return void 0;
|
|
2370
|
+
const remaining = Object.entries(record).filter(([candidate]) => candidate !== locale);
|
|
2371
|
+
return remaining.length === 0 ? void 0 : Object.fromEntries(remaining);
|
|
2372
|
+
}
|
|
2373
|
+
function removeLocalizedNodeLocale(node, locale) {
|
|
2374
|
+
const translations = removeLocaleRecord(node.translations, locale);
|
|
2375
|
+
const translationMetadata = removeLocaleRecord(node.translationMetadata, locale);
|
|
2376
|
+
const { translations: _translations, translationMetadata: _translationMetadata, ...base } = node;
|
|
2377
|
+
return {
|
|
2378
|
+
...base,
|
|
2379
|
+
...translations === void 0 ? {} : { translations },
|
|
2380
|
+
...translationMetadata === void 0 ? {} : { translationMetadata }
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
|
|
2384
|
+
if (customMigrator !== void 0) return customMigrator(metadata, sourceText);
|
|
2385
|
+
const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
|
|
2386
|
+
const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale ?? "";
|
|
2387
|
+
const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
|
|
2388
|
+
return {
|
|
2389
|
+
sourceLocale,
|
|
2390
|
+
sourceTextHash: computeSourceTextHash(sourceText),
|
|
2391
|
+
translationSource,
|
|
2392
|
+
...typeof record?.translatedAt === "string" ? { translatedAt: record.translatedAt } : {},
|
|
2393
|
+
...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
function migrateNodeMetadata(node, sourceTexts, defaultLocale, customMigrator) {
|
|
2397
|
+
if (node.translationMetadata === void 0) return node;
|
|
2398
|
+
const translationMetadata = Object.fromEntries(
|
|
2399
|
+
Object.entries(node.translationMetadata).map(([locale, properties]) => [
|
|
2400
|
+
locale,
|
|
2401
|
+
Object.fromEntries(
|
|
2402
|
+
Object.entries(properties).map(([property, metadata]) => [
|
|
2403
|
+
property,
|
|
2404
|
+
migrateMetadata(metadata, sourceTexts[property] ?? "", defaultLocale, customMigrator)
|
|
2405
|
+
])
|
|
2406
|
+
)
|
|
2407
|
+
])
|
|
2408
|
+
);
|
|
2409
|
+
return { ...node, translationMetadata };
|
|
2410
|
+
}
|
|
2411
|
+
var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
|
|
2412
|
+
const migratedSchema = migrateNodeMetadata(
|
|
2413
|
+
schema,
|
|
2414
|
+
{
|
|
2415
|
+
title: schema.title,
|
|
2416
|
+
...schema.description === void 0 ? {} : { description: schema.description },
|
|
2417
|
+
...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
|
|
2418
|
+
},
|
|
2419
|
+
schema.defaultLocale,
|
|
2420
|
+
customMigrator
|
|
2421
|
+
);
|
|
2422
|
+
const fields = schema.fields.map((field) => {
|
|
2423
|
+
const migratedField = migrateNodeMetadata(
|
|
2424
|
+
field,
|
|
2425
|
+
{ title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
|
|
2426
|
+
schema.defaultLocale,
|
|
2427
|
+
customMigrator
|
|
2428
|
+
);
|
|
2429
|
+
if (!("options" in migratedField)) return migratedField;
|
|
2430
|
+
return {
|
|
2431
|
+
...migratedField,
|
|
2432
|
+
options: migratedField.options.map(
|
|
2433
|
+
(option) => migrateNodeMetadata(option, { label: option.label }, schema.defaultLocale, customMigrator)
|
|
2434
|
+
)
|
|
2435
|
+
};
|
|
2436
|
+
});
|
|
2437
|
+
const pages = schema.pages?.map(
|
|
2438
|
+
(page) => migrateNodeMetadata(
|
|
2439
|
+
page,
|
|
2440
|
+
{
|
|
2441
|
+
...page.title === void 0 ? {} : { title: page.title },
|
|
2442
|
+
...page.description === void 0 ? {} : { description: page.description }
|
|
2443
|
+
},
|
|
2444
|
+
schema.defaultLocale,
|
|
2445
|
+
customMigrator
|
|
2446
|
+
)
|
|
2447
|
+
);
|
|
2448
|
+
return {
|
|
2449
|
+
...migratedSchema,
|
|
2450
|
+
fields,
|
|
2451
|
+
...pages === void 0 ? {} : { pages }
|
|
2452
|
+
};
|
|
2453
|
+
};
|
|
2454
|
+
var removeLocaleFromSchema = (schema, localeToRemove) => {
|
|
2455
|
+
if (localeToRemove === schema.defaultLocale) {
|
|
2456
|
+
throw new Error(`Cannot remove defaultLocale: ${localeToRemove}`);
|
|
2457
|
+
}
|
|
2458
|
+
const form = removeLocalizedNodeLocale(schema, localeToRemove);
|
|
2459
|
+
const fields = schema.fields.map((field) => {
|
|
2460
|
+
const localizedField = removeLocalizedNodeLocale(field, localeToRemove);
|
|
2461
|
+
if (!("options" in localizedField)) return localizedField;
|
|
2462
|
+
return {
|
|
2463
|
+
...localizedField,
|
|
2464
|
+
options: localizedField.options.map((option) => {
|
|
2465
|
+
const translations = removeLocaleRecord(option.translations, localeToRemove);
|
|
2466
|
+
const translationMetadata = removeLocaleRecord(option.translationMetadata, localeToRemove);
|
|
2467
|
+
const { translations: _translations, translationMetadata: _translationMetadata, ...base } = option;
|
|
2468
|
+
return {
|
|
2469
|
+
...base,
|
|
2470
|
+
...translations === void 0 ? {} : { translations },
|
|
2471
|
+
...translationMetadata === void 0 ? {} : { translationMetadata }
|
|
2472
|
+
};
|
|
2473
|
+
})
|
|
2474
|
+
};
|
|
2475
|
+
});
|
|
2476
|
+
const pages = schema.pages?.map((page) => removeLocalizedNodeLocale(page, localeToRemove));
|
|
2477
|
+
return {
|
|
2478
|
+
...form,
|
|
2479
|
+
supportedLocales: (schema.supportedLocales ?? []).filter((locale) => locale !== localeToRemove),
|
|
2480
|
+
fields,
|
|
2481
|
+
...pages === void 0 ? {} : { pages }
|
|
2482
|
+
};
|
|
2483
|
+
};
|
|
2484
|
+
function collectTranslationSlots(schema, locale) {
|
|
2485
|
+
return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
|
|
2486
|
+
}
|
|
2148
2487
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
2149
2488
|
if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
2150
2489
|
const formTranslation = schema.translations?.[targetLocale];
|
|
@@ -2201,17 +2540,32 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2201
2540
|
}
|
|
2202
2541
|
const updatedSlots = [];
|
|
2203
2542
|
const skippedSlots = [];
|
|
2543
|
+
const staleSlots = [];
|
|
2544
|
+
const skippedReasons = {};
|
|
2204
2545
|
let result = schema;
|
|
2205
2546
|
for (const locale of locales) {
|
|
2206
|
-
const descriptors = translationSlots(schema, locale);
|
|
2547
|
+
const descriptors = translationSlots(schema, locale, options);
|
|
2548
|
+
if (options.markStaleTranslations ?? true) {
|
|
2549
|
+
for (const descriptor of descriptors) {
|
|
2550
|
+
if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
|
|
2551
|
+
staleSlots.push(descriptor.slot);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2207
2554
|
const selected = [];
|
|
2208
2555
|
for (const descriptor of descriptors) {
|
|
2209
|
-
const
|
|
2556
|
+
const status = descriptor.slot.status ?? "missing";
|
|
2557
|
+
const manual = descriptor.manual || status === "manual" || status === "manual-stale";
|
|
2558
|
+
const requestedOverwrite = options.shouldOverwrite?.(descriptor.slot);
|
|
2559
|
+
const shouldTranslate = (options.preserveManualTranslations ?? true) && manual ? false : requestedOverwrite ?? (options.overwrite === "all" || (options.overwrite === "stale-and-missing" || options.overwrite === void 0 ? status === "missing" || status === "stale" || !(options.preserveManualTranslations ?? true) && status === "manual-stale" : status === "missing"));
|
|
2210
2560
|
if (shouldTranslate) selected.push(descriptor);
|
|
2211
|
-
else
|
|
2561
|
+
else {
|
|
2562
|
+
skippedSlots.push(descriptor.slot);
|
|
2563
|
+
skippedReasons[descriptor.slot.path ?? `${descriptor.slot.kind}.${descriptor.slot.nodeId}.${descriptor.slot.property}`] = manual ? "manual" : "unchanged";
|
|
2564
|
+
}
|
|
2212
2565
|
}
|
|
2213
2566
|
if (selected.length === 0) continue;
|
|
2214
|
-
const translated = await
|
|
2567
|
+
const translated = await translateBatch(
|
|
2568
|
+
adapter,
|
|
2215
2569
|
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
2216
2570
|
locale,
|
|
2217
2571
|
schema.defaultLocale
|
|
@@ -2222,7 +2576,12 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2222
2576
|
selected.forEach((descriptor, index) => {
|
|
2223
2577
|
const translatedText = translated[index];
|
|
2224
2578
|
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
2225
|
-
const metadata = options.createMetadata?.(descriptor.slot, translatedText)
|
|
2579
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
|
|
2580
|
+
sourceLocale: schema.defaultLocale ?? "",
|
|
2581
|
+
sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
|
|
2582
|
+
translationSource: "automatic",
|
|
2583
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2584
|
+
};
|
|
2226
2585
|
result = descriptor.apply(result, translatedText, metadata);
|
|
2227
2586
|
updatedSlots.push(descriptor.slot);
|
|
2228
2587
|
});
|
|
@@ -2236,7 +2595,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2236
2595
|
];
|
|
2237
2596
|
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
2238
2597
|
assertValidFormSchema(result);
|
|
2239
|
-
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
2598
|
+
return { schema: result, report: { updatedSlots, skippedSlots, staleSlots, skippedReasons } };
|
|
2240
2599
|
}
|
|
2241
2600
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
2242
2601
|
const populated = await populateSchemaTranslations(
|
|
@@ -2539,6 +2898,8 @@ export {
|
|
|
2539
2898
|
calculatePageVisibility,
|
|
2540
2899
|
cloneVersionToDraft,
|
|
2541
2900
|
collectSchemaLocales,
|
|
2901
|
+
collectTranslationSlots,
|
|
2902
|
+
computeSourceTextHash,
|
|
2542
2903
|
createCloneTransitionPlan,
|
|
2543
2904
|
createDeleteDraftTransitionPlan,
|
|
2544
2905
|
createPublishTransitionPlan,
|
|
@@ -2553,16 +2914,21 @@ export {
|
|
|
2553
2914
|
escapeCsvCell,
|
|
2554
2915
|
exportResponsesToCsv,
|
|
2555
2916
|
exportResponsesToCsvStream,
|
|
2917
|
+
getTranslationStatus,
|
|
2918
|
+
isDisplayConditionGroupSatisfied,
|
|
2556
2919
|
isDisplayConditionSatisfied,
|
|
2920
|
+
isManualTranslationMetadata,
|
|
2557
2921
|
isQuestionVisible,
|
|
2558
2922
|
iterateSubmissionPages,
|
|
2559
2923
|
jsonValuesEqual,
|
|
2560
2924
|
matchesSubmissionFilter,
|
|
2561
2925
|
matchesSubmissionPageFilters,
|
|
2926
|
+
migrateSchemaTranslationMetadata,
|
|
2562
2927
|
normalizeSubmissionPageSize,
|
|
2563
2928
|
pipeResponsesToCsvStream,
|
|
2564
2929
|
populateSchemaTranslations,
|
|
2565
2930
|
publishDraft,
|
|
2931
|
+
removeLocaleFromSchema,
|
|
2566
2932
|
resolveFormTranslation,
|
|
2567
2933
|
resolveLocalizedSchema,
|
|
2568
2934
|
sanitizeSchema,
|