@form-engine-ts/react 6.0.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/README.md CHANGED
@@ -93,6 +93,10 @@ localized values and metadata; the default locale remains protected.
93
93
  `onTranslationChange` with typed lifecycle payloads. The MUI workspace accepts a `confirmRemoveLocale` slot for an
94
94
  async confirmation UI; its `onConfirm` callback completes the locale removal.
95
95
 
96
+ `useTranslationComparison` provides a focused, controlled comparison model for form, page, field, and option text.
97
+ Its items expose source text, target text, status, canonical metadata, and path-based `updateTranslation`,
98
+ `translateSingle`, and `translateAll` actions.
99
+
96
100
  Wrap a builder or renderer in `FormEngineI18nProvider` to supply a UI locale and typed Core translator independently
97
101
  from the schema's `defaultLocale` and `supportedLocales`:
98
102
 
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");
@@ -3864,8 +3864,131 @@ function useTranslationWorkspace({
3864
3864
  };
3865
3865
  }
3866
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
+
3867
3990
  // src/receipt.ts
3868
- var import_react6 = require("react");
3991
+ var import_react7 = require("react");
3869
3992
  function submissionReceiptQueryKey(formId, formVersion) {
3870
3993
  return `${formId}:v${formVersion}`;
3871
3994
  }
@@ -3936,19 +4059,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3936
4059
  }
3937
4060
  function useSubmissionReceipts(store, queries) {
3938
4061
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3939
- const stableQueries = (0, import_react6.useMemo)(() => {
4062
+ const stableQueries = (0, import_react7.useMemo)(() => {
3940
4063
  const parsed = JSON.parse(querySignature);
3941
4064
  if (!Array.isArray(parsed)) return [];
3942
4065
  return parsed.flatMap(
3943
4066
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
3944
4067
  );
3945
4068
  }, [querySignature]);
3946
- const [state, setState] = (0, import_react6.useState)({
4069
+ const [state, setState] = (0, import_react7.useState)({
3947
4070
  receipts: /* @__PURE__ */ new Map(),
3948
4071
  isLoading: stableQueries.length > 0,
3949
4072
  error: null
3950
4073
  });
3951
- (0, import_react6.useEffect)(() => {
4074
+ (0, import_react7.useEffect)(() => {
3952
4075
  let active = true;
3953
4076
  if (stableQueries.length === 0) {
3954
4077
  setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
@@ -3982,8 +4105,8 @@ function useSubmissionReceipts(store, queries) {
3982
4105
  }
3983
4106
 
3984
4107
  // src/renderer.tsx
3985
- var import_core6 = require("@form-engine-ts/core");
3986
- var import_react7 = require("react");
4108
+ var import_core7 = require("@form-engine-ts/core");
4109
+ var import_react8 = require("react");
3987
4110
  var import_jsx_runtime4 = require("react/jsx-runtime");
3988
4111
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
3989
4112
  function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
@@ -4495,33 +4618,33 @@ function ContextFormRenderer({
4495
4618
  }) {
4496
4619
  const form = useForm();
4497
4620
  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);
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);
4515
4638
  const pages = form.schema.pages;
4516
- const visiblePageIndexes = (0, import_react7.useMemo)(
4639
+ const visiblePageIndexes = (0, import_react8.useMemo)(
4517
4640
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
4518
4641
  [form.pageVisibility, pages]
4519
4642
  );
4520
4643
  const activePage = pages?.[currentPageIndex];
4521
4644
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
4522
4645
  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)(
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)(
4525
4648
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
4526
4649
  [form.schema, form.translate, form.values, form.visibility]
4527
4650
  );
@@ -4530,7 +4653,7 @@ function ContextFormRenderer({
4530
4653
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
4531
4654
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
4532
4655
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
4533
- const resolveMessage = (0, import_react7.useCallback)(
4656
+ const resolveMessage = (0, import_react8.useCallback)(
4534
4657
  (key, fallback) => {
4535
4658
  const providerDefault = isProviderValue ? i18n.translator(`renderer.${key}`) : void 0;
4536
4659
  const defaultText = fallback ?? (providerDefault === "" ? void 0 : providerDefault) ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
@@ -4539,15 +4662,15 @@ function ContextFormRenderer({
4539
4662
  },
4540
4663
  [form.locale, i18n, isProviderValue, messageResolver, messages]
4541
4664
  );
4542
- const fieldTranslate = (0, import_react7.useCallback)(
4665
+ const fieldTranslate = (0, import_react8.useCallback)(
4543
4666
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
4544
4667
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
4545
4668
  );
4546
- const focusSubmitButton = (0, import_react7.useCallback)(() => {
4669
+ const focusSubmitButton = (0, import_react8.useCallback)(() => {
4547
4670
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
4548
4671
  button?.focus();
4549
4672
  }, []);
4550
- (0, import_react7.useEffect)(() => {
4673
+ (0, import_react8.useEffect)(() => {
4551
4674
  let active = true;
4552
4675
  if (receiptStore === void 0) {
4553
4676
  setReceipt(null);
@@ -4568,14 +4691,14 @@ function ContextFormRenderer({
4568
4691
  active = false;
4569
4692
  };
4570
4693
  }, [form.schema.id, form.schema.version, receiptStore]);
4571
- (0, import_react7.useEffect)(() => {
4694
+ (0, import_react8.useEffect)(() => {
4572
4695
  if (pages === void 0 || visiblePageIndexes.length === 0) {
4573
4696
  setCurrentPageIndex(0);
4574
4697
  return;
4575
4698
  }
4576
4699
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
4577
4700
  }, [currentPageIndex, pages, visiblePageIndexes]);
4578
- (0, import_react7.useEffect)(() => {
4701
+ (0, import_react8.useEffect)(() => {
4579
4702
  if (focusFieldId === null) return;
4580
4703
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
4581
4704
  (element) => element.dataset.fieldId === focusFieldId
@@ -4587,11 +4710,11 @@ function ContextFormRenderer({
4587
4710
  setFocusFieldId(null);
4588
4711
  }
4589
4712
  }, [focusFieldId]);
4590
- (0, import_react7.useEffect)(() => {
4713
+ (0, import_react8.useEffect)(() => {
4591
4714
  if (!isReplaceMode || form.submitStatus !== "success") return;
4592
4715
  completionRef.current?.focus();
4593
4716
  }, [form.submitStatus, isReplaceMode]);
4594
- (0, import_react7.useEffect)(() => {
4717
+ (0, import_react8.useEffect)(() => {
4595
4718
  if (confirmation === null) return;
4596
4719
  const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
4597
4720
  confirmButton?.focus();
@@ -4623,7 +4746,7 @@ function ContextFormRenderer({
4623
4746
  globalThis.addEventListener("keydown", onKeyDown);
4624
4747
  return () => globalThis.removeEventListener("keydown", onKeyDown);
4625
4748
  }, [confirmation, confirmationRenderMode, focusSubmitButton]);
4626
- (0, import_react7.useEffect)(() => {
4749
+ (0, import_react8.useEffect)(() => {
4627
4750
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
4628
4751
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
4629
4752
  if (loadedDraftKey.current === loadIdentity) return;
@@ -4635,7 +4758,7 @@ function ContextFormRenderer({
4635
4758
  form.restoreValues(draft.values);
4636
4759
  setDraftRestored(true);
4637
4760
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
4638
- (0, import_react7.useEffect)(() => {
4761
+ (0, import_react8.useEffect)(() => {
4639
4762
  if (form.submitStatus === "success") return;
4640
4763
  const timeout = globalThis.setTimeout(() => {
4641
4764
  onDraftSave?.(form.values);
@@ -4691,7 +4814,7 @@ function ContextFormRenderer({
4691
4814
  if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
4692
4815
  return { status: "cancelled" };
4693
4816
  }
4694
- const validation = (0, import_core6.validateAnswers)(form.schema, form.values);
4817
+ const validation = (0, import_core7.validateAnswers)(form.schema, form.values);
4695
4818
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
4696
4819
  if (validation.valid && !guardsConfirmed) {
4697
4820
  rendererSubmissionInFlight.current = true;
@@ -4754,8 +4877,9 @@ function ContextFormRenderer({
4754
4877
  return result;
4755
4878
  }
4756
4879
  if (result.status === "error") {
4757
- if (result.error instanceof FormSubmissionError) {
4758
- 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 ?? {};
4759
4883
  form.setServerErrors?.(fieldErrors);
4760
4884
  const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
4761
4885
  if (firstServerFieldId !== void 0) {
@@ -4763,6 +4887,9 @@ function ContextFormRenderer({
4763
4887
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
4764
4888
  focusFirstIssue(firstServerFieldId);
4765
4889
  }
4890
+ if (payload.piiFindings !== void 0 && payload.piiFindings.length > 0) {
4891
+ setConfirmation({ findings: payload.piiFindings, generic: false });
4892
+ }
4766
4893
  }
4767
4894
  return result;
4768
4895
  }
@@ -4993,7 +5120,7 @@ function ContextFormRenderer({
4993
5120
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
4994
5121
  };
4995
5122
  if (slots.renderField !== void 0) {
4996
- 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({
4997
5124
  question: field,
4998
5125
  value: form.values[field.id],
4999
5126
  onChange: (value) => {
@@ -5064,8 +5191,8 @@ function ContextFormRenderer({
5064
5191
  ] }),
5065
5192
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
5066
5193
  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,
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)),
5069
5196
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
5070
5197
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { role: "alert", children: [
5071
5198
  errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
@@ -5121,7 +5248,7 @@ var defaultRendererTranslator = {
5121
5248
  };
5122
5249
  function FormRenderer(props) {
5123
5250
  const i18n = useFormEngineI18n();
5124
- const isProviderValue = (0, import_react7.useContext)(FormEngineI18nProviderScopeContext);
5251
+ const isProviderValue = (0, import_react8.useContext)(FormEngineI18nProviderScopeContext);
5125
5252
  if (!("schema" in props)) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ContextFormRenderer, { ...props });
5126
5253
  const {
5127
5254
  schema,
@@ -5148,6 +5275,9 @@ function FormRenderer(props) {
5148
5275
  }
5149
5276
  );
5150
5277
  }
5278
+
5279
+ // src/types.ts
5280
+ var import_core8 = require("@form-engine-ts/core");
5151
5281
  // Annotate the CommonJS export names for ESM import in node:
5152
5282
  0 && (module.exports = {
5153
5283
  BUILDER_TRANSLATION_ALIASES,
@@ -5172,6 +5302,7 @@ function FormRenderer(props) {
5172
5302
  useFormBuilder,
5173
5303
  useFormEngineI18n,
5174
5304
  useSubmissionReceipts,
5305
+ useTranslationComparison,
5175
5306
  useTranslationWorkspace,
5176
5307
  validateLocalePipeline
5177
5308
  });
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _form_engine_ts_core from '@form-engine-ts/core';
2
- import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, LocaleOption, TranslationSlot, CanonicalTranslationMetadata, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, FieldType } from '@form-engine-ts/core';
3
- export { QuestionType } from '@form-engine-ts/core';
2
+ import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, LocaleOption, TranslationStatus, CanonicalTranslationMetadata, TranslationSlot, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, TranslationMissingKeyEvent, FieldType } from '@form-engine-ts/core';
3
+ export { FormSubmissionError, FormSubmissionSerializedError, QuestionType } from '@form-engine-ts/core';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
6
6
  import { SensitiveDataFinding } from '@form-engine-ts/privacy';
@@ -307,6 +307,7 @@ interface BuilderSlotBaseProps {
307
307
  readonly actions: BuilderSlotActions;
308
308
  readonly components: Required<FormBuilderComponents>;
309
309
  readonly translate: (key: string, params?: Record<string, unknown>) => string;
310
+ readonly onChange?: (schema: FormSchema) => void;
310
311
  }
311
312
  interface BuilderToolbarSlotProps extends BuilderSlotBaseProps {
312
313
  readonly kind: "page" | "field" | "option";
@@ -402,6 +403,58 @@ interface TranslationSlotRowProps {
402
403
  readonly onChange: (text: string) => void;
403
404
  readonly onTranslate: () => void;
404
405
  }
406
+ interface TranslationComparisonItem {
407
+ readonly id: string;
408
+ readonly path: string;
409
+ readonly targetKind: "form" | "page" | "field" | "option";
410
+ readonly targetProperty: "title" | "description" | "label" | "completionMessage";
411
+ readonly nodeTitle?: string;
412
+ readonly sourceText: string;
413
+ readonly translatedText: string;
414
+ readonly status: TranslationStatus;
415
+ readonly metadata?: CanonicalTranslationMetadata;
416
+ readonly translatable: boolean;
417
+ }
418
+ interface TranslationComparisonSummary {
419
+ readonly total: number;
420
+ readonly translated: number;
421
+ readonly missing: number;
422
+ readonly stale: number;
423
+ readonly manual: number;
424
+ }
425
+ interface TranslationComparisonHeaderProps {
426
+ readonly sourceLocale: string;
427
+ readonly targetLocale: string;
428
+ readonly summary: TranslationComparisonSummary;
429
+ readonly onTranslateAll: () => void;
430
+ readonly isTranslating: boolean;
431
+ readonly readOnly: boolean;
432
+ }
433
+ interface TranslationComparisonItemRowProps {
434
+ readonly item: TranslationComparisonItem;
435
+ readonly readOnly: boolean;
436
+ readonly onChange: (text: string) => void;
437
+ readonly onTranslate: () => void;
438
+ }
439
+ interface UseTranslationComparisonOptions {
440
+ readonly schema: FormSchema;
441
+ readonly sourceLocale?: string;
442
+ readonly targetLocale: string;
443
+ readonly translationAdapter?: TranslationAdapter;
444
+ readonly readOnly?: boolean;
445
+ readonly onChange?: (nextSchema: FormSchema) => void;
446
+ readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
447
+ }
448
+ interface UseTranslationComparisonResult {
449
+ readonly sourceLocale: string;
450
+ readonly targetLocale: string;
451
+ readonly items: readonly TranslationComparisonItem[];
452
+ readonly summary: TranslationComparisonSummary;
453
+ readonly isTranslating: boolean;
454
+ readonly updateTranslation: (path: string, text: string) => void;
455
+ readonly translateSingle: (path: string) => Promise<void>;
456
+ readonly translateAll: () => Promise<TranslationReport>;
457
+ }
405
458
  interface LocaleSelectorProps {
406
459
  readonly targetLocale: string;
407
460
  readonly targetLocales: readonly string[];
@@ -551,10 +604,6 @@ interface FormServerErrorPayload {
551
604
  readonly fieldErrors?: Readonly<Record<string, string>>;
552
605
  readonly formError?: string;
553
606
  }
554
- declare class FormSubmissionError extends Error {
555
- readonly payload: FormServerErrorPayload;
556
- constructor(message: string, payload?: FormServerErrorPayload);
557
- }
558
607
  type FormSubmitHandler = (answers: FormValues, context: SubmitContext) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
559
608
  type SubmissionGuardResult = {
560
609
  readonly status: "allow";
@@ -771,6 +820,8 @@ interface FieldState {
771
820
  }
772
821
  declare function useField(fieldId: string): FieldState;
773
822
 
823
+ declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
824
+
774
825
  interface UseTranslationWorkspaceOptions {
775
826
  readonly schema: FormSchema;
776
827
  readonly onChange?: (schema: FormSchema) => void;
@@ -909,10 +960,12 @@ interface FormEngineI18nProviderProps {
909
960
  readonly fallbackLocale?: string;
910
961
  readonly messages?: FormEngineMessages;
911
962
  readonly customCatalogs?: Record<string, FormEngineMessages>;
963
+ readonly onMissingKey?: (event: TranslationMissingKeyEvent) => void;
964
+ readonly strict?: boolean;
912
965
  readonly translator?: FormEngineTranslator;
913
966
  readonly children: ReactNode;
914
967
  }
915
- declare function FormEngineI18nProvider({ locale, fallbackLocale, messages, customCatalogs, translator: customTranslator, children }: FormEngineI18nProviderProps): react.JSX.Element;
968
+ declare function FormEngineI18nProvider({ locale, fallbackLocale, messages, customCatalogs, onMissingKey, strict, translator: customTranslator, children }: FormEngineI18nProviderProps): react.JSX.Element;
916
969
  declare function useFormEngineI18n(): FormEngineI18nContextValue;
917
970
 
918
971
  declare function resolveChoiceFieldLayout(type: FieldType, appearance?: FormRendererAppearance, groupedChoiceFieldsLegacy?: boolean): "default" | "grouped";
@@ -968,4 +1021,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
968
1021
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
969
1022
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
970
1023
 
971
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type ConfirmRemoveLocaleSlotProps, type CustomLocaleValidator, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, FormEngineI18nContext, type FormEngineI18nContextValue, FormEngineI18nProvider, type FormEngineI18nProviderProps, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocaleSelectorProps, type LocaleValidationContext, type LocaleValidationResult, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationEventPayload, type TranslationSlotChangeEvent, type TranslationSlotRowProps, type TranslationSummary, type TranslationWorkspaceActionsProps, type TranslationWorkspaceError, type TranslationWorkspaceHeaderProps, type TranslationWorkspaceSlots, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useFormEngineI18n, useSubmissionReceipts, useTranslationWorkspace, validateLocalePipeline };
1024
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type ConfirmRemoveLocaleSlotProps, type CustomLocaleValidator, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, FormEngineI18nContext, type FormEngineI18nContextValue, FormEngineI18nProvider, type FormEngineI18nProviderProps, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocaleSelectorProps, type LocaleValidationContext, type LocaleValidationResult, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationComparisonHeaderProps, type TranslationComparisonItem, type TranslationComparisonItemRowProps, type TranslationComparisonSummary, type TranslationEventPayload, type TranslationSlotChangeEvent, type TranslationSlotRowProps, type TranslationSummary, type TranslationWorkspaceActionsProps, type TranslationWorkspaceError, type TranslationWorkspaceHeaderProps, type TranslationWorkspaceSlots, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationComparisonOptions, type UseTranslationComparisonResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useFormEngineI18n, useSubmissionReceipts, useTranslationComparison, useTranslationWorkspace, validateLocalePipeline };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _form_engine_ts_core from '@form-engine-ts/core';
2
- import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, LocaleOption, TranslationSlot, CanonicalTranslationMetadata, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, FieldType } from '@form-engine-ts/core';
3
- export { QuestionType } from '@form-engine-ts/core';
2
+ import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, LocaleOption, TranslationStatus, CanonicalTranslationMetadata, TranslationSlot, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, TranslationMissingKeyEvent, FieldType } from '@form-engine-ts/core';
3
+ export { FormSubmissionError, FormSubmissionSerializedError, QuestionType } from '@form-engine-ts/core';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
6
6
  import { SensitiveDataFinding } from '@form-engine-ts/privacy';
@@ -307,6 +307,7 @@ interface BuilderSlotBaseProps {
307
307
  readonly actions: BuilderSlotActions;
308
308
  readonly components: Required<FormBuilderComponents>;
309
309
  readonly translate: (key: string, params?: Record<string, unknown>) => string;
310
+ readonly onChange?: (schema: FormSchema) => void;
310
311
  }
311
312
  interface BuilderToolbarSlotProps extends BuilderSlotBaseProps {
312
313
  readonly kind: "page" | "field" | "option";
@@ -402,6 +403,58 @@ interface TranslationSlotRowProps {
402
403
  readonly onChange: (text: string) => void;
403
404
  readonly onTranslate: () => void;
404
405
  }
406
+ interface TranslationComparisonItem {
407
+ readonly id: string;
408
+ readonly path: string;
409
+ readonly targetKind: "form" | "page" | "field" | "option";
410
+ readonly targetProperty: "title" | "description" | "label" | "completionMessage";
411
+ readonly nodeTitle?: string;
412
+ readonly sourceText: string;
413
+ readonly translatedText: string;
414
+ readonly status: TranslationStatus;
415
+ readonly metadata?: CanonicalTranslationMetadata;
416
+ readonly translatable: boolean;
417
+ }
418
+ interface TranslationComparisonSummary {
419
+ readonly total: number;
420
+ readonly translated: number;
421
+ readonly missing: number;
422
+ readonly stale: number;
423
+ readonly manual: number;
424
+ }
425
+ interface TranslationComparisonHeaderProps {
426
+ readonly sourceLocale: string;
427
+ readonly targetLocale: string;
428
+ readonly summary: TranslationComparisonSummary;
429
+ readonly onTranslateAll: () => void;
430
+ readonly isTranslating: boolean;
431
+ readonly readOnly: boolean;
432
+ }
433
+ interface TranslationComparisonItemRowProps {
434
+ readonly item: TranslationComparisonItem;
435
+ readonly readOnly: boolean;
436
+ readonly onChange: (text: string) => void;
437
+ readonly onTranslate: () => void;
438
+ }
439
+ interface UseTranslationComparisonOptions {
440
+ readonly schema: FormSchema;
441
+ readonly sourceLocale?: string;
442
+ readonly targetLocale: string;
443
+ readonly translationAdapter?: TranslationAdapter;
444
+ readonly readOnly?: boolean;
445
+ readonly onChange?: (nextSchema: FormSchema) => void;
446
+ readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
447
+ }
448
+ interface UseTranslationComparisonResult {
449
+ readonly sourceLocale: string;
450
+ readonly targetLocale: string;
451
+ readonly items: readonly TranslationComparisonItem[];
452
+ readonly summary: TranslationComparisonSummary;
453
+ readonly isTranslating: boolean;
454
+ readonly updateTranslation: (path: string, text: string) => void;
455
+ readonly translateSingle: (path: string) => Promise<void>;
456
+ readonly translateAll: () => Promise<TranslationReport>;
457
+ }
405
458
  interface LocaleSelectorProps {
406
459
  readonly targetLocale: string;
407
460
  readonly targetLocales: readonly string[];
@@ -551,10 +604,6 @@ interface FormServerErrorPayload {
551
604
  readonly fieldErrors?: Readonly<Record<string, string>>;
552
605
  readonly formError?: string;
553
606
  }
554
- declare class FormSubmissionError extends Error {
555
- readonly payload: FormServerErrorPayload;
556
- constructor(message: string, payload?: FormServerErrorPayload);
557
- }
558
607
  type FormSubmitHandler = (answers: FormValues, context: SubmitContext) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
559
608
  type SubmissionGuardResult = {
560
609
  readonly status: "allow";
@@ -771,6 +820,8 @@ interface FieldState {
771
820
  }
772
821
  declare function useField(fieldId: string): FieldState;
773
822
 
823
+ declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
824
+
774
825
  interface UseTranslationWorkspaceOptions {
775
826
  readonly schema: FormSchema;
776
827
  readonly onChange?: (schema: FormSchema) => void;
@@ -909,10 +960,12 @@ interface FormEngineI18nProviderProps {
909
960
  readonly fallbackLocale?: string;
910
961
  readonly messages?: FormEngineMessages;
911
962
  readonly customCatalogs?: Record<string, FormEngineMessages>;
963
+ readonly onMissingKey?: (event: TranslationMissingKeyEvent) => void;
964
+ readonly strict?: boolean;
912
965
  readonly translator?: FormEngineTranslator;
913
966
  readonly children: ReactNode;
914
967
  }
915
- declare function FormEngineI18nProvider({ locale, fallbackLocale, messages, customCatalogs, translator: customTranslator, children }: FormEngineI18nProviderProps): react.JSX.Element;
968
+ declare function FormEngineI18nProvider({ locale, fallbackLocale, messages, customCatalogs, onMissingKey, strict, translator: customTranslator, children }: FormEngineI18nProviderProps): react.JSX.Element;
916
969
  declare function useFormEngineI18n(): FormEngineI18nContextValue;
917
970
 
918
971
  declare function resolveChoiceFieldLayout(type: FieldType, appearance?: FormRendererAppearance, groupedChoiceFieldsLegacy?: boolean): "default" | "grouped";
@@ -968,4 +1021,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
968
1021
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
969
1022
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
970
1023
 
971
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type ConfirmRemoveLocaleSlotProps, type CustomLocaleValidator, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, FormEngineI18nContext, type FormEngineI18nContextValue, FormEngineI18nProvider, type FormEngineI18nProviderProps, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocaleSelectorProps, type LocaleValidationContext, type LocaleValidationResult, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationEventPayload, type TranslationSlotChangeEvent, type TranslationSlotRowProps, type TranslationSummary, type TranslationWorkspaceActionsProps, type TranslationWorkspaceError, type TranslationWorkspaceHeaderProps, type TranslationWorkspaceSlots, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useFormEngineI18n, useSubmissionReceipts, useTranslationWorkspace, validateLocalePipeline };
1024
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type ConfirmRemoveLocaleSlotProps, type CustomLocaleValidator, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, FormEngineI18nContext, type FormEngineI18nContextValue, FormEngineI18nProvider, type FormEngineI18nProviderProps, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocaleSelectorProps, type LocaleValidationContext, type LocaleValidationResult, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationComparisonHeaderProps, type TranslationComparisonItem, type TranslationComparisonItemRowProps, type TranslationComparisonSummary, type TranslationEventPayload, type TranslationSlotChangeEvent, type TranslationSlotRowProps, type TranslationSummary, type TranslationWorkspaceActionsProps, type TranslationWorkspaceError, type TranslationWorkspaceHeaderProps, type TranslationWorkspaceSlots, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationComparisonOptions, type UseTranslationComparisonResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useFormEngineI18n, useSubmissionReceipts, useTranslationComparison, useTranslationWorkspace, validateLocalePipeline };
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,
@@ -3837,8 +3840,131 @@ function useTranslationWorkspace({
3837
3840
  };
3838
3841
  }
3839
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
+
3840
3966
  // src/receipt.ts
3841
- 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";
3842
3968
  function submissionReceiptQueryKey(formId, formVersion) {
3843
3969
  return `${formId}:v${formVersion}`;
3844
3970
  }
@@ -3909,7 +4035,7 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3909
4035
  }
3910
4036
  function useSubmissionReceipts(store, queries) {
3911
4037
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3912
- const stableQueries = useMemo5(() => {
4038
+ const stableQueries = useMemo6(() => {
3913
4039
  const parsed = JSON.parse(querySignature);
3914
4040
  if (!Array.isArray(parsed)) return [];
3915
4041
  return parsed.flatMap(
@@ -3956,16 +4082,18 @@ function useSubmissionReceipts(store, queries) {
3956
4082
 
3957
4083
  // src/renderer.tsx
3958
4084
  import {
4085
+ FormSubmissionError as FormSubmissionError2,
4086
+ isFormSubmissionSerializedError as isFormSubmissionSerializedError2,
3959
4087
  selectVisibleAnswers as selectVisibleAnswers2,
3960
4088
  validateAnswers as validateAnswers2
3961
4089
  } from "@form-engine-ts/core";
3962
4090
  import {
3963
4091
  Fragment as Fragment2,
3964
- useCallback as useCallback4,
4092
+ useCallback as useCallback5,
3965
4093
  useContext as useContext4,
3966
4094
  useEffect as useEffect3,
3967
4095
  useId,
3968
- useMemo as useMemo6,
4096
+ useMemo as useMemo7,
3969
4097
  useRef as useRef3,
3970
4098
  useState as useState6
3971
4099
  } from "react";
@@ -4498,15 +4626,15 @@ function ContextFormRenderer({
4498
4626
  const completionRef = useRef3(null);
4499
4627
  const confirmationRef = useRef3(null);
4500
4628
  const pages = form.schema.pages;
4501
- const visiblePageIndexes = useMemo6(
4629
+ const visiblePageIndexes = useMemo7(
4502
4630
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
4503
4631
  [form.pageVisibility, pages]
4504
4632
  );
4505
4633
  const activePage = pages?.[currentPageIndex];
4506
4634
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
4507
4635
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
4508
- const visibleValues = useMemo6(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
4509
- const visibleItems = useMemo6(
4636
+ const visibleValues = useMemo7(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
4637
+ const visibleItems = useMemo7(
4510
4638
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
4511
4639
  [form.schema, form.translate, form.values, form.visibility]
4512
4640
  );
@@ -4515,7 +4643,7 @@ function ContextFormRenderer({
4515
4643
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
4516
4644
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
4517
4645
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
4518
- const resolveMessage = useCallback4(
4646
+ const resolveMessage = useCallback5(
4519
4647
  (key, fallback) => {
4520
4648
  const providerDefault = isProviderValue ? i18n.translator(`renderer.${key}`) : void 0;
4521
4649
  const defaultText = fallback ?? (providerDefault === "" ? void 0 : providerDefault) ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
@@ -4524,11 +4652,11 @@ function ContextFormRenderer({
4524
4652
  },
4525
4653
  [form.locale, i18n, isProviderValue, messageResolver, messages]
4526
4654
  );
4527
- const fieldTranslate = useCallback4(
4655
+ const fieldTranslate = useCallback5(
4528
4656
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
4529
4657
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
4530
4658
  );
4531
- const focusSubmitButton = useCallback4(() => {
4659
+ const focusSubmitButton = useCallback5(() => {
4532
4660
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
4533
4661
  button?.focus();
4534
4662
  }, []);
@@ -4739,8 +4867,9 @@ function ContextFormRenderer({
4739
4867
  return result;
4740
4868
  }
4741
4869
  if (result.status === "error") {
4742
- if (result.error instanceof FormSubmissionError) {
4743
- 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 ?? {};
4744
4873
  form.setServerErrors?.(fieldErrors);
4745
4874
  const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
4746
4875
  if (firstServerFieldId !== void 0) {
@@ -4748,6 +4877,9 @@ function ContextFormRenderer({
4748
4877
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
4749
4878
  focusFirstIssue(firstServerFieldId);
4750
4879
  }
4880
+ if (payload.piiFindings !== void 0 && payload.piiFindings.length > 0) {
4881
+ setConfirmation({ findings: payload.piiFindings, generic: false });
4882
+ }
4751
4883
  }
4752
4884
  return result;
4753
4885
  }
@@ -5049,8 +5181,8 @@ function ContextFormRenderer({
5049
5181
  ] }),
5050
5182
  /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
5051
5183
  form.submitStatus === "success" ? completionRegion : null,
5052
- 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: [
5053
- 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)),
5054
5186
  /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
5055
5187
  ] }) : /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
5056
5188
  errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
@@ -5133,6 +5265,9 @@ function FormRenderer(props) {
5133
5265
  }
5134
5266
  );
5135
5267
  }
5268
+
5269
+ // src/types.ts
5270
+ import { FormSubmissionError as FormSubmissionError3 } from "@form-engine-ts/core";
5136
5271
  export {
5137
5272
  BUILDER_TRANSLATION_ALIASES,
5138
5273
  BUILDER_TRANSLATION_KEYS,
@@ -5141,7 +5276,7 @@ export {
5141
5276
  FormEngineI18nProvider,
5142
5277
  FormProvider,
5143
5278
  FormRenderer,
5144
- FormSubmissionError,
5279
+ FormSubmissionError3 as FormSubmissionError,
5145
5280
  createLocalStorageSubmissionAttemptStore,
5146
5281
  createLocalStorageSubmissionReceiptStore,
5147
5282
  isTranslationUnresolved,
@@ -5156,6 +5291,7 @@ export {
5156
5291
  useFormBuilder,
5157
5292
  useFormEngineI18n,
5158
5293
  useSubmissionReceipts,
5294
+ useTranslationComparison,
5159
5295
  useTranslationWorkspace,
5160
5296
  validateLocalePipeline
5161
5297
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "6.0.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": "6.0.0",
46
- "@form-engine-ts/privacy": "6.0.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",