@form-engine-ts/react 6.0.0 → 6.2.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");
@@ -3489,6 +3489,7 @@ function useTranslationWorkspace({
3489
3489
  sourceLocale = schema.defaultLocale ?? "en",
3490
3490
  targetLocale,
3491
3491
  translationAdapter,
3492
+ signal,
3492
3493
  readOnly = false,
3493
3494
  policy,
3494
3495
  availableLocales,
@@ -3500,6 +3501,7 @@ function useTranslationWorkspace({
3500
3501
  slots: workspaceSlots,
3501
3502
  onTranslationStart,
3502
3503
  onTranslationSuccess,
3504
+ onTranslationReport,
3503
3505
  onTranslationError,
3504
3506
  onTranslationChange,
3505
3507
  validateLocale
@@ -3507,8 +3509,10 @@ function useTranslationWorkspace({
3507
3509
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3508
3510
  const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
3509
3511
  const [isTranslating, setIsTranslating] = (0, import_react5.useState)(false);
3512
+ const [progress, setProgress] = (0, import_react5.useState)();
3510
3513
  const [error, setError] = (0, import_react5.useState)();
3511
3514
  const [pendingRemoval, setPendingRemoval] = (0, import_react5.useState)();
3515
+ const activeAbortController = (0, import_react5.useRef)(void 0);
3512
3516
  const pendingRemovalResolver = (0, import_react5.useRef)(void 0);
3513
3517
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3514
3518
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
@@ -3748,7 +3752,15 @@ function useTranslationWorkspace({
3748
3752
  }
3749
3753
  setIsTranslating(true);
3750
3754
  setError(void 0);
3755
+ setProgress(void 0);
3751
3756
  onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3757
+ const controller = new AbortController();
3758
+ activeAbortController.current = controller;
3759
+ const operationSignal = options.signal ?? signal;
3760
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3761
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3762
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3763
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3752
3764
  try {
3753
3765
  const populated = await (0, import_core5.populateSchemaTranslations)(
3754
3766
  currentSchema,
@@ -3757,13 +3769,36 @@ function useTranslationWorkspace({
3757
3769
  {
3758
3770
  overwrite: "stale-and-missing",
3759
3771
  preserveManualTranslations: true,
3760
- ...options
3772
+ ...options,
3773
+ continueOnError: true,
3774
+ signal: controller.signal,
3775
+ onProgress: (nextProgress) => {
3776
+ setProgress(nextProgress);
3777
+ options.onProgress?.(nextProgress);
3778
+ }
3761
3779
  }
3762
3780
  );
3763
3781
  commit(populated.schema);
3782
+ onTranslationReport?.(populated.report);
3764
3783
  const missingSlotsCount = (0, import_core5.collectTranslationSlots)(populated.schema, activeLocale).filter(
3765
3784
  (slot) => slot.status === "missing"
3766
3785
  ).length;
3786
+ if (populated.report.cancelled === true) {
3787
+ const workspaceError = { type: "cancelled" };
3788
+ setError(workspaceError);
3789
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3790
+ return { success: false, report: populated.report, error: workspaceError };
3791
+ }
3792
+ if ((populated.report.failed ?? 0) > 0) {
3793
+ const workspaceError = {
3794
+ type: "partial_failure",
3795
+ succeeded: populated.report.succeeded ?? populated.report.updatedSlots.length,
3796
+ failed: populated.report.failed ?? 0
3797
+ };
3798
+ setError(workspaceError);
3799
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3800
+ return { success: false, report: populated.report, error: workspaceError };
3801
+ }
3767
3802
  onTranslationSuccess?.({
3768
3803
  sourceLocale,
3769
3804
  targetLocale: activeLocale,
@@ -3774,6 +3809,12 @@ function useTranslationWorkspace({
3774
3809
  });
3775
3810
  return { success: true, report: populated.report };
3776
3811
  } catch (cause) {
3812
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3813
+ const workspaceError2 = { type: "cancelled" };
3814
+ setError(workspaceError2);
3815
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError2 });
3816
+ return { success: false, error: workspaceError2 };
3817
+ }
3777
3818
  const workspaceError = {
3778
3819
  type: "translation_failed",
3779
3820
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3783,6 +3824,11 @@ function useTranslationWorkspace({
3783
3824
  onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3784
3825
  return { success: false, error: workspaceError };
3785
3826
  } finally {
3827
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3828
+ const listener = abortListener;
3829
+ operationSignal.removeEventListener("abort", listener);
3830
+ }
3831
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3786
3832
  setIsTranslating(false);
3787
3833
  }
3788
3834
  },
@@ -3794,14 +3840,19 @@ function useTranslationWorkspace({
3794
3840
  onTranslationError,
3795
3841
  onTranslationStart,
3796
3842
  onTranslationSuccess,
3843
+ onTranslationReport,
3797
3844
  readOnly,
3798
3845
  slots,
3799
3846
  sourceLocale,
3800
- translationAdapter
3847
+ translationAdapter,
3848
+ signal
3801
3849
  ]
3802
3850
  );
3851
+ const cancelTranslation = (0, import_react5.useCallback)(() => {
3852
+ activeAbortController.current?.abort(new DOMException("The translation was cancelled.", "AbortError"));
3853
+ }, []);
3803
3854
  const translateSlot = (0, import_react5.useCallback)(
3804
- async (slot) => {
3855
+ async (slot, options = {}) => {
3805
3856
  if (translationAdapter === void 0) {
3806
3857
  const workspaceError = { type: "adapter_not_configured" };
3807
3858
  setError(workspaceError);
@@ -3814,8 +3865,21 @@ function useTranslationWorkspace({
3814
3865
  }
3815
3866
  setIsTranslating(true);
3816
3867
  setError(void 0);
3868
+ setProgress({ total: 1, completed: 0, succeeded: 0, failed: 0, percentage: 0 });
3869
+ const controller = new AbortController();
3870
+ activeAbortController.current = controller;
3871
+ const operationSignal = options.signal ?? signal;
3872
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3873
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3874
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3875
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3817
3876
  try {
3818
- const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3877
+ const text = await asAsyncAdapter(translationAdapter).translateText(
3878
+ slot.sourceText,
3879
+ slot.locale,
3880
+ sourceLocale,
3881
+ controller.signal
3882
+ );
3819
3883
  const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3820
3884
  commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3821
3885
  onTranslationChange?.({
@@ -3825,8 +3889,15 @@ function useTranslationWorkspace({
3825
3889
  mode: "automatic",
3826
3890
  metadata
3827
3891
  });
3892
+ setProgress({ total: 1, completed: 1, succeeded: 1, failed: 0, percentage: 100 });
3828
3893
  return { success: true };
3829
3894
  } catch (cause) {
3895
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3896
+ const workspaceError2 = { type: "cancelled" };
3897
+ setError(workspaceError2);
3898
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError2 });
3899
+ return { success: false, error: workspaceError2 };
3900
+ }
3830
3901
  const workspaceError = {
3831
3902
  type: "translation_failed",
3832
3903
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3835,10 +3906,15 @@ function useTranslationWorkspace({
3835
3906
  setError(workspaceError);
3836
3907
  return { success: false, error: workspaceError };
3837
3908
  } finally {
3909
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3910
+ const listener = abortListener;
3911
+ operationSignal.removeEventListener("abort", listener);
3912
+ }
3913
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3838
3914
  setIsTranslating(false);
3839
3915
  }
3840
3916
  },
3841
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3917
+ [commit, currentSchema, onTranslationChange, onTranslationError, readOnly, signal, sourceLocale, translationAdapter]
3842
3918
  );
3843
3919
  return {
3844
3920
  sourceLocale,
@@ -3859,13 +3935,159 @@ function useTranslationWorkspace({
3859
3935
  setTranslation,
3860
3936
  translateAll,
3861
3937
  translateSlot,
3938
+ cancelTranslation,
3862
3939
  isTranslating,
3940
+ ...progress === void 0 ? {} : { progress },
3863
3941
  ...error === void 0 ? {} : { error }
3864
3942
  };
3865
3943
  }
3866
3944
 
3945
+ // src/hooks/useTranslationComparison.ts
3946
+ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3947
+ if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3948
+ if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3949
+ return {
3950
+ ...metadata,
3951
+ sourceLocale: metadata.sourceLocale,
3952
+ sourceTextHash: metadata.sourceTextHash,
3953
+ translationSource: metadata.translationSource,
3954
+ ...typeof metadata.translatedAt === "string" ? { translatedAt: metadata.translatedAt } : {},
3955
+ ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3956
+ };
3957
+ }
3958
+ const canonicalKeys = /* @__PURE__ */ new Set(["sourceLocale", "sourceTextHash", "translationSource", "translatedAt", "editedAt"]);
3959
+ const extensions = Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => !canonicalKeys.has(key)));
3960
+ return {
3961
+ ...extensions,
3962
+ sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3963
+ sourceTextHash: (0, import_core6.computeSourceTextHash)(sourceText),
3964
+ translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
3965
+ };
3966
+ }
3967
+ function optionParentId(slot) {
3968
+ if (slot.kind !== "option" || slot.path === void 0) return void 0;
3969
+ return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3970
+ }
3971
+ function getNodeTitle(schema, slot) {
3972
+ if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3973
+ if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3974
+ if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
3975
+ return void 0;
3976
+ }
3977
+ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3978
+ const path = slot.path ?? `${slot.kind}.${slot.nodeId}.${slot.property}`;
3979
+ const nodeTitle = getNodeTitle(schema, slot);
3980
+ const metadata = canonicalMetadata(
3981
+ slot.existingTranslationMetadata,
3982
+ slot.sourceText,
3983
+ sourceLocale,
3984
+ slot.existingText
3985
+ );
3986
+ return {
3987
+ id: path,
3988
+ path,
3989
+ targetKind: slot.kind,
3990
+ targetProperty: slot.property,
3991
+ ...nodeTitle === void 0 ? {} : { nodeTitle },
3992
+ sourceText: slot.sourceText,
3993
+ translatedText: slot.existingText ?? "",
3994
+ status: slot.status ?? "missing",
3995
+ ...metadata === void 0 ? {} : { metadata },
3996
+ translatable: hasAdapter && slot.sourceText.trim().length > 0
3997
+ };
3998
+ }
3999
+ function summaryFor(items) {
4000
+ const counts = {
4001
+ missing: 0,
4002
+ translated: 0,
4003
+ stale: 0,
4004
+ manual: 0,
4005
+ "manual-stale": 0
4006
+ };
4007
+ for (const item of items) counts[item.status] += 1;
4008
+ return {
4009
+ total: items.length,
4010
+ translated: counts.translated + counts.manual,
4011
+ missing: counts.missing,
4012
+ stale: counts.stale + counts["manual-stale"],
4013
+ manual: counts.manual + counts["manual-stale"]
4014
+ };
4015
+ }
4016
+ function useTranslationComparison({
4017
+ schema,
4018
+ sourceLocale = schema.defaultLocale ?? "en",
4019
+ targetLocale,
4020
+ translationAdapter,
4021
+ readOnly = false,
4022
+ onChange,
4023
+ onTranslationChange,
4024
+ onTranslationReport,
4025
+ onTranslationError,
4026
+ signal
4027
+ }) {
4028
+ const workspace = useTranslationWorkspace({
4029
+ schema,
4030
+ sourceLocale,
4031
+ targetLocale,
4032
+ ...translationAdapter === void 0 ? {} : { translationAdapter },
4033
+ readOnly,
4034
+ ...onChange === void 0 ? {} : { onChange },
4035
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4036
+ ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4037
+ ...onTranslationError === void 0 ? {} : { onTranslationError },
4038
+ ...signal === void 0 ? {} : { signal }
4039
+ });
4040
+ const [report, setReport] = (0, import_react6.useState)();
4041
+ const items = (0, import_react6.useMemo)(
4042
+ () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
4043
+ [schema, sourceLocale, translationAdapter, workspace.slots]
4044
+ );
4045
+ const itemByPath = (0, import_react6.useMemo)(() => new Map(workspace.slots.map((slot) => [slot.path, slot])), [workspace.slots]);
4046
+ const updateTranslation = (0, import_react6.useCallback)(
4047
+ (path, text) => {
4048
+ const slot = itemByPath.get(path);
4049
+ if (slot !== void 0) workspace.setTranslation(slot, text);
4050
+ },
4051
+ [itemByPath, workspace]
4052
+ );
4053
+ const translateSingle = (0, import_react6.useCallback)(
4054
+ async (path) => {
4055
+ const slot = itemByPath.get(path);
4056
+ if (slot === void 0) return;
4057
+ const result = await workspace.translateSlot(slot);
4058
+ if (!result.success) throw new Error(result.error?.type ?? "Translation failed.");
4059
+ },
4060
+ [itemByPath, workspace]
4061
+ );
4062
+ const translateAll = (0, import_react6.useCallback)(
4063
+ async (options) => {
4064
+ const result = await workspace.translateAll(options);
4065
+ if (result.report !== void 0) {
4066
+ setReport(result.report);
4067
+ return result.report;
4068
+ }
4069
+ throw new Error(result.error?.type ?? "Translation failed.");
4070
+ },
4071
+ [workspace]
4072
+ );
4073
+ return {
4074
+ sourceLocale: workspace.sourceLocale,
4075
+ targetLocale: workspace.targetLocale,
4076
+ items,
4077
+ summary: summaryFor(items),
4078
+ isTranslating: workspace.isTranslating,
4079
+ updateTranslation,
4080
+ translateSingle,
4081
+ translateAll,
4082
+ cancelTranslation: workspace.cancelTranslation,
4083
+ ...workspace.progress === void 0 ? {} : { progress: workspace.progress },
4084
+ ...workspace.error === void 0 ? {} : { error: workspace.error },
4085
+ ...report === void 0 ? {} : { report }
4086
+ };
4087
+ }
4088
+
3867
4089
  // src/receipt.ts
3868
- var import_react6 = require("react");
4090
+ var import_react7 = require("react");
3869
4091
  function submissionReceiptQueryKey(formId, formVersion) {
3870
4092
  return `${formId}:v${formVersion}`;
3871
4093
  }
@@ -3936,19 +4158,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3936
4158
  }
3937
4159
  function useSubmissionReceipts(store, queries) {
3938
4160
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3939
- const stableQueries = (0, import_react6.useMemo)(() => {
4161
+ const stableQueries = (0, import_react7.useMemo)(() => {
3940
4162
  const parsed = JSON.parse(querySignature);
3941
4163
  if (!Array.isArray(parsed)) return [];
3942
4164
  return parsed.flatMap(
3943
4165
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
3944
4166
  );
3945
4167
  }, [querySignature]);
3946
- const [state, setState] = (0, import_react6.useState)({
4168
+ const [state, setState] = (0, import_react7.useState)({
3947
4169
  receipts: /* @__PURE__ */ new Map(),
3948
4170
  isLoading: stableQueries.length > 0,
3949
4171
  error: null
3950
4172
  });
3951
- (0, import_react6.useEffect)(() => {
4173
+ (0, import_react7.useEffect)(() => {
3952
4174
  let active = true;
3953
4175
  if (stableQueries.length === 0) {
3954
4176
  setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
@@ -3982,8 +4204,8 @@ function useSubmissionReceipts(store, queries) {
3982
4204
  }
3983
4205
 
3984
4206
  // src/renderer.tsx
3985
- var import_core6 = require("@form-engine-ts/core");
3986
- var import_react7 = require("react");
4207
+ var import_core7 = require("@form-engine-ts/core");
4208
+ var import_react8 = require("react");
3987
4209
  var import_jsx_runtime4 = require("react/jsx-runtime");
3988
4210
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
3989
4211
  function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
@@ -4495,33 +4717,33 @@ function ContextFormRenderer({
4495
4717
  }) {
4496
4718
  const form = useForm();
4497
4719
  const i18n = useFormEngineI18n();
4498
- const isProviderValue = (0, import_react7.useContext)(FormEngineI18nProviderScopeContext);
4499
- const prefix = (0, import_react7.useId)().replace(/:/g, "");
4500
- const formRef = (0, import_react7.useRef)(null);
4501
- const loadedDraftKey = (0, import_react7.useRef)(null);
4502
- const [draftRestored, setDraftRestored] = (0, import_react7.useState)(false);
4503
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react7.useState)(0);
4504
- const [focusFieldId, setFocusFieldId] = (0, import_react7.useState)(null);
4505
- const [confirmation, setConfirmation] = (0, import_react7.useState)(null);
4506
- const [guardMessage, setGuardMessage] = (0, import_react7.useState)(null);
4507
- const [guardsPending, setGuardsPending] = (0, import_react7.useState)(false);
4508
- const [receipt, setReceipt] = (0, import_react7.useState)(null);
4509
- const [completionData, setCompletionData] = (0, import_react7.useState)(null);
4510
- const [receiptLoaded, setReceiptLoaded] = (0, import_react7.useState)(receiptStore === void 0);
4511
- const rendererSubmissionInFlight = (0, import_react7.useRef)(false);
4512
- const fallbackAttemptId = (0, import_react7.useRef)(null);
4513
- const completionRef = (0, import_react7.useRef)(null);
4514
- const confirmationRef = (0, import_react7.useRef)(null);
4720
+ const isProviderValue = (0, import_react8.useContext)(FormEngineI18nProviderScopeContext);
4721
+ const prefix = (0, import_react8.useId)().replace(/:/g, "");
4722
+ const formRef = (0, import_react8.useRef)(null);
4723
+ const loadedDraftKey = (0, import_react8.useRef)(null);
4724
+ const [draftRestored, setDraftRestored] = (0, import_react8.useState)(false);
4725
+ const [currentPageIndex, setCurrentPageIndex] = (0, import_react8.useState)(0);
4726
+ const [focusFieldId, setFocusFieldId] = (0, import_react8.useState)(null);
4727
+ const [confirmation, setConfirmation] = (0, import_react8.useState)(null);
4728
+ const [guardMessage, setGuardMessage] = (0, import_react8.useState)(null);
4729
+ const [guardsPending, setGuardsPending] = (0, import_react8.useState)(false);
4730
+ const [receipt, setReceipt] = (0, import_react8.useState)(null);
4731
+ const [completionData, setCompletionData] = (0, import_react8.useState)(null);
4732
+ const [receiptLoaded, setReceiptLoaded] = (0, import_react8.useState)(receiptStore === void 0);
4733
+ const rendererSubmissionInFlight = (0, import_react8.useRef)(false);
4734
+ const fallbackAttemptId = (0, import_react8.useRef)(null);
4735
+ const completionRef = (0, import_react8.useRef)(null);
4736
+ const confirmationRef = (0, import_react8.useRef)(null);
4515
4737
  const pages = form.schema.pages;
4516
- const visiblePageIndexes = (0, import_react7.useMemo)(
4738
+ const visiblePageIndexes = (0, import_react8.useMemo)(
4517
4739
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
4518
4740
  [form.pageVisibility, pages]
4519
4741
  );
4520
4742
  const activePage = pages?.[currentPageIndex];
4521
4743
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
4522
4744
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
4523
- const visibleValues = (0, import_react7.useMemo)(() => (0, import_core6.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
4524
- const visibleItems = (0, import_react7.useMemo)(
4745
+ const visibleValues = (0, import_react8.useMemo)(() => (0, import_core7.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
4746
+ const visibleItems = (0, import_react8.useMemo)(
4525
4747
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
4526
4748
  [form.schema, form.translate, form.values, form.visibility]
4527
4749
  );
@@ -4530,7 +4752,7 @@ function ContextFormRenderer({
4530
4752
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
4531
4753
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
4532
4754
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
4533
- const resolveMessage = (0, import_react7.useCallback)(
4755
+ const resolveMessage = (0, import_react8.useCallback)(
4534
4756
  (key, fallback) => {
4535
4757
  const providerDefault = isProviderValue ? i18n.translator(`renderer.${key}`) : void 0;
4536
4758
  const defaultText = fallback ?? (providerDefault === "" ? void 0 : providerDefault) ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
@@ -4539,15 +4761,15 @@ function ContextFormRenderer({
4539
4761
  },
4540
4762
  [form.locale, i18n, isProviderValue, messageResolver, messages]
4541
4763
  );
4542
- const fieldTranslate = (0, import_react7.useCallback)(
4764
+ const fieldTranslate = (0, import_react8.useCallback)(
4543
4765
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
4544
4766
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
4545
4767
  );
4546
- const focusSubmitButton = (0, import_react7.useCallback)(() => {
4768
+ const focusSubmitButton = (0, import_react8.useCallback)(() => {
4547
4769
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
4548
4770
  button?.focus();
4549
4771
  }, []);
4550
- (0, import_react7.useEffect)(() => {
4772
+ (0, import_react8.useEffect)(() => {
4551
4773
  let active = true;
4552
4774
  if (receiptStore === void 0) {
4553
4775
  setReceipt(null);
@@ -4568,14 +4790,14 @@ function ContextFormRenderer({
4568
4790
  active = false;
4569
4791
  };
4570
4792
  }, [form.schema.id, form.schema.version, receiptStore]);
4571
- (0, import_react7.useEffect)(() => {
4793
+ (0, import_react8.useEffect)(() => {
4572
4794
  if (pages === void 0 || visiblePageIndexes.length === 0) {
4573
4795
  setCurrentPageIndex(0);
4574
4796
  return;
4575
4797
  }
4576
4798
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
4577
4799
  }, [currentPageIndex, pages, visiblePageIndexes]);
4578
- (0, import_react7.useEffect)(() => {
4800
+ (0, import_react8.useEffect)(() => {
4579
4801
  if (focusFieldId === null) return;
4580
4802
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
4581
4803
  (element) => element.dataset.fieldId === focusFieldId
@@ -4587,11 +4809,11 @@ function ContextFormRenderer({
4587
4809
  setFocusFieldId(null);
4588
4810
  }
4589
4811
  }, [focusFieldId]);
4590
- (0, import_react7.useEffect)(() => {
4812
+ (0, import_react8.useEffect)(() => {
4591
4813
  if (!isReplaceMode || form.submitStatus !== "success") return;
4592
4814
  completionRef.current?.focus();
4593
4815
  }, [form.submitStatus, isReplaceMode]);
4594
- (0, import_react7.useEffect)(() => {
4816
+ (0, import_react8.useEffect)(() => {
4595
4817
  if (confirmation === null) return;
4596
4818
  const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
4597
4819
  confirmButton?.focus();
@@ -4623,7 +4845,7 @@ function ContextFormRenderer({
4623
4845
  globalThis.addEventListener("keydown", onKeyDown);
4624
4846
  return () => globalThis.removeEventListener("keydown", onKeyDown);
4625
4847
  }, [confirmation, confirmationRenderMode, focusSubmitButton]);
4626
- (0, import_react7.useEffect)(() => {
4848
+ (0, import_react8.useEffect)(() => {
4627
4849
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
4628
4850
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
4629
4851
  if (loadedDraftKey.current === loadIdentity) return;
@@ -4635,7 +4857,7 @@ function ContextFormRenderer({
4635
4857
  form.restoreValues(draft.values);
4636
4858
  setDraftRestored(true);
4637
4859
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
4638
- (0, import_react7.useEffect)(() => {
4860
+ (0, import_react8.useEffect)(() => {
4639
4861
  if (form.submitStatus === "success") return;
4640
4862
  const timeout = globalThis.setTimeout(() => {
4641
4863
  onDraftSave?.(form.values);
@@ -4691,7 +4913,7 @@ function ContextFormRenderer({
4691
4913
  if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
4692
4914
  return { status: "cancelled" };
4693
4915
  }
4694
- const validation = (0, import_core6.validateAnswers)(form.schema, form.values);
4916
+ const validation = (0, import_core7.validateAnswers)(form.schema, form.values);
4695
4917
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
4696
4918
  if (validation.valid && !guardsConfirmed) {
4697
4919
  rendererSubmissionInFlight.current = true;
@@ -4754,8 +4976,9 @@ function ContextFormRenderer({
4754
4976
  return result;
4755
4977
  }
4756
4978
  if (result.status === "error") {
4757
- if (result.error instanceof FormSubmissionError) {
4758
- const fieldErrors = result.error.payload.fieldErrors ?? {};
4979
+ const payload = result.error instanceof import_core7.FormSubmissionError ? result.error.payload : (0, import_core7.isFormSubmissionSerializedError)(result.error) ? result.error : void 0;
4980
+ if (payload !== void 0) {
4981
+ const fieldErrors = payload.fieldErrors ?? {};
4759
4982
  form.setServerErrors?.(fieldErrors);
4760
4983
  const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
4761
4984
  if (firstServerFieldId !== void 0) {
@@ -4763,6 +4986,9 @@ function ContextFormRenderer({
4763
4986
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
4764
4987
  focusFirstIssue(firstServerFieldId);
4765
4988
  }
4989
+ if (payload.piiFindings !== void 0 && payload.piiFindings.length > 0) {
4990
+ setConfirmation({ findings: payload.piiFindings, generic: false });
4991
+ }
4766
4992
  }
4767
4993
  return result;
4768
4994
  }
@@ -4993,7 +5219,7 @@ function ContextFormRenderer({
4993
5219
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
4994
5220
  };
4995
5221
  if (slots.renderField !== void 0) {
4996
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react7.Fragment, { children: slots.renderField({
5222
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react8.Fragment, { children: slots.renderField({
4997
5223
  question: field,
4998
5224
  value: form.values[field.id],
4999
5225
  onChange: (value) => {
@@ -5064,8 +5290,8 @@ function ContextFormRenderer({
5064
5290
  ] }),
5065
5291
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
5066
5292
  form.submitStatus === "success" ? completionRegion : null,
5067
- 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: [
5068
- form.submitError.payload.formError,
5293
+ 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: [
5294
+ form.submitError.payload.formErrors?.map((message) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: message }, message)),
5069
5295
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
5070
5296
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { role: "alert", children: [
5071
5297
  errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
@@ -5121,7 +5347,7 @@ var defaultRendererTranslator = {
5121
5347
  };
5122
5348
  function FormRenderer(props) {
5123
5349
  const i18n = useFormEngineI18n();
5124
- const isProviderValue = (0, import_react7.useContext)(FormEngineI18nProviderScopeContext);
5350
+ const isProviderValue = (0, import_react8.useContext)(FormEngineI18nProviderScopeContext);
5125
5351
  if (!("schema" in props)) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ContextFormRenderer, { ...props });
5126
5352
  const {
5127
5353
  schema,
@@ -5148,6 +5374,9 @@ function FormRenderer(props) {
5148
5374
  }
5149
5375
  );
5150
5376
  }
5377
+
5378
+ // src/types.ts
5379
+ var import_core8 = require("@form-engine-ts/core");
5151
5380
  // Annotate the CommonJS export names for ESM import in node:
5152
5381
  0 && (module.exports = {
5153
5382
  BUILDER_TRANSLATION_ALIASES,
@@ -5172,6 +5401,7 @@ function FormRenderer(props) {
5172
5401
  useFormBuilder,
5173
5402
  useFormEngineI18n,
5174
5403
  useSubmissionReceipts,
5404
+ useTranslationComparison,
5175
5405
  useTranslationWorkspace,
5176
5406
  validateLocalePipeline
5177
5407
  });