@form-engine-ts/react 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3274,7 +3274,7 @@ function useField(fieldId) {
3274
3274
 
3275
3275
  // src/hooks/useTranslationComparison.ts
3276
3276
  import { computeSourceTextHash as computeSourceTextHash2 } from "@form-engine-ts/core";
3277
- import { useCallback as useCallback4, useMemo as useMemo5 } from "react";
3277
+ import { useCallback as useCallback4, useMemo as useMemo5, useState as useState5 } from "react";
3278
3278
 
3279
3279
  // src/hooks/useTranslationWorkspace.ts
3280
3280
  import {
@@ -3301,7 +3301,7 @@ function asAsyncAdapter(adapter) {
3301
3301
  )
3302
3302
  };
3303
3303
  }
3304
- var validateLocalePipeline = (locale, schema, policy, customValidator, availableLocales) => {
3304
+ var validateLocalePipeline = (locale, schema, policy, customValidator, availableLocales, sourceLocale) => {
3305
3305
  const canonicalLocale = normalizeLocale(locale);
3306
3306
  if (canonicalLocale === null) {
3307
3307
  return {
@@ -3313,20 +3313,17 @@ var validateLocalePipeline = (locale, schema, policy, customValidator, available
3313
3313
  };
3314
3314
  }
3315
3315
  const currentLocales = (schema.supportedLocales ?? []).map((candidate) => normalizeLocale(candidate) ?? candidate);
3316
- const defaultLocale = schema.defaultLocale === void 0 ? "" : normalizeLocale(schema.defaultLocale) ?? schema.defaultLocale;
3317
- if (availableLocales !== void 0 && !availableLocales.some((candidate) => {
3318
- const value = typeof candidate === "string" ? candidate : candidate.locale;
3319
- return normalizeLocale(value) === canonicalLocale;
3320
- })) {
3316
+ const defaultLocale = normalizeLocale(sourceLocale ?? schema.defaultLocale ?? "") ?? sourceLocale ?? schema.defaultLocale ?? "";
3317
+ if (canonicalLocale === defaultLocale) {
3321
3318
  return {
3322
3319
  valid: false,
3323
3320
  error: {
3324
- type: "locale_not_allowed",
3325
- message: `Locale "${canonicalLocale}" is not available in the locale catalog.`
3321
+ type: "source_locale",
3322
+ message: `Locale "${canonicalLocale}" is the source locale and cannot be added as a translation.`
3326
3323
  }
3327
3324
  };
3328
3325
  }
3329
- if (canonicalLocale === defaultLocale || currentLocales.includes(canonicalLocale)) {
3326
+ if (currentLocales.includes(canonicalLocale)) {
3330
3327
  return {
3331
3328
  valid: false,
3332
3329
  error: {
@@ -3335,6 +3332,18 @@ var validateLocalePipeline = (locale, schema, policy, customValidator, available
3335
3332
  }
3336
3333
  };
3337
3334
  }
3335
+ if (availableLocales !== void 0 && !availableLocales.some((candidate) => {
3336
+ const value = typeof candidate === "string" ? candidate : candidate.locale;
3337
+ return normalizeLocale(value) === canonicalLocale;
3338
+ })) {
3339
+ return {
3340
+ valid: false,
3341
+ error: {
3342
+ type: "locale_not_allowed",
3343
+ message: `Locale "${canonicalLocale}" is not available in the locale catalog.`
3344
+ }
3345
+ };
3346
+ }
3338
3347
  if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.some((candidate) => normalizeLocale(candidate) === canonicalLocale)) {
3339
3348
  return {
3340
3349
  valid: false,
@@ -3388,6 +3397,7 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3388
3397
  if (error.type === "locale_already_exists") {
3389
3398
  return { type: "locale_already_exists", locale: requestedLocale };
3390
3399
  }
3400
+ if (error.type === "source_locale") return { type: "source_locale", locale: requestedLocale };
3391
3401
  if (error.type === "invalid_locale_format") {
3392
3402
  return { type: "invalid_locale_format", locale: requestedLocale };
3393
3403
  }
@@ -3405,19 +3415,29 @@ function translationMetadata(sourceText, sourceLocale, mode) {
3405
3415
  ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3406
3416
  };
3407
3417
  }
3408
- function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3418
+ function createTranslationMetadata(slot, sourceLocale, translatedText, mode, factory) {
3419
+ return {
3420
+ ...factory?.({ slot, translatedText, mode }) ?? {},
3421
+ ...translationMetadata(slot.sourceText, sourceLocale, mode)
3422
+ };
3423
+ }
3424
+ function setNodeMetadata(node, slot, text, sourceLocale, mode, metadataOverride) {
3409
3425
  const localeMetadata = node.translationMetadata?.[slot.locale];
3410
- const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: translationMetadata(slot.sourceText, sourceLocale, mode) };
3426
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : {
3427
+ ...localeMetadata,
3428
+ [slot.property]: metadataOverride ?? translationMetadata(slot.sourceText, sourceLocale, mode)
3429
+ };
3411
3430
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3412
3431
  }
3413
- function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3432
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual", metadataOverride) {
3414
3433
  if (slot.kind === "form") {
3415
3434
  return setNodeMetadata(
3416
3435
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3417
3436
  slot,
3418
3437
  text,
3419
3438
  sourceLocale,
3420
- mode
3439
+ mode,
3440
+ metadataOverride
3421
3441
  );
3422
3442
  }
3423
3443
  if (slot.kind === "field") {
@@ -3426,7 +3446,14 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manua
3426
3446
  fields: schema.fields.map((field) => {
3427
3447
  if (field.id !== slot.nodeId) return field;
3428
3448
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3429
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3449
+ return setNodeMetadata(
3450
+ { ...field, translations },
3451
+ slot,
3452
+ text,
3453
+ sourceLocale,
3454
+ mode,
3455
+ metadataOverride
3456
+ );
3430
3457
  })
3431
3458
  };
3432
3459
  }
@@ -3442,7 +3469,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manua
3442
3469
  const translations = text.trim().length === 0 ? Object.fromEntries(
3443
3470
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3444
3471
  ) : { ...option.translations, [slot.locale]: text };
3445
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3472
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode, metadataOverride);
3446
3473
  })
3447
3474
  };
3448
3475
  })
@@ -3454,7 +3481,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manua
3454
3481
  pages: schema.pages.map((page) => {
3455
3482
  if (page.id !== slot.nodeId) return page;
3456
3483
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3457
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3484
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode, metadataOverride);
3458
3485
  })
3459
3486
  }
3460
3487
  };
@@ -3465,6 +3492,7 @@ function useTranslationWorkspace({
3465
3492
  sourceLocale = schema.defaultLocale ?? "en",
3466
3493
  targetLocale,
3467
3494
  translationAdapter,
3495
+ signal,
3468
3496
  readOnly = false,
3469
3497
  policy,
3470
3498
  availableLocales,
@@ -3476,34 +3504,49 @@ function useTranslationWorkspace({
3476
3504
  slots: workspaceSlots,
3477
3505
  onTranslationStart,
3478
3506
  onTranslationSuccess,
3507
+ onTranslationReport,
3479
3508
  onTranslationError,
3480
3509
  onTranslationChange,
3481
- validateLocale
3510
+ validateLocale,
3511
+ createTranslationMetadata: metadataFactory
3482
3512
  }) {
3483
3513
  const [draftSchema, setDraftSchema] = useState4(schema);
3484
3514
  const [selectedLocale, setSelectedLocale] = useState4(normalizeLocale(targetLocale ?? "") ?? targetLocale ?? "");
3485
3515
  const [isTranslating, setIsTranslating] = useState4(false);
3516
+ const [progress, setProgress] = useState4();
3486
3517
  const [error, setError] = useState4();
3487
3518
  const [pendingRemoval, setPendingRemoval] = useState4();
3519
+ const activeAbortController = useRef2(void 0);
3488
3520
  const pendingRemovalResolver = useRef2(void 0);
3489
3521
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3490
3522
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3491
3523
  const targetLocales = useMemo4(() => {
3492
3524
  const locales = collectSchemaLocales2(currentSchema).allUniqueLocales;
3493
- return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
3494
- (locale) => locale !== sourceLocale
3495
- );
3525
+ const normalizedSourceLocale = normalizeLocale(sourceLocale) ?? sourceLocale;
3526
+ return [
3527
+ ...new Set(
3528
+ [...currentSchema.supportedLocales ?? [], ...locales].map((locale) => normalizeLocale(locale) ?? locale)
3529
+ )
3530
+ ].filter((locale) => locale !== normalizedSourceLocale);
3496
3531
  }, [currentSchema, sourceLocale]);
3497
3532
  const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3498
3533
  const localeOptions = useMemo4(() => {
3499
3534
  if (availableLocales === void 0) {
3500
3535
  return targetLocales.map((locale) => ({ locale, label: locale }));
3501
3536
  }
3502
- return availableLocales.map((candidate) => {
3503
- if (typeof candidate !== "string") return candidate;
3504
- return { locale: normalizeLocale(candidate) ?? candidate, label: candidate };
3505
- });
3506
- }, [availableLocales, targetLocales]);
3537
+ const targetLocaleSet = new Set(targetLocales);
3538
+ const allowedLocales = policy?.allowedLocales?.map((locale) => normalizeLocale(locale) ?? locale);
3539
+ const options = /* @__PURE__ */ new Map();
3540
+ for (const candidate of availableLocales) {
3541
+ const option = typeof candidate === "string" ? { locale: normalizeLocale(candidate) ?? candidate, label: candidate } : { ...candidate, locale: normalizeLocale(candidate.locale) ?? candidate.locale };
3542
+ const allowed = allowedLocales === void 0 || allowedLocales.includes(option.locale);
3543
+ if (allowed || targetLocaleSet.has(option.locale)) options.set(option.locale, option);
3544
+ }
3545
+ for (const locale of targetLocales) {
3546
+ if (!options.has(locale)) options.set(locale, { locale, label: locale });
3547
+ }
3548
+ return [...options.values()];
3549
+ }, [availableLocales, policy?.allowedLocales, targetLocales]);
3507
3550
  const slots = useMemo4(
3508
3551
  () => activeLocale.length === 0 ? [] : collectTranslationSlots(currentSchema, activeLocale),
3509
3552
  [activeLocale, currentSchema]
@@ -3537,8 +3580,8 @@ function useTranslationWorkspace({
3537
3580
  const setTranslation = useCallback3(
3538
3581
  (slot, text) => {
3539
3582
  if (readOnly) return;
3540
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3541
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual"));
3583
+ const metadata = createTranslationMetadata(slot, sourceLocale, text, "manual", metadataFactory);
3584
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual", metadata));
3542
3585
  onTranslationChange?.({
3543
3586
  slot,
3544
3587
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3547,7 +3590,7 @@ function useTranslationWorkspace({
3547
3590
  metadata
3548
3591
  });
3549
3592
  },
3550
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3593
+ [commit, currentSchema, metadataFactory, onTranslationChange, readOnly, sourceLocale]
3551
3594
  );
3552
3595
  const addLocale = useCallback3(
3553
3596
  (locale) => {
@@ -3559,14 +3602,21 @@ function useTranslationWorkspace({
3559
3602
  const normalized = normalizeLocale(locale);
3560
3603
  if (normalized === null) {
3561
3604
  const workspaceError = workspaceLocaleValidationError(
3562
- validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales),
3605
+ validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales, sourceLocale),
3563
3606
  collectSchemaLocales2(currentSchema).allUniqueLocales.size,
3564
3607
  locale
3565
3608
  );
3566
3609
  setError(workspaceError);
3567
3610
  return { success: false, error: workspaceError };
3568
3611
  }
3569
- const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales);
3612
+ const validation = validateLocalePipeline(
3613
+ normalized,
3614
+ currentSchema,
3615
+ policy,
3616
+ validateLocale,
3617
+ availableLocales,
3618
+ sourceLocale
3619
+ );
3570
3620
  if (!validation.valid) {
3571
3621
  const workspaceError = workspaceLocaleValidationError(
3572
3622
  validation,
@@ -3585,16 +3635,16 @@ function useTranslationWorkspace({
3585
3635
  onLocaleAdded?.(normalized);
3586
3636
  return { success: true };
3587
3637
  },
3588
- [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, validateLocale]
3638
+ [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, sourceLocale, validateLocale]
3589
3639
  );
3590
3640
  const isAddLocaleAllowed = useCallback3(
3591
3641
  (locale) => {
3592
3642
  if (readOnly) return false;
3593
3643
  const normalized = normalizeLocale(locale);
3594
3644
  if (normalized === null) return false;
3595
- return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales).valid;
3645
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales, sourceLocale).valid;
3596
3646
  },
3597
- [availableLocales, currentSchema, policy, readOnly, validateLocale]
3647
+ [availableLocales, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3598
3648
  );
3599
3649
  const removeLocale = useCallback3(
3600
3650
  (locale) => {
@@ -3724,7 +3774,15 @@ function useTranslationWorkspace({
3724
3774
  }
3725
3775
  setIsTranslating(true);
3726
3776
  setError(void 0);
3777
+ setProgress(void 0);
3727
3778
  onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3779
+ const controller = new AbortController();
3780
+ activeAbortController.current = controller;
3781
+ const operationSignal = options.signal ?? signal;
3782
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3783
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3784
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3785
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3728
3786
  try {
3729
3787
  const populated = await populateSchemaTranslations2(
3730
3788
  currentSchema,
@@ -3733,13 +3791,37 @@ function useTranslationWorkspace({
3733
3791
  {
3734
3792
  overwrite: "stale-and-missing",
3735
3793
  preserveManualTranslations: true,
3736
- ...options
3794
+ ...options,
3795
+ continueOnError: true,
3796
+ signal: controller.signal,
3797
+ createMetadata: (slot, translatedText) => options.createMetadata?.(slot, translatedText) ?? createTranslationMetadata(slot, sourceLocale, translatedText, "automatic", metadataFactory),
3798
+ onProgress: (nextProgress) => {
3799
+ setProgress(nextProgress);
3800
+ options.onProgress?.(nextProgress);
3801
+ }
3737
3802
  }
3738
3803
  );
3739
3804
  commit(populated.schema);
3805
+ onTranslationReport?.(populated.report);
3740
3806
  const missingSlotsCount = collectTranslationSlots(populated.schema, activeLocale).filter(
3741
3807
  (slot) => slot.status === "missing"
3742
3808
  ).length;
3809
+ if (populated.report.cancelled === true) {
3810
+ const workspaceError = { type: "cancelled" };
3811
+ setError(workspaceError);
3812
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3813
+ return { success: false, report: populated.report, error: workspaceError };
3814
+ }
3815
+ if ((populated.report.failed ?? 0) > 0) {
3816
+ const workspaceError = {
3817
+ type: "partial_failure",
3818
+ succeeded: populated.report.succeeded ?? populated.report.updatedSlots.length,
3819
+ failed: populated.report.failed ?? 0
3820
+ };
3821
+ setError(workspaceError);
3822
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3823
+ return { success: false, report: populated.report, error: workspaceError };
3824
+ }
3743
3825
  onTranslationSuccess?.({
3744
3826
  sourceLocale,
3745
3827
  targetLocale: activeLocale,
@@ -3750,6 +3832,12 @@ function useTranslationWorkspace({
3750
3832
  });
3751
3833
  return { success: true, report: populated.report };
3752
3834
  } catch (cause) {
3835
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3836
+ const workspaceError2 = { type: "cancelled" };
3837
+ setError(workspaceError2);
3838
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError2 });
3839
+ return { success: false, error: workspaceError2 };
3840
+ }
3753
3841
  const workspaceError = {
3754
3842
  type: "translation_failed",
3755
3843
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3759,6 +3847,11 @@ function useTranslationWorkspace({
3759
3847
  onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3760
3848
  return { success: false, error: workspaceError };
3761
3849
  } finally {
3850
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3851
+ const listener = abortListener;
3852
+ operationSignal.removeEventListener("abort", listener);
3853
+ }
3854
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3762
3855
  setIsTranslating(false);
3763
3856
  }
3764
3857
  },
@@ -3770,14 +3863,20 @@ function useTranslationWorkspace({
3770
3863
  onTranslationError,
3771
3864
  onTranslationStart,
3772
3865
  onTranslationSuccess,
3866
+ onTranslationReport,
3773
3867
  readOnly,
3774
3868
  slots,
3775
3869
  sourceLocale,
3776
- translationAdapter
3870
+ metadataFactory,
3871
+ translationAdapter,
3872
+ signal
3777
3873
  ]
3778
3874
  );
3875
+ const cancelTranslation = useCallback3(() => {
3876
+ activeAbortController.current?.abort(new DOMException("The translation was cancelled.", "AbortError"));
3877
+ }, []);
3779
3878
  const translateSlot = useCallback3(
3780
- async (slot) => {
3879
+ async (slot, options = {}) => {
3781
3880
  if (translationAdapter === void 0) {
3782
3881
  const workspaceError = { type: "adapter_not_configured" };
3783
3882
  setError(workspaceError);
@@ -3790,10 +3889,23 @@ function useTranslationWorkspace({
3790
3889
  }
3791
3890
  setIsTranslating(true);
3792
3891
  setError(void 0);
3892
+ setProgress({ total: 1, completed: 0, succeeded: 0, failed: 0, percentage: 0 });
3893
+ const controller = new AbortController();
3894
+ activeAbortController.current = controller;
3895
+ const operationSignal = options.signal ?? signal;
3896
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3897
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3898
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3899
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3793
3900
  try {
3794
- const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3795
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3796
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3901
+ const text = await asAsyncAdapter(translationAdapter).translateText(
3902
+ slot.sourceText,
3903
+ slot.locale,
3904
+ sourceLocale,
3905
+ controller.signal
3906
+ );
3907
+ const metadata = createTranslationMetadata(slot, sourceLocale, text, "automatic", metadataFactory);
3908
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic", metadata));
3797
3909
  onTranslationChange?.({
3798
3910
  slot,
3799
3911
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3801,20 +3913,43 @@ function useTranslationWorkspace({
3801
3913
  mode: "automatic",
3802
3914
  metadata
3803
3915
  });
3916
+ setProgress({ total: 1, completed: 1, succeeded: 1, failed: 0, percentage: 100 });
3804
3917
  return { success: true };
3805
3918
  } catch (cause) {
3919
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3920
+ const workspaceError2 = { type: "cancelled" };
3921
+ setError(workspaceError2);
3922
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError2 });
3923
+ return { success: false, error: workspaceError2 };
3924
+ }
3806
3925
  const workspaceError = {
3807
3926
  type: "translation_failed",
3808
3927
  message: cause instanceof Error ? cause.message : String(cause),
3809
3928
  cause
3810
3929
  };
3811
3930
  setError(workspaceError);
3931
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError });
3812
3932
  return { success: false, error: workspaceError };
3813
3933
  } finally {
3934
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3935
+ const listener = abortListener;
3936
+ operationSignal.removeEventListener("abort", listener);
3937
+ }
3938
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3814
3939
  setIsTranslating(false);
3815
3940
  }
3816
3941
  },
3817
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3942
+ [
3943
+ commit,
3944
+ currentSchema,
3945
+ metadataFactory,
3946
+ onTranslationChange,
3947
+ onTranslationError,
3948
+ readOnly,
3949
+ signal,
3950
+ sourceLocale,
3951
+ translationAdapter
3952
+ ]
3818
3953
  );
3819
3954
  return {
3820
3955
  sourceLocale,
@@ -3835,7 +3970,9 @@ function useTranslationWorkspace({
3835
3970
  setTranslation,
3836
3971
  translateAll,
3837
3972
  translateSlot,
3973
+ cancelTranslation,
3838
3974
  isTranslating,
3975
+ ...progress === void 0 ? {} : { progress },
3839
3976
  ...error === void 0 ? {} : { error }
3840
3977
  };
3841
3978
  }
@@ -3845,6 +3982,7 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3845
3982
  if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3846
3983
  if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3847
3984
  return {
3985
+ ...metadata,
3848
3986
  sourceLocale: metadata.sourceLocale,
3849
3987
  sourceTextHash: metadata.sourceTextHash,
3850
3988
  translationSource: metadata.translationSource,
@@ -3852,7 +3990,10 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3852
3990
  ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3853
3991
  };
3854
3992
  }
3993
+ const canonicalKeys = /* @__PURE__ */ new Set(["sourceLocale", "sourceTextHash", "translationSource", "translatedAt", "editedAt"]);
3994
+ const extensions = Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => !canonicalKeys.has(key)));
3855
3995
  return {
3996
+ ...extensions,
3856
3997
  sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3857
3998
  sourceTextHash: computeSourceTextHash2(sourceText),
3858
3999
  translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
@@ -3863,6 +4004,7 @@ function optionParentId(slot) {
3863
4004
  return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3864
4005
  }
3865
4006
  function getNodeTitle(schema, slot) {
4007
+ if (slot.kind === "form") return schema.title;
3866
4008
  if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3867
4009
  if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3868
4010
  if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
@@ -3880,6 +4022,7 @@ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3880
4022
  return {
3881
4023
  id: path,
3882
4024
  path,
4025
+ nodeId: slot.nodeId,
3883
4026
  targetKind: slot.kind,
3884
4027
  targetProperty: slot.property,
3885
4028
  ...nodeTitle === void 0 ? {} : { nodeTitle },
@@ -3913,18 +4056,47 @@ function useTranslationComparison({
3913
4056
  targetLocale,
3914
4057
  translationAdapter,
3915
4058
  readOnly = false,
4059
+ availableLocales,
4060
+ policy,
3916
4061
  onChange,
3917
- onTranslationChange
4062
+ onTranslationChange,
4063
+ onTranslationReport,
4064
+ onTranslationError,
4065
+ signal,
4066
+ onLocaleAdded,
4067
+ onLocaleRemoved,
4068
+ onLocaleChange,
4069
+ beforeRemoveLocale,
4070
+ confirmRemoveLocale,
4071
+ onTranslationStart,
4072
+ onTranslationSuccess,
4073
+ validateLocale,
4074
+ createTranslationMetadata: createTranslationMetadata2
3918
4075
  }) {
3919
4076
  const workspace = useTranslationWorkspace({
3920
4077
  schema,
3921
4078
  sourceLocale,
3922
4079
  targetLocale,
4080
+ ...availableLocales === void 0 ? {} : { availableLocales },
4081
+ ...policy === void 0 ? {} : { policy },
3923
4082
  ...translationAdapter === void 0 ? {} : { translationAdapter },
3924
4083
  readOnly,
3925
4084
  ...onChange === void 0 ? {} : { onChange },
3926
- ...onTranslationChange === void 0 ? {} : { onTranslationChange }
4085
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4086
+ ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4087
+ ...onTranslationError === void 0 ? {} : { onTranslationError },
4088
+ ...signal === void 0 ? {} : { signal },
4089
+ ...onLocaleAdded === void 0 ? {} : { onLocaleAdded },
4090
+ ...onLocaleRemoved === void 0 ? {} : { onLocaleRemoved },
4091
+ ...onLocaleChange === void 0 ? {} : { onLocaleChange },
4092
+ ...beforeRemoveLocale === void 0 ? {} : { beforeRemoveLocale },
4093
+ ...confirmRemoveLocale === void 0 ? {} : { confirmRemoveLocale },
4094
+ ...onTranslationStart === void 0 ? {} : { onTranslationStart },
4095
+ ...onTranslationSuccess === void 0 ? {} : { onTranslationSuccess },
4096
+ ...validateLocale === void 0 ? {} : { validateLocale },
4097
+ ...createTranslationMetadata2 === void 0 ? {} : { createTranslationMetadata: createTranslationMetadata2 }
3927
4098
  });
4099
+ const [report, setReport] = useState5();
3928
4100
  const items = useMemo5(
3929
4101
  () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
3930
4102
  [schema, sourceLocale, translationAdapter, workspace.slots]
@@ -3946,25 +4118,42 @@ function useTranslationComparison({
3946
4118
  },
3947
4119
  [itemByPath, workspace]
3948
4120
  );
3949
- const translateAll = useCallback4(async () => {
3950
- const result = await workspace.translateAll();
3951
- if (result.report !== void 0) return result.report;
3952
- throw new Error(result.error?.type ?? "Translation failed.");
3953
- }, [workspace]);
4121
+ const translateAll = useCallback4(
4122
+ async (options) => {
4123
+ const result = await workspace.translateAll(options);
4124
+ if (result.report !== void 0) {
4125
+ setReport(result.report);
4126
+ return result.report;
4127
+ }
4128
+ throw new Error(result.error?.type ?? "Translation failed.");
4129
+ },
4130
+ [workspace]
4131
+ );
3954
4132
  return {
3955
4133
  sourceLocale: workspace.sourceLocale,
3956
4134
  targetLocale: workspace.targetLocale,
4135
+ targetLocales: workspace.targetLocales,
4136
+ localeOptions: workspace.localeOptions,
4137
+ setTargetLocale: workspace.setTargetLocale,
4138
+ addLocale: workspace.addLocale,
4139
+ isAddLocaleAllowed: workspace.isAddLocaleAllowed,
4140
+ removeLocale: workspace.removeLocale,
4141
+ ...workspace.removeLocaleConfirmation === void 0 ? {} : { removeLocaleConfirmation: workspace.removeLocaleConfirmation },
3957
4142
  items,
3958
4143
  summary: summaryFor(items),
3959
4144
  isTranslating: workspace.isTranslating,
3960
4145
  updateTranslation,
3961
4146
  translateSingle,
3962
- translateAll
4147
+ translateAll,
4148
+ cancelTranslation: workspace.cancelTranslation,
4149
+ ...workspace.progress === void 0 ? {} : { progress: workspace.progress },
4150
+ ...workspace.error === void 0 ? {} : { error: workspace.error },
4151
+ ...report === void 0 ? {} : { report }
3963
4152
  };
3964
4153
  }
3965
4154
 
3966
4155
  // src/receipt.ts
3967
- import { useEffect as useEffect2, useMemo as useMemo6, useState as useState5 } from "react";
4156
+ import { useEffect as useEffect2, useMemo as useMemo6, useState as useState6 } from "react";
3968
4157
  function submissionReceiptQueryKey(formId, formVersion) {
3969
4158
  return `${formId}:v${formVersion}`;
3970
4159
  }
@@ -4042,7 +4231,7 @@ function useSubmissionReceipts(store, queries) {
4042
4231
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
4043
4232
  );
4044
4233
  }, [querySignature]);
4045
- const [state, setState] = useState5({
4234
+ const [state, setState] = useState6({
4046
4235
  receipts: /* @__PURE__ */ new Map(),
4047
4236
  isLoading: stableQueries.length > 0,
4048
4237
  error: null
@@ -4095,7 +4284,7 @@ import {
4095
4284
  useId,
4096
4285
  useMemo as useMemo7,
4097
4286
  useRef as useRef3,
4098
- useState as useState6
4287
+ useState as useState7
4099
4288
  } from "react";
4100
4289
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
4101
4290
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
@@ -4612,15 +4801,15 @@ function ContextFormRenderer({
4612
4801
  const prefix = useId().replace(/:/g, "");
4613
4802
  const formRef = useRef3(null);
4614
4803
  const loadedDraftKey = useRef3(null);
4615
- const [draftRestored, setDraftRestored] = useState6(false);
4616
- const [currentPageIndex, setCurrentPageIndex] = useState6(0);
4617
- const [focusFieldId, setFocusFieldId] = useState6(null);
4618
- const [confirmation, setConfirmation] = useState6(null);
4619
- const [guardMessage, setGuardMessage] = useState6(null);
4620
- const [guardsPending, setGuardsPending] = useState6(false);
4621
- const [receipt, setReceipt] = useState6(null);
4622
- const [completionData, setCompletionData] = useState6(null);
4623
- const [receiptLoaded, setReceiptLoaded] = useState6(receiptStore === void 0);
4804
+ const [draftRestored, setDraftRestored] = useState7(false);
4805
+ const [currentPageIndex, setCurrentPageIndex] = useState7(0);
4806
+ const [focusFieldId, setFocusFieldId] = useState7(null);
4807
+ const [confirmation, setConfirmation] = useState7(null);
4808
+ const [guardMessage, setGuardMessage] = useState7(null);
4809
+ const [guardsPending, setGuardsPending] = useState7(false);
4810
+ const [receipt, setReceipt] = useState7(null);
4811
+ const [completionData, setCompletionData] = useState7(null);
4812
+ const [receiptLoaded, setReceiptLoaded] = useState7(receiptStore === void 0);
4624
4813
  const rendererSubmissionInFlight = useRef3(false);
4625
4814
  const fallbackAttemptId = useRef3(null);
4626
4815
  const completionRef = useRef3(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "6.1.0",
3
+ "version": "6.3.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/privacy": "6.1.0",
46
- "@form-engine-ts/core": "6.1.0"
45
+ "@form-engine-ts/core": "6.3.0",
46
+ "@form-engine-ts/privacy": "6.3.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",