@form-engine-ts/core 4.3.2 → 4.5.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 +9 -0
- package/dist/index.cjs +279 -61
- package/dist/index.d.cts +57 -3
- package/dist/index.d.ts +57 -3
- package/dist/index.js +275 -61
- 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,30 @@ function createSubmission(schema, values, options) {
|
|
|
1984
2143
|
}
|
|
1985
2144
|
|
|
1986
2145
|
// src/translation.ts
|
|
2146
|
+
var computeSourceTextHash = (text) => {
|
|
2147
|
+
const normalized = text.normalize("NFKC").trim().replace(/\s+/gu, " ");
|
|
2148
|
+
let hash = 2166136261;
|
|
2149
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
2150
|
+
hash ^= normalized.charCodeAt(index);
|
|
2151
|
+
hash = Math.imul(hash, 16777619);
|
|
2152
|
+
}
|
|
2153
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2154
|
+
};
|
|
2155
|
+
function getTranslationStatus(sourceText, translatedText, metadata) {
|
|
2156
|
+
if (translatedText === void 0 || translatedText.trim() === "") return "missing";
|
|
2157
|
+
if (metadata === void 0) return "translated";
|
|
2158
|
+
const currentHash = computeSourceTextHash(sourceText);
|
|
2159
|
+
const sourceHash = typeof metadata.sourceTextHash === "string" ? metadata.sourceTextHash : void 0;
|
|
2160
|
+
const isManual = metadata.translationSource === "manual" || "isManual" in metadata && metadata.isManual === true;
|
|
2161
|
+
if (isManual) return sourceHash === void 0 || sourceHash === currentHash ? "manual" : "manual-stale";
|
|
2162
|
+
return sourceHash === void 0 || sourceHash === currentHash ? "translated" : "stale";
|
|
2163
|
+
}
|
|
2164
|
+
async function translateBatch(adapter, texts, locale, sourceLocale) {
|
|
2165
|
+
if ("translateBatch" in adapter) return adapter.translateBatch(texts, locale, sourceLocale);
|
|
2166
|
+
return texts.map(
|
|
2167
|
+
(text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
1987
2170
|
function mergeLocalizedText(translations, locale, property, value) {
|
|
1988
2171
|
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
1989
2172
|
}
|
|
@@ -2001,12 +2184,17 @@ function withTranslationMetadata(node, locale, property, metadata) {
|
|
|
2001
2184
|
};
|
|
2002
2185
|
}
|
|
2003
2186
|
function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
|
|
2187
|
+
const status = getTranslationStatus(sourceText, existingText, existingTranslationMetadata);
|
|
2004
2188
|
return {
|
|
2005
2189
|
kind,
|
|
2006
2190
|
nodeId,
|
|
2007
2191
|
property,
|
|
2008
2192
|
locale,
|
|
2009
2193
|
sourceText,
|
|
2194
|
+
target: { kind, ...nodeId.length === 0 ? {} : { id: nodeId }, property },
|
|
2195
|
+
path: `${kind}.${nodeId}.${property}`,
|
|
2196
|
+
sourceTextHash: computeSourceTextHash(sourceText),
|
|
2197
|
+
status,
|
|
2010
2198
|
...existingText === void 0 ? {} : { existingText },
|
|
2011
2199
|
...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
|
|
2012
2200
|
...existingTranslationMetadata === void 0 ? {} : { existingTranslationMetadata }
|
|
@@ -2145,6 +2333,9 @@ function translationSlots(schema, locale) {
|
|
|
2145
2333
|
});
|
|
2146
2334
|
return descriptors;
|
|
2147
2335
|
}
|
|
2336
|
+
function collectTranslationSlots(schema, locale) {
|
|
2337
|
+
return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
|
|
2338
|
+
}
|
|
2148
2339
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
2149
2340
|
if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
2150
2341
|
const formTranslation = schema.translations?.[targetLocale];
|
|
@@ -2201,17 +2392,31 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2201
2392
|
}
|
|
2202
2393
|
const updatedSlots = [];
|
|
2203
2394
|
const skippedSlots = [];
|
|
2395
|
+
const staleSlots = [];
|
|
2396
|
+
const skippedReasons = {};
|
|
2204
2397
|
let result = schema;
|
|
2205
2398
|
for (const locale of locales) {
|
|
2206
2399
|
const descriptors = translationSlots(schema, locale);
|
|
2400
|
+
if (options.markStaleTranslations ?? true) {
|
|
2401
|
+
for (const descriptor of descriptors) {
|
|
2402
|
+
if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
|
|
2403
|
+
staleSlots.push(descriptor.slot);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2207
2406
|
const selected = [];
|
|
2208
2407
|
for (const descriptor of descriptors) {
|
|
2209
|
-
const
|
|
2408
|
+
const status = descriptor.slot.status ?? "missing";
|
|
2409
|
+
const manual = status === "manual" || status === "manual-stale";
|
|
2410
|
+
const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? ((!(options.preserveManualTranslations ?? true) || !manual) && (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
2411
|
if (shouldTranslate) selected.push(descriptor);
|
|
2211
|
-
else
|
|
2412
|
+
else {
|
|
2413
|
+
skippedSlots.push(descriptor.slot);
|
|
2414
|
+
skippedReasons[descriptor.slot.path ?? `${descriptor.slot.kind}.${descriptor.slot.nodeId}.${descriptor.slot.property}`] = manual ? "manual" : "unchanged";
|
|
2415
|
+
}
|
|
2212
2416
|
}
|
|
2213
2417
|
if (selected.length === 0) continue;
|
|
2214
|
-
const translated = await
|
|
2418
|
+
const translated = await translateBatch(
|
|
2419
|
+
adapter,
|
|
2215
2420
|
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
2216
2421
|
locale,
|
|
2217
2422
|
schema.defaultLocale
|
|
@@ -2222,7 +2427,12 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2222
2427
|
selected.forEach((descriptor, index) => {
|
|
2223
2428
|
const translatedText = translated[index];
|
|
2224
2429
|
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
2225
|
-
const metadata = options.createMetadata?.(descriptor.slot, translatedText)
|
|
2430
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
|
|
2431
|
+
sourceLocale: schema.defaultLocale ?? "",
|
|
2432
|
+
sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
|
|
2433
|
+
translationSource: "automatic",
|
|
2434
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2435
|
+
};
|
|
2226
2436
|
result = descriptor.apply(result, translatedText, metadata);
|
|
2227
2437
|
updatedSlots.push(descriptor.slot);
|
|
2228
2438
|
});
|
|
@@ -2236,7 +2446,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2236
2446
|
];
|
|
2237
2447
|
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
2238
2448
|
assertValidFormSchema(result);
|
|
2239
|
-
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
2449
|
+
return { schema: result, report: { updatedSlots, skippedSlots, staleSlots, skippedReasons } };
|
|
2240
2450
|
}
|
|
2241
2451
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
2242
2452
|
const populated = await populateSchemaTranslations(
|
|
@@ -2539,6 +2749,8 @@ export {
|
|
|
2539
2749
|
calculatePageVisibility,
|
|
2540
2750
|
cloneVersionToDraft,
|
|
2541
2751
|
collectSchemaLocales,
|
|
2752
|
+
collectTranslationSlots,
|
|
2753
|
+
computeSourceTextHash,
|
|
2542
2754
|
createCloneTransitionPlan,
|
|
2543
2755
|
createDeleteDraftTransitionPlan,
|
|
2544
2756
|
createPublishTransitionPlan,
|
|
@@ -2553,6 +2765,8 @@ export {
|
|
|
2553
2765
|
escapeCsvCell,
|
|
2554
2766
|
exportResponsesToCsv,
|
|
2555
2767
|
exportResponsesToCsvStream,
|
|
2768
|
+
getTranslationStatus,
|
|
2769
|
+
isDisplayConditionGroupSatisfied,
|
|
2556
2770
|
isDisplayConditionSatisfied,
|
|
2557
2771
|
isQuestionVisible,
|
|
2558
2772
|
iterateSubmissionPages,
|