@form-engine-ts/react 6.2.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
@@ -97,8 +97,9 @@ its `onConfirm` callback completes the locale removal.
97
97
 
98
98
  `useTranslationComparison` provides a focused, controlled comparison model for form, page, field, and option text.
99
99
  Its items expose source text, target text, status, canonical metadata, and path-based `updateTranslation`,
100
- `translateSingle`, and `translateAll` actions. Comparison row slots receive locale labels, read-only state, field type,
101
- question and option indexes, plus async single-translation 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.
102
103
 
103
104
  Wrap a builder or renderer in `FormEngineI18nProvider` to supply a UI locale and typed Core translator independently
104
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
  };
@@ -3504,7 +3531,8 @@ function useTranslationWorkspace({
3504
3531
  onTranslationReport,
3505
3532
  onTranslationError,
3506
3533
  onTranslationChange,
3507
- validateLocale
3534
+ validateLocale,
3535
+ createTranslationMetadata: metadataFactory
3508
3536
  }) {
3509
3537
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3510
3538
  const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
@@ -3518,20 +3546,31 @@ function useTranslationWorkspace({
3518
3546
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3519
3547
  const targetLocales = (0, import_react5.useMemo)(() => {
3520
3548
  const locales = (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales;
3521
- return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
3522
- (locale) => locale !== sourceLocale
3523
- );
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);
3524
3555
  }, [currentSchema, sourceLocale]);
3525
3556
  const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3526
3557
  const localeOptions = (0, import_react5.useMemo)(() => {
3527
3558
  if (availableLocales === void 0) {
3528
3559
  return targetLocales.map((locale) => ({ locale, label: locale }));
3529
3560
  }
3530
- return availableLocales.map((candidate) => {
3531
- if (typeof candidate !== "string") return candidate;
3532
- return { locale: (0, import_core5.normalizeLocale)(candidate) ?? candidate, label: candidate };
3533
- });
3534
- }, [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]);
3535
3574
  const slots = (0, import_react5.useMemo)(
3536
3575
  () => activeLocale.length === 0 ? [] : (0, import_core5.collectTranslationSlots)(currentSchema, activeLocale),
3537
3576
  [activeLocale, currentSchema]
@@ -3565,8 +3604,8 @@ function useTranslationWorkspace({
3565
3604
  const setTranslation = (0, import_react5.useCallback)(
3566
3605
  (slot, text) => {
3567
3606
  if (readOnly) return;
3568
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3569
- 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));
3570
3609
  onTranslationChange?.({
3571
3610
  slot,
3572
3611
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3575,7 +3614,7 @@ function useTranslationWorkspace({
3575
3614
  metadata
3576
3615
  });
3577
3616
  },
3578
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3617
+ [commit, currentSchema, metadataFactory, onTranslationChange, readOnly, sourceLocale]
3579
3618
  );
3580
3619
  const addLocale = (0, import_react5.useCallback)(
3581
3620
  (locale) => {
@@ -3587,14 +3626,21 @@ function useTranslationWorkspace({
3587
3626
  const normalized = (0, import_core5.normalizeLocale)(locale);
3588
3627
  if (normalized === null) {
3589
3628
  const workspaceError = workspaceLocaleValidationError(
3590
- validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales),
3629
+ validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales, sourceLocale),
3591
3630
  (0, import_core5.collectSchemaLocales)(currentSchema).allUniqueLocales.size,
3592
3631
  locale
3593
3632
  );
3594
3633
  setError(workspaceError);
3595
3634
  return { success: false, error: workspaceError };
3596
3635
  }
3597
- 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
+ );
3598
3644
  if (!validation.valid) {
3599
3645
  const workspaceError = workspaceLocaleValidationError(
3600
3646
  validation,
@@ -3613,16 +3659,16 @@ function useTranslationWorkspace({
3613
3659
  onLocaleAdded?.(normalized);
3614
3660
  return { success: true };
3615
3661
  },
3616
- [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, validateLocale]
3662
+ [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, sourceLocale, validateLocale]
3617
3663
  );
3618
3664
  const isAddLocaleAllowed = (0, import_react5.useCallback)(
3619
3665
  (locale) => {
3620
3666
  if (readOnly) return false;
3621
3667
  const normalized = (0, import_core5.normalizeLocale)(locale);
3622
3668
  if (normalized === null) return false;
3623
- return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales).valid;
3669
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales, sourceLocale).valid;
3624
3670
  },
3625
- [availableLocales, currentSchema, policy, readOnly, validateLocale]
3671
+ [availableLocales, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3626
3672
  );
3627
3673
  const removeLocale = (0, import_react5.useCallback)(
3628
3674
  (locale) => {
@@ -3772,6 +3818,7 @@ function useTranslationWorkspace({
3772
3818
  ...options,
3773
3819
  continueOnError: true,
3774
3820
  signal: controller.signal,
3821
+ createMetadata: (slot, translatedText) => options.createMetadata?.(slot, translatedText) ?? createTranslationMetadata(slot, sourceLocale, translatedText, "automatic", metadataFactory),
3775
3822
  onProgress: (nextProgress) => {
3776
3823
  setProgress(nextProgress);
3777
3824
  options.onProgress?.(nextProgress);
@@ -3844,6 +3891,7 @@ function useTranslationWorkspace({
3844
3891
  readOnly,
3845
3892
  slots,
3846
3893
  sourceLocale,
3894
+ metadataFactory,
3847
3895
  translationAdapter,
3848
3896
  signal
3849
3897
  ]
@@ -3880,8 +3928,8 @@ function useTranslationWorkspace({
3880
3928
  sourceLocale,
3881
3929
  controller.signal
3882
3930
  );
3883
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3884
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3931
+ const metadata = createTranslationMetadata(slot, sourceLocale, text, "automatic", metadataFactory);
3932
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic", metadata));
3885
3933
  onTranslationChange?.({
3886
3934
  slot,
3887
3935
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3904,6 +3952,7 @@ function useTranslationWorkspace({
3904
3952
  cause
3905
3953
  };
3906
3954
  setError(workspaceError);
3955
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError });
3907
3956
  return { success: false, error: workspaceError };
3908
3957
  } finally {
3909
3958
  if (operationSignal !== void 0 && abortListener !== void 0) {
@@ -3914,7 +3963,17 @@ function useTranslationWorkspace({
3914
3963
  setIsTranslating(false);
3915
3964
  }
3916
3965
  },
3917
- [commit, currentSchema, onTranslationChange, onTranslationError, readOnly, signal, sourceLocale, translationAdapter]
3966
+ [
3967
+ commit,
3968
+ currentSchema,
3969
+ metadataFactory,
3970
+ onTranslationChange,
3971
+ onTranslationError,
3972
+ readOnly,
3973
+ signal,
3974
+ sourceLocale,
3975
+ translationAdapter
3976
+ ]
3918
3977
  );
3919
3978
  return {
3920
3979
  sourceLocale,
@@ -3969,6 +4028,7 @@ function optionParentId(slot) {
3969
4028
  return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3970
4029
  }
3971
4030
  function getNodeTitle(schema, slot) {
4031
+ if (slot.kind === "form") return schema.title;
3972
4032
  if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3973
4033
  if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3974
4034
  if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
@@ -3986,6 +4046,7 @@ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3986
4046
  return {
3987
4047
  id: path,
3988
4048
  path,
4049
+ nodeId: slot.nodeId,
3989
4050
  targetKind: slot.kind,
3990
4051
  targetProperty: slot.property,
3991
4052
  ...nodeTitle === void 0 ? {} : { nodeTitle },
@@ -4019,23 +4080,45 @@ function useTranslationComparison({
4019
4080
  targetLocale,
4020
4081
  translationAdapter,
4021
4082
  readOnly = false,
4083
+ availableLocales,
4084
+ policy,
4022
4085
  onChange,
4023
4086
  onTranslationChange,
4024
4087
  onTranslationReport,
4025
4088
  onTranslationError,
4026
- signal
4089
+ signal,
4090
+ onLocaleAdded,
4091
+ onLocaleRemoved,
4092
+ onLocaleChange,
4093
+ beforeRemoveLocale,
4094
+ confirmRemoveLocale,
4095
+ onTranslationStart,
4096
+ onTranslationSuccess,
4097
+ validateLocale,
4098
+ createTranslationMetadata: createTranslationMetadata2
4027
4099
  }) {
4028
4100
  const workspace = useTranslationWorkspace({
4029
4101
  schema,
4030
4102
  sourceLocale,
4031
4103
  targetLocale,
4104
+ ...availableLocales === void 0 ? {} : { availableLocales },
4105
+ ...policy === void 0 ? {} : { policy },
4032
4106
  ...translationAdapter === void 0 ? {} : { translationAdapter },
4033
4107
  readOnly,
4034
4108
  ...onChange === void 0 ? {} : { onChange },
4035
4109
  ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4036
4110
  ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4037
4111
  ...onTranslationError === void 0 ? {} : { onTranslationError },
4038
- ...signal === void 0 ? {} : { signal }
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 }
4039
4122
  });
4040
4123
  const [report, setReport] = (0, import_react6.useState)();
4041
4124
  const items = (0, import_react6.useMemo)(
@@ -4073,6 +4156,13 @@ function useTranslationComparison({
4073
4156
  return {
4074
4157
  sourceLocale: workspace.sourceLocale,
4075
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 },
4076
4166
  items,
4077
4167
  summary: summaryFor(items),
4078
4168
  isTranslating: workspace.isTranslating,
package/dist/index.d.cts CHANGED
@@ -154,6 +154,11 @@ interface UseTranslationWorkspaceOptions {
154
154
  readonly error: TranslationWorkspaceError;
155
155
  }) => void;
156
156
  readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
157
+ readonly createTranslationMetadata?: (params: {
158
+ readonly slot: TranslationSlot;
159
+ readonly translatedText: string;
160
+ readonly mode: "manual" | "automatic";
161
+ }) => Readonly<Record<string, _form_engine_ts_core.JsonValue>>;
157
162
  readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
158
163
  }
159
164
  interface LocaleValidationContext {
@@ -166,7 +171,7 @@ type CustomLocaleValidator = (locale: string, context: LocaleValidationContext)
166
171
  interface LocaleValidationResult {
167
172
  readonly valid: boolean;
168
173
  readonly error?: {
169
- readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
174
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "source_locale" | "custom_validation_failed";
170
175
  readonly message: string;
171
176
  };
172
177
  }
@@ -184,6 +189,9 @@ type TranslationWorkspaceError = {
184
189
  } | {
185
190
  readonly type: "locale_already_exists";
186
191
  readonly locale: string;
192
+ } | {
193
+ readonly type: "source_locale";
194
+ readonly locale: string;
187
195
  } | {
188
196
  readonly type: "invalid_locale_format";
189
197
  readonly locale: string;
@@ -243,8 +251,8 @@ interface UseTranslationWorkspaceResult {
243
251
  readonly progress?: TranslationProgress;
244
252
  readonly error?: TranslationWorkspaceError;
245
253
  }
246
- declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
247
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, signal, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationReport, onTranslationError, onTranslationChange, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
254
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[], sourceLocale?: string) => LocaleValidationResult;
255
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, signal, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationReport, onTranslationError, onTranslationChange, validateLocale, createTranslationMetadata: metadataFactory }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
248
256
 
249
257
  interface SubmissionReceipt {
250
258
  readonly formId: string;
@@ -529,6 +537,7 @@ interface TranslationSlotRowProps {
529
537
  interface TranslationComparisonItem {
530
538
  readonly id: string;
531
539
  readonly path: string;
540
+ readonly nodeId?: string;
532
541
  readonly targetKind: "form" | "page" | "field" | "option";
533
542
  readonly targetProperty: "title" | "description" | "label" | "completionMessage";
534
543
  readonly nodeTitle?: string;
@@ -558,6 +567,7 @@ interface TranslationComparisonHeaderProps {
558
567
  }
559
568
  interface TranslationComparisonItemRowProps {
560
569
  readonly item: TranslationComparisonItem;
570
+ readonly nodeKind?: "form" | "page" | "field" | "option";
561
571
  readonly questionIndex?: number;
562
572
  readonly fieldType?: string;
563
573
  readonly optionIndex?: number;
@@ -571,6 +581,8 @@ interface UseTranslationComparisonOptions {
571
581
  readonly schema: FormSchema;
572
582
  readonly sourceLocale?: string;
573
583
  readonly targetLocale: string;
584
+ readonly availableLocales?: readonly (string | LocaleOption)[];
585
+ readonly policy?: FormPolicy;
574
586
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
575
587
  readonly signal?: AbortSignal;
576
588
  readonly readOnly?: boolean;
@@ -581,10 +593,31 @@ interface UseTranslationComparisonOptions {
581
593
  readonly targetLocale: string;
582
594
  readonly error: TranslationWorkspaceError;
583
595
  }) => void;
596
+ readonly onLocaleAdded?: (locale: string) => void;
597
+ readonly onLocaleRemoved?: (locale: string) => void;
598
+ readonly onLocaleChange?: (locale: string) => void;
599
+ readonly beforeRemoveLocale?: (locale: string, context: {
600
+ readonly slotCount: number;
601
+ }) => Promise<boolean> | boolean;
602
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
603
+ readonly onTranslationStart?: (params: {
604
+ readonly targetLocale: string;
605
+ readonly mode: "manual" | "automatic";
606
+ }) => void;
607
+ readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
608
+ readonly validateLocale?: UseTranslationWorkspaceOptions["validateLocale"];
609
+ readonly createTranslationMetadata?: UseTranslationWorkspaceOptions["createTranslationMetadata"];
584
610
  }
585
611
  interface UseTranslationComparisonResult {
586
612
  readonly sourceLocale: string;
587
613
  readonly targetLocale: string;
614
+ readonly targetLocales: readonly string[];
615
+ readonly localeOptions: readonly LocaleOption[];
616
+ readonly setTargetLocale: (locale: string) => void;
617
+ readonly addLocale: UseTranslationWorkspaceResult["addLocale"];
618
+ readonly isAddLocaleAllowed: UseTranslationWorkspaceResult["isAddLocaleAllowed"];
619
+ readonly removeLocale: UseTranslationWorkspaceResult["removeLocale"];
620
+ readonly removeLocaleConfirmation?: ReactNode;
588
621
  readonly items: readonly TranslationComparisonItem[];
589
622
  readonly summary: TranslationComparisonSummary;
590
623
  readonly isTranslating: boolean;
@@ -962,7 +995,7 @@ interface FieldState {
962
995
  }
963
996
  declare function useField(fieldId: string): FieldState;
964
997
 
965
- declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange, onTranslationReport, onTranslationError, signal }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
998
+ declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, availableLocales, policy, onChange, onTranslationChange, onTranslationReport, onTranslationError, signal, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, onTranslationStart, onTranslationSuccess, validateLocale, createTranslationMetadata }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
966
999
 
967
1000
  declare const BUILDER_TRANSLATION_KEYS: {
968
1001
  readonly ADD_FIELD: "builder.actions.addField";
package/dist/index.d.ts CHANGED
@@ -154,6 +154,11 @@ interface UseTranslationWorkspaceOptions {
154
154
  readonly error: TranslationWorkspaceError;
155
155
  }) => void;
156
156
  readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
157
+ readonly createTranslationMetadata?: (params: {
158
+ readonly slot: TranslationSlot;
159
+ readonly translatedText: string;
160
+ readonly mode: "manual" | "automatic";
161
+ }) => Readonly<Record<string, _form_engine_ts_core.JsonValue>>;
157
162
  readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
158
163
  }
159
164
  interface LocaleValidationContext {
@@ -166,7 +171,7 @@ type CustomLocaleValidator = (locale: string, context: LocaleValidationContext)
166
171
  interface LocaleValidationResult {
167
172
  readonly valid: boolean;
168
173
  readonly error?: {
169
- readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
174
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "source_locale" | "custom_validation_failed";
170
175
  readonly message: string;
171
176
  };
172
177
  }
@@ -184,6 +189,9 @@ type TranslationWorkspaceError = {
184
189
  } | {
185
190
  readonly type: "locale_already_exists";
186
191
  readonly locale: string;
192
+ } | {
193
+ readonly type: "source_locale";
194
+ readonly locale: string;
187
195
  } | {
188
196
  readonly type: "invalid_locale_format";
189
197
  readonly locale: string;
@@ -243,8 +251,8 @@ interface UseTranslationWorkspaceResult {
243
251
  readonly progress?: TranslationProgress;
244
252
  readonly error?: TranslationWorkspaceError;
245
253
  }
246
- declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
247
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, signal, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationReport, onTranslationError, onTranslationChange, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
254
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[], sourceLocale?: string) => LocaleValidationResult;
255
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, signal, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationReport, onTranslationError, onTranslationChange, validateLocale, createTranslationMetadata: metadataFactory }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
248
256
 
249
257
  interface SubmissionReceipt {
250
258
  readonly formId: string;
@@ -529,6 +537,7 @@ interface TranslationSlotRowProps {
529
537
  interface TranslationComparisonItem {
530
538
  readonly id: string;
531
539
  readonly path: string;
540
+ readonly nodeId?: string;
532
541
  readonly targetKind: "form" | "page" | "field" | "option";
533
542
  readonly targetProperty: "title" | "description" | "label" | "completionMessage";
534
543
  readonly nodeTitle?: string;
@@ -558,6 +567,7 @@ interface TranslationComparisonHeaderProps {
558
567
  }
559
568
  interface TranslationComparisonItemRowProps {
560
569
  readonly item: TranslationComparisonItem;
570
+ readonly nodeKind?: "form" | "page" | "field" | "option";
561
571
  readonly questionIndex?: number;
562
572
  readonly fieldType?: string;
563
573
  readonly optionIndex?: number;
@@ -571,6 +581,8 @@ interface UseTranslationComparisonOptions {
571
581
  readonly schema: FormSchema;
572
582
  readonly sourceLocale?: string;
573
583
  readonly targetLocale: string;
584
+ readonly availableLocales?: readonly (string | LocaleOption)[];
585
+ readonly policy?: FormPolicy;
574
586
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
575
587
  readonly signal?: AbortSignal;
576
588
  readonly readOnly?: boolean;
@@ -581,10 +593,31 @@ interface UseTranslationComparisonOptions {
581
593
  readonly targetLocale: string;
582
594
  readonly error: TranslationWorkspaceError;
583
595
  }) => void;
596
+ readonly onLocaleAdded?: (locale: string) => void;
597
+ readonly onLocaleRemoved?: (locale: string) => void;
598
+ readonly onLocaleChange?: (locale: string) => void;
599
+ readonly beforeRemoveLocale?: (locale: string, context: {
600
+ readonly slotCount: number;
601
+ }) => Promise<boolean> | boolean;
602
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
603
+ readonly onTranslationStart?: (params: {
604
+ readonly targetLocale: string;
605
+ readonly mode: "manual" | "automatic";
606
+ }) => void;
607
+ readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
608
+ readonly validateLocale?: UseTranslationWorkspaceOptions["validateLocale"];
609
+ readonly createTranslationMetadata?: UseTranslationWorkspaceOptions["createTranslationMetadata"];
584
610
  }
585
611
  interface UseTranslationComparisonResult {
586
612
  readonly sourceLocale: string;
587
613
  readonly targetLocale: string;
614
+ readonly targetLocales: readonly string[];
615
+ readonly localeOptions: readonly LocaleOption[];
616
+ readonly setTargetLocale: (locale: string) => void;
617
+ readonly addLocale: UseTranslationWorkspaceResult["addLocale"];
618
+ readonly isAddLocaleAllowed: UseTranslationWorkspaceResult["isAddLocaleAllowed"];
619
+ readonly removeLocale: UseTranslationWorkspaceResult["removeLocale"];
620
+ readonly removeLocaleConfirmation?: ReactNode;
588
621
  readonly items: readonly TranslationComparisonItem[];
589
622
  readonly summary: TranslationComparisonSummary;
590
623
  readonly isTranslating: boolean;
@@ -962,7 +995,7 @@ interface FieldState {
962
995
  }
963
996
  declare function useField(fieldId: string): FieldState;
964
997
 
965
- declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange, onTranslationReport, onTranslationError, signal }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
998
+ declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, availableLocales, policy, onChange, onTranslationChange, onTranslationReport, onTranslationError, signal, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, onTranslationStart, onTranslationSuccess, validateLocale, createTranslationMetadata }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
966
999
 
967
1000
  declare const BUILDER_TRANSLATION_KEYS: {
968
1001
  readonly ADD_FIELD: "builder.actions.addField";
package/dist/index.js CHANGED
@@ -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
  };
@@ -3480,7 +3507,8 @@ function useTranslationWorkspace({
3480
3507
  onTranslationReport,
3481
3508
  onTranslationError,
3482
3509
  onTranslationChange,
3483
- validateLocale
3510
+ validateLocale,
3511
+ createTranslationMetadata: metadataFactory
3484
3512
  }) {
3485
3513
  const [draftSchema, setDraftSchema] = useState4(schema);
3486
3514
  const [selectedLocale, setSelectedLocale] = useState4(normalizeLocale(targetLocale ?? "") ?? targetLocale ?? "");
@@ -3494,20 +3522,31 @@ function useTranslationWorkspace({
3494
3522
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
3495
3523
  const targetLocales = useMemo4(() => {
3496
3524
  const locales = collectSchemaLocales2(currentSchema).allUniqueLocales;
3497
- return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
3498
- (locale) => locale !== sourceLocale
3499
- );
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);
3500
3531
  }, [currentSchema, sourceLocale]);
3501
3532
  const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3502
3533
  const localeOptions = useMemo4(() => {
3503
3534
  if (availableLocales === void 0) {
3504
3535
  return targetLocales.map((locale) => ({ locale, label: locale }));
3505
3536
  }
3506
- return availableLocales.map((candidate) => {
3507
- if (typeof candidate !== "string") return candidate;
3508
- return { locale: normalizeLocale(candidate) ?? candidate, label: candidate };
3509
- });
3510
- }, [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]);
3511
3550
  const slots = useMemo4(
3512
3551
  () => activeLocale.length === 0 ? [] : collectTranslationSlots(currentSchema, activeLocale),
3513
3552
  [activeLocale, currentSchema]
@@ -3541,8 +3580,8 @@ function useTranslationWorkspace({
3541
3580
  const setTranslation = useCallback3(
3542
3581
  (slot, text) => {
3543
3582
  if (readOnly) return;
3544
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "manual");
3545
- 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));
3546
3585
  onTranslationChange?.({
3547
3586
  slot,
3548
3587
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3551,7 +3590,7 @@ function useTranslationWorkspace({
3551
3590
  metadata
3552
3591
  });
3553
3592
  },
3554
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale]
3593
+ [commit, currentSchema, metadataFactory, onTranslationChange, readOnly, sourceLocale]
3555
3594
  );
3556
3595
  const addLocale = useCallback3(
3557
3596
  (locale) => {
@@ -3563,14 +3602,21 @@ function useTranslationWorkspace({
3563
3602
  const normalized = normalizeLocale(locale);
3564
3603
  if (normalized === null) {
3565
3604
  const workspaceError = workspaceLocaleValidationError(
3566
- validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales),
3605
+ validateLocalePipeline(locale, currentSchema, policy, validateLocale, availableLocales, sourceLocale),
3567
3606
  collectSchemaLocales2(currentSchema).allUniqueLocales.size,
3568
3607
  locale
3569
3608
  );
3570
3609
  setError(workspaceError);
3571
3610
  return { success: false, error: workspaceError };
3572
3611
  }
3573
- 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
+ );
3574
3620
  if (!validation.valid) {
3575
3621
  const workspaceError = workspaceLocaleValidationError(
3576
3622
  validation,
@@ -3589,16 +3635,16 @@ function useTranslationWorkspace({
3589
3635
  onLocaleAdded?.(normalized);
3590
3636
  return { success: true };
3591
3637
  },
3592
- [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, validateLocale]
3638
+ [availableLocales, commit, currentSchema, onLocaleAdded, policy, readOnly, sourceLocale, validateLocale]
3593
3639
  );
3594
3640
  const isAddLocaleAllowed = useCallback3(
3595
3641
  (locale) => {
3596
3642
  if (readOnly) return false;
3597
3643
  const normalized = normalizeLocale(locale);
3598
3644
  if (normalized === null) return false;
3599
- return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales).valid;
3645
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale, availableLocales, sourceLocale).valid;
3600
3646
  },
3601
- [availableLocales, currentSchema, policy, readOnly, validateLocale]
3647
+ [availableLocales, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3602
3648
  );
3603
3649
  const removeLocale = useCallback3(
3604
3650
  (locale) => {
@@ -3748,6 +3794,7 @@ function useTranslationWorkspace({
3748
3794
  ...options,
3749
3795
  continueOnError: true,
3750
3796
  signal: controller.signal,
3797
+ createMetadata: (slot, translatedText) => options.createMetadata?.(slot, translatedText) ?? createTranslationMetadata(slot, sourceLocale, translatedText, "automatic", metadataFactory),
3751
3798
  onProgress: (nextProgress) => {
3752
3799
  setProgress(nextProgress);
3753
3800
  options.onProgress?.(nextProgress);
@@ -3820,6 +3867,7 @@ function useTranslationWorkspace({
3820
3867
  readOnly,
3821
3868
  slots,
3822
3869
  sourceLocale,
3870
+ metadataFactory,
3823
3871
  translationAdapter,
3824
3872
  signal
3825
3873
  ]
@@ -3856,8 +3904,8 @@ function useTranslationWorkspace({
3856
3904
  sourceLocale,
3857
3905
  controller.signal
3858
3906
  );
3859
- const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3860
- commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3907
+ const metadata = createTranslationMetadata(slot, sourceLocale, text, "automatic", metadataFactory);
3908
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic", metadata));
3861
3909
  onTranslationChange?.({
3862
3910
  slot,
3863
3911
  ...slot.existingText === void 0 ? {} : { previousText: slot.existingText },
@@ -3880,6 +3928,7 @@ function useTranslationWorkspace({
3880
3928
  cause
3881
3929
  };
3882
3930
  setError(workspaceError);
3931
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError });
3883
3932
  return { success: false, error: workspaceError };
3884
3933
  } finally {
3885
3934
  if (operationSignal !== void 0 && abortListener !== void 0) {
@@ -3890,7 +3939,17 @@ function useTranslationWorkspace({
3890
3939
  setIsTranslating(false);
3891
3940
  }
3892
3941
  },
3893
- [commit, currentSchema, onTranslationChange, onTranslationError, readOnly, signal, sourceLocale, translationAdapter]
3942
+ [
3943
+ commit,
3944
+ currentSchema,
3945
+ metadataFactory,
3946
+ onTranslationChange,
3947
+ onTranslationError,
3948
+ readOnly,
3949
+ signal,
3950
+ sourceLocale,
3951
+ translationAdapter
3952
+ ]
3894
3953
  );
3895
3954
  return {
3896
3955
  sourceLocale,
@@ -3945,6 +4004,7 @@ function optionParentId(slot) {
3945
4004
  return /^fields\.([^.]+)\.options\./u.exec(slot.path)?.[1];
3946
4005
  }
3947
4006
  function getNodeTitle(schema, slot) {
4007
+ if (slot.kind === "form") return schema.title;
3948
4008
  if (slot.kind === "field") return schema.fields.find((field) => field.id === slot.nodeId)?.title;
3949
4009
  if (slot.kind === "page") return schema.pages?.find((page) => page.id === slot.nodeId)?.title;
3950
4010
  if (slot.kind === "option") return schema.fields.find((field) => field.id === optionParentId(slot))?.title;
@@ -3962,6 +4022,7 @@ function comparisonItem(schema, slot, sourceLocale, hasAdapter) {
3962
4022
  return {
3963
4023
  id: path,
3964
4024
  path,
4025
+ nodeId: slot.nodeId,
3965
4026
  targetKind: slot.kind,
3966
4027
  targetProperty: slot.property,
3967
4028
  ...nodeTitle === void 0 ? {} : { nodeTitle },
@@ -3995,23 +4056,45 @@ function useTranslationComparison({
3995
4056
  targetLocale,
3996
4057
  translationAdapter,
3997
4058
  readOnly = false,
4059
+ availableLocales,
4060
+ policy,
3998
4061
  onChange,
3999
4062
  onTranslationChange,
4000
4063
  onTranslationReport,
4001
4064
  onTranslationError,
4002
- signal
4065
+ signal,
4066
+ onLocaleAdded,
4067
+ onLocaleRemoved,
4068
+ onLocaleChange,
4069
+ beforeRemoveLocale,
4070
+ confirmRemoveLocale,
4071
+ onTranslationStart,
4072
+ onTranslationSuccess,
4073
+ validateLocale,
4074
+ createTranslationMetadata: createTranslationMetadata2
4003
4075
  }) {
4004
4076
  const workspace = useTranslationWorkspace({
4005
4077
  schema,
4006
4078
  sourceLocale,
4007
4079
  targetLocale,
4080
+ ...availableLocales === void 0 ? {} : { availableLocales },
4081
+ ...policy === void 0 ? {} : { policy },
4008
4082
  ...translationAdapter === void 0 ? {} : { translationAdapter },
4009
4083
  readOnly,
4010
4084
  ...onChange === void 0 ? {} : { onChange },
4011
4085
  ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4012
4086
  ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4013
4087
  ...onTranslationError === void 0 ? {} : { onTranslationError },
4014
- ...signal === void 0 ? {} : { signal }
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 }
4015
4098
  });
4016
4099
  const [report, setReport] = useState5();
4017
4100
  const items = useMemo5(
@@ -4049,6 +4132,13 @@ function useTranslationComparison({
4049
4132
  return {
4050
4133
  sourceLocale: workspace.sourceLocale,
4051
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 },
4052
4142
  items,
4053
4143
  summary: summaryFor(items),
4054
4144
  isTranslating: workspace.isTranslating,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "6.2.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/core": "6.2.0",
46
- "@form-engine-ts/privacy": "6.2.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",