@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/README.md CHANGED
@@ -89,13 +89,17 @@ 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.
92
+ `useTranslationWorkspace` accepts either a synchronous or asynchronous translation adapter, forwards an optional
93
+ `AbortSignal`, and exposes `cancelTranslation`, progress, and typed partial-failure/cancellation errors. It can notify
94
+ `onTranslationStart`, `onTranslationSuccess`, `onTranslationReport`, `onTranslationError`, and `onTranslationChange`
95
+ with typed lifecycle payloads. The MUI workspace accepts a `confirmRemoveLocale` slot for an async confirmation UI;
96
+ its `onConfirm` callback completes the locale removal.
95
97
 
96
98
  `useTranslationComparison` provides a focused, controlled comparison model for form, page, field, and option text.
97
99
  Its items expose source text, target text, status, canonical metadata, and path-based `updateTranslation`,
98
- `translateSingle`, and `translateAll` actions.
100
+ `translateSingle`, and `translateAll` actions. Comparison row slots receive `nodeKind`, locale labels, read-only state,
101
+ field type, question and option indexes, metadata, plus async single-translation actions. Use
102
+ `createTranslationMetadata` to add provider/model/hash fields while the canonical metadata fields are retained.
99
103
 
100
104
  Wrap a builder or renderer in `FormEngineI18nProvider` to supply a UI locale and typed Core translator independently
101
105
  from the schema's `defaultLocale` and `supportedLocales`:
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, availableLocales) => {
3328
+ var validateLocalePipeline = (locale, schema, policy, customValidator, availableLocales, sourceLocale) => {
3329
3329
  const canonicalLocale = (0, import_core5.normalizeLocale)(locale);
3330
3330
  if (canonicalLocale === null) {
3331
3331
  return {
@@ -3337,20 +3337,17 @@ var validateLocalePipeline = (locale, schema, policy, customValidator, available
3337
3337
  };
3338
3338
  }
3339
3339
  const currentLocales = (schema.supportedLocales ?? []).map((candidate) => (0, import_core5.normalizeLocale)(candidate) ?? candidate);
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
- })) {
3340
+ const defaultLocale = (0, import_core5.normalizeLocale)(sourceLocale ?? schema.defaultLocale ?? "") ?? sourceLocale ?? schema.defaultLocale ?? "";
3341
+ if (canonicalLocale === defaultLocale) {
3345
3342
  return {
3346
3343
  valid: false,
3347
3344
  error: {
3348
- type: "locale_not_allowed",
3349
- message: `Locale "${canonicalLocale}" is not available in the locale catalog.`
3345
+ type: "source_locale",
3346
+ message: `Locale "${canonicalLocale}" is the source locale and cannot be added as a translation.`
3350
3347
  }
3351
3348
  };
3352
3349
  }
3353
- if (canonicalLocale === defaultLocale || currentLocales.includes(canonicalLocale)) {
3350
+ if (currentLocales.includes(canonicalLocale)) {
3354
3351
  return {
3355
3352
  valid: false,
3356
3353
  error: {
@@ -3359,6 +3356,18 @@ var validateLocalePipeline = (locale, schema, policy, customValidator, available
3359
3356
  }
3360
3357
  };
3361
3358
  }
3359
+ if (availableLocales !== void 0 && !availableLocales.some((candidate) => {
3360
+ const value = typeof candidate === "string" ? candidate : candidate.locale;
3361
+ return (0, import_core5.normalizeLocale)(value) === canonicalLocale;
3362
+ })) {
3363
+ return {
3364
+ valid: false,
3365
+ error: {
3366
+ type: "locale_not_allowed",
3367
+ message: `Locale "${canonicalLocale}" is not available in the locale catalog.`
3368
+ }
3369
+ };
3370
+ }
3362
3371
  if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.some((candidate) => (0, import_core5.normalizeLocale)(candidate) === canonicalLocale)) {
3363
3372
  return {
3364
3373
  valid: false,
@@ -3412,6 +3421,7 @@ function workspaceLocaleValidationError(validation, currentLocales, requestedLoc
3412
3421
  if (error.type === "locale_already_exists") {
3413
3422
  return { type: "locale_already_exists", locale: requestedLocale };
3414
3423
  }
3424
+ if (error.type === "source_locale") return { type: "source_locale", locale: requestedLocale };
3415
3425
  if (error.type === "invalid_locale_format") {
3416
3426
  return { type: "invalid_locale_format", locale: requestedLocale };
3417
3427
  }
@@ -3429,19 +3439,29 @@ function translationMetadata(sourceText, sourceLocale, mode) {
3429
3439
  ...mode === "manual" ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : { translatedAt: (/* @__PURE__ */ new Date()).toISOString() }
3430
3440
  };
3431
3441
  }
3432
- function setNodeMetadata(node, slot, text, sourceLocale, mode) {
3442
+ function createTranslationMetadata(slot, sourceLocale, translatedText, mode, factory) {
3443
+ return {
3444
+ ...factory?.({ slot, translatedText, mode }) ?? {},
3445
+ ...translationMetadata(slot.sourceText, sourceLocale, mode)
3446
+ };
3447
+ }
3448
+ function setNodeMetadata(node, slot, text, sourceLocale, mode, metadataOverride) {
3433
3449
  const localeMetadata = node.translationMetadata?.[slot.locale];
3434
- const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: translationMetadata(slot.sourceText, sourceLocale, mode) };
3450
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : {
3451
+ ...localeMetadata,
3452
+ [slot.property]: metadataOverride ?? translationMetadata(slot.sourceText, sourceLocale, mode)
3453
+ };
3435
3454
  return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3436
3455
  }
3437
- function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual") {
3456
+ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manual", metadataOverride) {
3438
3457
  if (slot.kind === "form") {
3439
3458
  return setNodeMetadata(
3440
3459
  { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3441
3460
  slot,
3442
3461
  text,
3443
3462
  sourceLocale,
3444
- mode
3463
+ mode,
3464
+ metadataOverride
3445
3465
  );
3446
3466
  }
3447
3467
  if (slot.kind === "field") {
@@ -3450,7 +3470,14 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manua
3450
3470
  fields: schema.fields.map((field) => {
3451
3471
  if (field.id !== slot.nodeId) return field;
3452
3472
  const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3453
- return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale, mode);
3473
+ return setNodeMetadata(
3474
+ { ...field, translations },
3475
+ slot,
3476
+ text,
3477
+ sourceLocale,
3478
+ mode,
3479
+ metadataOverride
3480
+ );
3454
3481
  })
3455
3482
  };
3456
3483
  }
@@ -3466,7 +3493,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manua
3466
3493
  const translations = text.trim().length === 0 ? Object.fromEntries(
3467
3494
  Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3468
3495
  ) : { ...option.translations, [slot.locale]: text };
3469
- return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode);
3496
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale, mode, metadataOverride);
3470
3497
  })
3471
3498
  };
3472
3499
  })
@@ -3478,7 +3505,7 @@ function updateSchemaTranslation(schema, slot, text, sourceLocale, mode = "manua
3478
3505
  pages: schema.pages.map((page) => {
3479
3506
  if (page.id !== slot.nodeId) return page;
3480
3507
  const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3481
- return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode);
3508
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale, mode, metadataOverride);
3482
3509
  })
3483
3510
  }
3484
3511
  };
@@ -3489,6 +3516,7 @@ function useTranslationWorkspace({
3489
3516
  sourceLocale = schema.defaultLocale ?? "en",
3490
3517
  targetLocale,
3491
3518
  translationAdapter,
3519
+ signal,
3492
3520
  readOnly = false,
3493
3521
  policy,
3494
3522
  availableLocales,
@@ -3500,34 +3528,49 @@ function useTranslationWorkspace({
3500
3528
  slots: workspaceSlots,
3501
3529
  onTranslationStart,
3502
3530
  onTranslationSuccess,
3531
+ onTranslationReport,
3503
3532
  onTranslationError,
3504
3533
  onTranslationChange,
3505
- validateLocale
3534
+ validateLocale,
3535
+ createTranslationMetadata: metadataFactory
3506
3536
  }) {
3507
3537
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3508
3538
  const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
3509
3539
  const [isTranslating, setIsTranslating] = (0, import_react5.useState)(false);
3540
+ const [progress, setProgress] = (0, import_react5.useState)();
3510
3541
  const [error, setError] = (0, import_react5.useState)();
3511
3542
  const [pendingRemoval, setPendingRemoval] = (0, import_react5.useState)();
3543
+ const activeAbortController = (0, import_react5.useRef)(void 0);
3512
3544
  const pendingRemovalResolver = (0, import_react5.useRef)(void 0);
3513
3545
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3514
3546
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3515
3547
  const targetLocales = (0, import_react5.useMemo)(() => {
3516
3548
  const locales = (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales;
3517
- return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
3518
- (locale) => locale !== sourceLocale
3519
- );
3549
+ const normalizedSourceLocale = (0, import_core5.normalizeLocale)(sourceLocale) ?? sourceLocale;
3550
+ return [
3551
+ ...new Set(
3552
+ [...currentSchema.supportedLocales ?? [], ...locales].map((locale) => (0, import_core5.normalizeLocale)(locale) ?? locale)
3553
+ )
3554
+ ].filter((locale) => locale !== normalizedSourceLocale);
3520
3555
  }, [currentSchema, sourceLocale]);
3521
3556
  const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3522
3557
  const localeOptions = (0, import_react5.useMemo)(() => {
3523
3558
  if (availableLocales === void 0) {
3524
3559
  return targetLocales.map((locale) => ({ locale, label: locale }));
3525
3560
  }
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]);
3561
+ const targetLocaleSet = new Set(targetLocales);
3562
+ const allowedLocales = policy?.allowedLocales?.map((locale) => (0, import_core5.normalizeLocale)(locale) ?? locale);
3563
+ const options = /* @__PURE__ */ new Map();
3564
+ for (const candidate of availableLocales) {
3565
+ const option = typeof candidate === "string" ? { locale: (0, import_core5.normalizeLocale)(candidate) ?? candidate, label: candidate } : { ...candidate, locale: (0, import_core5.normalizeLocale)(candidate.locale) ?? candidate.locale };
3566
+ const allowed = allowedLocales === void 0 || allowedLocales.includes(option.locale);
3567
+ if (allowed || targetLocaleSet.has(option.locale)) options.set(option.locale, option);
3568
+ }
3569
+ for (const locale of targetLocales) {
3570
+ if (!options.has(locale)) options.set(locale, { locale, label: locale });
3571
+ }
3572
+ return [...options.values()];
3573
+ }, [availableLocales, policy?.allowedLocales, targetLocales]);
3531
3574
  const slots = (0, import_react5.useMemo)(
3532
3575
  () => activeLocale.length === 0 ? [] : (0, import_core5.collectTranslationSlots)(currentSchema, activeLocale),
3533
3576
  [activeLocale, currentSchema]
@@ -3561,8 +3604,8 @@ function useTranslationWorkspace({
3561
3604
  const setTranslation = (0, import_react5.useCallback)(
3562
3605
  (slot, text) => {
3563
3606
  if (readOnly) return;
3564
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3565
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual"));
3607
+ const metadata = createTranslationMetadata(slot, sourceLocale, text, "manual", metadataFactory);
3608
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "manual", metadata));
3566
3609
  onTranslationChange?.({
3567
3610
  slot,
3568
3611
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3571,7 +3614,7 @@ function useTranslationWorkspace({
3571
3614
  metadata
3572
3615
  });
3573
3616
  },
3574
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3617
+ [commit, currentSchema, metadataFactory, onTranslationChange, readOnly, sourceLocale]
3575
3618
  );
3576
3619
  const addLocale = (0, import_react5.useCallback)(
3577
3620
  (locale) => {
@@ -3583,14 +3626,21 @@ function useTranslationWorkspace({
3583
3626
  const normalized = (0, import_core5.normalizeLocale)(locale);
3584
3627
  if (normalized === null) {
3585
3628
  const workspaceError = workspaceLocaleValidationError(
3586
- validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales),
3629
+ validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales, sourceLocale),
3587
3630
  (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales.size,
3588
3631
  locale
3589
3632
  );
3590
3633
  setError(workspaceError);
3591
3634
  return { success: false, error: workspaceError };
3592
3635
  }
3593
- const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales);
3636
+ const validation = validateLocalePipeline(
3637
+ normalized,
3638
+ currentSchema,
3639
+ policy,
3640
+ validateLocale,
3641
+ availableLocales,
3642
+ sourceLocale
3643
+ );
3594
3644
  if (!validation.valid) {
3595
3645
  const workspaceError = workspaceLocaleValidationError(
3596
3646
  validation,
@@ -3609,16 +3659,16 @@ function useTranslationWorkspace({
3609
3659
  onLocaleAdded?.(normalized);
3610
3660
  return { success: true };
3611
3661
  },
3612
- [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, validateLocale]
3662
+ [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, sourceLocale, validateLocale]
3613
3663
  );
3614
3664
  const isAddLocaleAllowed = (0, import_react5.useCallback)(
3615
3665
  (locale) => {
3616
3666
  if (readOnly) return false;
3617
3667
  const normalized = (0, import_core5.normalizeLocale)(locale);
3618
3668
  if (normalized === null) return false;
3619
- return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales).valid;
3669
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales, sourceLocale).valid;
3620
3670
  },
3621
- [availableLocales, currentSchema, policy, readOnly, validateLocale]
3671
+ [availableLocales, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3622
3672
  );
3623
3673
  const removeLocale = (0, import_react5.useCallback)(
3624
3674
  (locale) => {
@@ -3748,7 +3798,15 @@ function useTranslationWorkspace({
3748
3798
  }
3749
3799
  setIsTranslating(true);
3750
3800
  setError(void 0);
3801
+ setProgress(void 0);
3751
3802
  onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3803
+ const controller = new AbortController();
3804
+ activeAbortController.current = controller;
3805
+ const operationSignal = options.signal ?? signal;
3806
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3807
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3808
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3809
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3752
3810
  try {
3753
3811
  const populated = await (0, import_core5.populateSchemaTranslations)(
3754
3812
  currentSchema,
@@ -3757,13 +3815,37 @@ function useTranslationWorkspace({
3757
3815
  {
3758
3816
  overwrite: "stale-and-missing",
3759
3817
  preserveManualTranslations: true,
3760
- ...options
3818
+ ...options,
3819
+ continueOnError: true,
3820
+ signal: controller.signal,
3821
+ createMetadata: (slot, translatedText) => options.createMetadata?.(slot, translatedText) ?? createTranslationMetadata(slot, sourceLocale, translatedText, "automatic", metadataFactory),
3822
+ onProgress: (nextProgress) => {
3823
+ setProgress(nextProgress);
3824
+ options.onProgress?.(nextProgress);
3825
+ }
3761
3826
  }
3762
3827
  );
3763
3828
  commit(populated.schema);
3829
+ onTranslationReport?.(populated.report);
3764
3830
  const missingSlotsCount = (0, import_core5.collectTranslationSlots)(populated.schema, activeLocale).filter(
3765
3831
  (slot) => slot.status === "missing"
3766
3832
  ).length;
3833
+ if (populated.report.cancelled === true) {
3834
+ const workspaceError = { type: "cancelled" };
3835
+ setError(workspaceError);
3836
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3837
+ return { success: false, report: populated.report, error: workspaceError };
3838
+ }
3839
+ if ((populated.report.failed ?? 0) > 0) {
3840
+ const workspaceError = {
3841
+ type: "partial_failure",
3842
+ succeeded: populated.report.succeeded ?? populated.report.updatedSlots.length,
3843
+ failed: populated.report.failed ?? 0
3844
+ };
3845
+ setError(workspaceError);
3846
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3847
+ return { success: false, report: populated.report, error: workspaceError };
3848
+ }
3767
3849
  onTranslationSuccess?.({
3768
3850
  sourceLocale,
3769
3851
  targetLocale: activeLocale,
@@ -3774,6 +3856,12 @@ function useTranslationWorkspace({
3774
3856
  });
3775
3857
  return { success: true, report: populated.report };
3776
3858
  } catch (cause) {
3859
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3860
+ const workspaceError2 = { type: "cancelled" };
3861
+ setError(workspaceError2);
3862
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError2 });
3863
+ return { success: false, error: workspaceError2 };
3864
+ }
3777
3865
  const workspaceError = {
3778
3866
  type: "translation_failed",
3779
3867
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3783,6 +3871,11 @@ function useTranslationWorkspace({
3783
3871
  onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3784
3872
  return { success: false, error: workspaceError };
3785
3873
  } finally {
3874
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3875
+ const listener = abortListener;
3876
+ operationSignal.removeEventListener("abort", listener);
3877
+ }
3878
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3786
3879
  setIsTranslating(false);
3787
3880
  }
3788
3881
  },
@@ -3794,14 +3887,20 @@ function useTranslationWorkspace({
3794
3887
  onTranslationError,
3795
3888
  onTranslationStart,
3796
3889
  onTranslationSuccess,
3890
+ onTranslationReport,
3797
3891
  readOnly,
3798
3892
  slots,
3799
3893
  sourceLocale,
3800
- translationAdapter
3894
+ metadataFactory,
3895
+ translationAdapter,
3896
+ signal
3801
3897
  ]
3802
3898
  );
3899
+ const cancelTranslation = (0, import_react5.useCallback)(() => {
3900
+ activeAbortController.current?.abort(new DOMException("The translation was cancelled.", "AbortError"));
3901
+ }, []);
3803
3902
  const translateSlot = (0, import_react5.useCallback)(
3804
- async (slot) => {
3903
+ async (slot, options = {}) => {
3805
3904
  if (translationAdapter === void 0) {
3806
3905
  const workspaceError = { type: "adapter_not_configured" };
3807
3906
  setError(workspaceError);
@@ -3814,10 +3913,23 @@ function useTranslationWorkspace({
3814
3913
  }
3815
3914
  setIsTranslating(true);
3816
3915
  setError(void 0);
3916
+ setProgress({ total: 1, completed: 0, succeeded: 0, failed: 0, percentage: 0 });
3917
+ const controller = new AbortController();
3918
+ activeAbortController.current = controller;
3919
+ const operationSignal = options.signal ?? signal;
3920
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3921
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3922
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3923
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3817
3924
  try {
3818
- const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3819
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3820
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3925
+ const text = await asAsyncAdapter(translationAdapter).translateText(
3926
+ slot.sourceText,
3927
+ slot.locale,
3928
+ sourceLocale,
3929
+ controller.signal
3930
+ );
3931
+ const metadata = createTranslationMetadata(slot, sourceLocale, text, "automatic", metadataFactory);
3932
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic", metadata));
3821
3933
  onTranslationChange?.({
3822
3934
  slot,
3823
3935
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3825,20 +3937,43 @@ function useTranslationWorkspace({
3825
3937
  mode: "automatic",
3826
3938
  metadata
3827
3939
  });
3940
+ setProgress({ total: 1, completed: 1, succeeded: 1, failed: 0, percentage: 100 });
3828
3941
  return { success: true };
3829
3942
  } catch (cause) {
3943
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3944
+ const workspaceError2 = { type: "cancelled" };
3945
+ setError(workspaceError2);
3946
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError2 });
3947
+ return { success: false, error: workspaceError2 };
3948
+ }
3830
3949
  const workspaceError = {
3831
3950
  type: "translation_failed",
3832
3951
  message: cause instanceof Error ? cause.message : String(cause),
3833
3952
  cause
3834
3953
  };
3835
3954
  setError(workspaceError);
3955
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError });
3836
3956
  return { success: false, error: workspaceError };
3837
3957
  } finally {
3958
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3959
+ const listener = abortListener;
3960
+ operationSignal.removeEventListener("abort", listener);
3961
+ }
3962
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3838
3963
  setIsTranslating(false);
3839
3964
  }
3840
3965
  },
3841
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3966
+ [
3967
+ commit,
3968
+ currentSchema,
3969
+ metadataFactory,
3970
+ onTranslationChange,
3971
+ onTranslationError,
3972
+ readOnly,
3973
+ signal,
3974
+ sourceLocale,
3975
+ translationAdapter
3976
+ ]
3842
3977
  );
3843
3978
  return {
3844
3979
  sourceLocale,
@@ -3859,7 +3994,9 @@ function useTranslationWorkspace({
3859
3994
  setTranslation,
3860
3995
  translateAll,
3861
3996
  translateSlot,
3997
+ cancelTranslation,
3862
3998
  isTranslating,
3999
+ ...progress === void 0 ? {} : { progress },
3863
4000
  ...error === void 0 ? {} : { error }
3864
4001
  };
3865
4002
  }
@@ -3869,6 +4006,7 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3869
4006
  if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3870
4007
  if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3871
4008
  return {
4009
+ ...metadata,
3872
4010
  sourceLocale: metadata.sourceLocale,
3873
4011
  sourceTextHash: metadata.sourceTextHash,
3874
4012
  translationSource: metadata.translationSource,
@@ -3876,7 +4014,10 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3876
4014
  ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3877
4015
  };
3878
4016
  }
4017
+ const canonicalKeys = /* @__PURE__ */ new Set(["sourceLocale", "sourceTextHash", "translationSource", "translatedAt", "editedAt"]);
4018
+ const extensions = Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => !canonicalKeys.has(key)));
3879
4019
  return {
4020
+ ...extensions,
3880
4021
  sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3881
4022
  sourceTextHash: (0, import_core6.computeSourceTextHash)(sourceText),
3882
4023
  translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
@@ -3887,6 +4028,7 @@ function optionParentId(slot) {
3887
4028
  return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3888
4029
  }
3889
4030
  function getNodeTitle(schema, slot) {
4031
+ if (slot.kind === "form") return schema.title;
3890
4032
  if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3891
4033
  if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3892
4034
  if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
@@ -3904,6 +4046,7 @@ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3904
4046
  return {
3905
4047
  id: path,
3906
4048
  path,
4049
+ nodeId: slot.nodeId,
3907
4050
  targetKind: slot.kind,
3908
4051
  targetProperty: slot.property,
3909
4052
  ...nodeTitle === void 0 ? {} : { nodeTitle },
@@ -3937,18 +4080,47 @@ function useTranslationComparison({
3937
4080
  targetLocale,
3938
4081
  translationAdapter,
3939
4082
  readOnly = false,
4083
+ availableLocales,
4084
+ policy,
3940
4085
  onChange,
3941
- onTranslationChange
4086
+ onTranslationChange,
4087
+ onTranslationReport,
4088
+ onTranslationError,
4089
+ signal,
4090
+ onLocaleAdded,
4091
+ onLocaleRemoved,
4092
+ onLocaleChange,
4093
+ beforeRemoveLocale,
4094
+ confirmRemoveLocale,
4095
+ onTranslationStart,
4096
+ onTranslationSuccess,
4097
+ validateLocale,
4098
+ createTranslationMetadata: createTranslationMetadata2
3942
4099
  }) {
3943
4100
  const workspace = useTranslationWorkspace({
3944
4101
  schema,
3945
4102
  sourceLocale,
3946
4103
  targetLocale,
4104
+ ...availableLocales === void 0 ? {} : { availableLocales },
4105
+ ...policy === void 0 ? {} : { policy },
3947
4106
  ...translationAdapter === void 0 ? {} : { translationAdapter },
3948
4107
  readOnly,
3949
4108
  ...onChange === void 0 ? {} : { onChange },
3950
- ...onTranslationChange === void 0 ? {} : { onTranslationChange }
4109
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4110
+ ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4111
+ ...onTranslationError === void 0 ? {} : { onTranslationError },
4112
+ ...signal === void 0 ? {} : { signal },
4113
+ ...onLocaleAdded === void 0 ? {} : { onLocaleAdded },
4114
+ ...onLocaleRemoved === void 0 ? {} : { onLocaleRemoved },
4115
+ ...onLocaleChange === void 0 ? {} : { onLocaleChange },
4116
+ ...beforeRemoveLocale === void 0 ? {} : { beforeRemoveLocale },
4117
+ ...confirmRemoveLocale === void 0 ? {} : { confirmRemoveLocale },
4118
+ ...onTranslationStart === void 0 ? {} : { onTranslationStart },
4119
+ ...onTranslationSuccess === void 0 ? {} : { onTranslationSuccess },
4120
+ ...validateLocale === void 0 ? {} : { validateLocale },
4121
+ ...createTranslationMetadata2 === void 0 ? {} : { createTranslationMetadata: createTranslationMetadata2 }
3951
4122
  });
4123
+ const [report, setReport] = (0, import_react6.useState)();
3952
4124
  const items = (0, import_react6.useMemo)(
3953
4125
  () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
3954
4126
  [schema, sourceLocale, translationAdapter, workspace.slots]
@@ -3970,20 +4142,37 @@ function useTranslationComparison({
3970
4142
  },
3971
4143
  [itemByPath, workspace]
3972
4144
  );
3973
- const translateAll = (0, import_react6.useCallback)(async () => {
3974
- const result = await workspace.translateAll();
3975
- if (result.report !== void 0) return result.report;
3976
- throw new Error(result.error?.type ?? "Translation failed.");
3977
- }, [workspace]);
4145
+ const translateAll = (0, import_react6.useCallback)(
4146
+ async (options) => {
4147
+ const result = await workspace.translateAll(options);
4148
+ if (result.report !== void 0) {
4149
+ setReport(result.report);
4150
+ return result.report;
4151
+ }
4152
+ throw new Error(result.error?.type ?? "Translation failed.");
4153
+ },
4154
+ [workspace]
4155
+ );
3978
4156
  return {
3979
4157
  sourceLocale: workspace.sourceLocale,
3980
4158
  targetLocale: workspace.targetLocale,
4159
+ targetLocales: workspace.targetLocales,
4160
+ localeOptions: workspace.localeOptions,
4161
+ setTargetLocale: workspace.setTargetLocale,
4162
+ addLocale: workspace.addLocale,
4163
+ isAddLocaleAllowed: workspace.isAddLocaleAllowed,
4164
+ removeLocale: workspace.removeLocale,
4165
+ ...workspace.removeLocaleConfirmation === void 0 ? {} : { removeLocaleConfirmation: workspace.removeLocaleConfirmation },
3981
4166
  items,
3982
4167
  summary: summaryFor(items),
3983
4168
  isTranslating: workspace.isTranslating,
3984
4169
  updateTranslation,
3985
4170
  translateSingle,
3986
- translateAll
4171
+ translateAll,
4172
+ cancelTranslation: workspace.cancelTranslation,
4173
+ ...workspace.progress === void 0 ? {} : { progress: workspace.progress },
4174
+ ...workspace.error === void 0 ? {} : { error: workspace.error },
4175
+ ...report === void 0 ? {} : { report }
3987
4176
  };
3988
4177
  }
3989
4178