@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.js CHANGED
@@ -922,7 +922,9 @@ function resolveTranslation(key, aliases, adapter, defaultCatalog, params = {},
922
922
  }
923
923
 
924
924
  // src/i18n/provider.tsx
925
- import { createFormEngineTranslator } from "@form-engine-ts/core";
925
+ import {
926
+ createFormEngineTranslator
927
+ } from "@form-engine-ts/core";
926
928
  import { createContext, useContext, useMemo as useMemo2 } from "react";
927
929
  import { jsx } from "react/jsx-runtime";
928
930
  var defaultTranslator = createFormEngineTranslator({ locale: "ja" });
@@ -936,6 +938,8 @@ function FormEngineI18nProvider({
936
938
  fallbackLocale = "en",
937
939
  messages,
938
940
  customCatalogs,
941
+ onMissingKey,
942
+ strict,
939
943
  translator: customTranslator,
940
944
  children
941
945
  }) {
@@ -945,9 +949,11 @@ function FormEngineI18nProvider({
945
949
  locale,
946
950
  fallbackLocale,
947
951
  ...messages === void 0 ? {} : { messages },
948
- ...customCatalogs === void 0 ? {} : { customCatalogs }
952
+ ...customCatalogs === void 0 ? {} : { customCatalogs },
953
+ ...onMissingKey === void 0 ? {} : { onMissingKey },
954
+ ...strict === void 0 ? {} : { strict }
949
955
  });
950
- }, [customCatalogs, customTranslator, fallbackLocale, locale, messages]);
956
+ }, [customCatalogs, customTranslator, fallbackLocale, locale, messages, onMissingKey, strict]);
951
957
  const value = useMemo2(() => ({ uiLocale: locale, translator }), [locale, translator]);
952
958
  return /* @__PURE__ */ jsx(FormEngineI18nProviderScopeContext.Provider, { value: true, children: /* @__PURE__ */ jsx(FormEngineI18nContext.Provider, { value, children }) });
953
959
  }
@@ -2524,6 +2530,7 @@ function FormBuilder({
2524
2530
  LocalizationSlot,
2525
2531
  {
2526
2532
  schema,
2533
+ onChange,
2527
2534
  translate,
2528
2535
  currentLocale: editingLocale,
2529
2536
  onCurrentLocaleChange: setEditingLocale,
@@ -3048,24 +3055,14 @@ import {
3048
3055
  assertValidFormSchema,
3049
3056
  calculateFieldVisibility,
3050
3057
  calculatePageVisibility,
3058
+ FormSubmissionError,
3059
+ isFormSubmissionSerializedError,
3051
3060
  resolveLocalizedSchema,
3052
3061
  selectVisibleAnswers,
3053
3062
  validateAnswers,
3054
3063
  validatePageAnswers
3055
3064
  } from "@form-engine-ts/core";
3056
3065
  import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useEffect, useMemo as useMemo3, useRef, useState as useState3 } from "react";
3057
-
3058
- // src/types.ts
3059
- var FormSubmissionError = class extends Error {
3060
- payload;
3061
- constructor(message, payload) {
3062
- super(message);
3063
- this.name = "FormSubmissionError";
3064
- this.payload = payload ?? { formError: message };
3065
- }
3066
- };
3067
-
3068
- // src/context.tsx
3069
3066
  import { jsx as jsx3 } from "react/jsx-runtime";
3070
3067
  var FormContext = createContext3(null);
3071
3068
  function issuesByField(issues) {
@@ -3202,10 +3199,12 @@ function FormProvider({
3202
3199
  setSubmitStatus("success");
3203
3200
  return response === void 0 ? { status: "success" } : { status: "success", response };
3204
3201
  } catch (cause) {
3205
- const error = isServerErrorPayload(cause) ? new FormSubmissionError(
3206
- cause.formError ?? (cause instanceof Error ? cause.message : "Form submission failed."),
3207
- cause
3208
- ) : cause instanceof Error ? cause : new Error(String(cause));
3202
+ const error = isFormSubmissionSerializedError(cause) ? new FormSubmissionError(cause) : isServerErrorPayload(cause) ? new FormSubmissionError({
3203
+ code: "VALIDATION_FAILED",
3204
+ messageKey: cause.formError ?? "Submission failed",
3205
+ fieldErrors: cause.fieldErrors ?? {},
3206
+ formErrors: cause.formError === void 0 ? [] : [cause.formError]
3207
+ }) : cause instanceof Error ? cause : new Error(String(cause));
3209
3208
  setSubmitError(error);
3210
3209
  setSubmitStatus("error");
3211
3210
  return { status: "error", error };
@@ -3273,6 +3272,10 @@ function useField(fieldId) {
3273
3272
  return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
3274
3273
  }
3275
3274
 
3275
+ // src/hooks/useTranslationComparison.ts
3276
+ import { computeSourceTextHash as computeSourceTextHash2 } from "@form-engine-ts/core";
3277
+ import { useCallback as useCallback4, useMemo as useMemo5 } from "react";
3278
+
3276
3279
  // src/hooks/useTranslationWorkspace.ts
3277
3280
  import {
3278
3281
  collectSchemaLocales as collectSchemaLocales2,
@@ -3282,7 +3285,7 @@ import {
3282
3285
  populateSchemaTranslations as populateSchemaTranslations2,
3283
3286
  removeLocaleFromSchema
3284
3287
  } from "@form-engine-ts/core";
3285
- import { useCallback as useCallback3, useMemo as useMemo4, useState as useState4 } from "react";
3288
+ import { useCallback as useCallback3, useMemo as useMemo4, useRef as useRef2, useState as useState4 } from "react";
3286
3289
  function updateTranslationMap(translations, locale, property, text) {
3287
3290
  if (property === "label") return translations ?? {};
3288
3291
  const current = translations?.[locale];
@@ -3394,26 +3397,27 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3394
3397
  }
3395
3398
  return { type: "custom_validation_failed", message: error.message };
3396
3399
  }
3397
- function manualMetadata(sourceText, sourceLocale) {
3400
+ function translationMetadata(sourceText, sourceLocale, mode) {
3398
3401
  return {
3399
3402
  sourceLocale,
3400
3403
  sourceTextHash: computeSourceTextHash(sourceText),
3401
- translationSource: "manual",
3402
- editedAt: (/* @__PURE__ */ new Date()).toISOString()
3404
+ translationSource: mode,
3405
+ ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3403
3406
  };
3404
3407
  }
3405
- function setNodeMetadata(node, slot, text, sourceLocale) {
3408
+ function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3406
3409
  const localeMetadata = node.translationMetadata?.[slot.locale];
3407
- const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
3410
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: translationMetadata(slot.sourceText, sourceLocale, mode) };
3408
3411
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3409
3412
  }
3410
- function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3413
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3411
3414
  if (slot.kind === "form") {
3412
3415
  return setNodeMetadata(
3413
3416
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3414
3417
  slot,
3415
3418
  text,
3416
- sourceLocale
3419
+ sourceLocale,
3420
+ mode
3417
3421
  );
3418
3422
  }
3419
3423
  if (slot.kind === "field") {
@@ -3422,7 +3426,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3422
3426
  fields: schema.fields.map((field) => {
3423
3427
  if (field.id !== slot.nodeId) return field;
3424
3428
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3425
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3429
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3426
3430
  })
3427
3431
  };
3428
3432
  }
@@ -3438,7 +3442,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3438
3442
  const translations = text.trim().length === 0 ? Object.fromEntries(
3439
3443
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3440
3444
  ) : { ...option.translations, [slot.locale]: text };
3441
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3445
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3442
3446
  })
3443
3447
  };
3444
3448
  })
@@ -3450,7 +3454,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3450
3454
  pages: schema.pages.map((page) => {
3451
3455
  if (page.id !== slot.nodeId) return page;
3452
3456
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3453
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3457
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3454
3458
  })
3455
3459
  }
3456
3460
  };
@@ -3468,13 +3472,22 @@ function useTranslationWorkspace({
3468
3472
  onLocaleRemoved,
3469
3473
  onLocaleChange,
3470
3474
  beforeRemoveLocale,
3475
+ confirmRemoveLocale,
3476
+ slots: workspaceSlots,
3477
+ onTranslationStart,
3478
+ onTranslationSuccess,
3479
+ onTranslationError,
3480
+ onTranslationChange,
3471
3481
  validateLocale
3472
3482
  }) {
3473
3483
  const [draftSchema, setDraftSchema] = useState4(schema);
3474
3484
  const [selectedLocale, setSelectedLocale] = useState4(normalizeLocale(targetLocale ?? "") ?? targetLocale ?? "");
3475
3485
  const [isTranslating, setIsTranslating] = useState4(false);
3476
3486
  const [error, setError] = useState4();
3487
+ const [pendingRemoval, setPendingRemoval] = useState4();
3488
+ const pendingRemovalResolver = useRef2(void 0);
3477
3489
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3490
+ const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3478
3491
  const targetLocales = useMemo4(() => {
3479
3492
  const locales = collectSchemaLocales2(currentSchema).allUniqueLocales;
3480
3493
  return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
@@ -3524,9 +3537,17 @@ function useTranslationWorkspace({
3524
3537
  const setTranslation = useCallback3(
3525
3538
  (slot, text) => {
3526
3539
  if (readOnly) return;
3527
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3540
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3541
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual"));
3542
+ onTranslationChange?.({
3543
+ slot,
3544
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3545
+ nextText: text,
3546
+ mode: "manual",
3547
+ metadata
3548
+ });
3528
3549
  },
3529
- [commit, currentSchema, readOnly, sourceLocale]
3550
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3530
3551
  );
3531
3552
  const addLocale = useCallback3(
3532
3553
  (locale) => {
@@ -3587,21 +3608,36 @@ function useTranslationWorkspace({
3587
3608
  onLocaleRemoved?.(normalized);
3588
3609
  };
3589
3610
  const slotCount = collectTranslationSlots(currentSchema, normalized).length;
3611
+ const translatedSlotsCount = collectTranslationSlots(currentSchema, normalized).filter(
3612
+ (slot) => slot.existingText !== void 0 && slot.existingText.trim().length > 0
3613
+ ).length;
3614
+ const requestConfirmation = () => new Promise((resolve) => {
3615
+ pendingRemovalResolver.current = resolve;
3616
+ setPendingRemoval({
3617
+ locale: normalized,
3618
+ localeLabel: localeOptions.find((option) => option.locale === normalized)?.label ?? normalized,
3619
+ translatedSlotsCount
3620
+ });
3621
+ });
3622
+ const removeAfterApproval = () => {
3623
+ if (confirmRemoveLocaleRenderer === void 0) {
3624
+ remove();
3625
+ return true;
3626
+ }
3627
+ return requestConfirmation();
3628
+ };
3590
3629
  if (beforeRemoveLocale === void 0) {
3591
- remove();
3592
- return true;
3630
+ return removeAfterApproval();
3593
3631
  }
3594
3632
  try {
3595
3633
  const decision = beforeRemoveLocale(normalized, { slotCount });
3596
3634
  if (typeof decision === "boolean") {
3597
3635
  if (!decision) return false;
3598
- remove();
3599
- return true;
3636
+ return removeAfterApproval();
3600
3637
  }
3601
3638
  return decision.then((allowed) => {
3602
3639
  if (!allowed) return false;
3603
- remove();
3604
- return true;
3640
+ return removeAfterApproval();
3605
3641
  });
3606
3642
  } catch (cause) {
3607
3643
  const workspaceError = {
@@ -3613,8 +3649,45 @@ function useTranslationWorkspace({
3613
3649
  return Promise.reject(cause);
3614
3650
  }
3615
3651
  },
3616
- [activeLocale, beforeRemoveLocale, commit, currentSchema, onLocaleRemoved, readOnly, sourceLocale]
3652
+ [
3653
+ activeLocale,
3654
+ beforeRemoveLocale,
3655
+ commit,
3656
+ confirmRemoveLocaleRenderer,
3657
+ currentSchema,
3658
+ localeOptions,
3659
+ onLocaleRemoved,
3660
+ readOnly,
3661
+ sourceLocale
3662
+ ]
3617
3663
  );
3664
+ const confirmPendingRemoval = useCallback3(() => {
3665
+ const pending = pendingRemoval;
3666
+ const resolve = pendingRemovalResolver.current;
3667
+ if (pending === void 0 || resolve === void 0) return;
3668
+ pendingRemovalResolver.current = void 0;
3669
+ setPendingRemoval(void 0);
3670
+ commit(removeLocaleFromSchema(currentSchema, pending.locale));
3671
+ if (activeLocale === pending.locale) setSelectedLocale("");
3672
+ onLocaleRemoved?.(pending.locale);
3673
+ resolve(true);
3674
+ }, [activeLocale, commit, currentSchema, onLocaleRemoved, pendingRemoval]);
3675
+ const cancelPendingRemoval = useCallback3(() => {
3676
+ const resolve = pendingRemovalResolver.current;
3677
+ if (resolve === void 0) return;
3678
+ pendingRemovalResolver.current = void 0;
3679
+ setPendingRemoval(void 0);
3680
+ resolve(false);
3681
+ }, []);
3682
+ const removeLocaleConfirmation = useMemo4(() => {
3683
+ if (confirmRemoveLocaleRenderer === void 0 || pendingRemoval === void 0) return null;
3684
+ return confirmRemoveLocaleRenderer({
3685
+ ...pendingRemoval,
3686
+ isOpen: true,
3687
+ onConfirm: confirmPendingRemoval,
3688
+ onCancel: cancelPendingRemoval
3689
+ });
3690
+ }, [cancelPendingRemoval, confirmPendingRemoval, confirmRemoveLocaleRenderer, pendingRemoval]);
3618
3691
  const translateAll = useCallback3(
3619
3692
  async (options = {}) => {
3620
3693
  if (readOnly) {
@@ -3651,6 +3724,7 @@ function useTranslationWorkspace({
3651
3724
  }
3652
3725
  setIsTranslating(true);
3653
3726
  setError(void 0);
3727
+ onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3654
3728
  try {
3655
3729
  const populated = await populateSchemaTranslations2(
3656
3730
  currentSchema,
@@ -3663,6 +3737,17 @@ function useTranslationWorkspace({
3663
3737
  }
3664
3738
  );
3665
3739
  commit(populated.schema);
3740
+ const missingSlotsCount = collectTranslationSlots(populated.schema, activeLocale).filter(
3741
+ (slot) => slot.status === "missing"
3742
+ ).length;
3743
+ onTranslationSuccess?.({
3744
+ sourceLocale,
3745
+ targetLocale: activeLocale,
3746
+ mode: "automatic",
3747
+ updatedSlots: populated.report.updatedSlots,
3748
+ skippedSlots: populated.report.skippedSlots,
3749
+ missingSlotsCount
3750
+ });
3666
3751
  return { success: true, report: populated.report };
3667
3752
  } catch (cause) {
3668
3753
  const workspaceError = {
@@ -3671,12 +3756,25 @@ function useTranslationWorkspace({
3671
3756
  cause
3672
3757
  };
3673
3758
  setError(workspaceError);
3759
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3674
3760
  return { success: false, error: workspaceError };
3675
3761
  } finally {
3676
3762
  setIsTranslating(false);
3677
3763
  }
3678
3764
  },
3679
- [activeLocale, commit, currentSchema, localeOptions, readOnly, slots, translationAdapter]
3765
+ [
3766
+ activeLocale,
3767
+ commit,
3768
+ currentSchema,
3769
+ localeOptions,
3770
+ onTranslationError,
3771
+ onTranslationStart,
3772
+ onTranslationSuccess,
3773
+ readOnly,
3774
+ slots,
3775
+ sourceLocale,
3776
+ translationAdapter
3777
+ ]
3680
3778
  );
3681
3779
  const translateSlot = useCallback3(
3682
3780
  async (slot) => {
@@ -3694,7 +3792,15 @@ function useTranslationWorkspace({
3694
3792
  setError(void 0);
3695
3793
  try {
3696
3794
  const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3697
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3795
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3796
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3797
+ onTranslationChange?.({
3798
+ slot,
3799
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3800
+ nextText: text,
3801
+ mode: "automatic",
3802
+ metadata
3803
+ });
3698
3804
  return { success: true };
3699
3805
  } catch (cause) {
3700
3806
  const workspaceError = {
@@ -3708,7 +3814,7 @@ function useTranslationWorkspace({
3708
3814
  setIsTranslating(false);
3709
3815
  }
3710
3816
  },
3711
- [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3817
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3712
3818
  );
3713
3819
  return {
3714
3820
  sourceLocale,
@@ -3725,6 +3831,7 @@ function useTranslationWorkspace({
3725
3831
  addLocale,
3726
3832
  isAddLocaleAllowed,
3727
3833
  removeLocale,
3834
+ ...removeLocaleConfirmation === null ? {} : { removeLocaleConfirmation },
3728
3835
  setTranslation,
3729
3836
  translateAll,
3730
3837
  translateSlot,
@@ -3733,8 +3840,131 @@ function useTranslationWorkspace({
3733
3840
  };
3734
3841
  }
3735
3842
 
3843
+ // src/hooks/useTranslationComparison.ts
3844
+ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3845
+ if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3846
+ if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3847
+ return {
3848
+ sourceLocale: metadata.sourceLocale,
3849
+ sourceTextHash: metadata.sourceTextHash,
3850
+ translationSource: metadata.translationSource,
3851
+ ...typeof metadata.translatedAt === "string" ? { translatedAt: metadata.translatedAt } : {},
3852
+ ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3853
+ };
3854
+ }
3855
+ return {
3856
+ sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3857
+ sourceTextHash: computeSourceTextHash2(sourceText),
3858
+ translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
3859
+ };
3860
+ }
3861
+ function optionParentId(slot) {
3862
+ if (slot.kind !== "option" || slot.path === void 0) return void 0;
3863
+ return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3864
+ }
3865
+ function getNodeTitle(schema, slot) {
3866
+ if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3867
+ if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3868
+ if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
3869
+ return void 0;
3870
+ }
3871
+ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3872
+ const path = slot.path ?? `${slot.kind}.${slot.nodeId}.${slot.property}`;
3873
+ const nodeTitle = getNodeTitle(schema, slot);
3874
+ const metadata = canonicalMetadata(
3875
+ slot.existingTranslationMetadata,
3876
+ slot.sourceText,
3877
+ sourceLocale,
3878
+ slot.existingText
3879
+ );
3880
+ return {
3881
+ id: path,
3882
+ path,
3883
+ targetKind: slot.kind,
3884
+ targetProperty: slot.property,
3885
+ ...nodeTitle === void 0 ? {} : { nodeTitle },
3886
+ sourceText: slot.sourceText,
3887
+ translatedText: slot.existingText ?? "",
3888
+ status: slot.status ?? "missing",
3889
+ ...metadata === void 0 ? {} : { metadata },
3890
+ translatable: hasAdapter && slot.sourceText.trim().length > 0
3891
+ };
3892
+ }
3893
+ function summaryFor(items) {
3894
+ const counts = {
3895
+ missing: 0,
3896
+ translated: 0,
3897
+ stale: 0,
3898
+ manual: 0,
3899
+ "manual-stale": 0
3900
+ };
3901
+ for (const item of items) counts[item.status] += 1;
3902
+ return {
3903
+ total: items.length,
3904
+ translated: counts.translated + counts.manual,
3905
+ missing: counts.missing,
3906
+ stale: counts.stale + counts["manual-stale"],
3907
+ manual: counts.manual + counts["manual-stale"]
3908
+ };
3909
+ }
3910
+ function useTranslationComparison({
3911
+ schema,
3912
+ sourceLocale = schema.defaultLocale ?? "en",
3913
+ targetLocale,
3914
+ translationAdapter,
3915
+ readOnly = false,
3916
+ onChange,
3917
+ onTranslationChange
3918
+ }) {
3919
+ const workspace = useTranslationWorkspace({
3920
+ schema,
3921
+ sourceLocale,
3922
+ targetLocale,
3923
+ ...translationAdapter === void 0 ? {} : { translationAdapter },
3924
+ readOnly,
3925
+ ...onChange === void 0 ? {} : { onChange },
3926
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange }
3927
+ });
3928
+ const items = useMemo5(
3929
+ () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
3930
+ [schema, sourceLocale, translationAdapter, workspace.slots]
3931
+ );
3932
+ const itemByPath = useMemo5(() => new Map(workspace.slots.map((slot) => [slot.path, slot])), [workspace.slots]);
3933
+ const updateTranslation = useCallback4(
3934
+ (path, text) => {
3935
+ const slot = itemByPath.get(path);
3936
+ if (slot !== void 0) workspace.setTranslation(slot, text);
3937
+ },
3938
+ [itemByPath, workspace]
3939
+ );
3940
+ const translateSingle = useCallback4(
3941
+ async (path) => {
3942
+ const slot = itemByPath.get(path);
3943
+ if (slot === void 0) return;
3944
+ const result = await workspace.translateSlot(slot);
3945
+ if (!result.success) throw new Error(result.error?.type ?? "Translation failed.");
3946
+ },
3947
+ [itemByPath, workspace]
3948
+ );
3949
+ const translateAll = useCallback4(async () => {
3950
+ const result = await workspace.translateAll();
3951
+ if (result.report !== void 0) return result.report;
3952
+ throw new Error(result.error?.type ?? "Translation failed.");
3953
+ }, [workspace]);
3954
+ return {
3955
+ sourceLocale: workspace.sourceLocale,
3956
+ targetLocale: workspace.targetLocale,
3957
+ items,
3958
+ summary: summaryFor(items),
3959
+ isTranslating: workspace.isTranslating,
3960
+ updateTranslation,
3961
+ translateSingle,
3962
+ translateAll
3963
+ };
3964
+ }
3965
+
3736
3966
  // src/receipt.ts
3737
- import { useEffect as useEffect2, useMemo as useMemo5, useState as useState5 } from "react";
3967
+ import { useEffect as useEffect2, useMemo as useMemo6, useState as useState5 } from "react";
3738
3968
  function submissionReceiptQueryKey(formId, formVersion) {
3739
3969
  return `${formId}:v${formVersion}`;
3740
3970
  }
@@ -3805,7 +4035,7 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3805
4035
  }
3806
4036
  function useSubmissionReceipts(store, queries) {
3807
4037
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3808
- const stableQueries = useMemo5(() => {
4038
+ const stableQueries = useMemo6(() => {
3809
4039
  const parsed = JSON.parse(querySignature);
3810
4040
  if (!Array.isArray(parsed)) return [];
3811
4041
  return parsed.flatMap(
@@ -3852,17 +4082,19 @@ function useSubmissionReceipts(store, queries) {
3852
4082
 
3853
4083
  // src/renderer.tsx
3854
4084
  import {
4085
+ FormSubmissionError as FormSubmissionError2,
4086
+ isFormSubmissionSerializedError as isFormSubmissionSerializedError2,
3855
4087
  selectVisibleAnswers as selectVisibleAnswers2,
3856
4088
  validateAnswers as validateAnswers2
3857
4089
  } from "@form-engine-ts/core";
3858
4090
  import {
3859
4091
  Fragment as Fragment2,
3860
- useCallback as useCallback4,
4092
+ useCallback as useCallback5,
3861
4093
  useContext as useContext4,
3862
4094
  useEffect as useEffect3,
3863
4095
  useId,
3864
- useMemo as useMemo6,
3865
- useRef as useRef2,
4096
+ useMemo as useMemo7,
4097
+ useRef as useRef3,
3866
4098
  useState as useState6
3867
4099
  } from "react";
3868
4100
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -4378,8 +4610,8 @@ function ContextFormRenderer({
4378
4610
  const i18n = useFormEngineI18n();
4379
4611
  const isProviderValue = useContext4(FormEngineI18nProviderScopeContext);
4380
4612
  const prefix = useId().replace(/:/g, "");
4381
- const formRef = useRef2(null);
4382
- const loadedDraftKey = useRef2(null);
4613
+ const formRef = useRef3(null);
4614
+ const loadedDraftKey = useRef3(null);
4383
4615
  const [draftRestored, setDraftRestored] = useState6(false);
4384
4616
  const [currentPageIndex, setCurrentPageIndex] = useState6(0);
4385
4617
  const [focusFieldId, setFocusFieldId] = useState6(null);
@@ -4389,20 +4621,20 @@ function ContextFormRenderer({
4389
4621
  const [receipt, setReceipt] = useState6(null);
4390
4622
  const [completionData, setCompletionData] = useState6(null);
4391
4623
  const [receiptLoaded, setReceiptLoaded] = useState6(receiptStore === void 0);
4392
- const rendererSubmissionInFlight = useRef2(false);
4393
- const fallbackAttemptId = useRef2(null);
4394
- const completionRef = useRef2(null);
4395
- const confirmationRef = useRef2(null);
4624
+ const rendererSubmissionInFlight = useRef3(false);
4625
+ const fallbackAttemptId = useRef3(null);
4626
+ const completionRef = useRef3(null);
4627
+ const confirmationRef = useRef3(null);
4396
4628
  const pages = form.schema.pages;
4397
- const visiblePageIndexes = useMemo6(
4629
+ const visiblePageIndexes = useMemo7(
4398
4630
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
4399
4631
  [form.pageVisibility, pages]
4400
4632
  );
4401
4633
  const activePage = pages?.[currentPageIndex];
4402
4634
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
4403
4635
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
4404
- const visibleValues = useMemo6(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
4405
- const visibleItems = useMemo6(
4636
+ const visibleValues = useMemo7(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
4637
+ const visibleItems = useMemo7(
4406
4638
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
4407
4639
  [form.schema, form.translate, form.values, form.visibility]
4408
4640
  );
@@ -4411,7 +4643,7 @@ function ContextFormRenderer({
4411
4643
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
4412
4644
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
4413
4645
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
4414
- const resolveMessage = useCallback4(
4646
+ const resolveMessage = useCallback5(
4415
4647
  (key, fallback) => {
4416
4648
  const providerDefault = isProviderValue ? i18n.translator(`renderer.${key}`) : void 0;
4417
4649
  const defaultText = fallback ?? (providerDefault === "" ? void 0 : providerDefault) ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
@@ -4420,11 +4652,11 @@ function ContextFormRenderer({
4420
4652
  },
4421
4653
  [form.locale, i18n, isProviderValue, messageResolver, messages]
4422
4654
  );
4423
- const fieldTranslate = useCallback4(
4655
+ const fieldTranslate = useCallback5(
4424
4656
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
4425
4657
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
4426
4658
  );
4427
- const focusSubmitButton = useCallback4(() => {
4659
+ const focusSubmitButton = useCallback5(() => {
4428
4660
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
4429
4661
  button?.focus();
4430
4662
  }, []);
@@ -4635,8 +4867,9 @@ function ContextFormRenderer({
4635
4867
  return result;
4636
4868
  }
4637
4869
  if (result.status === "error") {
4638
- if (result.error instanceof FormSubmissionError) {
4639
- const fieldErrors = result.error.payload.fieldErrors ?? {};
4870
+ const payload = result.error instanceof FormSubmissionError2 ? result.error.payload : isFormSubmissionSerializedError2(result.error) ? result.error : void 0;
4871
+ if (payload !== void 0) {
4872
+ const fieldErrors = payload.fieldErrors ?? {};
4640
4873
  form.setServerErrors?.(fieldErrors);
4641
4874
  const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
4642
4875
  if (firstServerFieldId !== void 0) {
@@ -4644,6 +4877,9 @@ function ContextFormRenderer({
4644
4877
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
4645
4878
  focusFirstIssue(firstServerFieldId);
4646
4879
  }
4880
+ if (payload.piiFindings !== void 0 && payload.piiFindings.length > 0) {
4881
+ setConfirmation({ findings: payload.piiFindings, generic: false });
4882
+ }
4647
4883
  }
4648
4884
  return result;
4649
4885
  }
@@ -4945,8 +5181,8 @@ function ContextFormRenderer({
4945
5181
  ] }),
4946
5182
  /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
4947
5183
  form.submitStatus === "success" ? completionRegion : null,
4948
- 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__ */ jsxs2("div", { role: "alert", children: [
4949
- form.submitError.payload.formError,
5184
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError2 && (form.submitError.payload.formErrors?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
5185
+ form.submitError.payload.formErrors?.map((message) => /* @__PURE__ */ jsx4("div", { children: message }, message)),
4950
5186
  /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
4951
5187
  ] }) : /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
4952
5188
  errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
@@ -5029,6 +5265,9 @@ function FormRenderer(props) {
5029
5265
  }
5030
5266
  );
5031
5267
  }
5268
+
5269
+ // src/types.ts
5270
+ import { FormSubmissionError as FormSubmissionError3 } from "@form-engine-ts/core";
5032
5271
  export {
5033
5272
  BUILDER_TRANSLATION_ALIASES,
5034
5273
  BUILDER_TRANSLATION_KEYS,
@@ -5037,7 +5276,7 @@ export {
5037
5276
  FormEngineI18nProvider,
5038
5277
  FormProvider,
5039
5278
  FormRenderer,
5040
- FormSubmissionError,
5279
+ FormSubmissionError3 as FormSubmissionError,
5041
5280
  createLocalStorageSubmissionAttemptStore,
5042
5281
  createLocalStorageSubmissionReceiptStore,
5043
5282
  isTranslationUnresolved,
@@ -5052,6 +5291,7 @@ export {
5052
5291
  useFormBuilder,
5053
5292
  useFormEngineI18n,
5054
5293
  useSubmissionReceipts,
5294
+ useTranslationComparison,
5055
5295
  useTranslationWorkspace,
5056
5296
  validateLocalePipeline
5057
5297
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "5.1.0",
3
+ "version": "6.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "5.1.0",
46
- "@form-engine-ts/privacy": "5.1.0"
45
+ "@form-engine-ts/privacy": "6.1.0",
46
+ "@form-engine-ts/core": "6.1.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",