@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/README.md
CHANGED
|
@@ -53,6 +53,15 @@ safe maximum-length corrections while purging unregistered locale content. Pass
|
|
|
53
53
|
`{ policy: { allowedLocales, maxLocales } }` to `populateSchemaTranslations` to reject inadmissible targets before the
|
|
54
54
|
translation adapter runs.
|
|
55
55
|
|
|
56
|
+
Fields can use a `displayRule` with nested `all`/`any` condition groups and `show` or `hide` actions. Supported
|
|
57
|
+
operators include equality, containment, emptiness, and numeric comparisons; the legacy `displayCondition` and
|
|
58
|
+
`not_empty` forms remain supported. `submissionSettings` can enable pre-submit confirmation and select its
|
|
59
|
+
`dialog`, `inline`, or `replace` presentation.
|
|
60
|
+
|
|
61
|
+
`collectTranslationSlots`, `computeSourceTextHash`, and `getTranslationStatus` expose canonical translation targets
|
|
62
|
+
and missing, translated, stale, or manual states for authoring tools. `populateSchemaTranslations` can populate stale
|
|
63
|
+
and missing entries while preserving manual translations and reports skipped reasons.
|
|
64
|
+
|
|
56
65
|
The React builder uses canonical keys with legacy aliases when resolving UI translations:
|
|
57
66
|
|
|
58
67
|
| Canonical key | Legacy alias |
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,8 @@ __export(index_exports, {
|
|
|
31
31
|
calculatePageVisibility: () => calculatePageVisibility,
|
|
32
32
|
cloneVersionToDraft: () => cloneVersionToDraft,
|
|
33
33
|
collectSchemaLocales: () => collectSchemaLocales,
|
|
34
|
+
collectTranslationSlots: () => collectTranslationSlots,
|
|
35
|
+
computeSourceTextHash: () => computeSourceTextHash,
|
|
34
36
|
createCloneTransitionPlan: () => createCloneTransitionPlan,
|
|
35
37
|
createDeleteDraftTransitionPlan: () => createDeleteDraftTransitionPlan,
|
|
36
38
|
createPublishTransitionPlan: () => createPublishTransitionPlan,
|
|
@@ -45,6 +47,8 @@ __export(index_exports, {
|
|
|
45
47
|
escapeCsvCell: () => escapeCsvCell,
|
|
46
48
|
exportResponsesToCsv: () => exportResponsesToCsv,
|
|
47
49
|
exportResponsesToCsvStream: () => exportResponsesToCsvStream,
|
|
50
|
+
getTranslationStatus: () => getTranslationStatus,
|
|
51
|
+
isDisplayConditionGroupSatisfied: () => isDisplayConditionGroupSatisfied,
|
|
48
52
|
isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
|
|
49
53
|
isQuestionVisible: () => isQuestionVisible,
|
|
50
54
|
iterateSubmissionPages: () => iterateSubmissionPages,
|
|
@@ -116,6 +120,19 @@ function collectSchemaLocales(schema) {
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
// src/sanitization.ts
|
|
123
|
+
function displayRuleSourceIds(field) {
|
|
124
|
+
if (field.displayRule === void 0)
|
|
125
|
+
return field.displayCondition?.questionId === void 0 ? [] : [field.displayCondition.questionId];
|
|
126
|
+
const ids = [];
|
|
127
|
+
const visit = (group) => {
|
|
128
|
+
for (const condition of group.conditions) {
|
|
129
|
+
if ("logic" in condition) visit(condition);
|
|
130
|
+
else ids.push(condition.fieldId);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
visit(field.displayRule.condition);
|
|
134
|
+
return ids;
|
|
135
|
+
}
|
|
119
136
|
function registeredEntries(value, registeredLocales) {
|
|
120
137
|
if (value === void 0) return void 0;
|
|
121
138
|
const entries = Object.entries(value).filter(([locale]) => registeredLocales.has(locale));
|
|
@@ -183,26 +200,28 @@ function cyclicQuestionIds(fields) {
|
|
|
183
200
|
if (!firstById.has(field.id)) firstById.set(field.id, field);
|
|
184
201
|
}
|
|
185
202
|
const cyclic = /* @__PURE__ */ new Set();
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (existingIndex !== void 0) {
|
|
195
|
-
for (const id of path.slice(existingIndex)) cyclic.add(id);
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
pathIndex.set(currentId, path.length);
|
|
199
|
-
path.push(currentId);
|
|
200
|
-
const field = firstById.get(currentId);
|
|
201
|
-
const sourceId = field?.displayCondition?.questionId;
|
|
202
|
-
currentId = sourceId === currentId ? void 0 : sourceId;
|
|
203
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
204
|
+
const visited = /* @__PURE__ */ new Set();
|
|
205
|
+
const path = [];
|
|
206
|
+
const visit = (id) => {
|
|
207
|
+
if (visiting.has(id)) {
|
|
208
|
+
const start = path.indexOf(id);
|
|
209
|
+
for (const member of path.slice(start < 0 ? 0 : start)) cyclic.add(member);
|
|
210
|
+
return;
|
|
203
211
|
}
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
if (visited.has(id)) return;
|
|
213
|
+
visiting.add(id);
|
|
214
|
+
path.push(id);
|
|
215
|
+
const field = firstById.get(id);
|
|
216
|
+
if (field === void 0) return;
|
|
217
|
+
for (const sourceId of displayRuleSourceIds(field)) {
|
|
218
|
+
if (sourceId !== id && firstById.has(sourceId)) visit(sourceId);
|
|
219
|
+
}
|
|
220
|
+
path.pop();
|
|
221
|
+
visiting.delete(id);
|
|
222
|
+
visited.add(id);
|
|
223
|
+
};
|
|
224
|
+
for (const id of firstById.keys()) visit(id);
|
|
206
225
|
return cyclic;
|
|
207
226
|
}
|
|
208
227
|
function validateSchemaStructure(schema) {
|
|
@@ -234,20 +253,20 @@ function validateSchemaStructure(schema) {
|
|
|
234
253
|
}
|
|
235
254
|
}
|
|
236
255
|
for (const field of schema.fields) {
|
|
237
|
-
const sourceId
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
256
|
+
for (const sourceId of displayRuleSourceIds(field)) {
|
|
257
|
+
if (sourceId === field.id) {
|
|
258
|
+
issues.push({
|
|
259
|
+
type: "self_condition_reference",
|
|
260
|
+
questionId: field.id,
|
|
261
|
+
message: `Question "${field.id}" cannot depend on itself.`
|
|
262
|
+
});
|
|
263
|
+
} else if (!questionIds.has(sourceId)) {
|
|
264
|
+
issues.push({
|
|
265
|
+
type: "dangling_condition_reference",
|
|
266
|
+
questionId: field.id,
|
|
267
|
+
message: `Question "${field.id}" references missing question "${sourceId}".`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
@@ -256,6 +275,11 @@ function validateSchemaStructure(schema) {
|
|
|
256
275
|
issues.push({
|
|
257
276
|
type: "cyclic_condition_reference",
|
|
258
277
|
questionId: field.id,
|
|
278
|
+
cycle: [
|
|
279
|
+
field.id,
|
|
280
|
+
...displayRuleSourceIds(field).filter((sourceId) => cyclic.has(sourceId)).slice(0, 1),
|
|
281
|
+
field.id
|
|
282
|
+
],
|
|
259
283
|
message: `Question "${field.id}" participates in a display-condition cycle.`
|
|
260
284
|
});
|
|
261
285
|
}
|
|
@@ -270,11 +294,17 @@ function sanitizeSchema(schema, options = {}) {
|
|
|
270
294
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
271
295
|
const sanitizedFields = schema.fields.map((sourceField) => {
|
|
272
296
|
const field = sanitizeFieldConstraints(sanitizeFieldLocales(sourceField, registeredLocales), options.policy);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
297
|
+
let sanitized = field;
|
|
298
|
+
const ruleSources = displayRuleSourceIds(field);
|
|
299
|
+
if (field.displayRule !== void 0 && (cyclic.has(field.id) || ruleSources.some((sourceId2) => sourceId2 === field.id || !existingQuestionIds.has(sourceId2)))) {
|
|
300
|
+
const { displayRule: _displayRule, ...withoutRule } = sanitized;
|
|
301
|
+
sanitized = withoutRule;
|
|
302
|
+
}
|
|
303
|
+
const sourceId = sanitized.displayCondition?.questionId;
|
|
304
|
+
if (sourceId !== void 0 && (!existingQuestionIds.has(sourceId) || sourceId === sanitized.id || cyclic.has(sanitized.id))) {
|
|
305
|
+
const { displayCondition: _displayCondition, ...withoutCondition } = sanitized;
|
|
306
|
+
return withoutCondition;
|
|
276
307
|
}
|
|
277
|
-
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
278
308
|
return sanitized;
|
|
279
309
|
});
|
|
280
310
|
const localizedSchema = sanitizeNodeLocales(schema, registeredLocales);
|
|
@@ -317,7 +347,17 @@ function sanitizeSchema(schema, options = {}) {
|
|
|
317
347
|
|
|
318
348
|
// src/schema.ts
|
|
319
349
|
var FIELD_TYPES = /* @__PURE__ */ new Set(["text", "textarea", "number", "rating", "select", "multi-select", "checkbox", "radio"]);
|
|
320
|
-
var CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
350
|
+
var CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
351
|
+
"equals",
|
|
352
|
+
"not_equals",
|
|
353
|
+
"contains",
|
|
354
|
+
"not_contains",
|
|
355
|
+
"is_empty",
|
|
356
|
+
"is_not_empty",
|
|
357
|
+
"greater_than",
|
|
358
|
+
"less_than",
|
|
359
|
+
"not_empty"
|
|
360
|
+
]);
|
|
321
361
|
function isRecord(value) {
|
|
322
362
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
323
363
|
}
|
|
@@ -474,7 +514,7 @@ function validateDisplayCondition(value, path, issues) {
|
|
|
474
514
|
return false;
|
|
475
515
|
}
|
|
476
516
|
const hasValue = Object.hasOwn(value, "value") && value.value !== void 0;
|
|
477
|
-
if (value.operator === "not_empty") {
|
|
517
|
+
if (value.operator === "not_empty" || value.operator === "is_empty" || value.operator === "is_not_empty") {
|
|
478
518
|
if (hasValue) issue(issues, `${path}.value`, "unexpected_condition_value", "not_empty does not accept a value.");
|
|
479
519
|
} else if (!hasValue) {
|
|
480
520
|
issue(issues, `${path}.value`, "missing_condition_value", `${value.operator} requires a value.`);
|
|
@@ -485,6 +525,75 @@ function validateDisplayCondition(value, path, issues) {
|
|
|
485
525
|
}
|
|
486
526
|
return true;
|
|
487
527
|
}
|
|
528
|
+
function validateDisplayConditionGroup(value, path, issues) {
|
|
529
|
+
if (!isRecord(value)) {
|
|
530
|
+
issue(issues, path, "invalid_condition_group", "Expected a display condition group.");
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (value.logic !== "all" && value.logic !== "any") {
|
|
534
|
+
issue(issues, `${path}.logic`, "invalid_condition_logic", "Expected all or any.");
|
|
535
|
+
}
|
|
536
|
+
if (!Array.isArray(value.conditions)) {
|
|
537
|
+
issue(issues, `${path}.conditions`, "invalid_condition_group", "Expected a conditions array.");
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
value.conditions.forEach((condition, index) => {
|
|
541
|
+
const conditionPath = `${path}.conditions[${index}]`;
|
|
542
|
+
if (isRecord(condition) && Object.hasOwn(condition, "logic")) {
|
|
543
|
+
validateDisplayConditionGroup(condition, conditionPath, issues);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!isRecord(condition)) {
|
|
547
|
+
issue(issues, conditionPath, "invalid_condition", "Expected a field display condition.");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!isNonEmptyString(condition.fieldId)) {
|
|
551
|
+
issue(issues, `${conditionPath}.fieldId`, "invalid_condition_source", "Expected a field ID.");
|
|
552
|
+
}
|
|
553
|
+
if (typeof condition.operator !== "string" || !CONDITION_OPERATORS.has(condition.operator)) {
|
|
554
|
+
issue(issues, `${conditionPath}.operator`, "invalid_condition_operator", "Unsupported condition operator.");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const needsValue = !["not_empty", "is_empty", "is_not_empty"].includes(condition.operator);
|
|
558
|
+
const hasValue = Object.hasOwn(condition, "value") && condition.value !== void 0;
|
|
559
|
+
if (needsValue && !hasValue)
|
|
560
|
+
issue(issues, `${conditionPath}.value`, "missing_condition_value", "A value is required.");
|
|
561
|
+
});
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
function validateDisplayRule(value, path, issues) {
|
|
565
|
+
if (!isRecord(value)) {
|
|
566
|
+
issue(issues, path, "invalid_display_rule", "Expected a display rule object.");
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
if (value.action !== "show" && value.action !== "hide") {
|
|
570
|
+
issue(issues, `${path}.action`, "invalid_display_action", "Expected show or hide.");
|
|
571
|
+
}
|
|
572
|
+
validateDisplayConditionGroup(value.condition, `${path}.condition`, issues);
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
function validateSubmissionSettings(value, path, issues) {
|
|
576
|
+
if (!isRecord(value)) {
|
|
577
|
+
issue(issues, path, "invalid_submission_settings", "Expected submission settings to be an object.");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
validateExtensibleNode(value, path, issues);
|
|
581
|
+
if (value.showConfirmationBeforeSubmit !== void 0 && typeof value.showConfirmationBeforeSubmit !== "boolean") {
|
|
582
|
+
issue(issues, `${path}.showConfirmationBeforeSubmit`, "invalid_submission_setting", "Expected a boolean.");
|
|
583
|
+
}
|
|
584
|
+
if (value.confirmationRenderMode !== void 0 && !["dialog", "inline", "replace"].includes(String(value.confirmationRenderMode))) {
|
|
585
|
+
issue(
|
|
586
|
+
issues,
|
|
587
|
+
`${path}.confirmationRenderMode`,
|
|
588
|
+
"invalid_submission_setting",
|
|
589
|
+
"Unsupported confirmation render mode."
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
for (const key of ["confirmButtonLabel", "cancelButtonLabel"]) {
|
|
593
|
+
if (value[key] !== void 0 && !isNonEmptyString(value[key]))
|
|
594
|
+
issue(issues, `${path}.${key}`, "invalid_submission_setting", "Expected non-empty text.");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
488
597
|
function validateField(value, path, issues) {
|
|
489
598
|
if (!isRecord(value)) {
|
|
490
599
|
issue(issues, path, "invalid_field", "Expected a field object.");
|
|
@@ -508,6 +617,7 @@ function validateField(value, path, issues) {
|
|
|
508
617
|
if (value.displayCondition !== void 0) {
|
|
509
618
|
validateDisplayCondition(value.displayCondition, `${path}.displayCondition`, issues);
|
|
510
619
|
}
|
|
620
|
+
if (value.displayRule !== void 0) validateDisplayRule(value.displayRule, `${path}.displayRule`, issues);
|
|
511
621
|
if (value.translations !== void 0) validateLocalizedTextMap(value.translations, `${path}.translations`, issues);
|
|
512
622
|
if (typeof value.type !== "string" || !FIELD_TYPES.has(value.type)) {
|
|
513
623
|
issue(issues, `${path}.type`, "invalid_field_type", "Unsupported field type.");
|
|
@@ -892,6 +1002,8 @@ function validateFormSchema(input, options = {}) {
|
|
|
892
1002
|
}
|
|
893
1003
|
}
|
|
894
1004
|
if (input.translations !== void 0) validateLocalizedTextMap(input.translations, "translations", issues);
|
|
1005
|
+
if (input.submissionSettings !== void 0)
|
|
1006
|
+
validateSubmissionSettings(input.submissionSettings, "submissionSettings", issues);
|
|
895
1007
|
if (!Array.isArray(input.fields) || input.fields.length === 0) {
|
|
896
1008
|
issue(issues, "fields", "invalid_fields", "Expected at least one field.");
|
|
897
1009
|
} else {
|
|
@@ -915,8 +1027,19 @@ function validateFormSchema(input, options = {}) {
|
|
|
915
1027
|
continue;
|
|
916
1028
|
}
|
|
917
1029
|
const fieldIndex = inputFields.findIndex((field) => isRecord(field) && field.id === structuralIssue.questionId);
|
|
918
|
-
const
|
|
919
|
-
|
|
1030
|
+
const currentField = inputFields[fieldIndex];
|
|
1031
|
+
const usesDisplayRule = isRecord(currentField) && currentField.displayRule !== void 0;
|
|
1032
|
+
const code = structuralIssue.type === "dangling_condition_reference" ? "unknown_condition_source" : structuralIssue.type === "self_condition_reference" ? "self_condition" : usesDisplayRule ? "circular_display_condition" : "condition_cycle";
|
|
1033
|
+
const path = usesDisplayRule ? `fields[${fieldIndex}].displayRule` : `fields[${fieldIndex}].displayCondition`;
|
|
1034
|
+
if (usesDisplayRule && structuralIssue.type === "cyclic_condition_reference") {
|
|
1035
|
+
issues.push({
|
|
1036
|
+
path,
|
|
1037
|
+
code,
|
|
1038
|
+
type: code,
|
|
1039
|
+
message: structuralIssue.message,
|
|
1040
|
+
...structuralIssue.cycle === void 0 ? {} : { cycle: structuralIssue.cycle }
|
|
1041
|
+
});
|
|
1042
|
+
} else issue(issues, path, code, structuralIssue.message);
|
|
920
1043
|
}
|
|
921
1044
|
if (input.pages !== void 0) {
|
|
922
1045
|
if (!Array.isArray(input.pages) || input.pages.length === 0) {
|
|
@@ -1025,23 +1148,61 @@ function normalizeString(value) {
|
|
|
1025
1148
|
return value.trim().normalize("NFKC").toLowerCase();
|
|
1026
1149
|
}
|
|
1027
1150
|
function valuesEqual(left, right) {
|
|
1028
|
-
|
|
1151
|
+
if (typeof left === "string" && typeof right === "string") return normalizeString(left) === normalizeString(right);
|
|
1152
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
1153
|
+
return left.length === right.length && left.every((item, index) => valuesEqual(item, right[index]));
|
|
1154
|
+
}
|
|
1155
|
+
return left === right;
|
|
1156
|
+
}
|
|
1157
|
+
function evaluateFieldCondition(condition, currentAnswers) {
|
|
1158
|
+
const sourceId = "fieldId" in condition ? condition.fieldId : condition.questionId;
|
|
1159
|
+
const answer = currentAnswers[sourceId];
|
|
1160
|
+
if (isEmpty(answer) && ["equals", "not_equals", "contains"].includes(condition.operator)) return false;
|
|
1161
|
+
if (condition.operator === "is_empty") return isEmpty(answer);
|
|
1162
|
+
if (condition.operator === "is_not_empty" || condition.operator === "not_empty") return !isEmpty(answer);
|
|
1163
|
+
if (condition.operator === "contains" || condition.operator === "not_contains") {
|
|
1164
|
+
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));
|
|
1165
|
+
return condition.operator === "not_contains" ? !contains : contains;
|
|
1166
|
+
}
|
|
1167
|
+
if (condition.value === void 0) return false;
|
|
1168
|
+
if (condition.operator === "equals") return valuesEqual(answer, condition.value);
|
|
1169
|
+
if (condition.operator === "not_equals") return !valuesEqual(answer, condition.value);
|
|
1170
|
+
if (condition.operator === "greater_than")
|
|
1171
|
+
return typeof answer === "number" && typeof condition.value === "number" && answer > condition.value;
|
|
1172
|
+
if (condition.operator === "less_than")
|
|
1173
|
+
return typeof answer === "number" && typeof condition.value === "number" && answer < condition.value;
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
function isDisplayConditionGroupSatisfied(group, currentAnswers) {
|
|
1177
|
+
const results = group.conditions.map(
|
|
1178
|
+
(condition) => "logic" in condition ? isDisplayConditionGroupSatisfied(condition, currentAnswers) : evaluateFieldCondition(condition, currentAnswers)
|
|
1179
|
+
);
|
|
1180
|
+
return group.logic === "all" ? results.every(Boolean) : results.some(Boolean);
|
|
1029
1181
|
}
|
|
1030
1182
|
function isQuestionVisible(question, currentAnswers) {
|
|
1183
|
+
if (question.displayRule !== void 0) {
|
|
1184
|
+
const satisfied = isDisplayConditionGroupSatisfied(question.displayRule.condition, currentAnswers);
|
|
1185
|
+
return question.displayRule.action === "hide" ? !satisfied : satisfied;
|
|
1186
|
+
}
|
|
1031
1187
|
return isDisplayConditionSatisfied(question.displayCondition, currentAnswers);
|
|
1032
1188
|
}
|
|
1033
1189
|
function isDisplayConditionSatisfied(condition, currentAnswers) {
|
|
1034
1190
|
if (condition === void 0) return true;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (condition
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1191
|
+
if ("logic" in condition) return isDisplayConditionGroupSatisfied(condition, currentAnswers);
|
|
1192
|
+
return evaluateFieldCondition(condition, currentAnswers);
|
|
1193
|
+
}
|
|
1194
|
+
function conditionSourceIds(condition) {
|
|
1195
|
+
if (condition === void 0) return [];
|
|
1196
|
+
if ("questionId" in condition) return [condition.questionId];
|
|
1197
|
+
const ids = [];
|
|
1198
|
+
const visit = (group) => {
|
|
1199
|
+
for (const item of group.conditions) {
|
|
1200
|
+
if ("logic" in item) visit(item);
|
|
1201
|
+
else ids.push(item.fieldId);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
visit(condition.condition);
|
|
1205
|
+
return ids;
|
|
1045
1206
|
}
|
|
1046
1207
|
function calculateBaseFieldVisibility(schema, currentAnswers) {
|
|
1047
1208
|
const fields = new Map(schema.fields.map((field) => [field.id, field]));
|
|
@@ -1052,10 +1213,12 @@ function calculateBaseFieldVisibility(schema, currentAnswers) {
|
|
|
1052
1213
|
if (existing !== void 0) return existing;
|
|
1053
1214
|
if (resolving.has(field.id)) return false;
|
|
1054
1215
|
resolving.add(field.id);
|
|
1055
|
-
const
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1216
|
+
const sources = field.displayRule === void 0 ? conditionSourceIds(field.displayCondition) : conditionSourceIds(field.displayRule);
|
|
1217
|
+
const sourcesVisible = sources.every((sourceId) => {
|
|
1218
|
+
const source = fields.get(sourceId);
|
|
1219
|
+
return source !== void 0 && source.id !== field.id && resolve(source);
|
|
1220
|
+
});
|
|
1221
|
+
const visible = sourcesVisible && isQuestionVisible(field, currentAnswers);
|
|
1059
1222
|
resolving.delete(field.id);
|
|
1060
1223
|
resolved.set(field.id, visible);
|
|
1061
1224
|
return visible;
|
|
@@ -2053,6 +2216,30 @@ function createSubmission(schema, values, options) {
|
|
|
2053
2216
|
}
|
|
2054
2217
|
|
|
2055
2218
|
// src/translation.ts
|
|
2219
|
+
var computeSourceTextHash = (text) => {
|
|
2220
|
+
const normalized = text.normalize("NFKC").trim().replace(/\s+/gu, " ");
|
|
2221
|
+
let hash = 2166136261;
|
|
2222
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
2223
|
+
hash ^= normalized.charCodeAt(index);
|
|
2224
|
+
hash = Math.imul(hash, 16777619);
|
|
2225
|
+
}
|
|
2226
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
2227
|
+
};
|
|
2228
|
+
function getTranslationStatus(sourceText, translatedText, metadata) {
|
|
2229
|
+
if (translatedText === void 0 || translatedText.trim() === "") return "missing";
|
|
2230
|
+
if (metadata === void 0) return "translated";
|
|
2231
|
+
const currentHash = computeSourceTextHash(sourceText);
|
|
2232
|
+
const sourceHash = typeof metadata.sourceTextHash === "string" ? metadata.sourceTextHash : void 0;
|
|
2233
|
+
const isManual = metadata.translationSource === "manual" || "isManual" in metadata && metadata.isManual === true;
|
|
2234
|
+
if (isManual) return sourceHash === void 0 || sourceHash === currentHash ? "manual" : "manual-stale";
|
|
2235
|
+
return sourceHash === void 0 || sourceHash === currentHash ? "translated" : "stale";
|
|
2236
|
+
}
|
|
2237
|
+
async function translateBatch(adapter, texts, locale, sourceLocale) {
|
|
2238
|
+
if ("translateBatch" in adapter) return adapter.translateBatch(texts, locale, sourceLocale);
|
|
2239
|
+
return texts.map(
|
|
2240
|
+
(text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2056
2243
|
function mergeLocalizedText(translations, locale, property, value) {
|
|
2057
2244
|
return { ...translations, [locale]: { ...translations?.[locale], [property]: value } };
|
|
2058
2245
|
}
|
|
@@ -2070,12 +2257,17 @@ function withTranslationMetadata(node, locale, property, metadata) {
|
|
|
2070
2257
|
};
|
|
2071
2258
|
}
|
|
2072
2259
|
function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
|
|
2260
|
+
const status = getTranslationStatus(sourceText, existingText, existingTranslationMetadata);
|
|
2073
2261
|
return {
|
|
2074
2262
|
kind,
|
|
2075
2263
|
nodeId,
|
|
2076
2264
|
property,
|
|
2077
2265
|
locale,
|
|
2078
2266
|
sourceText,
|
|
2267
|
+
target: { kind, ...nodeId.length === 0 ? {} : { id: nodeId }, property },
|
|
2268
|
+
path: `${kind}.${nodeId}.${property}`,
|
|
2269
|
+
sourceTextHash: computeSourceTextHash(sourceText),
|
|
2270
|
+
status,
|
|
2079
2271
|
...existingText === void 0 ? {} : { existingText },
|
|
2080
2272
|
...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
|
|
2081
2273
|
...existingTranslationMetadata === void 0 ? {} : { existingTranslationMetadata }
|
|
@@ -2214,6 +2406,9 @@ function translationSlots(schema, locale) {
|
|
|
2214
2406
|
});
|
|
2215
2407
|
return descriptors;
|
|
2216
2408
|
}
|
|
2409
|
+
function collectTranslationSlots(schema, locale) {
|
|
2410
|
+
return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
|
|
2411
|
+
}
|
|
2217
2412
|
function resolveLocalizedSchema(schema, targetLocale) {
|
|
2218
2413
|
if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
2219
2414
|
const formTranslation = schema.translations?.[targetLocale];
|
|
@@ -2270,17 +2465,31 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2270
2465
|
}
|
|
2271
2466
|
const updatedSlots = [];
|
|
2272
2467
|
const skippedSlots = [];
|
|
2468
|
+
const staleSlots = [];
|
|
2469
|
+
const skippedReasons = {};
|
|
2273
2470
|
let result = schema;
|
|
2274
2471
|
for (const locale of locales) {
|
|
2275
2472
|
const descriptors = translationSlots(schema, locale);
|
|
2473
|
+
if (options.markStaleTranslations ?? true) {
|
|
2474
|
+
for (const descriptor of descriptors) {
|
|
2475
|
+
if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
|
|
2476
|
+
staleSlots.push(descriptor.slot);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2276
2479
|
const selected = [];
|
|
2277
2480
|
for (const descriptor of descriptors) {
|
|
2278
|
-
const
|
|
2481
|
+
const status = descriptor.slot.status ?? "missing";
|
|
2482
|
+
const manual = status === "manual" || status === "manual-stale";
|
|
2483
|
+
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")));
|
|
2279
2484
|
if (shouldTranslate) selected.push(descriptor);
|
|
2280
|
-
else
|
|
2485
|
+
else {
|
|
2486
|
+
skippedSlots.push(descriptor.slot);
|
|
2487
|
+
skippedReasons[descriptor.slot.path ?? `${descriptor.slot.kind}.${descriptor.slot.nodeId}.${descriptor.slot.property}`] = manual ? "manual" : "unchanged";
|
|
2488
|
+
}
|
|
2281
2489
|
}
|
|
2282
2490
|
if (selected.length === 0) continue;
|
|
2283
|
-
const translated = await
|
|
2491
|
+
const translated = await translateBatch(
|
|
2492
|
+
adapter,
|
|
2284
2493
|
selected.map((descriptor) => descriptor.slot.sourceText),
|
|
2285
2494
|
locale,
|
|
2286
2495
|
schema.defaultLocale
|
|
@@ -2291,7 +2500,12 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2291
2500
|
selected.forEach((descriptor, index) => {
|
|
2292
2501
|
const translatedText = translated[index];
|
|
2293
2502
|
if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
2294
|
-
const metadata = options.createMetadata?.(descriptor.slot, translatedText)
|
|
2503
|
+
const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
|
|
2504
|
+
sourceLocale: schema.defaultLocale ?? "",
|
|
2505
|
+
sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
|
|
2506
|
+
translationSource: "automatic",
|
|
2507
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2508
|
+
};
|
|
2295
2509
|
result = descriptor.apply(result, translatedText, metadata);
|
|
2296
2510
|
updatedSlots.push(descriptor.slot);
|
|
2297
2511
|
});
|
|
@@ -2305,7 +2519,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
|
|
|
2305
2519
|
];
|
|
2306
2520
|
if (supportedLocales.length > 0) result = { ...result, supportedLocales };
|
|
2307
2521
|
assertValidFormSchema(result);
|
|
2308
|
-
return { schema: result, report: { updatedSlots, skippedSlots } };
|
|
2522
|
+
return { schema: result, report: { updatedSlots, skippedSlots, staleSlots, skippedReasons } };
|
|
2309
2523
|
}
|
|
2310
2524
|
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
2311
2525
|
const populated = await populateSchemaTranslations(
|
|
@@ -2609,6 +2823,8 @@ function assertVersionMutable(status) {
|
|
|
2609
2823
|
calculatePageVisibility,
|
|
2610
2824
|
cloneVersionToDraft,
|
|
2611
2825
|
collectSchemaLocales,
|
|
2826
|
+
collectTranslationSlots,
|
|
2827
|
+
computeSourceTextHash,
|
|
2612
2828
|
createCloneTransitionPlan,
|
|
2613
2829
|
createDeleteDraftTransitionPlan,
|
|
2614
2830
|
createPublishTransitionPlan,
|
|
@@ -2623,6 +2839,8 @@ function assertVersionMutable(status) {
|
|
|
2623
2839
|
escapeCsvCell,
|
|
2624
2840
|
exportResponsesToCsv,
|
|
2625
2841
|
exportResponsesToCsvStream,
|
|
2842
|
+
getTranslationStatus,
|
|
2843
|
+
isDisplayConditionGroupSatisfied,
|
|
2626
2844
|
isDisplayConditionSatisfied,
|
|
2627
2845
|
isQuestionVisible,
|
|
2628
2846
|
iterateSubmissionPages,
|