@form-engine-ts/react 5.0.1 → 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
@@ -3325,7 +3325,7 @@ function asAsyncAdapter(adapter) {
3325
3325
  )
3326
3326
  };
3327
3327
  }
3328
- var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3328
+ var validateLocalePipeline = (locale, schema, policy, customValidator, availableLocales) => {
3329
3329
  const canonicalLocale = (0, import_core5.normalizeLocale)(locale);
3330
3330
  if (canonicalLocale === null) {
3331
3331
  return {
@@ -3338,6 +3338,18 @@ var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3338
3338
  }
3339
3339
  const currentLocales = (schema.supportedLocales ?? []).map((candidate) => (0, import_core5.normalizeLocale)(candidate) ?? candidate);
3340
3340
  const defaultLocale = schema.defaultLocale === void 0 ? "" : (0, import_core5.normalizeLocale)(schema.defaultLocale) ?? schema.defaultLocale;
3341
+ if (availableLocales !== void 0 && !availableLocales.some((candidate) => {
3342
+ const value = typeof candidate === "string" ? candidate : candidate.locale;
3343
+ return (0, import_core5.normalizeLocale)(value) === canonicalLocale;
3344
+ })) {
3345
+ return {
3346
+ valid: false,
3347
+ error: {
3348
+ type: "locale_not_allowed",
3349
+ message: `Locale "${canonicalLocale}" is not available in the locale catalog.`
3350
+ }
3351
+ };
3352
+ }
3341
3353
  if (canonicalLocale === defaultLocale || currentLocales.includes(canonicalLocale)) {
3342
3354
  return {
3343
3355
  valid: false,
@@ -3409,26 +3421,27 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3409
3421
  }
3410
3422
  return { type: "custom_validation_failed", message: error.message };
3411
3423
  }
3412
- function manualMetadata(sourceText, sourceLocale) {
3424
+ function translationMetadata(sourceText, sourceLocale, mode) {
3413
3425
  return {
3414
3426
  sourceLocale,
3415
3427
  sourceTextHash: (0, import_core5.computeSourceTextHash)(sourceText),
3416
- translationSource: "manual",
3417
- editedAt: (/* @__PURE__ */ new Date()).toISOString()
3428
+ translationSource: mode,
3429
+ ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3418
3430
  };
3419
3431
  }
3420
- function setNodeMetadata(node, slot, text, sourceLocale) {
3432
+ function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3421
3433
  const localeMetadata = node.translationMetadata?.[slot.locale];
3422
- 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) };
3423
3435
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3424
3436
  }
3425
- function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3437
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3426
3438
  if (slot.kind === "form") {
3427
3439
  return setNodeMetadata(
3428
3440
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3429
3441
  slot,
3430
3442
  text,
3431
- sourceLocale
3443
+ sourceLocale,
3444
+ mode
3432
3445
  );
3433
3446
  }
3434
3447
  if (slot.kind === "field") {
@@ -3437,7 +3450,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3437
3450
  fields: schema.fields.map((field) => {
3438
3451
  if (field.id !== slot.nodeId) return field;
3439
3452
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3440
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3453
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3441
3454
  })
3442
3455
  };
3443
3456
  }
@@ -3453,7 +3466,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3453
3466
  const translations = text.trim().length === 0 ? Object.fromEntries(
3454
3467
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3455
3468
  ) : { ...option.translations, [slot.locale]: text };
3456
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3469
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3457
3470
  })
3458
3471
  };
3459
3472
  })
@@ -3465,7 +3478,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3465
3478
  pages: schema.pages.map((page) => {
3466
3479
  if (page.id !== slot.nodeId) return page;
3467
3480
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3468
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3481
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3469
3482
  })
3470
3483
  }
3471
3484
  };
@@ -3478,14 +3491,27 @@ function useTranslationWorkspace({
3478
3491
  translationAdapter,
3479
3492
  readOnly = false,
3480
3493
  policy,
3494
+ availableLocales,
3495
+ onLocaleAdded,
3496
+ onLocaleRemoved,
3497
+ onLocaleChange,
3481
3498
  beforeRemoveLocale,
3499
+ confirmRemoveLocale,
3500
+ slots: workspaceSlots,
3501
+ onTranslationStart,
3502
+ onTranslationSuccess,
3503
+ onTranslationError,
3504
+ onTranslationChange,
3482
3505
  validateLocale
3483
3506
  }) {
3484
3507
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3485
- const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)(targetLocale ?? "");
3508
+ const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
3486
3509
  const [isTranslating, setIsTranslating] = (0, import_react5.useState)(false);
3487
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);
3488
3513
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3514
+ const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3489
3515
  const targetLocales = (0, import_react5.useMemo)(() => {
3490
3516
  const locales = (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales;
3491
3517
  return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
@@ -3493,6 +3519,15 @@ function useTranslationWorkspace({
3493
3519
  );
3494
3520
  }, [currentSchema, sourceLocale]);
3495
3521
  const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3522
+ const localeOptions = (0, import_react5.useMemo)(() => {
3523
+ if (availableLocales === void 0) {
3524
+ return targetLocales.map((locale) => ({ locale, label: locale }));
3525
+ }
3526
+ return availableLocales.map((candidate) => {
3527
+ if (typeof candidate !== "string") return candidate;
3528
+ return { locale: (0, import_core5.normalizeLocale)(candidate) ?? candidate, label: candidate };
3529
+ });
3530
+ }, [availableLocales, targetLocales]);
3496
3531
  const slots = (0, import_react5.useMemo)(
3497
3532
  () => activeLocale.length === 0 ? [] : (0, import_core5.collectTranslationSlots)(currentSchema, activeLocale),
3498
3533
  [activeLocale, currentSchema]
@@ -3526,9 +3561,17 @@ function useTranslationWorkspace({
3526
3561
  const setTranslation = (0, import_react5.useCallback)(
3527
3562
  (slot, text) => {
3528
3563
  if (readOnly) return;
3529
- 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
+ });
3530
3573
  },
3531
- [commit, currentSchema, readOnly, sourceLocale]
3574
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3532
3575
  );
3533
3576
  const addLocale = (0, import_react5.useCallback)(
3534
3577
  (locale) => {
@@ -3540,14 +3583,14 @@ function useTranslationWorkspace({
3540
3583
  const normalized = (0, import_core5.normalizeLocale)(locale);
3541
3584
  if (normalized === null) {
3542
3585
  const workspaceError = workspaceLocaleValidationError(
3543
- validateLocalePipeline(locale, currentSchema, policy, validateLocale),
3586
+ validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales),
3544
3587
  (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales.size,
3545
3588
  locale
3546
3589
  );
3547
3590
  setError(workspaceError);
3548
3591
  return { success: false, error: workspaceError };
3549
3592
  }
3550
- const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3593
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales);
3551
3594
  if (!validation.valid) {
3552
3595
  const workspaceError = workspaceLocaleValidationError(
3553
3596
  validation,
@@ -3563,42 +3606,62 @@ function useTranslationWorkspace({
3563
3606
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3564
3607
  });
3565
3608
  setSelectedLocale(normalized);
3609
+ onLocaleAdded?.(normalized);
3566
3610
  return { success: true };
3567
3611
  },
3568
- [commit, currentSchema, policy, readOnly, validateLocale]
3612
+ [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, validateLocale]
3569
3613
  );
3570
3614
  const isAddLocaleAllowed = (0, import_react5.useCallback)(
3571
3615
  (locale) => {
3572
3616
  if (readOnly) return false;
3573
3617
  const normalized = (0, import_core5.normalizeLocale)(locale);
3574
3618
  if (normalized === null) return false;
3575
- return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3619
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales).valid;
3576
3620
  },
3577
- [currentSchema, policy, readOnly, validateLocale]
3621
+ [availableLocales, currentSchema, policy, readOnly, validateLocale]
3578
3622
  );
3579
3623
  const removeLocale = (0, import_react5.useCallback)(
3580
3624
  (locale) => {
3581
- if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return false;
3625
+ const normalized = (0, import_core5.normalizeLocale)(locale) ?? locale;
3626
+ const normalizedSourceLocale = (0, import_core5.normalizeLocale)(sourceLocale) ?? sourceLocale;
3627
+ const normalizedDefaultLocale = currentSchema.defaultLocale === void 0 ? void 0 : (0, import_core5.normalizeLocale)(currentSchema.defaultLocale) ?? currentSchema.defaultLocale;
3628
+ if (readOnly || normalized === normalizedSourceLocale || normalized === normalizedDefaultLocale) return false;
3582
3629
  const remove = () => {
3583
- commit((0, import_core5.removeLocaleFromSchema)(currentSchema, locale));
3584
- if (activeLocale === locale) setSelectedLocale("");
3630
+ commit((0, import_core5.removeLocaleFromSchema)(currentSchema, normalized));
3631
+ if (activeLocale === normalized) setSelectedLocale("");
3632
+ onLocaleRemoved?.(normalized);
3633
+ };
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();
3585
3652
  };
3586
- const slotCount = (0, import_core5.collectTranslationSlots)(currentSchema, locale).length;
3587
3653
  if (beforeRemoveLocale === void 0) {
3588
- remove();
3589
- return true;
3654
+ return removeAfterApproval();
3590
3655
  }
3591
3656
  try {
3592
- const decision = beforeRemoveLocale(locale, { slotCount });
3657
+ const decision = beforeRemoveLocale(normalized, { slotCount });
3593
3658
  if (typeof decision === "boolean") {
3594
3659
  if (!decision) return false;
3595
- remove();
3596
- return true;
3660
+ return removeAfterApproval();
3597
3661
  }
3598
3662
  return decision.then((allowed) => {
3599
3663
  if (!allowed) return false;
3600
- remove();
3601
- return true;
3664
+ return removeAfterApproval();
3602
3665
  });
3603
3666
  } catch (cause) {
3604
3667
  const workspaceError = {
@@ -3610,8 +3673,45 @@ function useTranslationWorkspace({
3610
3673
  return Promise.reject(cause);
3611
3674
  }
3612
3675
  },
3613
- [activeLocale, beforeRemoveLocale, commit, currentSchema, readOnly, sourceLocale]
3676
+ [
3677
+ activeLocale,
3678
+ beforeRemoveLocale,
3679
+ commit,
3680
+ confirmRemoveLocaleRenderer,
3681
+ currentSchema,
3682
+ localeOptions,
3683
+ onLocaleRemoved,
3684
+ readOnly,
3685
+ sourceLocale
3686
+ ]
3614
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]);
3615
3715
  const translateAll = (0, import_react5.useCallback)(
3616
3716
  async (options = {}) => {
3617
3717
  if (readOnly) {
@@ -3629,8 +3729,26 @@ function useTranslationWorkspace({
3629
3729
  setError(workspaceError);
3630
3730
  return { success: false, error: workspaceError };
3631
3731
  }
3732
+ const selectedLocaleOption = localeOptions.find(
3733
+ (option) => ((0, import_core5.normalizeLocale)(option.locale) ?? option.locale) === activeLocale
3734
+ );
3735
+ if (selectedLocaleOption?.translatable === false) {
3736
+ setError(void 0);
3737
+ return {
3738
+ success: true,
3739
+ report: {
3740
+ updatedSlots: [],
3741
+ skippedSlots: slots,
3742
+ staleSlots: [],
3743
+ skippedReasons: Object.fromEntries(
3744
+ slots.map((slot) => [slot.path ?? `${slot.kind}.${slot.nodeId}.${slot.property}`, "unsupported"])
3745
+ )
3746
+ }
3747
+ };
3748
+ }
3632
3749
  setIsTranslating(true);
3633
3750
  setError(void 0);
3751
+ onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3634
3752
  try {
3635
3753
  const populated = await (0, import_core5.populateSchemaTranslations)(
3636
3754
  currentSchema,
@@ -3643,6 +3761,17 @@ function useTranslationWorkspace({
3643
3761
  }
3644
3762
  );
3645
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
+ });
3646
3775
  return { success: true, report: populated.report };
3647
3776
  } catch (cause) {
3648
3777
  const workspaceError = {
@@ -3651,12 +3780,25 @@ function useTranslationWorkspace({
3651
3780
  cause
3652
3781
  };
3653
3782
  setError(workspaceError);
3783
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3654
3784
  return { success: false, error: workspaceError };
3655
3785
  } finally {
3656
3786
  setIsTranslating(false);
3657
3787
  }
3658
3788
  },
3659
- [activeLocale, commit, currentSchema, readOnly, 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
+ ]
3660
3802
  );
3661
3803
  const translateSlot = (0, import_react5.useCallback)(
3662
3804
  async (slot) => {
@@ -3674,7 +3816,15 @@ function useTranslationWorkspace({
3674
3816
  setError(void 0);
3675
3817
  try {
3676
3818
  const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3677
- 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
+ });
3678
3828
  return { success: true };
3679
3829
  } catch (cause) {
3680
3830
  const workspaceError = {
@@ -3688,18 +3838,24 @@ function useTranslationWorkspace({
3688
3838
  setIsTranslating(false);
3689
3839
  }
3690
3840
  },
3691
- [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3841
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3692
3842
  );
3693
3843
  return {
3694
3844
  sourceLocale,
3695
3845
  targetLocale: activeLocale,
3696
3846
  targetLocales,
3697
- setTargetLocale: setSelectedLocale,
3847
+ localeOptions,
3848
+ setTargetLocale: (locale) => {
3849
+ const normalized = (0, import_core5.normalizeLocale)(locale) ?? locale;
3850
+ setSelectedLocale(normalized);
3851
+ onLocaleChange?.(normalized);
3852
+ },
3698
3853
  slots,
3699
3854
  summary,
3700
3855
  addLocale,
3701
3856
  isAddLocaleAllowed,
3702
3857
  removeLocale,
3858
+ ...removeLocaleConfirmation === null ? {} : { removeLocaleConfirmation },
3703
3859
  setTranslation,
3704
3860
  translateAll,
3705
3861
  translateSlot,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, TranslationSlot, FormEngineTranslator, FormEngineMessages, FieldType } from '@form-engine-ts/core';
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';
2
3
  export { QuestionType } from '@form-engine-ts/core';
3
4
  import * as react from 'react';
4
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
@@ -379,6 +380,76 @@ interface BuilderLocalizationSlotProps extends BuilderSlotBaseProps {
379
380
  readonly policy?: FormPolicy;
380
381
  readonly translationAdapterAvailable?: boolean;
381
382
  }
383
+ interface TranslationWorkspaceHeaderProps {
384
+ readonly schema: FormSchema;
385
+ readonly sourceLocale: string;
386
+ readonly targetLocale: string;
387
+ readonly summary: {
388
+ readonly totalSlots: number;
389
+ readonly translatedCount: number;
390
+ readonly missingCount: number;
391
+ readonly staleCount: number;
392
+ readonly manualCount: number;
393
+ readonly completionPercentage: number;
394
+ };
395
+ readonly onTranslateAll: () => void;
396
+ readonly isTranslating: boolean;
397
+ readonly readOnly: boolean;
398
+ }
399
+ interface TranslationSlotRowProps {
400
+ readonly slot: _form_engine_ts_core.TranslationSlot;
401
+ readonly readOnly: boolean;
402
+ readonly onChange: (text: string) => void;
403
+ readonly onTranslate: () => void;
404
+ }
405
+ interface LocaleSelectorProps {
406
+ readonly targetLocale: string;
407
+ readonly targetLocales: readonly string[];
408
+ readonly localeOptions: readonly LocaleOption[];
409
+ readonly newLocale: string;
410
+ readonly readOnly: boolean;
411
+ readonly onTargetLocaleChange: (locale: string) => void;
412
+ readonly onNewLocaleChange: (locale: string) => void;
413
+ readonly onAddLocale: () => void;
414
+ }
415
+ interface TranslationWorkspaceActionsProps {
416
+ readonly onTranslateAll: () => void;
417
+ readonly isTranslating: boolean;
418
+ readonly readOnly: boolean;
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
+ }
443
+ interface TranslationWorkspaceSlots {
444
+ readonly renderHeader?: (props: TranslationWorkspaceHeaderProps) => ReactNode;
445
+ readonly renderSlotRow?: (props: TranslationSlotRowProps) => ReactNode;
446
+ readonly renderLocaleSelector?: (props: LocaleSelectorProps) => ReactNode;
447
+ readonly renderStatusBadge?: (props: {
448
+ readonly status: _form_engine_ts_core.TranslationStatus;
449
+ }) => ReactNode;
450
+ readonly renderActions?: (props: TranslationWorkspaceActionsProps) => ReactNode;
451
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
452
+ }
382
453
  interface LocalizationSummaryContext {
383
454
  readonly defaultLocale: string;
384
455
  readonly supportedLocales: readonly string[];
@@ -708,9 +779,25 @@ interface UseTranslationWorkspaceOptions {
708
779
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
709
780
  readonly readOnly?: boolean;
710
781
  readonly policy?: FormPolicy;
782
+ readonly availableLocales?: readonly (string | LocaleOption)[];
783
+ readonly onLocaleAdded?: (locale: string) => void;
784
+ readonly onLocaleRemoved?: (locale: string) => void;
785
+ readonly onLocaleChange?: (targetLocale: string) => void;
711
786
  readonly beforeRemoveLocale?: (locale: string, context: {
712
787
  readonly slotCount: number;
713
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;
714
801
  readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
715
802
  }
716
803
  interface LocaleValidationContext {
@@ -766,6 +853,7 @@ interface UseTranslationWorkspaceResult {
766
853
  readonly sourceLocale: string;
767
854
  readonly targetLocale: string;
768
855
  readonly targetLocales: readonly string[];
856
+ readonly localeOptions: readonly LocaleOption[];
769
857
  readonly setTargetLocale: (locale: string) => void;
770
858
  readonly slots: readonly TranslationSlot[];
771
859
  readonly summary: TranslationSummary;
@@ -775,6 +863,7 @@ interface UseTranslationWorkspaceResult {
775
863
  };
776
864
  readonly isAddLocaleAllowed: (locale: string) => boolean;
777
865
  readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
866
+ readonly removeLocaleConfirmation?: ReactNode;
778
867
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
779
868
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
780
869
  readonly success: boolean;
@@ -788,8 +877,8 @@ interface UseTranslationWorkspaceResult {
788
877
  readonly isTranslating: boolean;
789
878
  readonly error?: TranslationWorkspaceError;
790
879
  }
791
- declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator) => LocaleValidationResult;
792
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, beforeRemoveLocale, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
880
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
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;
793
882
 
794
883
  declare const BUILDER_TRANSLATION_KEYS: {
795
884
  readonly ADD_FIELD: "builder.actions.addField";
@@ -879,4 +968,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
879
968
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
880
969
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
881
970
 
882
- 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 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 TranslationSummary, type TranslationWorkspaceError, 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,4 +1,5 @@
1
- import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, TranslationSlot, FormEngineTranslator, FormEngineMessages, FieldType } from '@form-engine-ts/core';
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';
2
3
  export { QuestionType } from '@form-engine-ts/core';
3
4
  import * as react from 'react';
4
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
@@ -379,6 +380,76 @@ interface BuilderLocalizationSlotProps extends BuilderSlotBaseProps {
379
380
  readonly policy?: FormPolicy;
380
381
  readonly translationAdapterAvailable?: boolean;
381
382
  }
383
+ interface TranslationWorkspaceHeaderProps {
384
+ readonly schema: FormSchema;
385
+ readonly sourceLocale: string;
386
+ readonly targetLocale: string;
387
+ readonly summary: {
388
+ readonly totalSlots: number;
389
+ readonly translatedCount: number;
390
+ readonly missingCount: number;
391
+ readonly staleCount: number;
392
+ readonly manualCount: number;
393
+ readonly completionPercentage: number;
394
+ };
395
+ readonly onTranslateAll: () => void;
396
+ readonly isTranslating: boolean;
397
+ readonly readOnly: boolean;
398
+ }
399
+ interface TranslationSlotRowProps {
400
+ readonly slot: _form_engine_ts_core.TranslationSlot;
401
+ readonly readOnly: boolean;
402
+ readonly onChange: (text: string) => void;
403
+ readonly onTranslate: () => void;
404
+ }
405
+ interface LocaleSelectorProps {
406
+ readonly targetLocale: string;
407
+ readonly targetLocales: readonly string[];
408
+ readonly localeOptions: readonly LocaleOption[];
409
+ readonly newLocale: string;
410
+ readonly readOnly: boolean;
411
+ readonly onTargetLocaleChange: (locale: string) => void;
412
+ readonly onNewLocaleChange: (locale: string) => void;
413
+ readonly onAddLocale: () => void;
414
+ }
415
+ interface TranslationWorkspaceActionsProps {
416
+ readonly onTranslateAll: () => void;
417
+ readonly isTranslating: boolean;
418
+ readonly readOnly: boolean;
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
+ }
443
+ interface TranslationWorkspaceSlots {
444
+ readonly renderHeader?: (props: TranslationWorkspaceHeaderProps) => ReactNode;
445
+ readonly renderSlotRow?: (props: TranslationSlotRowProps) => ReactNode;
446
+ readonly renderLocaleSelector?: (props: LocaleSelectorProps) => ReactNode;
447
+ readonly renderStatusBadge?: (props: {
448
+ readonly status: _form_engine_ts_core.TranslationStatus;
449
+ }) => ReactNode;
450
+ readonly renderActions?: (props: TranslationWorkspaceActionsProps) => ReactNode;
451
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
452
+ }
382
453
  interface LocalizationSummaryContext {
383
454
  readonly defaultLocale: string;
384
455
  readonly supportedLocales: readonly string[];
@@ -708,9 +779,25 @@ interface UseTranslationWorkspaceOptions {
708
779
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
709
780
  readonly readOnly?: boolean;
710
781
  readonly policy?: FormPolicy;
782
+ readonly availableLocales?: readonly (string | LocaleOption)[];
783
+ readonly onLocaleAdded?: (locale: string) => void;
784
+ readonly onLocaleRemoved?: (locale: string) => void;
785
+ readonly onLocaleChange?: (targetLocale: string) => void;
711
786
  readonly beforeRemoveLocale?: (locale: string, context: {
712
787
  readonly slotCount: number;
713
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;
714
801
  readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
715
802
  }
716
803
  interface LocaleValidationContext {
@@ -766,6 +853,7 @@ interface UseTranslationWorkspaceResult {
766
853
  readonly sourceLocale: string;
767
854
  readonly targetLocale: string;
768
855
  readonly targetLocales: readonly string[];
856
+ readonly localeOptions: readonly LocaleOption[];
769
857
  readonly setTargetLocale: (locale: string) => void;
770
858
  readonly slots: readonly TranslationSlot[];
771
859
  readonly summary: TranslationSummary;
@@ -775,6 +863,7 @@ interface UseTranslationWorkspaceResult {
775
863
  };
776
864
  readonly isAddLocaleAllowed: (locale: string) => boolean;
777
865
  readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
866
+ readonly removeLocaleConfirmation?: ReactNode;
778
867
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
779
868
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
780
869
  readonly success: boolean;
@@ -788,8 +877,8 @@ interface UseTranslationWorkspaceResult {
788
877
  readonly isTranslating: boolean;
789
878
  readonly error?: TranslationWorkspaceError;
790
879
  }
791
- declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator) => LocaleValidationResult;
792
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, beforeRemoveLocale, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
880
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
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;
793
882
 
794
883
  declare const BUILDER_TRANSLATION_KEYS: {
795
884
  readonly ADD_FIELD: "builder.actions.addField";
@@ -879,4 +968,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
879
968
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
880
969
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
881
970
 
882
- 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 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 TranslationSummary, type TranslationWorkspaceError, 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];
@@ -3298,7 +3298,7 @@ function asAsyncAdapter(adapter) {
3298
3298
  )
3299
3299
  };
3300
3300
  }
3301
- var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3301
+ var validateLocalePipeline = (locale, schema, policy, customValidator, availableLocales) => {
3302
3302
  const canonicalLocale = normalizeLocale(locale);
3303
3303
  if (canonicalLocale === null) {
3304
3304
  return {
@@ -3311,6 +3311,18 @@ var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3311
3311
  }
3312
3312
  const currentLocales = (schema.supportedLocales ?? []).map((candidate) => normalizeLocale(candidate) ?? candidate);
3313
3313
  const defaultLocale = schema.defaultLocale === void 0 ? "" : normalizeLocale(schema.defaultLocale) ?? schema.defaultLocale;
3314
+ if (availableLocales !== void 0 && !availableLocales.some((candidate) => {
3315
+ const value = typeof candidate === "string" ? candidate : candidate.locale;
3316
+ return normalizeLocale(value) === canonicalLocale;
3317
+ })) {
3318
+ return {
3319
+ valid: false,
3320
+ error: {
3321
+ type: "locale_not_allowed",
3322
+ message: `Locale "${canonicalLocale}" is not available in the locale catalog.`
3323
+ }
3324
+ };
3325
+ }
3314
3326
  if (canonicalLocale === defaultLocale || currentLocales.includes(canonicalLocale)) {
3315
3327
  return {
3316
3328
  valid: false,
@@ -3382,26 +3394,27 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3382
3394
  }
3383
3395
  return { type: "custom_validation_failed", message: error.message };
3384
3396
  }
3385
- function manualMetadata(sourceText, sourceLocale) {
3397
+ function translationMetadata(sourceText, sourceLocale, mode) {
3386
3398
  return {
3387
3399
  sourceLocale,
3388
3400
  sourceTextHash: computeSourceTextHash(sourceText),
3389
- translationSource: "manual",
3390
- editedAt: (/* @__PURE__ */ new Date()).toISOString()
3401
+ translationSource: mode,
3402
+ ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3391
3403
  };
3392
3404
  }
3393
- function setNodeMetadata(node, slot, text, sourceLocale) {
3405
+ function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3394
3406
  const localeMetadata = node.translationMetadata?.[slot.locale];
3395
- 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) };
3396
3408
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3397
3409
  }
3398
- function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3410
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3399
3411
  if (slot.kind === "form") {
3400
3412
  return setNodeMetadata(
3401
3413
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3402
3414
  slot,
3403
3415
  text,
3404
- sourceLocale
3416
+ sourceLocale,
3417
+ mode
3405
3418
  );
3406
3419
  }
3407
3420
  if (slot.kind === "field") {
@@ -3410,7 +3423,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3410
3423
  fields: schema.fields.map((field) => {
3411
3424
  if (field.id !== slot.nodeId) return field;
3412
3425
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3413
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3426
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3414
3427
  })
3415
3428
  };
3416
3429
  }
@@ -3426,7 +3439,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3426
3439
  const translations = text.trim().length === 0 ? Object.fromEntries(
3427
3440
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3428
3441
  ) : { ...option.translations, [slot.locale]: text };
3429
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3442
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3430
3443
  })
3431
3444
  };
3432
3445
  })
@@ -3438,7 +3451,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3438
3451
  pages: schema.pages.map((page) => {
3439
3452
  if (page.id !== slot.nodeId) return page;
3440
3453
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3441
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3454
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3442
3455
  })
3443
3456
  }
3444
3457
  };
@@ -3451,14 +3464,27 @@ function useTranslationWorkspace({
3451
3464
  translationAdapter,
3452
3465
  readOnly = false,
3453
3466
  policy,
3467
+ availableLocales,
3468
+ onLocaleAdded,
3469
+ onLocaleRemoved,
3470
+ onLocaleChange,
3454
3471
  beforeRemoveLocale,
3472
+ confirmRemoveLocale,
3473
+ slots: workspaceSlots,
3474
+ onTranslationStart,
3475
+ onTranslationSuccess,
3476
+ onTranslationError,
3477
+ onTranslationChange,
3455
3478
  validateLocale
3456
3479
  }) {
3457
3480
  const [draftSchema, setDraftSchema] = useState4(schema);
3458
- const [selectedLocale, setSelectedLocale] = useState4(targetLocale ?? "");
3481
+ const [selectedLocale, setSelectedLocale] = useState4(normalizeLocale(targetLocale ?? "") ?? targetLocale ?? "");
3459
3482
  const [isTranslating, setIsTranslating] = useState4(false);
3460
3483
  const [error, setError] = useState4();
3484
+ const [pendingRemoval, setPendingRemoval] = useState4();
3485
+ const pendingRemovalResolver = useRef2(void 0);
3461
3486
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3487
+ const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3462
3488
  const targetLocales = useMemo4(() => {
3463
3489
  const locales = collectSchemaLocales2(currentSchema).allUniqueLocales;
3464
3490
  return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
@@ -3466,6 +3492,15 @@ function useTranslationWorkspace({
3466
3492
  );
3467
3493
  }, [currentSchema, sourceLocale]);
3468
3494
  const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3495
+ const localeOptions = useMemo4(() => {
3496
+ if (availableLocales === void 0) {
3497
+ return targetLocales.map((locale) => ({ locale, label: locale }));
3498
+ }
3499
+ return availableLocales.map((candidate) => {
3500
+ if (typeof candidate !== "string") return candidate;
3501
+ return { locale: normalizeLocale(candidate) ?? candidate, label: candidate };
3502
+ });
3503
+ }, [availableLocales, targetLocales]);
3469
3504
  const slots = useMemo4(
3470
3505
  () => activeLocale.length === 0 ? [] : collectTranslationSlots(currentSchema, activeLocale),
3471
3506
  [activeLocale, currentSchema]
@@ -3499,9 +3534,17 @@ function useTranslationWorkspace({
3499
3534
  const setTranslation = useCallback3(
3500
3535
  (slot, text) => {
3501
3536
  if (readOnly) return;
3502
- 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
+ });
3503
3546
  },
3504
- [commit, currentSchema, readOnly, sourceLocale]
3547
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3505
3548
  );
3506
3549
  const addLocale = useCallback3(
3507
3550
  (locale) => {
@@ -3513,14 +3556,14 @@ function useTranslationWorkspace({
3513
3556
  const normalized = normalizeLocale(locale);
3514
3557
  if (normalized === null) {
3515
3558
  const workspaceError = workspaceLocaleValidationError(
3516
- validateLocalePipeline(locale, currentSchema, policy, validateLocale),
3559
+ validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales),
3517
3560
  collectSchemaLocales2(currentSchema).allUniqueLocales.size,
3518
3561
  locale
3519
3562
  );
3520
3563
  setError(workspaceError);
3521
3564
  return { success: false, error: workspaceError };
3522
3565
  }
3523
- const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3566
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales);
3524
3567
  if (!validation.valid) {
3525
3568
  const workspaceError = workspaceLocaleValidationError(
3526
3569
  validation,
@@ -3536,42 +3579,62 @@ function useTranslationWorkspace({
3536
3579
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3537
3580
  });
3538
3581
  setSelectedLocale(normalized);
3582
+ onLocaleAdded?.(normalized);
3539
3583
  return { success: true };
3540
3584
  },
3541
- [commit, currentSchema, policy, readOnly, validateLocale]
3585
+ [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, validateLocale]
3542
3586
  );
3543
3587
  const isAddLocaleAllowed = useCallback3(
3544
3588
  (locale) => {
3545
3589
  if (readOnly) return false;
3546
3590
  const normalized = normalizeLocale(locale);
3547
3591
  if (normalized === null) return false;
3548
- return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3592
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales).valid;
3549
3593
  },
3550
- [currentSchema, policy, readOnly, validateLocale]
3594
+ [availableLocales, currentSchema, policy, readOnly, validateLocale]
3551
3595
  );
3552
3596
  const removeLocale = useCallback3(
3553
3597
  (locale) => {
3554
- if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return false;
3598
+ const normalized = normalizeLocale(locale) ?? locale;
3599
+ const normalizedSourceLocale = normalizeLocale(sourceLocale) ?? sourceLocale;
3600
+ const normalizedDefaultLocale = currentSchema.defaultLocale === void 0 ? void 0 : normalizeLocale(currentSchema.defaultLocale) ?? currentSchema.defaultLocale;
3601
+ if (readOnly || normalized === normalizedSourceLocale || normalized === normalizedDefaultLocale) return false;
3555
3602
  const remove = () => {
3556
- commit(removeLocaleFromSchema(currentSchema, locale));
3557
- if (activeLocale === locale) setSelectedLocale("");
3603
+ commit(removeLocaleFromSchema(currentSchema, normalized));
3604
+ if (activeLocale === normalized) setSelectedLocale("");
3605
+ onLocaleRemoved?.(normalized);
3606
+ };
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();
3558
3625
  };
3559
- const slotCount = collectTranslationSlots(currentSchema, locale).length;
3560
3626
  if (beforeRemoveLocale === void 0) {
3561
- remove();
3562
- return true;
3627
+ return removeAfterApproval();
3563
3628
  }
3564
3629
  try {
3565
- const decision = beforeRemoveLocale(locale, { slotCount });
3630
+ const decision = beforeRemoveLocale(normalized, { slotCount });
3566
3631
  if (typeof decision === "boolean") {
3567
3632
  if (!decision) return false;
3568
- remove();
3569
- return true;
3633
+ return removeAfterApproval();
3570
3634
  }
3571
3635
  return decision.then((allowed) => {
3572
3636
  if (!allowed) return false;
3573
- remove();
3574
- return true;
3637
+ return removeAfterApproval();
3575
3638
  });
3576
3639
  } catch (cause) {
3577
3640
  const workspaceError = {
@@ -3583,8 +3646,45 @@ function useTranslationWorkspace({
3583
3646
  return Promise.reject(cause);
3584
3647
  }
3585
3648
  },
3586
- [activeLocale, beforeRemoveLocale, commit, currentSchema, readOnly, sourceLocale]
3649
+ [
3650
+ activeLocale,
3651
+ beforeRemoveLocale,
3652
+ commit,
3653
+ confirmRemoveLocaleRenderer,
3654
+ currentSchema,
3655
+ localeOptions,
3656
+ onLocaleRemoved,
3657
+ readOnly,
3658
+ sourceLocale
3659
+ ]
3587
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]);
3588
3688
  const translateAll = useCallback3(
3589
3689
  async (options = {}) => {
3590
3690
  if (readOnly) {
@@ -3602,8 +3702,26 @@ function useTranslationWorkspace({
3602
3702
  setError(workspaceError);
3603
3703
  return { success: false, error: workspaceError };
3604
3704
  }
3705
+ const selectedLocaleOption = localeOptions.find(
3706
+ (option) => (normalizeLocale(option.locale) ?? option.locale) === activeLocale
3707
+ );
3708
+ if (selectedLocaleOption?.translatable === false) {
3709
+ setError(void 0);
3710
+ return {
3711
+ success: true,
3712
+ report: {
3713
+ updatedSlots: [],
3714
+ skippedSlots: slots,
3715
+ staleSlots: [],
3716
+ skippedReasons: Object.fromEntries(
3717
+ slots.map((slot) => [slot.path ?? `${slot.kind}.${slot.nodeId}.${slot.property}`, "unsupported"])
3718
+ )
3719
+ }
3720
+ };
3721
+ }
3605
3722
  setIsTranslating(true);
3606
3723
  setError(void 0);
3724
+ onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3607
3725
  try {
3608
3726
  const populated = await populateSchemaTranslations2(
3609
3727
  currentSchema,
@@ -3616,6 +3734,17 @@ function useTranslationWorkspace({
3616
3734
  }
3617
3735
  );
3618
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
+ });
3619
3748
  return { success: true, report: populated.report };
3620
3749
  } catch (cause) {
3621
3750
  const workspaceError = {
@@ -3624,12 +3753,25 @@ function useTranslationWorkspace({
3624
3753
  cause
3625
3754
  };
3626
3755
  setError(workspaceError);
3756
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3627
3757
  return { success: false, error: workspaceError };
3628
3758
  } finally {
3629
3759
  setIsTranslating(false);
3630
3760
  }
3631
3761
  },
3632
- [activeLocale, commit, currentSchema, readOnly, 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
+ ]
3633
3775
  );
3634
3776
  const translateSlot = useCallback3(
3635
3777
  async (slot) => {
@@ -3647,7 +3789,15 @@ function useTranslationWorkspace({
3647
3789
  setError(void 0);
3648
3790
  try {
3649
3791
  const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3650
- 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
+ });
3651
3801
  return { success: true };
3652
3802
  } catch (cause) {
3653
3803
  const workspaceError = {
@@ -3661,18 +3811,24 @@ function useTranslationWorkspace({
3661
3811
  setIsTranslating(false);
3662
3812
  }
3663
3813
  },
3664
- [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3814
+ [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3665
3815
  );
3666
3816
  return {
3667
3817
  sourceLocale,
3668
3818
  targetLocale: activeLocale,
3669
3819
  targetLocales,
3670
- setTargetLocale: setSelectedLocale,
3820
+ localeOptions,
3821
+ setTargetLocale: (locale) => {
3822
+ const normalized = normalizeLocale(locale) ?? locale;
3823
+ setSelectedLocale(normalized);
3824
+ onLocaleChange?.(normalized);
3825
+ },
3671
3826
  slots,
3672
3827
  summary,
3673
3828
  addLocale,
3674
3829
  isAddLocaleAllowed,
3675
3830
  removeLocale,
3831
+ ...removeLocaleConfirmation === null ? {} : { removeLocaleConfirmation },
3676
3832
  setTranslation,
3677
3833
  translateAll,
3678
3834
  translateSlot,
@@ -3810,7 +3966,7 @@ import {
3810
3966
  useEffect as useEffect3,
3811
3967
  useId,
3812
3968
  useMemo as useMemo6,
3813
- useRef as useRef2,
3969
+ useRef as useRef3,
3814
3970
  useState as useState6
3815
3971
  } from "react";
3816
3972
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -4326,8 +4482,8 @@ function ContextFormRenderer({
4326
4482
  const i18n = useFormEngineI18n();
4327
4483
  const isProviderValue = useContext4(FormEngineI18nProviderScopeContext);
4328
4484
  const prefix = useId().replace(/:/g, "");
4329
- const formRef = useRef2(null);
4330
- const loadedDraftKey = useRef2(null);
4485
+ const formRef = useRef3(null);
4486
+ const loadedDraftKey = useRef3(null);
4331
4487
  const [draftRestored, setDraftRestored] = useState6(false);
4332
4488
  const [currentPageIndex, setCurrentPageIndex] = useState6(0);
4333
4489
  const [focusFieldId, setFocusFieldId] = useState6(null);
@@ -4337,10 +4493,10 @@ function ContextFormRenderer({
4337
4493
  const [receipt, setReceipt] = useState6(null);
4338
4494
  const [completionData, setCompletionData] = useState6(null);
4339
4495
  const [receiptLoaded, setReceiptLoaded] = useState6(receiptStore === void 0);
4340
- const rendererSubmissionInFlight = useRef2(false);
4341
- const fallbackAttemptId = useRef2(null);
4342
- const completionRef = useRef2(null);
4343
- const confirmationRef = useRef2(null);
4496
+ const rendererSubmissionInFlight = useRef3(false);
4497
+ const fallbackAttemptId = useRef3(null);
4498
+ const completionRef = useRef3(null);
4499
+ const confirmationRef = useRef3(null);
4344
4500
  const pages = form.schema.pages;
4345
4501
  const visiblePageIndexes = useMemo6(
4346
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.0.1",
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.0.1",
46
- "@form-engine-ts/privacy": "5.0.1"
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",