@form-engine-ts/core 1.0.0 → 1.1.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 +25 -0
- package/dist/index.cjs +447 -50
- package/dist/index.d.cts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.js +440 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -86,17 +86,46 @@ function validateSchemaStructure(schema) {
|
|
|
86
86
|
function sanitizeSchema(schema) {
|
|
87
87
|
const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
|
|
88
88
|
const cyclic = cyclicQuestionIds(schema.fields);
|
|
89
|
-
|
|
89
|
+
const sanitizedFields = schema.fields.map((field) => {
|
|
90
|
+
const sourceId = field.displayCondition?.questionId;
|
|
91
|
+
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
92
|
+
return field;
|
|
93
|
+
}
|
|
94
|
+
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
95
|
+
return sanitized;
|
|
96
|
+
});
|
|
97
|
+
const base = {
|
|
90
98
|
...schema,
|
|
91
|
-
fields:
|
|
92
|
-
const sourceId = field.displayCondition?.questionId;
|
|
93
|
-
if (sourceId === void 0 || existingQuestionIds.has(sourceId) && sourceId !== field.id && !cyclic.has(field.id)) {
|
|
94
|
-
return field;
|
|
95
|
-
}
|
|
96
|
-
const { displayCondition: _displayCondition, ...sanitized } = field;
|
|
97
|
-
return sanitized;
|
|
98
|
-
})
|
|
99
|
+
fields: sanitizedFields
|
|
99
100
|
};
|
|
101
|
+
if (schema.pages === void 0) return base;
|
|
102
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
103
|
+
const pages = schema.pages.map((page) => ({
|
|
104
|
+
...page,
|
|
105
|
+
questionIds: page.questionIds.filter((id) => {
|
|
106
|
+
if (!existingQuestionIds.has(id) || assigned.has(id)) return false;
|
|
107
|
+
assigned.add(id);
|
|
108
|
+
return true;
|
|
109
|
+
})
|
|
110
|
+
})).filter((page) => page.questionIds.length > 0);
|
|
111
|
+
if (pages.length === 0) {
|
|
112
|
+
const { pages: _pages, ...singlePage } = base;
|
|
113
|
+
return singlePage;
|
|
114
|
+
}
|
|
115
|
+
const unassigned = schema.fields.map((field) => field.id).filter((id) => !assigned.has(id));
|
|
116
|
+
const completePages = pages.map(
|
|
117
|
+
(page, index) => index === pages.length - 1 ? { ...page, questionIds: [...page.questionIds, ...unassigned] } : page
|
|
118
|
+
);
|
|
119
|
+
const pageIndexByQuestion = new Map(
|
|
120
|
+
completePages.flatMap((page, pageIndex) => page.questionIds.map((id) => [id, pageIndex]))
|
|
121
|
+
);
|
|
122
|
+
const safePages = completePages.map((page, pageIndex) => {
|
|
123
|
+
const sourceIndex = page.displayCondition === void 0 ? void 0 : pageIndexByQuestion.get(page.displayCondition.questionId);
|
|
124
|
+
if (page.displayCondition === void 0 || sourceIndex !== void 0 && sourceIndex < pageIndex) return page;
|
|
125
|
+
const { displayCondition: _displayCondition, ...safePage } = page;
|
|
126
|
+
return safePage;
|
|
127
|
+
});
|
|
128
|
+
return { ...base, pages: safePages };
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
// src/schema.ts
|
|
@@ -111,6 +140,34 @@ function isNonEmptyString(value) {
|
|
|
111
140
|
function issue(issues, path, code, message) {
|
|
112
141
|
issues.push({ path, code, message });
|
|
113
142
|
}
|
|
143
|
+
function validateLocalizedTextMap(value, path, issues) {
|
|
144
|
+
if (!isRecord(value)) {
|
|
145
|
+
issue(issues, path, "invalid_translations", "Expected a locale-to-translation object.");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const [locale, translation] of Object.entries(value)) {
|
|
149
|
+
if (!isNonEmptyString(locale) || !isRecord(translation)) {
|
|
150
|
+
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a translation object.");
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const key of ["title", "description"]) {
|
|
154
|
+
if (translation[key] !== void 0 && !isNonEmptyString(translation[key])) {
|
|
155
|
+
issue(issues, `${path}.${locale}.${key}`, "invalid_translation", "Expected non-empty translated text.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function validateOptionTranslations(value, path, issues) {
|
|
161
|
+
if (!isRecord(value)) {
|
|
162
|
+
issue(issues, path, "invalid_translations", "Expected a locale-to-label object.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const [locale, label] of Object.entries(value)) {
|
|
166
|
+
if (!isNonEmptyString(locale) || !isNonEmptyString(label)) {
|
|
167
|
+
issue(issues, `${path}.${locale}`, "invalid_translation", "Expected a non-empty translated label.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
114
171
|
function rejectLegacyProperties(value, path, properties, issues) {
|
|
115
172
|
for (const property of properties) {
|
|
116
173
|
if (Object.hasOwn(value, property)) {
|
|
@@ -154,6 +211,8 @@ function validateOptions(value, path, issues) {
|
|
|
154
211
|
if (!isNonEmptyString(option.label)) {
|
|
155
212
|
issue(issues, `${optionPath}.label`, "invalid_label", "Expected a non-empty option label.");
|
|
156
213
|
}
|
|
214
|
+
if (option.translations !== void 0)
|
|
215
|
+
validateOptionTranslations(option.translations, `${optionPath}.translations`, issues);
|
|
157
216
|
});
|
|
158
217
|
return true;
|
|
159
218
|
}
|
|
@@ -203,6 +262,7 @@ function validateField(value, path, issues) {
|
|
|
203
262
|
if (value.displayCondition !== void 0) {
|
|
204
263
|
validateDisplayCondition(value.displayCondition, `${path}.displayCondition`, issues);
|
|
205
264
|
}
|
|
265
|
+
if (value.translations !== void 0) validateLocalizedTextMap(value.translations, `${path}.translations`, issues);
|
|
206
266
|
if (typeof value.type !== "string" || !FIELD_TYPES.has(value.type)) {
|
|
207
267
|
issue(issues, `${path}.type`, "invalid_field_type", "Unsupported field type.");
|
|
208
268
|
return false;
|
|
@@ -288,6 +348,26 @@ function validateFormSchema(input) {
|
|
|
288
348
|
);
|
|
289
349
|
}
|
|
290
350
|
}
|
|
351
|
+
if (input.defaultLocale !== void 0 && !isNonEmptyString(input.defaultLocale)) {
|
|
352
|
+
issue(issues, "defaultLocale", "invalid_locale", "Expected a non-empty default locale.");
|
|
353
|
+
}
|
|
354
|
+
if (input.supportedLocales !== void 0) {
|
|
355
|
+
if (!Array.isArray(input.supportedLocales) || input.supportedLocales.length === 0) {
|
|
356
|
+
issue(issues, "supportedLocales", "invalid_locales", "Expected at least one supported locale.");
|
|
357
|
+
} else {
|
|
358
|
+
const locales = /* @__PURE__ */ new Set();
|
|
359
|
+
input.supportedLocales.forEach((locale, index) => {
|
|
360
|
+
if (!isNonEmptyString(locale)) {
|
|
361
|
+
issue(issues, `supportedLocales[${index}]`, "invalid_locale", "Expected a non-empty locale.");
|
|
362
|
+
} else if (locales.has(locale)) {
|
|
363
|
+
issue(issues, `supportedLocales[${index}]`, "duplicate_locale", "Locales must be unique.");
|
|
364
|
+
} else {
|
|
365
|
+
locales.add(locale);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (input.translations !== void 0) validateLocalizedTextMap(input.translations, "translations", issues);
|
|
291
371
|
if (!Array.isArray(input.fields) || input.fields.length === 0) {
|
|
292
372
|
issue(issues, "fields", "invalid_fields", "Expected at least one field.");
|
|
293
373
|
} else {
|
|
@@ -314,6 +394,80 @@ function validateFormSchema(input) {
|
|
|
314
394
|
const code = structuralIssue.type === "dangling_condition_reference" ? "unknown_condition_source" : structuralIssue.type === "self_condition_reference" ? "self_condition" : "condition_cycle";
|
|
315
395
|
issue(issues, `fields[${fieldIndex}].displayCondition`, code, structuralIssue.message);
|
|
316
396
|
}
|
|
397
|
+
if (input.pages !== void 0) {
|
|
398
|
+
if (!Array.isArray(input.pages) || input.pages.length === 0) {
|
|
399
|
+
issue(issues, "pages", "invalid_pages", "Expected at least one page when pages is defined.");
|
|
400
|
+
} else {
|
|
401
|
+
const pageIds = /* @__PURE__ */ new Set();
|
|
402
|
+
const assigned = /* @__PURE__ */ new Map();
|
|
403
|
+
input.pages.forEach((page, pageIndex) => {
|
|
404
|
+
const pagePath = `pages[${pageIndex}]`;
|
|
405
|
+
if (!isRecord(page)) {
|
|
406
|
+
issue(issues, pagePath, "invalid_page", "Expected a page object.");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!isNonEmptyString(page.id)) {
|
|
410
|
+
issue(issues, `${pagePath}.id`, "invalid_page_id", "Expected a non-empty page ID.");
|
|
411
|
+
} else if (pageIds.has(page.id)) {
|
|
412
|
+
issue(issues, `${pagePath}.id`, "duplicate_page", "Page IDs must be unique.");
|
|
413
|
+
} else {
|
|
414
|
+
pageIds.add(page.id);
|
|
415
|
+
}
|
|
416
|
+
for (const key of ["title", "description"]) {
|
|
417
|
+
if (page[key] !== void 0 && !isNonEmptyString(page[key])) {
|
|
418
|
+
issue(issues, `${pagePath}.${key}`, "invalid_page_text", "Expected non-empty page text.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (page.translations !== void 0) {
|
|
422
|
+
validateLocalizedTextMap(page.translations, `${pagePath}.translations`, issues);
|
|
423
|
+
}
|
|
424
|
+
if (page.displayCondition !== void 0) {
|
|
425
|
+
validateDisplayCondition(page.displayCondition, `${pagePath}.displayCondition`, issues);
|
|
426
|
+
}
|
|
427
|
+
if (!Array.isArray(page.questionIds) || page.questionIds.length === 0) {
|
|
428
|
+
issue(issues, `${pagePath}.questionIds`, "invalid_page_questions", "Expected at least one question ID.");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
page.questionIds.forEach((questionId, questionIndex) => {
|
|
432
|
+
const questionPath = `${pagePath}.questionIds[${questionIndex}]`;
|
|
433
|
+
if (!isNonEmptyString(questionId)) {
|
|
434
|
+
issue(issues, questionPath, "invalid_question_reference", "Expected a question ID.");
|
|
435
|
+
} else if (!ids.has(questionId)) {
|
|
436
|
+
issue(issues, questionPath, "unknown_page_question", "Page references an unknown question.");
|
|
437
|
+
} else if (assigned.has(questionId)) {
|
|
438
|
+
issue(issues, questionPath, "duplicate_page_question", "A question may belong to only one page.");
|
|
439
|
+
} else {
|
|
440
|
+
assigned.set(questionId, pageIndex);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
for (const fieldId of ids) {
|
|
445
|
+
if (!assigned.has(fieldId))
|
|
446
|
+
issue(issues, "pages", "unassigned_page_question", `Question ${fieldId} has no page.`);
|
|
447
|
+
}
|
|
448
|
+
input.pages.forEach((page, pageIndex) => {
|
|
449
|
+
if (!isRecord(page) || !isRecord(page.displayCondition)) return;
|
|
450
|
+
const sourceId = page.displayCondition.questionId;
|
|
451
|
+
if (typeof sourceId !== "string") return;
|
|
452
|
+
const sourcePageIndex = assigned.get(sourceId);
|
|
453
|
+
if (sourcePageIndex === void 0) {
|
|
454
|
+
issue(
|
|
455
|
+
issues,
|
|
456
|
+
`pages[${pageIndex}].displayCondition.questionId`,
|
|
457
|
+
"unknown_page_condition_source",
|
|
458
|
+
"Page condition references an unknown question."
|
|
459
|
+
);
|
|
460
|
+
} else if (sourcePageIndex >= pageIndex) {
|
|
461
|
+
issue(
|
|
462
|
+
issues,
|
|
463
|
+
`pages[${pageIndex}].displayCondition.questionId`,
|
|
464
|
+
"forward_page_condition",
|
|
465
|
+
"Page conditions must reference a question on an earlier page."
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
317
471
|
}
|
|
318
472
|
return issues.length === 0 ? { valid: true, value: input, issues: [] } : { valid: false, issues };
|
|
319
473
|
}
|
|
@@ -341,7 +495,9 @@ function valuesEqual(left, right) {
|
|
|
341
495
|
return typeof left === "string" && typeof right === "string" ? normalizeString(left) === normalizeString(right) : left === right;
|
|
342
496
|
}
|
|
343
497
|
function isQuestionVisible(question, currentAnswers) {
|
|
344
|
-
|
|
498
|
+
return isDisplayConditionSatisfied(question.displayCondition, currentAnswers);
|
|
499
|
+
}
|
|
500
|
+
function isDisplayConditionSatisfied(condition, currentAnswers) {
|
|
345
501
|
if (condition === void 0) return true;
|
|
346
502
|
const answer = currentAnswers[condition.questionId];
|
|
347
503
|
if (isEmpty(answer)) return false;
|
|
@@ -354,7 +510,7 @@ function isQuestionVisible(question, currentAnswers) {
|
|
|
354
510
|
}
|
|
355
511
|
return Array.isArray(answer) && answer.some((item) => valuesEqual(item, condition.value));
|
|
356
512
|
}
|
|
357
|
-
function
|
|
513
|
+
function calculateBaseFieldVisibility(schema, currentAnswers) {
|
|
358
514
|
const fields = new Map(schema.fields.map((field) => [field.id, field]));
|
|
359
515
|
const resolved = /* @__PURE__ */ new Map();
|
|
360
516
|
const resolving = /* @__PURE__ */ new Set();
|
|
@@ -372,7 +528,34 @@ function calculateFieldVisibility(schema, currentAnswers) {
|
|
|
372
528
|
return visible;
|
|
373
529
|
};
|
|
374
530
|
for (const field of schema.fields) resolve(field);
|
|
375
|
-
return Object.
|
|
531
|
+
return Object.fromEntries(resolved);
|
|
532
|
+
}
|
|
533
|
+
function calculatePageVisibility(schema, currentAnswers) {
|
|
534
|
+
if (schema.pages === void 0 || schema.pages.length === 0) return {};
|
|
535
|
+
const baseFieldVisibility = calculateBaseFieldVisibility(schema, currentAnswers);
|
|
536
|
+
const pageByQuestion = new Map(schema.pages.flatMap((page) => page.questionIds.map((id) => [id, page.id])));
|
|
537
|
+
const pageVisibility = {};
|
|
538
|
+
for (const page of schema.pages) {
|
|
539
|
+
const sourceId = page.displayCondition?.questionId;
|
|
540
|
+
const sourcePageId = sourceId === void 0 ? void 0 : pageByQuestion.get(sourceId);
|
|
541
|
+
const sourceVisible = sourceId === void 0 || baseFieldVisibility[sourceId] === true && sourcePageId !== void 0 && pageVisibility[sourcePageId] === true;
|
|
542
|
+
pageVisibility[page.id] = sourceVisible && isDisplayConditionSatisfied(page.displayCondition, currentAnswers);
|
|
543
|
+
}
|
|
544
|
+
return Object.freeze(pageVisibility);
|
|
545
|
+
}
|
|
546
|
+
function calculateFieldVisibility(schema, currentAnswers) {
|
|
547
|
+
const baseVisibility = calculateBaseFieldVisibility(schema, currentAnswers);
|
|
548
|
+
if (schema.pages === void 0 || schema.pages.length === 0) return Object.freeze(baseVisibility);
|
|
549
|
+
const pageVisibility = calculatePageVisibility(schema, currentAnswers);
|
|
550
|
+
const pageByQuestion = new Map(schema.pages.flatMap((page) => page.questionIds.map((id) => [id, page.id])));
|
|
551
|
+
return Object.freeze(
|
|
552
|
+
Object.fromEntries(
|
|
553
|
+
schema.fields.map((field) => {
|
|
554
|
+
const pageId = pageByQuestion.get(field.id);
|
|
555
|
+
return [field.id, baseVisibility[field.id] === true && pageId !== void 0 && pageVisibility[pageId] === true];
|
|
556
|
+
})
|
|
557
|
+
)
|
|
558
|
+
);
|
|
376
559
|
}
|
|
377
560
|
function selectVisibleAnswers(schema, currentAnswers) {
|
|
378
561
|
const visibility = calculateFieldVisibility(schema, currentAnswers);
|
|
@@ -406,6 +589,23 @@ function calculateNumericSummary(responses, questionId) {
|
|
|
406
589
|
max: numbers.length === 0 ? null : Math.max(...numbers)
|
|
407
590
|
};
|
|
408
591
|
}
|
|
592
|
+
function calculateCrossTabulation(responses, rowQuestionId, colQuestionId) {
|
|
593
|
+
const matrix = {};
|
|
594
|
+
const rowTotals = {};
|
|
595
|
+
const colTotals = {};
|
|
596
|
+
let grandTotal = 0;
|
|
597
|
+
for (const response of responses) {
|
|
598
|
+
const row = response.values[rowQuestionId];
|
|
599
|
+
const col = response.values[colQuestionId];
|
|
600
|
+
if (typeof row !== "string" || row.length === 0 || typeof col !== "string" || col.length === 0) continue;
|
|
601
|
+
matrix[row] ??= {};
|
|
602
|
+
matrix[row][col] = (matrix[row][col] ?? 0) + 1;
|
|
603
|
+
rowTotals[row] = (rowTotals[row] ?? 0) + 1;
|
|
604
|
+
colTotals[col] = (colTotals[col] ?? 0) + 1;
|
|
605
|
+
grandTotal += 1;
|
|
606
|
+
}
|
|
607
|
+
return { rowQuestionId, colQuestionId, matrix, rowTotals, colTotals, grandTotal };
|
|
608
|
+
}
|
|
409
609
|
function valueIsValid(field, value) {
|
|
410
610
|
if (value === void 0 || value === "") return false;
|
|
411
611
|
if (field.type === "text" || field.type === "textarea") {
|
|
@@ -530,6 +730,47 @@ function exportResponsesToCsv(schema, responses, options = {}) {
|
|
|
530
730
|
return options.withBom ?? true ? `\uFEFF${csv}` : csv;
|
|
531
731
|
}
|
|
532
732
|
|
|
733
|
+
// src/events.ts
|
|
734
|
+
function bytesToHex(bytes) {
|
|
735
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
736
|
+
}
|
|
737
|
+
async function signPayload(payload, secret) {
|
|
738
|
+
if (globalThis.crypto?.subtle === void 0) throw new Error("Web Crypto is unavailable for webhook signing.");
|
|
739
|
+
const encoder = new TextEncoder();
|
|
740
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
741
|
+
"raw",
|
|
742
|
+
encoder.encode(secret),
|
|
743
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
744
|
+
false,
|
|
745
|
+
["sign"]
|
|
746
|
+
);
|
|
747
|
+
return bytesToHex(await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(payload)));
|
|
748
|
+
}
|
|
749
|
+
async function dispatchWebhook(event, config, fetchImpl = globalThis.fetch) {
|
|
750
|
+
const body = JSON.stringify(event);
|
|
751
|
+
const controller = new AbortController();
|
|
752
|
+
const timeoutMs = config.timeoutMs ?? 5e3;
|
|
753
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
754
|
+
try {
|
|
755
|
+
const headers = { "content-type": "application/json", ...config.headers };
|
|
756
|
+
if (config.secret !== void 0) {
|
|
757
|
+
headers["X-Form-Engine-Signature"] = await signPayload(body, config.secret);
|
|
758
|
+
}
|
|
759
|
+
const response = await fetchImpl(config.url, {
|
|
760
|
+
method: "POST",
|
|
761
|
+
headers,
|
|
762
|
+
body,
|
|
763
|
+
signal: controller.signal
|
|
764
|
+
});
|
|
765
|
+
return response.ok ? { success: true, status: response.status } : { success: false, status: response.status, error: `Webhook returned HTTP ${response.status}.` };
|
|
766
|
+
} catch (cause) {
|
|
767
|
+
const error = controller.signal.aborted ? `Webhook request timed out after ${timeoutMs}ms.` : cause instanceof Error ? cause.message : String(cause);
|
|
768
|
+
return { success: false, error };
|
|
769
|
+
} finally {
|
|
770
|
+
globalThis.clearTimeout(timeout);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
533
774
|
// src/validation.ts
|
|
534
775
|
var DEFAULT_MESSAGES = {
|
|
535
776
|
required: "validation.required",
|
|
@@ -656,6 +897,18 @@ function validateAnswers(schema, values) {
|
|
|
656
897
|
}
|
|
657
898
|
return issues.length === 0 ? { valid: true, issues: [] } : { valid: false, issues };
|
|
658
899
|
}
|
|
900
|
+
function validatePageAnswers(schema, pageIndex, values) {
|
|
901
|
+
if (schema.pages === void 0 || schema.pages.length === 0) return validateAnswers(schema, values);
|
|
902
|
+
const page = schema.pages[pageIndex];
|
|
903
|
+
if (page === void 0) return { valid: true, issues: [] };
|
|
904
|
+
const targetIds = new Set(page.questionIds);
|
|
905
|
+
const visibility = calculateFieldVisibility(schema, values);
|
|
906
|
+
const issues = [];
|
|
907
|
+
for (const field of schema.fields) {
|
|
908
|
+
if (targetIds.has(field.id) && visibility[field.id] === true) validateField2(field, values[field.id], issues);
|
|
909
|
+
}
|
|
910
|
+
return issues.length === 0 ? { valid: true, issues: [] } : { valid: false, issues };
|
|
911
|
+
}
|
|
659
912
|
|
|
660
913
|
// src/submission.ts
|
|
661
914
|
function cloneValues(values) {
|
|
@@ -688,62 +941,199 @@ function createSubmission(schema, values, options) {
|
|
|
688
941
|
}
|
|
689
942
|
|
|
690
943
|
// src/translation.ts
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
944
|
+
function mergeLocalizedText(translations, locale, key, value) {
|
|
945
|
+
return { ...translations, [locale]: { ...translations?.[locale], [key]: value } };
|
|
946
|
+
}
|
|
947
|
+
function translationSlots(schema) {
|
|
948
|
+
const slots = [
|
|
949
|
+
{
|
|
950
|
+
text: schema.title,
|
|
951
|
+
apply: (current, value, locale) => ({
|
|
952
|
+
...current,
|
|
953
|
+
translations: mergeLocalizedText(current.translations, locale, "title", value)
|
|
954
|
+
})
|
|
955
|
+
}
|
|
956
|
+
];
|
|
957
|
+
if (schema.description !== void 0) {
|
|
958
|
+
slots.push({
|
|
959
|
+
text: schema.description,
|
|
960
|
+
apply: (current, value, locale) => ({
|
|
961
|
+
...current,
|
|
962
|
+
translations: mergeLocalizedText(current.translations, locale, "description", value)
|
|
963
|
+
})
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
schema.fields.forEach((field, fieldIndex) => {
|
|
967
|
+
slots.push({
|
|
968
|
+
text: field.title,
|
|
969
|
+
apply: (current, value, locale) => ({
|
|
970
|
+
...current,
|
|
971
|
+
fields: current.fields.map(
|
|
972
|
+
(item, index) => index === fieldIndex ? { ...item, translations: mergeLocalizedText(item.translations, locale, "title", value) } : item
|
|
973
|
+
)
|
|
974
|
+
})
|
|
975
|
+
});
|
|
976
|
+
if (field.description !== void 0) {
|
|
977
|
+
slots.push({
|
|
978
|
+
text: field.description,
|
|
979
|
+
apply: (current, value, locale) => ({
|
|
980
|
+
...current,
|
|
981
|
+
fields: current.fields.map(
|
|
982
|
+
(item, index) => index === fieldIndex ? {
|
|
983
|
+
...item,
|
|
984
|
+
translations: mergeLocalizedText(item.translations, locale, "description", value)
|
|
985
|
+
} : item
|
|
986
|
+
)
|
|
987
|
+
})
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
if ("options" in field) {
|
|
991
|
+
field.options.forEach((option, optionIndex) => {
|
|
992
|
+
slots.push({
|
|
993
|
+
text: option.label,
|
|
994
|
+
apply: (current, value, locale) => ({
|
|
995
|
+
...current,
|
|
996
|
+
fields: current.fields.map((item, index) => {
|
|
997
|
+
if (index !== fieldIndex || !("options" in item)) return item;
|
|
998
|
+
return {
|
|
999
|
+
...item,
|
|
1000
|
+
options: item.options.map(
|
|
1001
|
+
(candidate, candidateIndex) => candidateIndex === optionIndex ? { ...candidate, translations: { ...candidate.translations, [locale]: value } } : candidate
|
|
1002
|
+
)
|
|
1003
|
+
};
|
|
1004
|
+
})
|
|
1005
|
+
})
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
schema.pages?.forEach((page, pageIndex) => {
|
|
1011
|
+
if (page.title !== void 0) {
|
|
1012
|
+
slots.push({
|
|
1013
|
+
text: page.title,
|
|
1014
|
+
apply: (current, value, locale) => ({
|
|
1015
|
+
...current,
|
|
1016
|
+
...current.pages === void 0 ? {} : {
|
|
1017
|
+
pages: current.pages.map(
|
|
1018
|
+
(item, index) => index === pageIndex ? { ...item, translations: mergeLocalizedText(item.translations, locale, "title", value) } : item
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
})
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
if (page.description !== void 0) {
|
|
1025
|
+
slots.push({
|
|
1026
|
+
text: page.description,
|
|
1027
|
+
apply: (current, value, locale) => ({
|
|
1028
|
+
...current,
|
|
1029
|
+
...current.pages === void 0 ? {} : {
|
|
1030
|
+
pages: current.pages.map(
|
|
1031
|
+
(item, index) => index === pageIndex ? { ...item, translations: mergeLocalizedText(item.translations, locale, "description", value) } : item
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
1034
|
+
})
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
723
1037
|
});
|
|
724
|
-
|
|
1038
|
+
return slots;
|
|
1039
|
+
}
|
|
1040
|
+
function resolveLocalizedSchema(schema, targetLocale) {
|
|
1041
|
+
if (targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
|
|
1042
|
+
const formTranslation = schema.translations?.[targetLocale];
|
|
1043
|
+
return {
|
|
725
1044
|
...schema,
|
|
726
|
-
title,
|
|
727
|
-
...description === void 0 ? {} : { description },
|
|
728
|
-
fields
|
|
1045
|
+
title: formTranslation?.title ?? schema.title,
|
|
1046
|
+
...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
|
|
1047
|
+
fields: schema.fields.map((field) => {
|
|
1048
|
+
const translation = field.translations?.[targetLocale];
|
|
1049
|
+
const localized = {
|
|
1050
|
+
...field,
|
|
1051
|
+
title: translation?.title ?? field.title,
|
|
1052
|
+
...(translation?.description ?? field.description) === void 0 ? {} : { description: translation?.description ?? field.description }
|
|
1053
|
+
};
|
|
1054
|
+
if (!("options" in field)) return localized;
|
|
1055
|
+
return {
|
|
1056
|
+
...localized,
|
|
1057
|
+
options: field.options.map((option) => ({
|
|
1058
|
+
...option,
|
|
1059
|
+
label: option.translations?.[targetLocale] ?? option.label
|
|
1060
|
+
}))
|
|
1061
|
+
};
|
|
1062
|
+
}),
|
|
1063
|
+
...schema.pages === void 0 ? {} : {
|
|
1064
|
+
pages: schema.pages.map((page) => {
|
|
1065
|
+
const translation = page.translations?.[targetLocale];
|
|
1066
|
+
const title = translation?.title ?? page.title;
|
|
1067
|
+
const description = translation?.description ?? page.description;
|
|
1068
|
+
return {
|
|
1069
|
+
...page,
|
|
1070
|
+
...title === void 0 ? {} : { title },
|
|
1071
|
+
...description === void 0 ? {} : { description }
|
|
1072
|
+
};
|
|
1073
|
+
})
|
|
1074
|
+
}
|
|
729
1075
|
};
|
|
730
|
-
|
|
731
|
-
|
|
1076
|
+
}
|
|
1077
|
+
async function populateSchemaTranslations(schema, targetLocales, adapter) {
|
|
1078
|
+
assertValidFormSchema(schema);
|
|
1079
|
+
const slots = translationSlots(schema);
|
|
1080
|
+
const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
|
|
1081
|
+
let result = schema;
|
|
1082
|
+
for (const locale of locales) {
|
|
1083
|
+
const translated = await adapter.translateBatch(
|
|
1084
|
+
slots.map((slot) => slot.text),
|
|
1085
|
+
locale,
|
|
1086
|
+
schema.defaultLocale
|
|
1087
|
+
);
|
|
1088
|
+
if (translated.length !== slots.length) {
|
|
1089
|
+
throw new Error(`Translation adapter returned ${translated.length} texts for ${slots.length} inputs.`);
|
|
1090
|
+
}
|
|
1091
|
+
translated.forEach((value, index) => {
|
|
1092
|
+
const slot = slots[index];
|
|
1093
|
+
if (slot === void 0) throw new Error("Translation adapter returned an unexpected result.");
|
|
1094
|
+
result = slot.apply(result, value, locale);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const supportedLocales = [
|
|
1098
|
+
.../* @__PURE__ */ new Set([
|
|
1099
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
1100
|
+
...schema.supportedLocales ?? [],
|
|
1101
|
+
...locales
|
|
1102
|
+
])
|
|
1103
|
+
];
|
|
1104
|
+
result = supportedLocales.length === 0 ? result : { ...result, supportedLocales };
|
|
1105
|
+
assertValidFormSchema(result);
|
|
1106
|
+
return result;
|
|
1107
|
+
}
|
|
1108
|
+
async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocale) {
|
|
1109
|
+
const populated = await populateSchemaTranslations(
|
|
1110
|
+
sourceLocale === void 0 ? schema : { ...schema, defaultLocale: sourceLocale },
|
|
1111
|
+
[targetLocale],
|
|
1112
|
+
adapter
|
|
1113
|
+
);
|
|
1114
|
+
return resolveLocalizedSchema(populated, targetLocale);
|
|
732
1115
|
}
|
|
733
1116
|
export {
|
|
734
1117
|
aggregateResponses,
|
|
735
1118
|
assertValidFormSchema,
|
|
736
1119
|
calculateChoiceDistribution,
|
|
1120
|
+
calculateCrossTabulation,
|
|
737
1121
|
calculateFieldVisibility,
|
|
738
1122
|
calculateNumericSummary,
|
|
1123
|
+
calculatePageVisibility,
|
|
739
1124
|
createSubmission,
|
|
1125
|
+
dispatchWebhook,
|
|
740
1126
|
escapeCsvCell,
|
|
741
1127
|
exportResponsesToCsv,
|
|
1128
|
+
isDisplayConditionSatisfied,
|
|
742
1129
|
isQuestionVisible,
|
|
1130
|
+
populateSchemaTranslations,
|
|
743
1131
|
resolveFormTranslation,
|
|
1132
|
+
resolveLocalizedSchema,
|
|
744
1133
|
sanitizeSchema,
|
|
745
1134
|
selectVisibleAnswers,
|
|
746
1135
|
validateAnswers,
|
|
747
1136
|
validateFormSchema,
|
|
1137
|
+
validatePageAnswers,
|
|
748
1138
|
validateSchemaStructure
|
|
749
1139
|
};
|