@form-engine-ts/react 5.1.0 → 6.0.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
@@ -89,6 +89,10 @@ underscore-separated values such as `EN_us` are accepted as compatibility input.
89
89
  Use `isAddLocaleAllowed` to disable locale controls before submission. Removing a locale also clears its
90
90
  localized values and metadata; the default locale remains protected.
91
91
 
92
+ `useTranslationWorkspace` can notify `onTranslationStart`, `onTranslationSuccess`, `onTranslationError`, and
93
+ `onTranslationChange` with typed lifecycle payloads. The MUI workspace accepts a `confirmRemoveLocale` slot for an
94
+ async confirmation UI; its `onConfirm` callback completes the locale removal.
95
+
92
96
  Wrap a builder or renderer in `FormEngineI18nProvider` to supply a UI locale and typed Core translator independently
93
97
  from the schema's `defaultLocale` and `supportedLocales`:
94
98
 
package/dist/index.cjs CHANGED
@@ -3421,26 +3421,27 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3421
3421
  }
3422
3422
  return { type: "custom_validation_failed", message: error.message };
3423
3423
  }
3424
- function manualMetadata(sourceText, sourceLocale) {
3424
+ function translationMetadata(sourceText, sourceLocale, mode) {
3425
3425
  return {
3426
3426
  sourceLocale,
3427
3427
  sourceTextHash: (0, import_core5.computeSourceTextHash)(sourceText),
3428
- translationSource: "manual",
3429
- editedAt: (/* @__PURE__ */ new Date()).toISOString()
3428
+ translationSource: mode,
3429
+ ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3430
3430
  };
3431
3431
  }
3432
- function setNodeMetadata(node, slot, text, sourceLocale) {
3432
+ function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3433
3433
  const localeMetadata = node.translationMetadata?.[slot.locale];
3434
- const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
3434
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: translationMetadata(slot.sourceText, sourceLocale, mode) };
3435
3435
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3436
3436
  }
3437
- function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3437
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3438
3438
  if (slot.kind === "form") {
3439
3439
  return setNodeMetadata(
3440
3440
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3441
3441
  slot,
3442
3442
  text,
3443
- sourceLocale
3443
+ sourceLocale,
3444
+ mode
3444
3445
  );
3445
3446
  }
3446
3447
  if (slot.kind === "field") {
@@ -3449,7 +3450,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3449
3450
  fields: schema.fields.map((field) => {
3450
3451
  if (field.id !== slot.nodeId) return field;
3451
3452
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3452
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3453
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3453
3454
  })
3454
3455
  };
3455
3456
  }
@@ -3465,7 +3466,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3465
3466
  const translations = text.trim().length === 0 ? Object.fromEntries(
3466
3467
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3467
3468
  ) : { ...option.translations, [slot.locale]: text };
3468
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3469
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3469
3470
  })
3470
3471
  };
3471
3472
  })
@@ -3477,7 +3478,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3477
3478
  pages: schema.pages.map((page) => {
3478
3479
  if (page.id !== slot.nodeId) return page;
3479
3480
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3480
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3481
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3481
3482
  })
3482
3483
  }
3483
3484
  };
@@ -3495,13 +3496,22 @@ function useTranslationWorkspace({
3495
3496
  onLocaleRemoved,
3496
3497
  onLocaleChange,
3497
3498
  beforeRemoveLocale,
3499
+ confirmRemoveLocale,
3500
+ slots: workspaceSlots,
3501
+ onTranslationStart,
3502
+ onTranslationSuccess,
3503
+ onTranslationError,
3504
+ onTranslationChange,
3498
3505
  validateLocale
3499
3506
  }) {
3500
3507
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3501
3508
  const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
3502
3509
  const [isTranslating, setIsTranslating] = (0, import_react5.useState)(false);
3503
3510
  const [error, setError] = (0, import_react5.useState)();
3511
+ const [pendingRemoval, setPendingRemoval] = (0, import_react5.useState)();
3512
+ const pendingRemovalResolver = (0, import_react5.useRef)(void 0);
3504
3513
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3514
+ const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3505
3515
  const targetLocales = (0, import_react5.useMemo)(() => {
3506
3516
  const locales = (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales;
3507
3517
  return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
@@ -3551,9 +3561,17 @@ function useTranslationWorkspace({
3551
3561
  const setTranslation = (0, import_react5.useCallback)(
3552
3562
  (slot, text) => {
3553
3563
  if (readOnly) return;
3554
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3564
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3565
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual"));
3566
+ onTranslationChange?.({
3567
+ slot,
3568
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3569
+ nextText: text,
3570
+ mode: "manual",
3571
+ metadata
3572
+ });
3555
3573
  },
3556
- [commit, currentSchema, readOnly, sourceLocale]
3574
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3557
3575
  );
3558
3576
  const addLocale = (0, import_react5.useCallback)(
3559
3577
  (locale) => {
@@ -3614,21 +3632,36 @@ function useTranslationWorkspace({
3614
3632
  onLocaleRemoved?.(normalized);
3615
3633
  };
3616
3634
  const slotCount = (0, import_core5.collectTranslationSlots)(currentSchema, normalized).length;
3635
+ const translatedSlotsCount = (0, import_core5.collectTranslationSlots)(currentSchema, normalized).filter(
3636
+ (slot) => slot.existingText !== void 0 && slot.existingText.trim().length > 0
3637
+ ).length;
3638
+ const requestConfirmation = () => new Promise((resolve) => {
3639
+ pendingRemovalResolver.current = resolve;
3640
+ setPendingRemoval({
3641
+ locale: normalized,
3642
+ localeLabel: localeOptions.find((option) => option.locale === normalized)?.label ?? normalized,
3643
+ translatedSlotsCount
3644
+ });
3645
+ });
3646
+ const removeAfterApproval = () => {
3647
+ if (confirmRemoveLocaleRenderer === void 0) {
3648
+ remove();
3649
+ return true;
3650
+ }
3651
+ return requestConfirmation();
3652
+ };
3617
3653
  if (beforeRemoveLocale === void 0) {
3618
- remove();
3619
- return true;
3654
+ return removeAfterApproval();
3620
3655
  }
3621
3656
  try {
3622
3657
  const decision = beforeRemoveLocale(normalized, { slotCount });
3623
3658
  if (typeof decision === "boolean") {
3624
3659
  if (!decision) return false;
3625
- remove();
3626
- return true;
3660
+ return removeAfterApproval();
3627
3661
  }
3628
3662
  return decision.then((allowed) => {
3629
3663
  if (!allowed) return false;
3630
- remove();
3631
- return true;
3664
+ return removeAfterApproval();
3632
3665
  });
3633
3666
  } catch (cause) {
3634
3667
  const workspaceError = {
@@ -3640,8 +3673,45 @@ function useTranslationWorkspace({
3640
3673
  return Promise.reject(cause);
3641
3674
  }
3642
3675
  },
3643
- [activeLocale, beforeRemoveLocale, commit, currentSchema, onLocaleRemoved, readOnly, sourceLocale]
3676
+ [
3677
+ activeLocale,
3678
+ beforeRemoveLocale,
3679
+ commit,
3680
+ confirmRemoveLocaleRenderer,
3681
+ currentSchema,
3682
+ localeOptions,
3683
+ onLocaleRemoved,
3684
+ readOnly,
3685
+ sourceLocale
3686
+ ]
3644
3687
  );
3688
+ const confirmPendingRemoval = (0, import_react5.useCallback)(() => {
3689
+ const pending = pendingRemoval;
3690
+ const resolve = pendingRemovalResolver.current;
3691
+ if (pending === void 0 || resolve === void 0) return;
3692
+ pendingRemovalResolver.current = void 0;
3693
+ setPendingRemoval(void 0);
3694
+ commit((0, import_core5.removeLocaleFromSchema)(currentSchema, pending.locale));
3695
+ if (activeLocale === pending.locale) setSelectedLocale("");
3696
+ onLocaleRemoved?.(pending.locale);
3697
+ resolve(true);
3698
+ }, [activeLocale, commit, currentSchema, onLocaleRemoved, pendingRemoval]);
3699
+ const cancelPendingRemoval = (0, import_react5.useCallback)(() => {
3700
+ const resolve = pendingRemovalResolver.current;
3701
+ if (resolve === void 0) return;
3702
+ pendingRemovalResolver.current = void 0;
3703
+ setPendingRemoval(void 0);
3704
+ resolve(false);
3705
+ }, []);
3706
+ const removeLocaleConfirmation = (0, import_react5.useMemo)(() => {
3707
+ if (confirmRemoveLocaleRenderer === void 0 || pendingRemoval === void 0) return null;
3708
+ return confirmRemoveLocaleRenderer({
3709
+ ...pendingRemoval,
3710
+ isOpen: true,
3711
+ onConfirm: confirmPendingRemoval,
3712
+ onCancel: cancelPendingRemoval
3713
+ });
3714
+ }, [cancelPendingRemoval, confirmPendingRemoval, confirmRemoveLocaleRenderer, pendingRemoval]);
3645
3715
  const translateAll = (0, import_react5.useCallback)(
3646
3716
  async (options = {}) => {
3647
3717
  if (readOnly) {
@@ -3678,6 +3748,7 @@ function useTranslationWorkspace({
3678
3748
  }
3679
3749
  setIsTranslating(true);
3680
3750
  setError(void 0);
3751
+ onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3681
3752
  try {
3682
3753
  const populated = await (0, import_core5.populateSchemaTranslations)(
3683
3754
  currentSchema,
@@ -3690,6 +3761,17 @@ function useTranslationWorkspace({
3690
3761
  }
3691
3762
  );
3692
3763
  commit(populated.schema);
3764
+ const missingSlotsCount = (0, import_core5.collectTranslationSlots)(populated.schema, activeLocale).filter(
3765
+ (slot) => slot.status === "missing"
3766
+ ).length;
3767
+ onTranslationSuccess?.({
3768
+ sourceLocale,
3769
+ targetLocale: activeLocale,
3770
+ mode: "automatic",
3771
+ updatedSlots: populated.report.updatedSlots,
3772
+ skippedSlots: populated.report.skippedSlots,
3773
+ missingSlotsCount
3774
+ });
3693
3775
  return { success: true, report: populated.report };
3694
3776
  } catch (cause) {
3695
3777
  const workspaceError = {
@@ -3698,12 +3780,25 @@ function useTranslationWorkspace({
3698
3780
  cause
3699
3781
  };
3700
3782
  setError(workspaceError);
3783
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3701
3784
  return { success: false, error: workspaceError };
3702
3785
  } finally {
3703
3786
  setIsTranslating(false);
3704
3787
  }
3705
3788
  },
3706
- [activeLocale, commit, currentSchema, localeOptions, readOnly, slots, translationAdapter]
3789
+ [
3790
+ activeLocale,
3791
+ commit,
3792
+ currentSchema,
3793
+ localeOptions,
3794
+ onTranslationError,
3795
+ onTranslationStart,
3796
+ onTranslationSuccess,
3797
+ readOnly,
3798
+ slots,
3799
+ sourceLocale,
3800
+ translationAdapter
3801
+ ]
3707
3802
  );
3708
3803
  const translateSlot = (0, import_react5.useCallback)(
3709
3804
  async (slot) => {
@@ -3721,7 +3816,15 @@ function useTranslationWorkspace({
3721
3816
  setError(void 0);
3722
3817
  try {
3723
3818
  const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3724
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3819
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3820
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3821
+ onTranslationChange?.({
3822
+ slot,
3823
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3824
+ nextText: text,
3825
+ mode: "automatic",
3826
+ metadata
3827
+ });
3725
3828
  return { success: true };
3726
3829
  } catch (cause) {
3727
3830
  const workspaceError = {
@@ -3735,7 +3838,7 @@ function useTranslationWorkspace({
3735
3838
  setIsTranslating(false);
3736
3839
  }
3737
3840
  },
3738
- [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3841
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3739
3842
  );
3740
3843
  return {
3741
3844
  sourceLocale,
@@ -3752,6 +3855,7 @@ function useTranslationWorkspace({
3752
3855
  addLocale,
3753
3856
  isAddLocaleAllowed,
3754
3857
  removeLocale,
3858
+ ...removeLocaleConfirmation === null ? {} : { removeLocaleConfirmation },
3755
3859
  setTranslation,
3756
3860
  translateAll,
3757
3861
  translateSlot,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
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, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, TranslationSlot, FormEngineTranslator, FormEngineMessages, FieldType } 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
3
  export { QuestionType } from '@form-engine-ts/core';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
@@ -417,6 +417,29 @@ interface TranslationWorkspaceActionsProps {
417
417
  readonly isTranslating: boolean;
418
418
  readonly readOnly: boolean;
419
419
  }
420
+ interface TranslationEventPayload {
421
+ readonly sourceLocale: string;
422
+ readonly targetLocale: string;
423
+ readonly mode: "manual" | "automatic";
424
+ readonly updatedSlots: readonly TranslationSlot[];
425
+ readonly skippedSlots: readonly TranslationSlot[];
426
+ readonly missingSlotsCount: number;
427
+ }
428
+ interface TranslationSlotChangeEvent {
429
+ readonly slot: TranslationSlot;
430
+ readonly previousText?: string;
431
+ readonly nextText: string;
432
+ readonly mode: "manual" | "automatic";
433
+ readonly metadata: CanonicalTranslationMetadata;
434
+ }
435
+ interface ConfirmRemoveLocaleSlotProps {
436
+ readonly locale: string;
437
+ readonly localeLabel: string;
438
+ readonly translatedSlotsCount: number;
439
+ readonly isOpen: boolean;
440
+ readonly onConfirm: () => void;
441
+ readonly onCancel: () => void;
442
+ }
420
443
  interface TranslationWorkspaceSlots {
421
444
  readonly renderHeader?: (props: TranslationWorkspaceHeaderProps) => ReactNode;
422
445
  readonly renderSlotRow?: (props: TranslationSlotRowProps) => ReactNode;
@@ -425,6 +448,7 @@ interface TranslationWorkspaceSlots {
425
448
  readonly status: _form_engine_ts_core.TranslationStatus;
426
449
  }) => ReactNode;
427
450
  readonly renderActions?: (props: TranslationWorkspaceActionsProps) => ReactNode;
451
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
428
452
  }
429
453
  interface LocalizationSummaryContext {
430
454
  readonly defaultLocale: string;
@@ -762,6 +786,18 @@ interface UseTranslationWorkspaceOptions {
762
786
  readonly beforeRemoveLocale?: (locale: string, context: {
763
787
  readonly slotCount: number;
764
788
  }) => Promise<boolean> | boolean;
789
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
790
+ readonly slots?: Pick<TranslationWorkspaceSlots, "confirmRemoveLocale">;
791
+ readonly onTranslationStart?: (params: {
792
+ readonly targetLocale: string;
793
+ readonly mode: "manual" | "automatic";
794
+ }) => void;
795
+ readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
796
+ readonly onTranslationError?: (params: {
797
+ readonly targetLocale: string;
798
+ readonly error: TranslationWorkspaceError;
799
+ }) => void;
800
+ readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
765
801
  readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
766
802
  }
767
803
  interface LocaleValidationContext {
@@ -827,6 +863,7 @@ interface UseTranslationWorkspaceResult {
827
863
  };
828
864
  readonly isAddLocaleAllowed: (locale: string) => boolean;
829
865
  readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
866
+ readonly removeLocaleConfirmation?: ReactNode;
830
867
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
831
868
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
832
869
  readonly success: boolean;
@@ -841,7 +878,7 @@ interface UseTranslationWorkspaceResult {
841
878
  readonly error?: TranslationWorkspaceError;
842
879
  }
843
880
  declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
844
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
881
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationError, onTranslationChange, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
845
882
 
846
883
  declare const BUILDER_TRANSLATION_KEYS: {
847
884
  readonly ADD_FIELD: "builder.actions.addField";
@@ -931,4 +968,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
931
968
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
932
969
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
933
970
 
934
- 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 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 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, TranslationSlot, FormEngineTranslator, FormEngineMessages, FieldType } 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
3
  export { QuestionType } from '@form-engine-ts/core';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
@@ -417,6 +417,29 @@ interface TranslationWorkspaceActionsProps {
417
417
  readonly isTranslating: boolean;
418
418
  readonly readOnly: boolean;
419
419
  }
420
+ interface TranslationEventPayload {
421
+ readonly sourceLocale: string;
422
+ readonly targetLocale: string;
423
+ readonly mode: "manual" | "automatic";
424
+ readonly updatedSlots: readonly TranslationSlot[];
425
+ readonly skippedSlots: readonly TranslationSlot[];
426
+ readonly missingSlotsCount: number;
427
+ }
428
+ interface TranslationSlotChangeEvent {
429
+ readonly slot: TranslationSlot;
430
+ readonly previousText?: string;
431
+ readonly nextText: string;
432
+ readonly mode: "manual" | "automatic";
433
+ readonly metadata: CanonicalTranslationMetadata;
434
+ }
435
+ interface ConfirmRemoveLocaleSlotProps {
436
+ readonly locale: string;
437
+ readonly localeLabel: string;
438
+ readonly translatedSlotsCount: number;
439
+ readonly isOpen: boolean;
440
+ readonly onConfirm: () => void;
441
+ readonly onCancel: () => void;
442
+ }
420
443
  interface TranslationWorkspaceSlots {
421
444
  readonly renderHeader?: (props: TranslationWorkspaceHeaderProps) => ReactNode;
422
445
  readonly renderSlotRow?: (props: TranslationSlotRowProps) => ReactNode;
@@ -425,6 +448,7 @@ interface TranslationWorkspaceSlots {
425
448
  readonly status: _form_engine_ts_core.TranslationStatus;
426
449
  }) => ReactNode;
427
450
  readonly renderActions?: (props: TranslationWorkspaceActionsProps) => ReactNode;
451
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
428
452
  }
429
453
  interface LocalizationSummaryContext {
430
454
  readonly defaultLocale: string;
@@ -762,6 +786,18 @@ interface UseTranslationWorkspaceOptions {
762
786
  readonly beforeRemoveLocale?: (locale: string, context: {
763
787
  readonly slotCount: number;
764
788
  }) => Promise<boolean> | boolean;
789
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
790
+ readonly slots?: Pick<TranslationWorkspaceSlots, "confirmRemoveLocale">;
791
+ readonly onTranslationStart?: (params: {
792
+ readonly targetLocale: string;
793
+ readonly mode: "manual" | "automatic";
794
+ }) => void;
795
+ readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
796
+ readonly onTranslationError?: (params: {
797
+ readonly targetLocale: string;
798
+ readonly error: TranslationWorkspaceError;
799
+ }) => void;
800
+ readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
765
801
  readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
766
802
  }
767
803
  interface LocaleValidationContext {
@@ -827,6 +863,7 @@ interface UseTranslationWorkspaceResult {
827
863
  };
828
864
  readonly isAddLocaleAllowed: (locale: string) => boolean;
829
865
  readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
866
+ readonly removeLocaleConfirmation?: ReactNode;
830
867
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
831
868
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
832
869
  readonly success: boolean;
@@ -841,7 +878,7 @@ interface UseTranslationWorkspaceResult {
841
878
  readonly error?: TranslationWorkspaceError;
842
879
  }
843
880
  declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
844
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
881
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationError, onTranslationChange, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
845
882
 
846
883
  declare const BUILDER_TRANSLATION_KEYS: {
847
884
  readonly ADD_FIELD: "builder.actions.addField";
@@ -931,4 +968,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
931
968
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
932
969
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
933
970
 
934
- 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 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 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 };
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 };
package/dist/index.js CHANGED
@@ -3282,7 +3282,7 @@ import {
3282
3282
  populateSchemaTranslations as populateSchemaTranslations2,
3283
3283
  removeLocaleFromSchema
3284
3284
  } from "@form-engine-ts/core";
3285
- import { useCallback as useCallback3, useMemo as useMemo4, useState as useState4 } from "react";
3285
+ import { useCallback as useCallback3, useMemo as useMemo4, useRef as useRef2, useState as useState4 } from "react";
3286
3286
  function updateTranslationMap(translations, locale, property, text) {
3287
3287
  if (property === "label") return translations ?? {};
3288
3288
  const current = translations?.[locale];
@@ -3394,26 +3394,27 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3394
3394
  }
3395
3395
  return { type: "custom_validation_failed", message: error.message };
3396
3396
  }
3397
- function manualMetadata(sourceText, sourceLocale) {
3397
+ function translationMetadata(sourceText, sourceLocale, mode) {
3398
3398
  return {
3399
3399
  sourceLocale,
3400
3400
  sourceTextHash: computeSourceTextHash(sourceText),
3401
- translationSource: "manual",
3402
- editedAt: (/* @__PURE__ */ new Date()).toISOString()
3401
+ translationSource: mode,
3402
+ ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3403
3403
  };
3404
3404
  }
3405
- function setNodeMetadata(node, slot, text, sourceLocale) {
3405
+ function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3406
3406
  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) };
3407
+ 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
3408
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3409
3409
  }
3410
- function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3410
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3411
3411
  if (slot.kind === "form") {
3412
3412
  return setNodeMetadata(
3413
3413
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3414
3414
  slot,
3415
3415
  text,
3416
- sourceLocale
3416
+ sourceLocale,
3417
+ mode
3417
3418
  );
3418
3419
  }
3419
3420
  if (slot.kind === "field") {
@@ -3422,7 +3423,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3422
3423
  fields: schema.fields.map((field) => {
3423
3424
  if (field.id !== slot.nodeId) return field;
3424
3425
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3425
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3426
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3426
3427
  })
3427
3428
  };
3428
3429
  }
@@ -3438,7 +3439,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3438
3439
  const translations = text.trim().length === 0 ? Object.fromEntries(
3439
3440
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3440
3441
  ) : { ...option.translations, [slot.locale]: text };
3441
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3442
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3442
3443
  })
3443
3444
  };
3444
3445
  })
@@ -3450,7 +3451,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3450
3451
  pages: schema.pages.map((page) => {
3451
3452
  if (page.id !== slot.nodeId) return page;
3452
3453
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3453
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3454
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3454
3455
  })
3455
3456
  }
3456
3457
  };
@@ -3468,13 +3469,22 @@ function useTranslationWorkspace({
3468
3469
  onLocaleRemoved,
3469
3470
  onLocaleChange,
3470
3471
  beforeRemoveLocale,
3472
+ confirmRemoveLocale,
3473
+ slots: workspaceSlots,
3474
+ onTranslationStart,
3475
+ onTranslationSuccess,
3476
+ onTranslationError,
3477
+ onTranslationChange,
3471
3478
  validateLocale
3472
3479
  }) {
3473
3480
  const [draftSchema, setDraftSchema] = useState4(schema);
3474
3481
  const [selectedLocale, setSelectedLocale] = useState4(normalizeLocale(targetLocale ?? "") ?? targetLocale ?? "");
3475
3482
  const [isTranslating, setIsTranslating] = useState4(false);
3476
3483
  const [error, setError] = useState4();
3484
+ const [pendingRemoval, setPendingRemoval] = useState4();
3485
+ const pendingRemovalResolver = useRef2(void 0);
3477
3486
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3487
+ const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3478
3488
  const targetLocales = useMemo4(() => {
3479
3489
  const locales = collectSchemaLocales2(currentSchema).allUniqueLocales;
3480
3490
  return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
@@ -3524,9 +3534,17 @@ function useTranslationWorkspace({
3524
3534
  const setTranslation = useCallback3(
3525
3535
  (slot, text) => {
3526
3536
  if (readOnly) return;
3527
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3537
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3538
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual"));
3539
+ onTranslationChange?.({
3540
+ slot,
3541
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3542
+ nextText: text,
3543
+ mode: "manual",
3544
+ metadata
3545
+ });
3528
3546
  },
3529
- [commit, currentSchema, readOnly, sourceLocale]
3547
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3530
3548
  );
3531
3549
  const addLocale = useCallback3(
3532
3550
  (locale) => {
@@ -3587,21 +3605,36 @@ function useTranslationWorkspace({
3587
3605
  onLocaleRemoved?.(normalized);
3588
3606
  };
3589
3607
  const slotCount = collectTranslationSlots(currentSchema, normalized).length;
3608
+ const translatedSlotsCount = collectTranslationSlots(currentSchema, normalized).filter(
3609
+ (slot) => slot.existingText !== void 0 && slot.existingText.trim().length > 0
3610
+ ).length;
3611
+ const requestConfirmation = () => new Promise((resolve) => {
3612
+ pendingRemovalResolver.current = resolve;
3613
+ setPendingRemoval({
3614
+ locale: normalized,
3615
+ localeLabel: localeOptions.find((option) => option.locale === normalized)?.label ?? normalized,
3616
+ translatedSlotsCount
3617
+ });
3618
+ });
3619
+ const removeAfterApproval = () => {
3620
+ if (confirmRemoveLocaleRenderer === void 0) {
3621
+ remove();
3622
+ return true;
3623
+ }
3624
+ return requestConfirmation();
3625
+ };
3590
3626
  if (beforeRemoveLocale === void 0) {
3591
- remove();
3592
- return true;
3627
+ return removeAfterApproval();
3593
3628
  }
3594
3629
  try {
3595
3630
  const decision = beforeRemoveLocale(normalized, { slotCount });
3596
3631
  if (typeof decision === "boolean") {
3597
3632
  if (!decision) return false;
3598
- remove();
3599
- return true;
3633
+ return removeAfterApproval();
3600
3634
  }
3601
3635
  return decision.then((allowed) => {
3602
3636
  if (!allowed) return false;
3603
- remove();
3604
- return true;
3637
+ return removeAfterApproval();
3605
3638
  });
3606
3639
  } catch (cause) {
3607
3640
  const workspaceError = {
@@ -3613,8 +3646,45 @@ function useTranslationWorkspace({
3613
3646
  return Promise.reject(cause);
3614
3647
  }
3615
3648
  },
3616
- [activeLocale, beforeRemoveLocale, commit, currentSchema, onLocaleRemoved, readOnly, sourceLocale]
3649
+ [
3650
+ activeLocale,
3651
+ beforeRemoveLocale,
3652
+ commit,
3653
+ confirmRemoveLocaleRenderer,
3654
+ currentSchema,
3655
+ localeOptions,
3656
+ onLocaleRemoved,
3657
+ readOnly,
3658
+ sourceLocale
3659
+ ]
3617
3660
  );
3661
+ const confirmPendingRemoval = useCallback3(() => {
3662
+ const pending = pendingRemoval;
3663
+ const resolve = pendingRemovalResolver.current;
3664
+ if (pending === void 0 || resolve === void 0) return;
3665
+ pendingRemovalResolver.current = void 0;
3666
+ setPendingRemoval(void 0);
3667
+ commit(removeLocaleFromSchema(currentSchema, pending.locale));
3668
+ if (activeLocale === pending.locale) setSelectedLocale("");
3669
+ onLocaleRemoved?.(pending.locale);
3670
+ resolve(true);
3671
+ }, [activeLocale, commit, currentSchema, onLocaleRemoved, pendingRemoval]);
3672
+ const cancelPendingRemoval = useCallback3(() => {
3673
+ const resolve = pendingRemovalResolver.current;
3674
+ if (resolve === void 0) return;
3675
+ pendingRemovalResolver.current = void 0;
3676
+ setPendingRemoval(void 0);
3677
+ resolve(false);
3678
+ }, []);
3679
+ const removeLocaleConfirmation = useMemo4(() => {
3680
+ if (confirmRemoveLocaleRenderer === void 0 || pendingRemoval === void 0) return null;
3681
+ return confirmRemoveLocaleRenderer({
3682
+ ...pendingRemoval,
3683
+ isOpen: true,
3684
+ onConfirm: confirmPendingRemoval,
3685
+ onCancel: cancelPendingRemoval
3686
+ });
3687
+ }, [cancelPendingRemoval, confirmPendingRemoval, confirmRemoveLocaleRenderer, pendingRemoval]);
3618
3688
  const translateAll = useCallback3(
3619
3689
  async (options = {}) => {
3620
3690
  if (readOnly) {
@@ -3651,6 +3721,7 @@ function useTranslationWorkspace({
3651
3721
  }
3652
3722
  setIsTranslating(true);
3653
3723
  setError(void 0);
3724
+ onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3654
3725
  try {
3655
3726
  const populated = await populateSchemaTranslations2(
3656
3727
  currentSchema,
@@ -3663,6 +3734,17 @@ function useTranslationWorkspace({
3663
3734
  }
3664
3735
  );
3665
3736
  commit(populated.schema);
3737
+ const missingSlotsCount = collectTranslationSlots(populated.schema, activeLocale).filter(
3738
+ (slot) => slot.status === "missing"
3739
+ ).length;
3740
+ onTranslationSuccess?.({
3741
+ sourceLocale,
3742
+ targetLocale: activeLocale,
3743
+ mode: "automatic",
3744
+ updatedSlots: populated.report.updatedSlots,
3745
+ skippedSlots: populated.report.skippedSlots,
3746
+ missingSlotsCount
3747
+ });
3666
3748
  return { success: true, report: populated.report };
3667
3749
  } catch (cause) {
3668
3750
  const workspaceError = {
@@ -3671,12 +3753,25 @@ function useTranslationWorkspace({
3671
3753
  cause
3672
3754
  };
3673
3755
  setError(workspaceError);
3756
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3674
3757
  return { success: false, error: workspaceError };
3675
3758
  } finally {
3676
3759
  setIsTranslating(false);
3677
3760
  }
3678
3761
  },
3679
- [activeLocale, commit, currentSchema, localeOptions, readOnly, slots, translationAdapter]
3762
+ [
3763
+ activeLocale,
3764
+ commit,
3765
+ currentSchema,
3766
+ localeOptions,
3767
+ onTranslationError,
3768
+ onTranslationStart,
3769
+ onTranslationSuccess,
3770
+ readOnly,
3771
+ slots,
3772
+ sourceLocale,
3773
+ translationAdapter
3774
+ ]
3680
3775
  );
3681
3776
  const translateSlot = useCallback3(
3682
3777
  async (slot) => {
@@ -3694,7 +3789,15 @@ function useTranslationWorkspace({
3694
3789
  setError(void 0);
3695
3790
  try {
3696
3791
  const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3697
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3792
+ const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3793
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3794
+ onTranslationChange?.({
3795
+ slot,
3796
+ ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
3797
+ nextText: text,
3798
+ mode: "automatic",
3799
+ metadata
3800
+ });
3698
3801
  return { success: true };
3699
3802
  } catch (cause) {
3700
3803
  const workspaceError = {
@@ -3708,7 +3811,7 @@ function useTranslationWorkspace({
3708
3811
  setIsTranslating(false);
3709
3812
  }
3710
3813
  },
3711
- [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3814
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3712
3815
  );
3713
3816
  return {
3714
3817
  sourceLocale,
@@ -3725,6 +3828,7 @@ function useTranslationWorkspace({
3725
3828
  addLocale,
3726
3829
  isAddLocaleAllowed,
3727
3830
  removeLocale,
3831
+ ...removeLocaleConfirmation === null ? {} : { removeLocaleConfirmation },
3728
3832
  setTranslation,
3729
3833
  translateAll,
3730
3834
  translateSlot,
@@ -3862,7 +3966,7 @@ import {
3862
3966
  useEffect as useEffect3,
3863
3967
  useId,
3864
3968
  useMemo as useMemo6,
3865
- useRef as useRef2,
3969
+ useRef as useRef3,
3866
3970
  useState as useState6
3867
3971
  } from "react";
3868
3972
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -4378,8 +4482,8 @@ function ContextFormRenderer({
4378
4482
  const i18n = useFormEngineI18n();
4379
4483
  const isProviderValue = useContext4(FormEngineI18nProviderScopeContext);
4380
4484
  const prefix = useId().replace(/:/g, "");
4381
- const formRef = useRef2(null);
4382
- const loadedDraftKey = useRef2(null);
4485
+ const formRef = useRef3(null);
4486
+ const loadedDraftKey = useRef3(null);
4383
4487
  const [draftRestored, setDraftRestored] = useState6(false);
4384
4488
  const [currentPageIndex, setCurrentPageIndex] = useState6(0);
4385
4489
  const [focusFieldId, setFocusFieldId] = useState6(null);
@@ -4389,10 +4493,10 @@ function ContextFormRenderer({
4389
4493
  const [receipt, setReceipt] = useState6(null);
4390
4494
  const [completionData, setCompletionData] = useState6(null);
4391
4495
  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);
4496
+ const rendererSubmissionInFlight = useRef3(false);
4497
+ const fallbackAttemptId = useRef3(null);
4498
+ const completionRef = useRef3(null);
4499
+ const confirmationRef = useRef3(null);
4396
4500
  const pages = form.schema.pages;
4397
4501
  const visiblePageIndexes = useMemo6(
4398
4502
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
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.0.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/core": "6.0.0",
46
+ "@form-engine-ts/privacy": "6.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",