@form-engine-ts/react 4.4.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -38,7 +38,8 @@ __export(index_exports, {
38
38
  useField: () => useField,
39
39
  useForm: () => useForm,
40
40
  useFormBuilder: () => useFormBuilder,
41
- useSubmissionReceipts: () => useSubmissionReceipts
41
+ useSubmissionReceipts: () => useSubmissionReceipts,
42
+ useTranslationWorkspace: () => useTranslationWorkspace
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
45
 
@@ -234,8 +235,20 @@ function move(items, sourceIndex, targetIndex) {
234
235
  result.splice(targetIndex, 0, item);
235
236
  return result;
236
237
  }
238
+ function displayRuleSourceIds(field) {
239
+ if (field.displayRule === void 0) return [];
240
+ const ids = [];
241
+ const visit = (group) => {
242
+ for (const condition of group.conditions) {
243
+ if ("logic" in condition) visit(condition);
244
+ else ids.push(condition.fieldId);
245
+ }
246
+ };
247
+ visit(field.displayRule.condition);
248
+ return ids;
249
+ }
237
250
  function withoutDisplayCondition(field) {
238
- const { displayCondition: _displayCondition, ...rest } = field;
251
+ const { displayCondition: _displayCondition, displayRule: _displayRule, ...rest } = field;
239
252
  return rest;
240
253
  }
241
254
  function removeLocalizedProperty(translations, locale, property) {
@@ -258,8 +271,23 @@ function useFormBuilder({
258
271
  onChange,
259
272
  policy,
260
273
  idFactory = defaultIdFactory,
261
- factories = {}
274
+ factories = {},
275
+ fieldEditorMode = "all",
276
+ activeFieldId: controlledActiveFieldId,
277
+ defaultActiveFieldId,
278
+ onActiveFieldChange
262
279
  }) {
280
+ const [internalActiveFieldId, setInternalActiveFieldId] = (0, import_react.useState)(
281
+ defaultActiveFieldId ?? (fieldEditorMode === "single" ? schema.fields[0]?.id : void 0)
282
+ );
283
+ const activeFieldId = controlledActiveFieldId ?? internalActiveFieldId;
284
+ const setActiveFieldId = (0, import_react.useCallback)(
285
+ (fieldId) => {
286
+ if (controlledActiveFieldId === void 0) setInternalActiveFieldId(fieldId);
287
+ onActiveFieldChange?.(fieldId);
288
+ },
289
+ [controlledActiveFieldId, onActiveFieldChange]
290
+ );
263
291
  const createId = (0, import_react.useCallback)(
264
292
  (kind, existingIds) => {
265
293
  const rawId = idFactory(kind, existingIds);
@@ -372,9 +400,10 @@ function useFormBuilder({
372
400
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
373
401
  }));
374
402
  onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
403
+ setActiveFieldId(field.id);
375
404
  return { success: true };
376
405
  },
377
- [createId, factories, onChange, policy, schema]
406
+ [createId, factories, onChange, policy, schema, setActiveFieldId]
378
407
  );
379
408
  const removeField = (0, import_react.useCallback)(
380
409
  (fieldId) => {
@@ -382,15 +411,19 @@ function useFormBuilder({
382
411
  return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
383
412
  if (schema.fields.length <= 1)
384
413
  return { success: false, error: { type: "invalid_operation", message: "A form must contain one field." } };
385
- const fields = schema.fields.filter((field) => field.id !== fieldId).map((field) => field.displayCondition?.questionId === fieldId ? withoutDisplayCondition(field) : field);
414
+ const removedIndex = schema.fields.findIndex((field) => field.id === fieldId);
415
+ const fields = schema.fields.filter((field) => field.id !== fieldId).map(
416
+ (field) => field.displayCondition?.questionId === fieldId || displayRuleSourceIds(field).includes(fieldId) ? withoutDisplayCondition(field) : field
417
+ );
386
418
  const pages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
387
419
  if (schema.pages !== void 0 && pages?.length === 0) {
388
420
  const { pages: _pages, ...single } = schema;
389
421
  onChange({ ...single, fields });
390
422
  } else onChange({ ...schema, fields, ...pages === void 0 ? {} : { pages } });
423
+ if (activeFieldId === fieldId) setActiveFieldId(fields[removedIndex]?.id ?? fields.at(-1)?.id);
391
424
  return { success: true };
392
425
  },
393
- [onChange, schema]
426
+ [activeFieldId, onChange, schema, setActiveFieldId]
394
427
  );
395
428
  const moveField = (0, import_react.useCallback)(
396
429
  (fieldId, targetIndex) => {
@@ -403,8 +436,8 @@ function useFormBuilder({
403
436
  onChange({
404
437
  ...schema,
405
438
  fields: fields.map((field, index) => {
406
- const source = field.displayCondition?.questionId;
407
- return source === void 0 || (indexById.get(source) ?? index) < index ? field : withoutDisplayCondition(field);
439
+ const sources = field.displayCondition?.questionId === void 0 ? displayRuleSourceIds(field) : [field.displayCondition.questionId];
440
+ return sources.every((source) => (indexById.get(source) ?? index) < index) ? field : withoutDisplayCondition(field);
408
441
  })
409
442
  });
410
443
  return { success: true };
@@ -828,6 +861,14 @@ function useFormBuilder({
828
861
  const result = (0, import_core.validateFormSchema)(schema, policy === void 0 ? {} : { policy });
829
862
  return result.valid ? [] : result.issues;
830
863
  }, [policy, schema]);
864
+ const getFieldEditorProps = (0, import_react.useCallback)(
865
+ (fieldId) => ({
866
+ isActive: activeFieldId === fieldId,
867
+ isVisible: fieldEditorMode === "all" || activeFieldId === fieldId,
868
+ onSelect: () => setActiveFieldId(fieldId)
869
+ }),
870
+ [activeFieldId, fieldEditorMode, setActiveFieldId]
871
+ );
831
872
  return {
832
873
  schema,
833
874
  addField,
@@ -849,7 +890,10 @@ function useFormBuilder({
849
890
  setLocaleTranslation,
850
891
  addLocale,
851
892
  setDefaultLocale,
852
- validationIssues
893
+ validationIssues,
894
+ ...activeFieldId === void 0 ? {} : { activeFieldId },
895
+ setActiveFieldId,
896
+ getFieldEditorProps
853
897
  };
854
898
  }
855
899
 
@@ -1502,7 +1546,10 @@ var BUILDER_DEFAULTS = {
1502
1546
  "builder.operator.equals": "equals",
1503
1547
  "builder.operator.not_equals": "does not equal",
1504
1548
  "builder.operator.contains": "contains",
1505
- "builder.operator.not_empty": "is not empty"
1549
+ "builder.operator.not_empty": "is not empty",
1550
+ "builder.submissionSettings": "Submission settings",
1551
+ "builder.showConfirmationBeforeSubmit": "Show confirmation before submit",
1552
+ "builder.confirmationRenderMode": "Confirmation display mode"
1506
1553
  };
1507
1554
  function BuilderSectionGroup({ children }) {
1508
1555
  return children;
@@ -1679,7 +1726,12 @@ function FormBuilder({
1679
1726
  slots,
1680
1727
  sectionOrder,
1681
1728
  disableDefaultStyles = false,
1682
- unstyled = false
1729
+ unstyled = false,
1730
+ fieldEditorMode = "all",
1731
+ activeFieldId,
1732
+ defaultActiveFieldId,
1733
+ onActiveFieldChange,
1734
+ submissionSettingsOptions
1683
1735
  }) {
1684
1736
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1685
1737
  const components = {
@@ -1702,7 +1754,11 @@ function FormBuilder({
1702
1754
  onChange,
1703
1755
  ...policy === void 0 ? {} : { policy },
1704
1756
  ...idFactory === void 0 ? {} : { idFactory },
1705
- ...factories === void 0 ? {} : { factories }
1757
+ ...factories === void 0 ? {} : { factories },
1758
+ fieldEditorMode,
1759
+ ...activeFieldId === void 0 ? {} : { activeFieldId },
1760
+ ...defaultActiveFieldId === void 0 ? {} : { defaultActiveFieldId },
1761
+ ...onActiveFieldChange === void 0 ? {} : { onActiveFieldChange }
1706
1762
  });
1707
1763
  const [newPageQuestionId, setNewPageQuestionId] = (0, import_react2.useState)("");
1708
1764
  const [newLocale, setNewLocale] = (0, import_react2.useState)("");
@@ -2042,6 +2098,47 @@ function FormBuilder({
2042
2098
  ] })
2043
2099
  }
2044
2100
  ) }),
2101
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "submissionSettings", children: submissionSettingsOptions?.enabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2102
+ Section,
2103
+ {
2104
+ className: builderClass("form-engine-builder__submission-settings"),
2105
+ title: translate("builder.submissionSettings"),
2106
+ children: [
2107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2108
+ Checkbox,
2109
+ {
2110
+ id: "builder-show-confirmation",
2111
+ label: translate("builder.showConfirmationBeforeSubmit"),
2112
+ checked: schema.submissionSettings?.showConfirmationBeforeSubmit === true,
2113
+ onChange: (checked) => onChange({
2114
+ ...schema,
2115
+ submissionSettings: { ...schema.submissionSettings, showConfirmationBeforeSubmit: checked }
2116
+ })
2117
+ }
2118
+ ),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2120
+ Select,
2121
+ {
2122
+ id: "builder-confirmation-render-mode",
2123
+ label: translate("builder.confirmationRenderMode"),
2124
+ value: schema.submissionSettings?.confirmationRenderMode ?? "inline",
2125
+ options: [
2126
+ { value: "dialog", label: "Dialog" },
2127
+ { value: "inline", label: "Inline" },
2128
+ { value: "replace", label: "Replace" }
2129
+ ],
2130
+ onChange: (value) => {
2131
+ if (value !== "dialog" && value !== "inline" && value !== "replace") return;
2132
+ onChange({
2133
+ ...schema,
2134
+ submissionSettings: { ...schema.submissionSettings, confirmationRenderMode: value }
2135
+ });
2136
+ }
2137
+ }
2138
+ )
2139
+ ]
2140
+ }
2141
+ ) : null }),
2045
2142
  resolvedSectionOrder === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "completionMessage", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { headingId: "builder-completion-message-heading", title: translate("builder.completionMessage"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2046
2143
  TextInput,
2047
2144
  {
@@ -2440,9 +2537,25 @@ function FormBuilder({
2440
2537
  ) : null }),
2441
2538
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
2442
2539
  const controls = resolveFieldEditorControls(fieldEditorControls);
2540
+ const editorState = headless.getFieldEditorProps?.(field.id) ?? {
2541
+ isActive: true,
2542
+ isVisible: true,
2543
+ onSelect: () => void 0
2544
+ };
2443
2545
  const condition = field.displayCondition;
2444
2546
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2445
2547
  const availableSources = schema.fields.slice(0, index);
2548
+ if (!editorState.isVisible) {
2549
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2550
+ "div",
2551
+ {
2552
+ className: builderClass("form-engine-builder__question-preview"),
2553
+ "data-field-id": field.id,
2554
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: editorState.onSelect, children: field.title })
2555
+ },
2556
+ field.id
2557
+ );
2558
+ }
2446
2559
  if (FieldEditorSlot !== void 0) {
2447
2560
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2448
2561
  FieldEditorSlot,
@@ -3150,8 +3263,294 @@ function useField(fieldId) {
3150
3263
  return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
3151
3264
  }
3152
3265
 
3153
- // src/receipt.ts
3266
+ // src/hooks/useTranslationWorkspace.ts
3267
+ var import_core4 = require("@form-engine-ts/core");
3154
3268
  var import_react4 = require("react");
3269
+ function updateTranslationMap(translations, locale, property, text) {
3270
+ if (property === "label") return translations ?? {};
3271
+ const current = translations?.[locale];
3272
+ const next = { ...current, [property]: text };
3273
+ return { ...translations, [locale]: next };
3274
+ }
3275
+ function asAsyncAdapter(adapter) {
3276
+ if ("translateBatch" in adapter) return adapter;
3277
+ return {
3278
+ translateText: async (text, locale, sourceLocale) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text,
3279
+ translateBatch: async (texts, locale, sourceLocale) => texts.map(
3280
+ (text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
3281
+ )
3282
+ };
3283
+ }
3284
+ function validateLocaleByPolicy(locale, currentLocales, policy) {
3285
+ if (locale.length === 0) {
3286
+ return {
3287
+ valid: false,
3288
+ error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3289
+ };
3290
+ }
3291
+ try {
3292
+ Intl.getCanonicalLocales(locale);
3293
+ } catch {
3294
+ return {
3295
+ valid: false,
3296
+ error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3297
+ };
3298
+ }
3299
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3300
+ return {
3301
+ valid: false,
3302
+ error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3303
+ };
3304
+ }
3305
+ if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3306
+ return {
3307
+ valid: false,
3308
+ error: {
3309
+ type: "max_locales_exceeded",
3310
+ message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3311
+ }
3312
+ };
3313
+ }
3314
+ return { valid: true };
3315
+ }
3316
+ function manualMetadata(sourceText, sourceLocale) {
3317
+ return {
3318
+ sourceLocale,
3319
+ sourceTextHash: (0, import_core4.computeSourceTextHash)(sourceText),
3320
+ translationSource: "manual",
3321
+ editedAt: (/* @__PURE__ */ new Date()).toISOString()
3322
+ };
3323
+ }
3324
+ function setNodeMetadata(node, slot, text, sourceLocale) {
3325
+ const localeMetadata = node.translationMetadata?.[slot.locale];
3326
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
3327
+ return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3328
+ }
3329
+ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3330
+ if (slot.kind === "form") {
3331
+ return setNodeMetadata(
3332
+ { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3333
+ slot,
3334
+ text,
3335
+ sourceLocale
3336
+ );
3337
+ }
3338
+ if (slot.kind === "field") {
3339
+ return {
3340
+ ...schema,
3341
+ fields: schema.fields.map((field) => {
3342
+ if (field.id !== slot.nodeId) return field;
3343
+ const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3344
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3345
+ })
3346
+ };
3347
+ }
3348
+ if (slot.kind === "option") {
3349
+ return {
3350
+ ...schema,
3351
+ fields: schema.fields.map((field) => {
3352
+ if (!("options" in field)) return field;
3353
+ return {
3354
+ ...field,
3355
+ options: field.options.map((option) => {
3356
+ if (option.id !== slot.nodeId) return option;
3357
+ const translations = text.trim().length === 0 ? Object.fromEntries(
3358
+ Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3359
+ ) : { ...option.translations, [slot.locale]: text };
3360
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3361
+ })
3362
+ };
3363
+ })
3364
+ };
3365
+ }
3366
+ return {
3367
+ ...schema,
3368
+ ...schema.pages === void 0 ? {} : {
3369
+ pages: schema.pages.map((page) => {
3370
+ if (page.id !== slot.nodeId) return page;
3371
+ const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3372
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3373
+ })
3374
+ }
3375
+ };
3376
+ }
3377
+ function useTranslationWorkspace({
3378
+ schema,
3379
+ onChange,
3380
+ sourceLocale = schema.defaultLocale ?? "en",
3381
+ targetLocale,
3382
+ translationAdapter,
3383
+ readOnly = false,
3384
+ policy,
3385
+ validateLocale
3386
+ }) {
3387
+ const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
3388
+ const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
3389
+ const [isTranslating, setIsTranslating] = (0, import_react4.useState)(false);
3390
+ const [error, setError] = (0, import_react4.useState)();
3391
+ const currentSchema = onChange === void 0 ? draftSchema : schema;
3392
+ const targetLocales = (0, import_react4.useMemo)(() => {
3393
+ const locales = (0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales;
3394
+ return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
3395
+ (locale) => locale !== sourceLocale
3396
+ );
3397
+ }, [currentSchema, sourceLocale]);
3398
+ const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3399
+ const slots = (0, import_react4.useMemo)(
3400
+ () => activeLocale.length === 0 ? [] : (0, import_core4.collectTranslationSlots)(currentSchema, activeLocale),
3401
+ [activeLocale, currentSchema]
3402
+ );
3403
+ const summary = (0, import_react4.useMemo)(() => {
3404
+ const counts = {
3405
+ missing: 0,
3406
+ translated: 0,
3407
+ stale: 0,
3408
+ manual: 0,
3409
+ "manual-stale": 0
3410
+ };
3411
+ for (const slot of slots) counts[slot.status ?? "missing"] += 1;
3412
+ const complete = counts.translated + counts.manual;
3413
+ return {
3414
+ totalSlots: slots.length,
3415
+ translatedCount: complete,
3416
+ missingCount: counts.missing,
3417
+ staleCount: counts.stale + counts["manual-stale"],
3418
+ manualCount: counts.manual + counts["manual-stale"],
3419
+ completionPercentage: slots.length === 0 ? 100 : Math.round(complete / slots.length * 100)
3420
+ };
3421
+ }, [slots]);
3422
+ const commit = (0, import_react4.useCallback)(
3423
+ (next) => {
3424
+ setDraftSchema(next);
3425
+ onChange?.(next);
3426
+ },
3427
+ [onChange]
3428
+ );
3429
+ const setTranslation = (0, import_react4.useCallback)(
3430
+ (slot, text) => {
3431
+ if (readOnly) return;
3432
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3433
+ },
3434
+ [commit, currentSchema, readOnly, sourceLocale]
3435
+ );
3436
+ const addLocale = (0, import_react4.useCallback)(
3437
+ (locale) => {
3438
+ if (readOnly) return { success: false, error: "Workspace is read-only." };
3439
+ const normalized = locale.trim();
3440
+ const currentLocales = [
3441
+ .../* @__PURE__ */ new Set([
3442
+ ...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
3443
+ sourceLocale,
3444
+ ...(0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales
3445
+ ])
3446
+ ];
3447
+ const validation = validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy);
3448
+ if (!validation.valid) return { success: false, error: validation.error?.message ?? "Locale is not valid." };
3449
+ const alreadyRegistered = normalized === sourceLocale || normalized === currentSchema.defaultLocale || (currentSchema.supportedLocales ?? []).includes(normalized);
3450
+ if (alreadyRegistered) {
3451
+ setSelectedLocale(normalized);
3452
+ return { success: true };
3453
+ }
3454
+ commit({
3455
+ ...currentSchema,
3456
+ supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3457
+ });
3458
+ setSelectedLocale(normalized);
3459
+ return { success: true };
3460
+ },
3461
+ [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3462
+ );
3463
+ const isAddLocaleAllowed = (0, import_react4.useCallback)(
3464
+ (locale) => {
3465
+ if (readOnly) return false;
3466
+ const normalized = locale.trim();
3467
+ const currentLocales = [
3468
+ .../* @__PURE__ */ new Set([
3469
+ ...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
3470
+ sourceLocale,
3471
+ ...(0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales
3472
+ ])
3473
+ ];
3474
+ return (validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy)).valid;
3475
+ },
3476
+ [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3477
+ );
3478
+ const removeLocale = (0, import_react4.useCallback)(
3479
+ (locale) => {
3480
+ if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return;
3481
+ commit((0, import_core4.removeLocaleFromSchema)(currentSchema, locale));
3482
+ if (activeLocale === locale) setSelectedLocale("");
3483
+ },
3484
+ [activeLocale, commit, currentSchema, readOnly, sourceLocale]
3485
+ );
3486
+ const translateAll = (0, import_react4.useCallback)(
3487
+ async (options = {}) => {
3488
+ if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
3489
+ if (activeLocale.length === 0) throw new Error("A target locale is required.");
3490
+ setIsTranslating(true);
3491
+ setError(void 0);
3492
+ try {
3493
+ const populated = await (0, import_core4.populateSchemaTranslations)(
3494
+ currentSchema,
3495
+ [activeLocale],
3496
+ asAsyncAdapter(translationAdapter),
3497
+ {
3498
+ overwrite: "stale-and-missing",
3499
+ preserveManualTranslations: true,
3500
+ ...options
3501
+ }
3502
+ );
3503
+ commit(populated.schema);
3504
+ return populated.report;
3505
+ } catch (cause) {
3506
+ const message = cause instanceof Error ? cause.message : String(cause);
3507
+ setError(message);
3508
+ throw cause;
3509
+ } finally {
3510
+ setIsTranslating(false);
3511
+ }
3512
+ },
3513
+ [activeLocale, commit, currentSchema, translationAdapter]
3514
+ );
3515
+ const translateSlot = (0, import_react4.useCallback)(
3516
+ async (slot) => {
3517
+ if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
3518
+ if (readOnly) return;
3519
+ setIsTranslating(true);
3520
+ setError(void 0);
3521
+ try {
3522
+ const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3523
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3524
+ } catch (cause) {
3525
+ const message = cause instanceof Error ? cause.message : String(cause);
3526
+ setError(message);
3527
+ throw cause;
3528
+ } finally {
3529
+ setIsTranslating(false);
3530
+ }
3531
+ },
3532
+ [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3533
+ );
3534
+ return {
3535
+ sourceLocale,
3536
+ targetLocale: activeLocale,
3537
+ targetLocales,
3538
+ setTargetLocale: setSelectedLocale,
3539
+ slots,
3540
+ summary,
3541
+ addLocale,
3542
+ isAddLocaleAllowed,
3543
+ removeLocale,
3544
+ setTranslation,
3545
+ translateAll,
3546
+ translateSlot,
3547
+ isTranslating,
3548
+ ...error === void 0 ? {} : { error }
3549
+ };
3550
+ }
3551
+
3552
+ // src/receipt.ts
3553
+ var import_react5 = require("react");
3155
3554
  function submissionReceiptQueryKey(formId, formVersion) {
3156
3555
  return `${formId}:v${formVersion}`;
3157
3556
  }
@@ -3222,19 +3621,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3222
3621
  }
3223
3622
  function useSubmissionReceipts(store, queries) {
3224
3623
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3225
- const stableQueries = (0, import_react4.useMemo)(() => {
3624
+ const stableQueries = (0, import_react5.useMemo)(() => {
3226
3625
  const parsed = JSON.parse(querySignature);
3227
3626
  if (!Array.isArray(parsed)) return [];
3228
3627
  return parsed.flatMap(
3229
3628
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
3230
3629
  );
3231
3630
  }, [querySignature]);
3232
- const [state, setState] = (0, import_react4.useState)({
3631
+ const [state, setState] = (0, import_react5.useState)({
3233
3632
  receipts: /* @__PURE__ */ new Map(),
3234
3633
  isLoading: stableQueries.length > 0,
3235
3634
  error: null
3236
3635
  });
3237
- (0, import_react4.useEffect)(() => {
3636
+ (0, import_react5.useEffect)(() => {
3238
3637
  let active = true;
3239
3638
  if (stableQueries.length === 0) {
3240
3639
  setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
@@ -3268,8 +3667,8 @@ function useSubmissionReceipts(store, queries) {
3268
3667
  }
3269
3668
 
3270
3669
  // src/renderer.tsx
3271
- var import_core4 = require("@form-engine-ts/core");
3272
- var import_react5 = require("react");
3670
+ var import_core5 = require("@form-engine-ts/core");
3671
+ var import_react6 = require("react");
3273
3672
  var import_jsx_runtime3 = require("react/jsx-runtime");
3274
3673
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
3275
3674
  function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
@@ -3780,41 +4179,41 @@ function ContextFormRenderer({
3780
4179
  slots = {}
3781
4180
  }) {
3782
4181
  const form = useForm();
3783
- const prefix = (0, import_react5.useId)().replace(/:/g, "");
3784
- const formRef = (0, import_react5.useRef)(null);
3785
- const loadedDraftKey = (0, import_react5.useRef)(null);
3786
- const [draftRestored, setDraftRestored] = (0, import_react5.useState)(false);
3787
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react5.useState)(0);
3788
- const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
3789
- const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
3790
- const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
3791
- const [guardsPending, setGuardsPending] = (0, import_react5.useState)(false);
3792
- const [receipt, setReceipt] = (0, import_react5.useState)(null);
3793
- const [completionData, setCompletionData] = (0, import_react5.useState)(null);
3794
- const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3795
- const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3796
- const fallbackAttemptId = (0, import_react5.useRef)(null);
3797
- const completionRef = (0, import_react5.useRef)(null);
3798
- const confirmationRef = (0, import_react5.useRef)(null);
4182
+ const prefix = (0, import_react6.useId)().replace(/:/g, "");
4183
+ const formRef = (0, import_react6.useRef)(null);
4184
+ const loadedDraftKey = (0, import_react6.useRef)(null);
4185
+ const [draftRestored, setDraftRestored] = (0, import_react6.useState)(false);
4186
+ const [currentPageIndex, setCurrentPageIndex] = (0, import_react6.useState)(0);
4187
+ const [focusFieldId, setFocusFieldId] = (0, import_react6.useState)(null);
4188
+ const [confirmation, setConfirmation] = (0, import_react6.useState)(null);
4189
+ const [guardMessage, setGuardMessage] = (0, import_react6.useState)(null);
4190
+ const [guardsPending, setGuardsPending] = (0, import_react6.useState)(false);
4191
+ const [receipt, setReceipt] = (0, import_react6.useState)(null);
4192
+ const [completionData, setCompletionData] = (0, import_react6.useState)(null);
4193
+ const [receiptLoaded, setReceiptLoaded] = (0, import_react6.useState)(receiptStore === void 0);
4194
+ const rendererSubmissionInFlight = (0, import_react6.useRef)(false);
4195
+ const fallbackAttemptId = (0, import_react6.useRef)(null);
4196
+ const completionRef = (0, import_react6.useRef)(null);
4197
+ const confirmationRef = (0, import_react6.useRef)(null);
3799
4198
  const pages = form.schema.pages;
3800
- const visiblePageIndexes = (0, import_react5.useMemo)(
4199
+ const visiblePageIndexes = (0, import_react6.useMemo)(
3801
4200
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
3802
4201
  [form.pageVisibility, pages]
3803
4202
  );
3804
4203
  const activePage = pages?.[currentPageIndex];
3805
4204
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
3806
4205
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
3807
- const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
3808
- const visibleItems = (0, import_react5.useMemo)(
4206
+ const visibleValues = (0, import_react6.useMemo)(() => (0, import_core5.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
4207
+ const visibleItems = (0, import_react6.useMemo)(
3809
4208
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
3810
4209
  [form.schema, form.translate, form.values, form.visibility]
3811
4210
  );
3812
- const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
3813
- const confirmationEnabled = submissionConfirmation?.enabled === true;
4211
+ const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? form.schema.submissionSettings?.confirmationRenderMode ?? "inline";
4212
+ const confirmationEnabled = submissionConfirmation?.enabled ?? form.schema.submissionSettings?.showConfirmationBeforeSubmit ?? false;
3814
4213
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3815
4214
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3816
4215
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3817
- const resolveMessage = (0, import_react5.useCallback)(
4216
+ const resolveMessage = (0, import_react6.useCallback)(
3818
4217
  (key, fallback) => {
3819
4218
  const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3820
4219
  const configured = messages[key];
@@ -3822,15 +4221,15 @@ function ContextFormRenderer({
3822
4221
  },
3823
4222
  [form.locale, messageResolver, messages]
3824
4223
  );
3825
- const fieldTranslate = (0, import_react5.useCallback)(
4224
+ const fieldTranslate = (0, import_react6.useCallback)(
3826
4225
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3827
4226
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
3828
4227
  );
3829
- const focusSubmitButton = (0, import_react5.useCallback)(() => {
4228
+ const focusSubmitButton = (0, import_react6.useCallback)(() => {
3830
4229
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3831
4230
  button?.focus();
3832
4231
  }, []);
3833
- (0, import_react5.useEffect)(() => {
4232
+ (0, import_react6.useEffect)(() => {
3834
4233
  let active = true;
3835
4234
  if (receiptStore === void 0) {
3836
4235
  setReceipt(null);
@@ -3851,14 +4250,14 @@ function ContextFormRenderer({
3851
4250
  active = false;
3852
4251
  };
3853
4252
  }, [form.schema.id, form.schema.version, receiptStore]);
3854
- (0, import_react5.useEffect)(() => {
4253
+ (0, import_react6.useEffect)(() => {
3855
4254
  if (pages === void 0 || visiblePageIndexes.length === 0) {
3856
4255
  setCurrentPageIndex(0);
3857
4256
  return;
3858
4257
  }
3859
4258
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
3860
4259
  }, [currentPageIndex, pages, visiblePageIndexes]);
3861
- (0, import_react5.useEffect)(() => {
4260
+ (0, import_react6.useEffect)(() => {
3862
4261
  if (focusFieldId === null) return;
3863
4262
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
3864
4263
  (element) => element.dataset.fieldId === focusFieldId
@@ -3870,11 +4269,11 @@ function ContextFormRenderer({
3870
4269
  setFocusFieldId(null);
3871
4270
  }
3872
4271
  }, [focusFieldId]);
3873
- (0, import_react5.useEffect)(() => {
4272
+ (0, import_react6.useEffect)(() => {
3874
4273
  if (!isReplaceMode || form.submitStatus !== "success") return;
3875
4274
  completionRef.current?.focus();
3876
4275
  }, [form.submitStatus, isReplaceMode]);
3877
- (0, import_react5.useEffect)(() => {
4276
+ (0, import_react6.useEffect)(() => {
3878
4277
  if (confirmation === null) return;
3879
4278
  const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
3880
4279
  confirmButton?.focus();
@@ -3906,7 +4305,7 @@ function ContextFormRenderer({
3906
4305
  globalThis.addEventListener("keydown", onKeyDown);
3907
4306
  return () => globalThis.removeEventListener("keydown", onKeyDown);
3908
4307
  }, [confirmation, confirmationRenderMode, focusSubmitButton]);
3909
- (0, import_react5.useEffect)(() => {
4308
+ (0, import_react6.useEffect)(() => {
3910
4309
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3911
4310
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
3912
4311
  if (loadedDraftKey.current === loadIdentity) return;
@@ -3918,7 +4317,7 @@ function ContextFormRenderer({
3918
4317
  form.restoreValues(draft.values);
3919
4318
  setDraftRestored(true);
3920
4319
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
3921
- (0, import_react5.useEffect)(() => {
4320
+ (0, import_react6.useEffect)(() => {
3922
4321
  if (form.submitStatus === "success") return;
3923
4322
  const timeout = globalThis.setTimeout(() => {
3924
4323
  onDraftSave?.(form.values);
@@ -3974,7 +4373,7 @@ function ContextFormRenderer({
3974
4373
  if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
3975
4374
  return { status: "cancelled" };
3976
4375
  }
3977
- const validation = (0, import_core4.validateAnswers)(form.schema, form.values);
4376
+ const validation = (0, import_core5.validateAnswers)(form.schema, form.values);
3978
4377
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
3979
4378
  if (validation.valid && !guardsConfirmed) {
3980
4379
  rendererSubmissionInFlight.current = true;
@@ -4276,7 +4675,7 @@ function ContextFormRenderer({
4276
4675
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
4277
4676
  };
4278
4677
  if (slots.renderField !== void 0) {
4279
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
4678
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react6.Fragment, { children: slots.renderField({
4280
4679
  question: field,
4281
4680
  value: form.values[field.id],
4282
4681
  onChange: (value) => {
@@ -4446,5 +4845,6 @@ function FormRenderer(props) {
4446
4845
  useField,
4447
4846
  useForm,
4448
4847
  useFormBuilder,
4449
- useSubmissionReceipts
4848
+ useSubmissionReceipts,
4849
+ useTranslationWorkspace
4450
4850
  });