@form-engine-ts/react 5.1.0 → 6.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/dist/index.cjs CHANGED
@@ -27,7 +27,7 @@ __export(index_exports, {
27
27
  FormEngineI18nProvider: () => FormEngineI18nProvider,
28
28
  FormProvider: () => FormProvider,
29
29
  FormRenderer: () => FormRenderer,
30
- FormSubmissionError: () => FormSubmissionError,
30
+ FormSubmissionError: () => import_core8.FormSubmissionError,
31
31
  createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
32
32
  createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
33
33
  isTranslationUnresolved: () => isTranslationUnresolved,
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  useFormBuilder: () => useFormBuilder,
43
43
  useFormEngineI18n: () => useFormEngineI18n,
44
44
  useSubmissionReceipts: () => useSubmissionReceipts,
45
+ useTranslationComparison: () => useTranslationComparison,
45
46
  useTranslationWorkspace: () => useTranslationWorkspace,
46
47
  validateLocalePipeline: () => validateLocalePipeline
47
48
  });
@@ -978,6 +979,8 @@ function FormEngineI18nProvider({
978
979
  fallbackLocale = "en",
979
980
  messages,
980
981
  customCatalogs,
982
+ onMissingKey,
983
+ strict,
981
984
  translator: customTranslator,
982
985
  children
983
986
  }) {
@@ -987,9 +990,11 @@ function FormEngineI18nProvider({
987
990
  locale,
988
991
  fallbackLocale,
989
992
  ...messages === void 0 ? {} : { messages },
990
- ...customCatalogs === void 0 ? {} : { customCatalogs }
993
+ ...customCatalogs === void 0 ? {} : { customCatalogs },
994
+ ...onMissingKey === void 0 ? {} : { onMissingKey },
995
+ ...strict === void 0 ? {} : { strict }
991
996
  });
992
- }, [customCatalogs, customTranslator, fallbackLocale, locale, messages]);
997
+ }, [customCatalogs, customTranslator, fallbackLocale, locale, messages, onMissingKey, strict]);
993
998
  const value = (0, import_react2.useMemo)(() => ({ uiLocale: locale, translator }), [locale, translator]);
994
999
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormEngineI18nProviderScopeContext.Provider, { value: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormEngineI18nContext.Provider, { value, children }) });
995
1000
  }
@@ -2566,6 +2571,7 @@ function FormBuilder({
2566
2571
  LocalizationSlot,
2567
2572
  {
2568
2573
  schema,
2574
+ onChange,
2569
2575
  translate,
2570
2576
  currentLocale: editingLocale,
2571
2577
  onCurrentLocaleChange: setEditingLocale,
@@ -3088,18 +3094,6 @@ function FormBuilder({
3088
3094
  // src/context.tsx
3089
3095
  var import_core4 = require("@form-engine-ts/core");
3090
3096
  var import_react4 = require("react");
3091
-
3092
- // src/types.ts
3093
- var FormSubmissionError = class extends Error {
3094
- payload;
3095
- constructor(message, payload) {
3096
- super(message);
3097
- this.name = "FormSubmissionError";
3098
- this.payload = payload ?? { formError: message };
3099
- }
3100
- };
3101
-
3102
- // src/context.tsx
3103
3097
  var import_jsx_runtime3 = require("react/jsx-runtime");
3104
3098
  var FormContext = (0, import_react4.createContext)(null);
3105
3099
  function issuesByField(issues) {
@@ -3236,10 +3230,12 @@ function FormProvider({
3236
3230
  setSubmitStatus("success");
3237
3231
  return response === void 0 ? { status: "success" } : { status: "success", response };
3238
3232
  } catch (cause) {
3239
- const error = isServerErrorPayload(cause) ? new FormSubmissionError(
3240
- cause.formError ?? (cause instanceof Error ? cause.message : "Form submission failed."),
3241
- cause
3242
- ) : cause instanceof Error ? cause : new Error(String(cause));
3233
+ const error = (0, import_core4.isFormSubmissionSerializedError)(cause) ? new import_core4.FormSubmissionError(cause) : isServerErrorPayload(cause) ? new import_core4.FormSubmissionError({
3234
+ code: "VALIDATION_FAILED",
3235
+ messageKey: cause.formError ?? "Submission failed",
3236
+ fieldErrors: cause.fieldErrors ?? {},
3237
+ formErrors: cause.formError === void 0 ? [] : [cause.formError]
3238
+ }) : cause instanceof Error ? cause : new Error(String(cause));
3243
3239
  setSubmitError(error);
3244
3240
  setSubmitStatus("error");
3245
3241
  return { status: "error", error };
@@ -3307,6 +3303,10 @@ function useField(fieldId) {
3307
3303
  return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
3308
3304
  }
3309
3305
 
3306
+ // src/hooks/useTranslationComparison.ts
3307
+ var import_core6 = require("@form-engine-ts/core");
3308
+ var import_react6 = require("react");
3309
+
3310
3310
  // src/hooks/useTranslationWorkspace.ts
3311
3311
  var import_core5 = require("@form-engine-ts/core");
3312
3312
  var import_react5 = require("react");
@@ -3421,26 +3421,27 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3421
3421
  }
3422
3422
  return { type: "custom_validation_failed", message: error.message };
3423
3423
  }
3424
- function manualMetadata(sourceText, sourceLocale) {
3424
+ function translationMetadata(sourceText, sourceLocale, mode) {
3425
3425
  return {
3426
3426
  sourceLocale,
3427
3427
  sourceTextHash: (0, import_core5.computeSourceTextHash)(sourceText),
3428
- translationSource: "manual",
3429
- editedAt: (/* @__PURE__ */ new Date()).toISOString()
3428
+ translationSource: mode,
3429
+ ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3430
3430
  };
3431
3431
  }
3432
- function setNodeMetadata(node, slot, text, sourceLocale) {
3432
+ function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3433
3433
  const localeMetadata = node.translationMetadata?.[slot.locale];
3434
- const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
3434
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: translationMetadata(slot.sourceText, sourceLocale, mode) };
3435
3435
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3436
3436
  }
3437
- function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3437
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3438
3438
  if (slot.kind === "form") {
3439
3439
  return setNodeMetadata(
3440
3440
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3441
3441
  slot,
3442
3442
  text,
3443
- sourceLocale
3443
+ sourceLocale,
3444
+ mode
3444
3445
  );
3445
3446
  }
3446
3447
  if (slot.kind === "field") {
@@ -3449,7 +3450,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3449
3450
  fields: schema.fields.map((field) => {
3450
3451
  if (field.id !== slot.nodeId) return field;
3451
3452
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3452
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3453
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3453
3454
  })
3454
3455
  };
3455
3456
  }
@@ -3465,7 +3466,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3465
3466
  const translations = text.trim().length === 0 ? Object.fromEntries(
3466
3467
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3467
3468
  ) : { ...option.translations, [slot.locale]: text };
3468
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3469
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3469
3470
  })
3470
3471
  };
3471
3472
  })
@@ -3477,7 +3478,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3477
3478
  pages: schema.pages.map((page) => {
3478
3479
  if (page.id !== slot.nodeId) return page;
3479
3480
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3480
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3481
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3481
3482
  })
3482
3483
  }
3483
3484
  };
@@ -3495,13 +3496,22 @@ function useTranslationWorkspace({
3495
3496
  onLocaleRemoved,
3496
3497
  onLocaleChange,
3497
3498
  beforeRemoveLocale,
3499
+ confirmRemoveLocale,
3500
+ slots: workspaceSlots,
3501
+ onTranslationStart,
3502
+ onTranslationSuccess,
3503
+ onTranslationError,
3504
+ onTranslationChange,
3498
3505
  validateLocale
3499
3506
  }) {
3500
3507
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3501
3508
  const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
3502
3509
  const [isTranslating, setIsTranslating] = (0, import_react5.useState)(false);
3503
3510
  const [error, setError] = (0, import_react5.useState)();
3511
+ const [pendingRemoval, setPendingRemoval] = (0, import_react5.useState)();
3512
+ const pendingRemovalResolver = (0, import_react5.useRef)(void 0);
3504
3513
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3514
+ const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3505
3515
  const targetLocales = (0, import_react5.useMemo)(() => {
3506
3516
  const locales = (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales;
3507
3517
  return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
@@ -3551,9 +3561,17 @@ function useTranslationWorkspace({
3551
3561
  const setTranslation = (0, import_react5.useCallback)(
3552
3562
  (slot, text) => {
3553
3563
  if (readOnly) return;
3554
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3564
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3565
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual"));
3566
+ onTranslationChange?.({
3567
+ slot,
3568
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3569
+ nextText: text,
3570
+ mode: "manual",
3571
+ metadata
3572
+ });
3555
3573
  },
3556
- [commit, currentSchema, readOnly, sourceLocale]
3574
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3557
3575
  );
3558
3576
  const addLocale = (0, import_react5.useCallback)(
3559
3577
  (locale) => {
@@ -3614,21 +3632,36 @@ function useTranslationWorkspace({
3614
3632
  onLocaleRemoved?.(normalized);
3615
3633
  };
3616
3634
  const slotCount = (0, import_core5.collectTranslationSlots)(currentSchema, normalized).length;
3635
+ const translatedSlotsCount = (0, import_core5.collectTranslationSlots)(currentSchema, normalized).filter(
3636
+ (slot) => slot.existingText !== void 0 && slot.existingText.trim().length > 0
3637
+ ).length;
3638
+ const requestConfirmation = () => new Promise((resolve) => {
3639
+ pendingRemovalResolver.current = resolve;
3640
+ setPendingRemoval({
3641
+ locale: normalized,
3642
+ localeLabel: localeOptions.find((option) => option.locale === normalized)?.label ?? normalized,
3643
+ translatedSlotsCount
3644
+ });
3645
+ });
3646
+ const removeAfterApproval = () => {
3647
+ if (confirmRemoveLocaleRenderer === void 0) {
3648
+ remove();
3649
+ return true;
3650
+ }
3651
+ return requestConfirmation();
3652
+ };
3617
3653
  if (beforeRemoveLocale === void 0) {
3618
- remove();
3619
- return true;
3654
+ return removeAfterApproval();
3620
3655
  }
3621
3656
  try {
3622
3657
  const decision = beforeRemoveLocale(normalized, { slotCount });
3623
3658
  if (typeof decision === "boolean") {
3624
3659
  if (!decision) return false;
3625
- remove();
3626
- return true;
3660
+ return removeAfterApproval();
3627
3661
  }
3628
3662
  return decision.then((allowed) => {
3629
3663
  if (!allowed) return false;
3630
- remove();
3631
- return true;
3664
+ return removeAfterApproval();
3632
3665
  });
3633
3666
  } catch (cause) {
3634
3667
  const workspaceError = {
@@ -3640,8 +3673,45 @@ function useTranslationWorkspace({
3640
3673
  return Promise.reject(cause);
3641
3674
  }
3642
3675
  },
3643
- [activeLocale, beforeRemoveLocale, commit, currentSchema, onLocaleRemoved, readOnly, sourceLocale]
3676
+ [
3677
+ activeLocale,
3678
+ beforeRemoveLocale,
3679
+ commit,
3680
+ confirmRemoveLocaleRenderer,
3681
+ currentSchema,
3682
+ localeOptions,
3683
+ onLocaleRemoved,
3684
+ readOnly,
3685
+ sourceLocale
3686
+ ]
3644
3687
  );
3688
+ const confirmPendingRemoval = (0, import_react5.useCallback)(() => {
3689
+ const pending = pendingRemoval;
3690
+ const resolve = pendingRemovalResolver.current;
3691
+ if (pending === void 0 || resolve === void 0) return;
3692
+ pendingRemovalResolver.current = void 0;
3693
+ setPendingRemoval(void 0);
3694
+ commit((0, import_core5.removeLocaleFromSchema)(currentSchema, pending.locale));
3695
+ if (activeLocale === pending.locale) setSelectedLocale("");
3696
+ onLocaleRemoved?.(pending.locale);
3697
+ resolve(true);
3698
+ }, [activeLocale, commit, currentSchema, onLocaleRemoved, pendingRemoval]);
3699
+ const cancelPendingRemoval = (0, import_react5.useCallback)(() => {
3700
+ const resolve = pendingRemovalResolver.current;
3701
+ if (resolve === void 0) return;
3702
+ pendingRemovalResolver.current = void 0;
3703
+ setPendingRemoval(void 0);
3704
+ resolve(false);
3705
+ }, []);
3706
+ const removeLocaleConfirmation = (0, import_react5.useMemo)(() => {
3707
+ if (confirmRemoveLocaleRenderer === void 0 || pendingRemoval === void 0) return null;
3708
+ return confirmRemoveLocaleRenderer({
3709
+ ...pendingRemoval,
3710
+ isOpen: true,
3711
+ onConfirm: confirmPendingRemoval,
3712
+ onCancel: cancelPendingRemoval
3713
+ });
3714
+ }, [cancelPendingRemoval, confirmPendingRemoval, confirmRemoveLocaleRenderer, pendingRemoval]);
3645
3715
  const translateAll = (0, import_react5.useCallback)(
3646
3716
  async (options = {}) => {
3647
3717
  if (readOnly) {
@@ -3678,6 +3748,7 @@ function useTranslationWorkspace({
3678
3748
  }
3679
3749
  setIsTranslating(true);
3680
3750
  setError(void 0);
3751
+ onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3681
3752
  try {
3682
3753
  const populated = await (0, import_core5.populateSchemaTranslations)(
3683
3754
  currentSchema,
@@ -3690,6 +3761,17 @@ function useTranslationWorkspace({
3690
3761
  }
3691
3762
  );
3692
3763
  commit(populated.schema);
3764
+ const missingSlotsCount = (0, import_core5.collectTranslationSlots)(populated.schema, activeLocale).filter(
3765
+ (slot) => slot.status === "missing"
3766
+ ).length;
3767
+ onTranslationSuccess?.({
3768
+ sourceLocale,
3769
+ targetLocale: activeLocale,
3770
+ mode: "automatic",
3771
+ updatedSlots: populated.report.updatedSlots,
3772
+ skippedSlots: populated.report.skippedSlots,
3773
+ missingSlotsCount
3774
+ });
3693
3775
  return { success: true, report: populated.report };
3694
3776
  } catch (cause) {
3695
3777
  const workspaceError = {
@@ -3698,12 +3780,25 @@ function useTranslationWorkspace({
3698
3780
  cause
3699
3781
  };
3700
3782
  setError(workspaceError);
3783
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3701
3784
  return { success: false, error: workspaceError };
3702
3785
  } finally {
3703
3786
  setIsTranslating(false);
3704
3787
  }
3705
3788
  },
3706
- [activeLocale, commit, currentSchema, localeOptions, readOnly, slots, translationAdapter]
3789
+ [
3790
+ activeLocale,
3791
+ commit,
3792
+ currentSchema,
3793
+ localeOptions,
3794
+ onTranslationError,
3795
+ onTranslationStart,
3796
+ onTranslationSuccess,
3797
+ readOnly,
3798
+ slots,
3799
+ sourceLocale,
3800
+ translationAdapter
3801
+ ]
3707
3802
  );
3708
3803
  const translateSlot = (0, import_react5.useCallback)(
3709
3804
  async (slot) => {
@@ -3721,7 +3816,15 @@ function useTranslationWorkspace({
3721
3816
  setError(void 0);
3722
3817
  try {
3723
3818
  const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3724
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3819
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3820
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3821
+ onTranslationChange?.({
3822
+ slot,
3823
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3824
+ nextText: text,
3825
+ mode: "automatic",
3826
+ metadata
3827
+ });
3725
3828
  return { success: true };
3726
3829
  } catch (cause) {
3727
3830
  const workspaceError = {
@@ -3735,7 +3838,7 @@ function useTranslationWorkspace({
3735
3838
  setIsTranslating(false);
3736
3839
  }
3737
3840
  },
3738
- [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3841
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3739
3842
  );
3740
3843
  return {
3741
3844
  sourceLocale,
@@ -3752,6 +3855,7 @@ function useTranslationWorkspace({
3752
3855
  addLocale,
3753
3856
  isAddLocaleAllowed,
3754
3857
  removeLocale,
3858
+ ...removeLocaleConfirmation === null ? {} : { removeLocaleConfirmation },
3755
3859
  setTranslation,
3756
3860
  translateAll,
3757
3861
  translateSlot,
@@ -3760,8 +3864,131 @@ function useTranslationWorkspace({
3760
3864
  };
3761
3865
  }
3762
3866
 
3867
+ // src/hooks/useTranslationComparison.ts
3868
+ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3869
+ if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3870
+ if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3871
+ return {
3872
+ sourceLocale: metadata.sourceLocale,
3873
+ sourceTextHash: metadata.sourceTextHash,
3874
+ translationSource: metadata.translationSource,
3875
+ ...typeof metadata.translatedAt === "string" ? { translatedAt: metadata.translatedAt } : {},
3876
+ ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3877
+ };
3878
+ }
3879
+ return {
3880
+ sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3881
+ sourceTextHash: (0, import_core6.computeSourceTextHash)(sourceText),
3882
+ translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
3883
+ };
3884
+ }
3885
+ function optionParentId(slot) {
3886
+ if (slot.kind !== "option" || slot.path === void 0) return void 0;
3887
+ return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3888
+ }
3889
+ function getNodeTitle(schema, slot) {
3890
+ if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3891
+ if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3892
+ if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
3893
+ return void 0;
3894
+ }
3895
+ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3896
+ const path = slot.path ?? `${slot.kind}.${slot.nodeId}.${slot.property}`;
3897
+ const nodeTitle = getNodeTitle(schema, slot);
3898
+ const metadata = canonicalMetadata(
3899
+ slot.existingTranslationMetadata,
3900
+ slot.sourceText,
3901
+ sourceLocale,
3902
+ slot.existingText
3903
+ );
3904
+ return {
3905
+ id: path,
3906
+ path,
3907
+ targetKind: slot.kind,
3908
+ targetProperty: slot.property,
3909
+ ...nodeTitle === void 0 ? {} : { nodeTitle },
3910
+ sourceText: slot.sourceText,
3911
+ translatedText: slot.existingText ?? "",
3912
+ status: slot.status ?? "missing",
3913
+ ...metadata === void 0 ? {} : { metadata },
3914
+ translatable: hasAdapter && slot.sourceText.trim().length > 0
3915
+ };
3916
+ }
3917
+ function summaryFor(items) {
3918
+ const counts = {
3919
+ missing: 0,
3920
+ translated: 0,
3921
+ stale: 0,
3922
+ manual: 0,
3923
+ "manual-stale": 0
3924
+ };
3925
+ for (const item of items) counts[item.status] += 1;
3926
+ return {
3927
+ total: items.length,
3928
+ translated: counts.translated + counts.manual,
3929
+ missing: counts.missing,
3930
+ stale: counts.stale + counts["manual-stale"],
3931
+ manual: counts.manual + counts["manual-stale"]
3932
+ };
3933
+ }
3934
+ function useTranslationComparison({
3935
+ schema,
3936
+ sourceLocale = schema.defaultLocale ?? "en",
3937
+ targetLocale,
3938
+ translationAdapter,
3939
+ readOnly = false,
3940
+ onChange,
3941
+ onTranslationChange
3942
+ }) {
3943
+ const workspace = useTranslationWorkspace({
3944
+ schema,
3945
+ sourceLocale,
3946
+ targetLocale,
3947
+ ...translationAdapter === void 0 ? {} : { translationAdapter },
3948
+ readOnly,
3949
+ ...onChange === void 0 ? {} : { onChange },
3950
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange }
3951
+ });
3952
+ const items = (0, import_react6.useMemo)(
3953
+ () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
3954
+ [schema, sourceLocale, translationAdapter, workspace.slots]
3955
+ );
3956
+ const itemByPath = (0, import_react6.useMemo)(() => new Map(workspace.slots.map((slot) => [slot.path, slot])), [workspace.slots]);
3957
+ const updateTranslation = (0, import_react6.useCallback)(
3958
+ (path, text) => {
3959
+ const slot = itemByPath.get(path);
3960
+ if (slot !== void 0) workspace.setTranslation(slot, text);
3961
+ },
3962
+ [itemByPath, workspace]
3963
+ );
3964
+ const translateSingle = (0, import_react6.useCallback)(
3965
+ async (path) => {
3966
+ const slot = itemByPath.get(path);
3967
+ if (slot === void 0) return;
3968
+ const result = await workspace.translateSlot(slot);
3969
+ if (!result.success) throw new Error(result.error?.type ?? "Translation failed.");
3970
+ },
3971
+ [itemByPath, workspace]
3972
+ );
3973
+ const translateAll = (0, import_react6.useCallback)(async () => {
3974
+ const result = await workspace.translateAll();
3975
+ if (result.report !== void 0) return result.report;
3976
+ throw new Error(result.error?.type ?? "Translation failed.");
3977
+ }, [workspace]);
3978
+ return {
3979
+ sourceLocale: workspace.sourceLocale,
3980
+ targetLocale: workspace.targetLocale,
3981
+ items,
3982
+ summary: summaryFor(items),
3983
+ isTranslating: workspace.isTranslating,
3984
+ updateTranslation,
3985
+ translateSingle,
3986
+ translateAll
3987
+ };
3988
+ }
3989
+
3763
3990
  // src/receipt.ts
3764
- var import_react6 = require("react");
3991
+ var import_react7 = require("react");
3765
3992
  function submissionReceiptQueryKey(formId, formVersion) {
3766
3993
  return `${formId}:v${formVersion}`;
3767
3994
  }
@@ -3832,19 +4059,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3832
4059
  }
3833
4060
  function useSubmissionReceipts(store, queries) {
3834
4061
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3835
- const stableQueries = (0, import_react6.useMemo)(() => {
4062
+ const stableQueries = (0, import_react7.useMemo)(() => {
3836
4063
  const parsed = JSON.parse(querySignature);
3837
4064
  if (!Array.isArray(parsed)) return [];
3838
4065
  return parsed.flatMap(
3839
4066
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
3840
4067
  );
3841
4068
  }, [querySignature]);
3842
- const [state, setState] = (0, import_react6.useState)({
4069
+ const [state, setState] = (0, import_react7.useState)({
3843
4070
  receipts: /* @__PURE__ */ new Map(),
3844
4071
  isLoading: stableQueries.length > 0,
3845
4072
  error: null
3846
4073
  });
3847
- (0, import_react6.useEffect)(() => {
4074
+ (0, import_react7.useEffect)(() => {
3848
4075
  let active = true;
3849
4076
  if (stableQueries.length === 0) {
3850
4077
  setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
@@ -3878,8 +4105,8 @@ function useSubmissionReceipts(store, queries) {
3878
4105
  }
3879
4106
 
3880
4107
  // src/renderer.tsx
3881
- var import_core6 = require("@form-engine-ts/core");
3882
- var import_react7 = require("react");
4108
+ var import_core7 = require("@form-engine-ts/core");
4109
+ var import_react8 = require("react");
3883
4110
  var import_jsx_runtime4 = require("react/jsx-runtime");
3884
4111
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
3885
4112
  function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
@@ -4391,33 +4618,33 @@ function ContextFormRenderer({
4391
4618
  }) {
4392
4619
  const form = useForm();
4393
4620
  const i18n = useFormEngineI18n();
4394
- const isProviderValue = (0, import_react7.useContext)(FormEngineI18nProviderScopeContext);
4395
- const prefix = (0, import_react7.useId)().replace(/:/g, "");
4396
- const formRef = (0, import_react7.useRef)(null);
4397
- const loadedDraftKey = (0, import_react7.useRef)(null);
4398
- const [draftRestored, setDraftRestored] = (0, import_react7.useState)(false);
4399
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react7.useState)(0);
4400
- const [focusFieldId, setFocusFieldId] = (0, import_react7.useState)(null);
4401
- const [confirmation, setConfirmation] = (0, import_react7.useState)(null);
4402
- const [guardMessage, setGuardMessage] = (0, import_react7.useState)(null);
4403
- const [guardsPending, setGuardsPending] = (0, import_react7.useState)(false);
4404
- const [receipt, setReceipt] = (0, import_react7.useState)(null);
4405
- const [completionData, setCompletionData] = (0, import_react7.useState)(null);
4406
- const [receiptLoaded, setReceiptLoaded] = (0, import_react7.useState)(receiptStore === void 0);
4407
- const rendererSubmissionInFlight = (0, import_react7.useRef)(false);
4408
- const fallbackAttemptId = (0, import_react7.useRef)(null);
4409
- const completionRef = (0, import_react7.useRef)(null);
4410
- const confirmationRef = (0, import_react7.useRef)(null);
4621
+ const isProviderValue = (0, import_react8.useContext)(FormEngineI18nProviderScopeContext);
4622
+ const prefix = (0, import_react8.useId)().replace(/:/g, "");
4623
+ const formRef = (0, import_react8.useRef)(null);
4624
+ const loadedDraftKey = (0, import_react8.useRef)(null);
4625
+ const [draftRestored, setDraftRestored] = (0, import_react8.useState)(false);
4626
+ const [currentPageIndex, setCurrentPageIndex] = (0, import_react8.useState)(0);
4627
+ const [focusFieldId, setFocusFieldId] = (0, import_react8.useState)(null);
4628
+ const [confirmation, setConfirmation] = (0, import_react8.useState)(null);
4629
+ const [guardMessage, setGuardMessage] = (0, import_react8.useState)(null);
4630
+ const [guardsPending, setGuardsPending] = (0, import_react8.useState)(false);
4631
+ const [receipt, setReceipt] = (0, import_react8.useState)(null);
4632
+ const [completionData, setCompletionData] = (0, import_react8.useState)(null);
4633
+ const [receiptLoaded, setReceiptLoaded] = (0, import_react8.useState)(receiptStore === void 0);
4634
+ const rendererSubmissionInFlight = (0, import_react8.useRef)(false);
4635
+ const fallbackAttemptId = (0, import_react8.useRef)(null);
4636
+ const completionRef = (0, import_react8.useRef)(null);
4637
+ const confirmationRef = (0, import_react8.useRef)(null);
4411
4638
  const pages = form.schema.pages;
4412
- const visiblePageIndexes = (0, import_react7.useMemo)(
4639
+ const visiblePageIndexes = (0, import_react8.useMemo)(
4413
4640
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
4414
4641
  [form.pageVisibility, pages]
4415
4642
  );
4416
4643
  const activePage = pages?.[currentPageIndex];
4417
4644
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
4418
4645
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
4419
- const visibleValues = (0, import_react7.useMemo)(() => (0, import_core6.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
4420
- const visibleItems = (0, import_react7.useMemo)(
4646
+ const visibleValues = (0, import_react8.useMemo)(() => (0, import_core7.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
4647
+ const visibleItems = (0, import_react8.useMemo)(
4421
4648
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
4422
4649
  [form.schema, form.translate, form.values, form.visibility]
4423
4650
  );
@@ -4426,7 +4653,7 @@ function ContextFormRenderer({
4426
4653
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
4427
4654
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
4428
4655
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
4429
- const resolveMessage = (0, import_react7.useCallback)(
4656
+ const resolveMessage = (0, import_react8.useCallback)(
4430
4657
  (key, fallback) => {
4431
4658
  const providerDefault = isProviderValue ? i18n.translator(`renderer.${key}`) : void 0;
4432
4659
  const defaultText = fallback ?? (providerDefault === "" ? void 0 : providerDefault) ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
@@ -4435,15 +4662,15 @@ function ContextFormRenderer({
4435
4662
  },
4436
4663
  [form.locale, i18n, isProviderValue, messageResolver, messages]
4437
4664
  );
4438
- const fieldTranslate = (0, import_react7.useCallback)(
4665
+ const fieldTranslate = (0, import_react8.useCallback)(
4439
4666
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
4440
4667
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
4441
4668
  );
4442
- const focusSubmitButton = (0, import_react7.useCallback)(() => {
4669
+ const focusSubmitButton = (0, import_react8.useCallback)(() => {
4443
4670
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
4444
4671
  button?.focus();
4445
4672
  }, []);
4446
- (0, import_react7.useEffect)(() => {
4673
+ (0, import_react8.useEffect)(() => {
4447
4674
  let active = true;
4448
4675
  if (receiptStore === void 0) {
4449
4676
  setReceipt(null);
@@ -4464,14 +4691,14 @@ function ContextFormRenderer({
4464
4691
  active = false;
4465
4692
  };
4466
4693
  }, [form.schema.id, form.schema.version, receiptStore]);
4467
- (0, import_react7.useEffect)(() => {
4694
+ (0, import_react8.useEffect)(() => {
4468
4695
  if (pages === void 0 || visiblePageIndexes.length === 0) {
4469
4696
  setCurrentPageIndex(0);
4470
4697
  return;
4471
4698
  }
4472
4699
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
4473
4700
  }, [currentPageIndex, pages, visiblePageIndexes]);
4474
- (0, import_react7.useEffect)(() => {
4701
+ (0, import_react8.useEffect)(() => {
4475
4702
  if (focusFieldId === null) return;
4476
4703
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
4477
4704
  (element) => element.dataset.fieldId === focusFieldId
@@ -4483,11 +4710,11 @@ function ContextFormRenderer({
4483
4710
  setFocusFieldId(null);
4484
4711
  }
4485
4712
  }, [focusFieldId]);
4486
- (0, import_react7.useEffect)(() => {
4713
+ (0, import_react8.useEffect)(() => {
4487
4714
  if (!isReplaceMode || form.submitStatus !== "success") return;
4488
4715
  completionRef.current?.focus();
4489
4716
  }, [form.submitStatus, isReplaceMode]);
4490
- (0, import_react7.useEffect)(() => {
4717
+ (0, import_react8.useEffect)(() => {
4491
4718
  if (confirmation === null) return;
4492
4719
  const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
4493
4720
  confirmButton?.focus();
@@ -4519,7 +4746,7 @@ function ContextFormRenderer({
4519
4746
  globalThis.addEventListener("keydown", onKeyDown);
4520
4747
  return () => globalThis.removeEventListener("keydown", onKeyDown);
4521
4748
  }, [confirmation, confirmationRenderMode, focusSubmitButton]);
4522
- (0, import_react7.useEffect)(() => {
4749
+ (0, import_react8.useEffect)(() => {
4523
4750
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
4524
4751
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
4525
4752
  if (loadedDraftKey.current === loadIdentity) return;
@@ -4531,7 +4758,7 @@ function ContextFormRenderer({
4531
4758
  form.restoreValues(draft.values);
4532
4759
  setDraftRestored(true);
4533
4760
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
4534
- (0, import_react7.useEffect)(() => {
4761
+ (0, import_react8.useEffect)(() => {
4535
4762
  if (form.submitStatus === "success") return;
4536
4763
  const timeout = globalThis.setTimeout(() => {
4537
4764
  onDraftSave?.(form.values);
@@ -4587,7 +4814,7 @@ function ContextFormRenderer({
4587
4814
  if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
4588
4815
  return { status: "cancelled" };
4589
4816
  }
4590
- const validation = (0, import_core6.validateAnswers)(form.schema, form.values);
4817
+ const validation = (0, import_core7.validateAnswers)(form.schema, form.values);
4591
4818
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
4592
4819
  if (validation.valid && !guardsConfirmed) {
4593
4820
  rendererSubmissionInFlight.current = true;
@@ -4650,8 +4877,9 @@ function ContextFormRenderer({
4650
4877
  return result;
4651
4878
  }
4652
4879
  if (result.status === "error") {
4653
- if (result.error instanceof FormSubmissionError) {
4654
- const fieldErrors = result.error.payload.fieldErrors ?? {};
4880
+ const payload = result.error instanceof import_core7.FormSubmissionError ? result.error.payload : (0, import_core7.isFormSubmissionSerializedError)(result.error) ? result.error : void 0;
4881
+ if (payload !== void 0) {
4882
+ const fieldErrors = payload.fieldErrors ?? {};
4655
4883
  form.setServerErrors?.(fieldErrors);
4656
4884
  const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
4657
4885
  if (firstServerFieldId !== void 0) {
@@ -4659,6 +4887,9 @@ function ContextFormRenderer({
4659
4887
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
4660
4888
  focusFirstIssue(firstServerFieldId);
4661
4889
  }
4890
+ if (payload.piiFindings !== void 0 && payload.piiFindings.length > 0) {
4891
+ setConfirmation({ findings: payload.piiFindings, generic: false });
4892
+ }
4662
4893
  }
4663
4894
  return result;
4664
4895
  }
@@ -4889,7 +5120,7 @@ function ContextFormRenderer({
4889
5120
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
4890
5121
  };
4891
5122
  if (slots.renderField !== void 0) {
4892
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react7.Fragment, { children: slots.renderField({
5123
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react8.Fragment, { children: slots.renderField({
4893
5124
  question: field,
4894
5125
  value: form.values[field.id],
4895
5126
  onChange: (value) => {
@@ -4960,8 +5191,8 @@ function ContextFormRenderer({
4960
5191
  ] }),
4961
5192
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
4962
5193
  form.submitStatus === "success" ? completionRegion : null,
4963
- form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { role: "alert", children: [
4964
- form.submitError.payload.formError,
5194
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof import_core7.FormSubmissionError && (form.submitError.payload.formErrors?.length ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { role: "alert", children: [
5195
+ form.submitError.payload.formErrors?.map((message) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: message }, message)),
4965
5196
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
4966
5197
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { role: "alert", children: [
4967
5198
  errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
@@ -5017,7 +5248,7 @@ var defaultRendererTranslator = {
5017
5248
  };
5018
5249
  function FormRenderer(props) {
5019
5250
  const i18n = useFormEngineI18n();
5020
- const isProviderValue = (0, import_react7.useContext)(FormEngineI18nProviderScopeContext);
5251
+ const isProviderValue = (0, import_react8.useContext)(FormEngineI18nProviderScopeContext);
5021
5252
  if (!("schema" in props)) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ContextFormRenderer, { ...props });
5022
5253
  const {
5023
5254
  schema,
@@ -5044,6 +5275,9 @@ function FormRenderer(props) {
5044
5275
  }
5045
5276
  );
5046
5277
  }
5278
+
5279
+ // src/types.ts
5280
+ var import_core8 = require("@form-engine-ts/core");
5047
5281
  // Annotate the CommonJS export names for ESM import in node:
5048
5282
  0 && (module.exports = {
5049
5283
  BUILDER_TRANSLATION_ALIASES,
@@ -5068,6 +5302,7 @@ function FormRenderer(props) {
5068
5302
  useFormBuilder,
5069
5303
  useFormEngineI18n,
5070
5304
  useSubmissionReceipts,
5305
+ useTranslationComparison,
5071
5306
  useTranslationWorkspace,
5072
5307
  validateLocalePipeline
5073
5308
  });