@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/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 resolved = /* @__PURE__ */ new Set();
118
- for (const startId of firstById.keys()) {
119
- if (resolved.has(startId)) continue;
120
- const path = [];
121
- const pathIndex = /* @__PURE__ */ new Map();
122
- let currentId = startId;
123
- while (currentId !== void 0 && firstById.has(currentId) && !resolved.has(currentId)) {
124
- const existingIndex = pathIndex.get(currentId);
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
- for (const id of path) resolved.add(id);
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 = field.displayCondition?.questionId;
169
- if (sourceId === void 0) continue;
170
- if (sourceId === field.id) {
171
- issues.push({
172
- type: "self_condition_reference",
173
- questionId: field.id,
174
- message: `Question "${field.id}" cannot depend on itself.`
175
- });
176
- } else if (!questionIds.has(sourceId)) {
177
- issues.push({
178
- type: "dangling_condition_reference",
179
- questionId: field.id,
180
- message: `Question "${field.id}" references missing question "${sourceId}".`
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
- const sourceId = field.displayCondition?.questionId;
205
- if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
206
- return field;
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(["equals", "not_equals", "contains", "not_empty"]);
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 code = structuralIssue.type === "dangling_condition_reference" ? "unknown_condition_source" : structuralIssue.type === "self_condition_reference" ? "self_condition" : "condition_cycle";
850
- issue(issues, `fields[${fieldIndex}].displayCondition`, code, structuralIssue.message);
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
- return typeof left === "string" && typeof right === "string" ? normalizeString(left) === normalizeString(right) : left === right;
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
- const answer = currentAnswers[condition.questionId];
967
- if (isEmpty(answer)) return false;
968
- if (condition.operator === "not_empty") return true;
969
- if (condition.value === void 0) return false;
970
- if (condition.operator === "equals") return valuesEqual(answer, condition.value);
971
- if (condition.operator === "not_equals") return !valuesEqual(answer, condition.value);
972
- if (typeof answer === "string" && typeof condition.value === "string") {
973
- return normalizeString(answer).includes(normalizeString(condition.value));
974
- }
975
- return Array.isArray(answer) && answer.some((item) => valuesEqual(item, condition.value));
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 sourceId = field.displayCondition?.questionId;
987
- const source = sourceId === void 0 ? void 0 : fields.get(sourceId);
988
- const sourceVisible = source === void 0 ? sourceId === void 0 : resolve(source);
989
- const visible = sourceVisible && isQuestionVisible(field, currentAnswers);
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 shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? (options.overwrite === "all" || descriptor.slot.existingText === void 0);
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 skippedSlots.push(descriptor.slot);
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 adapter.translateBatch(
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "4.3.2",
3
+ "version": "4.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },