@form-engine-ts/react 6.1.0 → 6.2.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,16 @@ 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 locale labels, read-only state, field type,
101
+ question and option indexes, plus async single-translation actions.
99
102
 
100
103
  Wrap a builder or renderer in `FormEngineI18nProvider` to supply a UI locale and typed Core translator independently
101
104
  from the schema's `defaultLocale` and `supportedLocales`:
package/dist/index.cjs CHANGED
@@ -3489,6 +3489,7 @@ function useTranslationWorkspace({
3489
3489
  sourceLocale = schema.defaultLocale ?? "en",
3490
3490
  targetLocale,
3491
3491
  translationAdapter,
3492
+ signal,
3492
3493
  readOnly = false,
3493
3494
  policy,
3494
3495
  availableLocales,
@@ -3500,6 +3501,7 @@ function useTranslationWorkspace({
3500
3501
  slots: workspaceSlots,
3501
3502
  onTranslationStart,
3502
3503
  onTranslationSuccess,
3504
+ onTranslationReport,
3503
3505
  onTranslationError,
3504
3506
  onTranslationChange,
3505
3507
  validateLocale
@@ -3507,8 +3509,10 @@ function useTranslationWorkspace({
3507
3509
  const [draftSchema, setDraftSchema] = (0, import_react5.useState)(schema);
3508
3510
  const [selectedLocale, setSelectedLocale] = (0, import_react5.useState)((0, import_core5.normalizeLocale)(targetLocale ?? "") ?? targetLocale ?? "");
3509
3511
  const [isTranslating, setIsTranslating] = (0, import_react5.useState)(false);
3512
+ const [progress, setProgress] = (0, import_react5.useState)();
3510
3513
  const [error, setError] = (0, import_react5.useState)();
3511
3514
  const [pendingRemoval, setPendingRemoval] = (0, import_react5.useState)();
3515
+ const activeAbortController = (0, import_react5.useRef)(void 0);
3512
3516
  const pendingRemovalResolver = (0, import_react5.useRef)(void 0);
3513
3517
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3514
3518
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
@@ -3748,7 +3752,15 @@ function useTranslationWorkspace({
3748
3752
  }
3749
3753
  setIsTranslating(true);
3750
3754
  setError(void 0);
3755
+ setProgress(void 0);
3751
3756
  onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3757
+ const controller = new AbortController();
3758
+ activeAbortController.current = controller;
3759
+ const operationSignal = options.signal ?? signal;
3760
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3761
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3762
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3763
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3752
3764
  try {
3753
3765
  const populated = await (0, import_core5.populateSchemaTranslations)(
3754
3766
  currentSchema,
@@ -3757,13 +3769,36 @@ function useTranslationWorkspace({
3757
3769
  {
3758
3770
  overwrite: "stale-and-missing",
3759
3771
  preserveManualTranslations: true,
3760
- ...options
3772
+ ...options,
3773
+ continueOnError: true,
3774
+ signal: controller.signal,
3775
+ onProgress: (nextProgress) => {
3776
+ setProgress(nextProgress);
3777
+ options.onProgress?.(nextProgress);
3778
+ }
3761
3779
  }
3762
3780
  );
3763
3781
  commit(populated.schema);
3782
+ onTranslationReport?.(populated.report);
3764
3783
  const missingSlotsCount = (0, import_core5.collectTranslationSlots)(populated.schema, activeLocale).filter(
3765
3784
  (slot) => slot.status === "missing"
3766
3785
  ).length;
3786
+ if (populated.report.cancelled === true) {
3787
+ const workspaceError = { type: "cancelled" };
3788
+ setError(workspaceError);
3789
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3790
+ return { success: false, report: populated.report, error: workspaceError };
3791
+ }
3792
+ if ((populated.report.failed ?? 0) > 0) {
3793
+ const workspaceError = {
3794
+ type: "partial_failure",
3795
+ succeeded: populated.report.succeeded ?? populated.report.updatedSlots.length,
3796
+ failed: populated.report.failed ?? 0
3797
+ };
3798
+ setError(workspaceError);
3799
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3800
+ return { success: false, report: populated.report, error: workspaceError };
3801
+ }
3767
3802
  onTranslationSuccess?.({
3768
3803
  sourceLocale,
3769
3804
  targetLocale: activeLocale,
@@ -3774,6 +3809,12 @@ function useTranslationWorkspace({
3774
3809
  });
3775
3810
  return { success: true, report: populated.report };
3776
3811
  } catch (cause) {
3812
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3813
+ const workspaceError2 = { type: "cancelled" };
3814
+ setError(workspaceError2);
3815
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError2 });
3816
+ return { success: false, error: workspaceError2 };
3817
+ }
3777
3818
  const workspaceError = {
3778
3819
  type: "translation_failed",
3779
3820
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3783,6 +3824,11 @@ function useTranslationWorkspace({
3783
3824
  onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3784
3825
  return { success: false, error: workspaceError };
3785
3826
  } finally {
3827
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3828
+ const listener = abortListener;
3829
+ operationSignal.removeEventListener("abort", listener);
3830
+ }
3831
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3786
3832
  setIsTranslating(false);
3787
3833
  }
3788
3834
  },
@@ -3794,14 +3840,19 @@ function useTranslationWorkspace({
3794
3840
  onTranslationError,
3795
3841
  onTranslationStart,
3796
3842
  onTranslationSuccess,
3843
+ onTranslationReport,
3797
3844
  readOnly,
3798
3845
  slots,
3799
3846
  sourceLocale,
3800
- translationAdapter
3847
+ translationAdapter,
3848
+ signal
3801
3849
  ]
3802
3850
  );
3851
+ const cancelTranslation = (0, import_react5.useCallback)(() => {
3852
+ activeAbortController.current?.abort(new DOMException("The translation was cancelled.", "AbortError"));
3853
+ }, []);
3803
3854
  const translateSlot = (0, import_react5.useCallback)(
3804
- async (slot) => {
3855
+ async (slot, options = {}) => {
3805
3856
  if (translationAdapter === void 0) {
3806
3857
  const workspaceError = { type: "adapter_not_configured" };
3807
3858
  setError(workspaceError);
@@ -3814,8 +3865,21 @@ function useTranslationWorkspace({
3814
3865
  }
3815
3866
  setIsTranslating(true);
3816
3867
  setError(void 0);
3868
+ setProgress({ total: 1, completed: 0, succeeded: 0, failed: 0, percentage: 0 });
3869
+ const controller = new AbortController();
3870
+ activeAbortController.current = controller;
3871
+ const operationSignal = options.signal ?? signal;
3872
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3873
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3874
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3875
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3817
3876
  try {
3818
- const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3877
+ const text = await asAsyncAdapter(translationAdapter).translateText(
3878
+ slot.sourceText,
3879
+ slot.locale,
3880
+ sourceLocale,
3881
+ controller.signal
3882
+ );
3819
3883
  const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3820
3884
  commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3821
3885
  onTranslationChange?.({
@@ -3825,8 +3889,15 @@ function useTranslationWorkspace({
3825
3889
  mode: "automatic",
3826
3890
  metadata
3827
3891
  });
3892
+ setProgress({ total: 1, completed: 1, succeeded: 1, failed: 0, percentage: 100 });
3828
3893
  return { success: true };
3829
3894
  } catch (cause) {
3895
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3896
+ const workspaceError2 = { type: "cancelled" };
3897
+ setError(workspaceError2);
3898
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError2 });
3899
+ return { success: false, error: workspaceError2 };
3900
+ }
3830
3901
  const workspaceError = {
3831
3902
  type: "translation_failed",
3832
3903
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3835,10 +3906,15 @@ function useTranslationWorkspace({
3835
3906
  setError(workspaceError);
3836
3907
  return { success: false, error: workspaceError };
3837
3908
  } finally {
3909
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3910
+ const listener = abortListener;
3911
+ operationSignal.removeEventListener("abort", listener);
3912
+ }
3913
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3838
3914
  setIsTranslating(false);
3839
3915
  }
3840
3916
  },
3841
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3917
+ [commit, currentSchema, onTranslationChange, onTranslationError, readOnly, signal, sourceLocale, translationAdapter]
3842
3918
  );
3843
3919
  return {
3844
3920
  sourceLocale,
@@ -3859,7 +3935,9 @@ function useTranslationWorkspace({
3859
3935
  setTranslation,
3860
3936
  translateAll,
3861
3937
  translateSlot,
3938
+ cancelTranslation,
3862
3939
  isTranslating,
3940
+ ...progress === void 0 ? {} : { progress },
3863
3941
  ...error === void 0 ? {} : { error }
3864
3942
  };
3865
3943
  }
@@ -3869,6 +3947,7 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3869
3947
  if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3870
3948
  if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3871
3949
  return {
3950
+ ...metadata,
3872
3951
  sourceLocale: metadata.sourceLocale,
3873
3952
  sourceTextHash: metadata.sourceTextHash,
3874
3953
  translationSource: metadata.translationSource,
@@ -3876,7 +3955,10 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3876
3955
  ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3877
3956
  };
3878
3957
  }
3958
+ const canonicalKeys = /* @__PURE__ */ new Set(["sourceLocale", "sourceTextHash", "translationSource", "translatedAt", "editedAt"]);
3959
+ const extensions = Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => !canonicalKeys.has(key)));
3879
3960
  return {
3961
+ ...extensions,
3880
3962
  sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3881
3963
  sourceTextHash: (0, import_core6.computeSourceTextHash)(sourceText),
3882
3964
  translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
@@ -3938,7 +4020,10 @@ function useTranslationComparison({
3938
4020
  translationAdapter,
3939
4021
  readOnly = false,
3940
4022
  onChange,
3941
- onTranslationChange
4023
+ onTranslationChange,
4024
+ onTranslationReport,
4025
+ onTranslationError,
4026
+ signal
3942
4027
  }) {
3943
4028
  const workspace = useTranslationWorkspace({
3944
4029
  schema,
@@ -3947,8 +4032,12 @@ function useTranslationComparison({
3947
4032
  ...translationAdapter === void 0 ? {} : { translationAdapter },
3948
4033
  readOnly,
3949
4034
  ...onChange === void 0 ? {} : { onChange },
3950
- ...onTranslationChange === void 0 ? {} : { onTranslationChange }
4035
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4036
+ ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4037
+ ...onTranslationError === void 0 ? {} : { onTranslationError },
4038
+ ...signal === void 0 ? {} : { signal }
3951
4039
  });
4040
+ const [report, setReport] = (0, import_react6.useState)();
3952
4041
  const items = (0, import_react6.useMemo)(
3953
4042
  () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
3954
4043
  [schema, sourceLocale, translationAdapter, workspace.slots]
@@ -3970,11 +4059,17 @@ function useTranslationComparison({
3970
4059
  },
3971
4060
  [itemByPath, workspace]
3972
4061
  );
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]);
4062
+ const translateAll = (0, import_react6.useCallback)(
4063
+ async (options) => {
4064
+ const result = await workspace.translateAll(options);
4065
+ if (result.report !== void 0) {
4066
+ setReport(result.report);
4067
+ return result.report;
4068
+ }
4069
+ throw new Error(result.error?.type ?? "Translation failed.");
4070
+ },
4071
+ [workspace]
4072
+ );
3978
4073
  return {
3979
4074
  sourceLocale: workspace.sourceLocale,
3980
4075
  targetLocale: workspace.targetLocale,
@@ -3983,7 +4078,11 @@ function useTranslationComparison({
3983
4078
  isTranslating: workspace.isTranslating,
3984
4079
  updateTranslation,
3985
4080
  translateSingle,
3986
- translateAll
4081
+ translateAll,
4082
+ cancelTranslation: workspace.cancelTranslation,
4083
+ ...workspace.progress === void 0 ? {} : { progress: workspace.progress },
4084
+ ...workspace.error === void 0 ? {} : { error: workspace.error },
4085
+ ...report === void 0 ? {} : { report }
3987
4086
  };
3988
4087
  }
3989
4088
 
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _form_engine_ts_core from '@form-engine-ts/core';
2
- import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, LocaleOption, TranslationStatus, CanonicalTranslationMetadata, TranslationSlot, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, TranslationMissingKeyEvent, FieldType } from '@form-engine-ts/core';
2
+ import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, TranslationAdapter, AsyncTranslationAdapter, LocaleOption, TranslationReport, TranslationSlot, PopulateTranslationOptions, TranslationProgress, Question, FieldOption, ValidationIssue, ValidationError, FormValues, TranslationStatus, CanonicalTranslationMetadata, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, TranslationMissingKeyEvent, FieldType } from '@form-engine-ts/core';
3
3
  export { FormSubmissionError, FormSubmissionSerializedError, QuestionType } from '@form-engine-ts/core';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
@@ -125,6 +125,127 @@ interface UseFormBuilderResult extends FormBuilderResult {
125
125
  }
126
126
  declare function useFormBuilder({ schema, onChange, policy, idFactory, factories, fieldEditorMode, activeFieldId: controlledActiveFieldId, defaultActiveFieldId, onActiveFieldChange }: FormBuilderOptions): FormBuilderResult;
127
127
 
128
+ interface UseTranslationWorkspaceOptions {
129
+ readonly schema: FormSchema;
130
+ readonly onChange?: (schema: FormSchema) => void;
131
+ readonly sourceLocale?: string;
132
+ readonly targetLocale?: string;
133
+ readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
134
+ readonly signal?: AbortSignal;
135
+ readonly readOnly?: boolean;
136
+ readonly policy?: FormPolicy;
137
+ readonly availableLocales?: readonly (string | LocaleOption)[];
138
+ readonly onLocaleAdded?: (locale: string) => void;
139
+ readonly onLocaleRemoved?: (locale: string) => void;
140
+ readonly onLocaleChange?: (targetLocale: string) => void;
141
+ readonly beforeRemoveLocale?: (locale: string, context: {
142
+ readonly slotCount: number;
143
+ }) => Promise<boolean> | boolean;
144
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
145
+ readonly slots?: Pick<TranslationWorkspaceSlots, "confirmRemoveLocale">;
146
+ readonly onTranslationStart?: (params: {
147
+ readonly targetLocale: string;
148
+ readonly mode: "manual" | "automatic";
149
+ }) => void;
150
+ readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
151
+ readonly onTranslationReport?: (report: TranslationReport) => void;
152
+ readonly onTranslationError?: (params: {
153
+ readonly targetLocale: string;
154
+ readonly error: TranslationWorkspaceError;
155
+ }) => void;
156
+ readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
157
+ readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
158
+ }
159
+ interface LocaleValidationContext {
160
+ readonly locale: string;
161
+ readonly defaultLocale: string;
162
+ readonly currentLocales: readonly string[];
163
+ readonly policy?: FormPolicy;
164
+ }
165
+ type CustomLocaleValidator = (locale: string, context: LocaleValidationContext) => LocaleValidationResult | boolean | string | undefined | void;
166
+ interface LocaleValidationResult {
167
+ readonly valid: boolean;
168
+ readonly error?: {
169
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
170
+ readonly message: string;
171
+ };
172
+ }
173
+ interface TranslationSummary {
174
+ readonly totalSlots: number;
175
+ readonly translatedCount: number;
176
+ readonly missingCount: number;
177
+ readonly staleCount: number;
178
+ readonly manualCount: number;
179
+ readonly completionPercentage: number;
180
+ }
181
+ type TranslationWorkspaceError = {
182
+ readonly type: "locale_not_allowed";
183
+ readonly locale: string;
184
+ } | {
185
+ readonly type: "locale_already_exists";
186
+ readonly locale: string;
187
+ } | {
188
+ readonly type: "invalid_locale_format";
189
+ readonly locale: string;
190
+ } | {
191
+ readonly type: "max_locales_exceeded";
192
+ readonly max: number;
193
+ readonly current: number;
194
+ } | {
195
+ readonly type: "read_only_mode";
196
+ } | {
197
+ readonly type: "adapter_not_configured";
198
+ } | {
199
+ readonly type: "target_locale_missing";
200
+ } | {
201
+ readonly type: "translation_failed";
202
+ readonly message: string;
203
+ readonly cause?: unknown;
204
+ } | {
205
+ readonly type: "partial_failure";
206
+ readonly succeeded: number;
207
+ readonly failed: number;
208
+ } | {
209
+ readonly type: "cancelled";
210
+ } | {
211
+ readonly type: "custom_validation_failed";
212
+ readonly message: string;
213
+ };
214
+ interface UseTranslationWorkspaceResult {
215
+ readonly sourceLocale: string;
216
+ readonly targetLocale: string;
217
+ readonly targetLocales: readonly string[];
218
+ readonly localeOptions: readonly LocaleOption[];
219
+ readonly setTargetLocale: (locale: string) => void;
220
+ readonly slots: readonly TranslationSlot[];
221
+ readonly summary: TranslationSummary;
222
+ readonly addLocale: (locale: string) => {
223
+ readonly success: boolean;
224
+ readonly error?: TranslationWorkspaceError;
225
+ };
226
+ readonly isAddLocaleAllowed: (locale: string) => boolean;
227
+ readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
228
+ readonly removeLocaleConfirmation?: ReactNode;
229
+ readonly setTranslation: (slot: TranslationSlot, text: string) => void;
230
+ readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
231
+ readonly success: boolean;
232
+ readonly report?: TranslationReport;
233
+ readonly error?: TranslationWorkspaceError;
234
+ }>;
235
+ readonly translateSlot: (slot: TranslationSlot, options?: {
236
+ readonly signal?: AbortSignal;
237
+ }) => Promise<{
238
+ readonly success: boolean;
239
+ readonly error?: TranslationWorkspaceError;
240
+ }>;
241
+ readonly cancelTranslation: () => void;
242
+ readonly isTranslating: boolean;
243
+ readonly progress?: TranslationProgress;
244
+ readonly error?: TranslationWorkspaceError;
245
+ }
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;
248
+
128
249
  interface SubmissionReceipt {
129
250
  readonly formId: string;
130
251
  readonly formVersion: number;
@@ -396,6 +517,8 @@ interface TranslationWorkspaceHeaderProps {
396
517
  readonly onTranslateAll: () => void;
397
518
  readonly isTranslating: boolean;
398
519
  readonly readOnly: boolean;
520
+ readonly progress?: TranslationProgress;
521
+ readonly onCancel?: () => void;
399
522
  }
400
523
  interface TranslationSlotRowProps {
401
524
  readonly slot: _form_engine_ts_core.TranslationSlot;
@@ -429,21 +552,35 @@ interface TranslationComparisonHeaderProps {
429
552
  readonly onTranslateAll: () => void;
430
553
  readonly isTranslating: boolean;
431
554
  readonly readOnly: boolean;
555
+ readonly report?: TranslationReport;
556
+ readonly progress?: TranslationProgress;
557
+ readonly onCancel?: () => void;
432
558
  }
433
559
  interface TranslationComparisonItemRowProps {
434
560
  readonly item: TranslationComparisonItem;
561
+ readonly questionIndex?: number;
562
+ readonly fieldType?: string;
563
+ readonly optionIndex?: number;
564
+ readonly sourceLocaleLabel?: string;
565
+ readonly targetLocaleLabel?: string;
435
566
  readonly readOnly: boolean;
436
567
  readonly onChange: (text: string) => void;
437
- readonly onTranslate: () => void;
568
+ readonly onTranslate: () => Promise<void>;
438
569
  }
439
570
  interface UseTranslationComparisonOptions {
440
571
  readonly schema: FormSchema;
441
572
  readonly sourceLocale?: string;
442
573
  readonly targetLocale: string;
443
- readonly translationAdapter?: TranslationAdapter;
574
+ readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
575
+ readonly signal?: AbortSignal;
444
576
  readonly readOnly?: boolean;
445
577
  readonly onChange?: (nextSchema: FormSchema) => void;
446
578
  readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
579
+ readonly onTranslationReport?: (report: TranslationReport) => void;
580
+ readonly onTranslationError?: (params: {
581
+ readonly targetLocale: string;
582
+ readonly error: TranslationWorkspaceError;
583
+ }) => void;
447
584
  }
448
585
  interface UseTranslationComparisonResult {
449
586
  readonly sourceLocale: string;
@@ -453,7 +590,11 @@ interface UseTranslationComparisonResult {
453
590
  readonly isTranslating: boolean;
454
591
  readonly updateTranslation: (path: string, text: string) => void;
455
592
  readonly translateSingle: (path: string) => Promise<void>;
456
- readonly translateAll: () => Promise<TranslationReport>;
593
+ readonly translateAll: (options?: PopulateTranslationOptions) => Promise<TranslationReport>;
594
+ readonly cancelTranslation: () => void;
595
+ readonly progress?: TranslationProgress;
596
+ readonly error?: TranslationWorkspaceError;
597
+ readonly report?: TranslationReport;
457
598
  }
458
599
  interface LocaleSelectorProps {
459
600
  readonly targetLocale: string;
@@ -496,6 +637,7 @@ interface ConfirmRemoveLocaleSlotProps {
496
637
  interface TranslationWorkspaceSlots {
497
638
  readonly renderHeader?: (props: TranslationWorkspaceHeaderProps) => ReactNode;
498
639
  readonly renderSlotRow?: (props: TranslationSlotRowProps) => ReactNode;
640
+ readonly renderItemRow?: (props: TranslationComparisonItemRowProps) => ReactNode;
499
641
  readonly renderLocaleSelector?: (props: LocaleSelectorProps) => ReactNode;
500
642
  readonly renderStatusBadge?: (props: {
501
643
  readonly status: _form_engine_ts_core.TranslationStatus;
@@ -820,116 +962,7 @@ interface FieldState {
820
962
  }
821
963
  declare function useField(fieldId: string): FieldState;
822
964
 
823
- declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
824
-
825
- interface UseTranslationWorkspaceOptions {
826
- readonly schema: FormSchema;
827
- readonly onChange?: (schema: FormSchema) => void;
828
- readonly sourceLocale?: string;
829
- readonly targetLocale?: string;
830
- readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
831
- readonly readOnly?: boolean;
832
- readonly policy?: FormPolicy;
833
- readonly availableLocales?: readonly (string | LocaleOption)[];
834
- readonly onLocaleAdded?: (locale: string) => void;
835
- readonly onLocaleRemoved?: (locale: string) => void;
836
- readonly onLocaleChange?: (targetLocale: string) => void;
837
- readonly beforeRemoveLocale?: (locale: string, context: {
838
- readonly slotCount: number;
839
- }) => Promise<boolean> | boolean;
840
- readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
841
- readonly slots?: Pick<TranslationWorkspaceSlots, "confirmRemoveLocale">;
842
- readonly onTranslationStart?: (params: {
843
- readonly targetLocale: string;
844
- readonly mode: "manual" | "automatic";
845
- }) => void;
846
- readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
847
- readonly onTranslationError?: (params: {
848
- readonly targetLocale: string;
849
- readonly error: TranslationWorkspaceError;
850
- }) => void;
851
- readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
852
- readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
853
- }
854
- interface LocaleValidationContext {
855
- readonly locale: string;
856
- readonly defaultLocale: string;
857
- readonly currentLocales: readonly string[];
858
- readonly policy?: FormPolicy;
859
- }
860
- type CustomLocaleValidator = (locale: string, context: LocaleValidationContext) => LocaleValidationResult | boolean | string | undefined | void;
861
- interface LocaleValidationResult {
862
- readonly valid: boolean;
863
- readonly error?: {
864
- readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
865
- readonly message: string;
866
- };
867
- }
868
- interface TranslationSummary {
869
- readonly totalSlots: number;
870
- readonly translatedCount: number;
871
- readonly missingCount: number;
872
- readonly staleCount: number;
873
- readonly manualCount: number;
874
- readonly completionPercentage: number;
875
- }
876
- type TranslationWorkspaceError = {
877
- readonly type: "locale_not_allowed";
878
- readonly locale: string;
879
- } | {
880
- readonly type: "locale_already_exists";
881
- readonly locale: string;
882
- } | {
883
- readonly type: "invalid_locale_format";
884
- readonly locale: string;
885
- } | {
886
- readonly type: "max_locales_exceeded";
887
- readonly max: number;
888
- readonly current: number;
889
- } | {
890
- readonly type: "read_only_mode";
891
- } | {
892
- readonly type: "adapter_not_configured";
893
- } | {
894
- readonly type: "target_locale_missing";
895
- } | {
896
- readonly type: "translation_failed";
897
- readonly message: string;
898
- readonly cause?: unknown;
899
- } | {
900
- readonly type: "custom_validation_failed";
901
- readonly message: string;
902
- };
903
- interface UseTranslationWorkspaceResult {
904
- readonly sourceLocale: string;
905
- readonly targetLocale: string;
906
- readonly targetLocales: readonly string[];
907
- readonly localeOptions: readonly LocaleOption[];
908
- readonly setTargetLocale: (locale: string) => void;
909
- readonly slots: readonly TranslationSlot[];
910
- readonly summary: TranslationSummary;
911
- readonly addLocale: (locale: string) => {
912
- readonly success: boolean;
913
- readonly error?: TranslationWorkspaceError;
914
- };
915
- readonly isAddLocaleAllowed: (locale: string) => boolean;
916
- readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
917
- readonly removeLocaleConfirmation?: ReactNode;
918
- readonly setTranslation: (slot: TranslationSlot, text: string) => void;
919
- readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
920
- readonly success: boolean;
921
- readonly report?: TranslationReport;
922
- readonly error?: TranslationWorkspaceError;
923
- }>;
924
- readonly translateSlot: (slot: TranslationSlot) => Promise<{
925
- readonly success: boolean;
926
- readonly error?: TranslationWorkspaceError;
927
- }>;
928
- readonly isTranslating: boolean;
929
- readonly error?: TranslationWorkspaceError;
930
- }
931
- declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
932
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationError, onTranslationChange, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
965
+ declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange, onTranslationReport, onTranslationError, signal }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
933
966
 
934
967
  declare const BUILDER_TRANSLATION_KEYS: {
935
968
  readonly ADD_FIELD: "builder.actions.addField";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _form_engine_ts_core from '@form-engine-ts/core';
2
- import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, Question, FieldOption, TranslationReport, ValidationIssue, ValidationError, FormValues, LocaleOption, TranslationStatus, CanonicalTranslationMetadata, TranslationSlot, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, TranslationMissingKeyEvent, FieldType } from '@form-engine-ts/core';
2
+ import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, TranslationAdapter, AsyncTranslationAdapter, LocaleOption, TranslationReport, TranslationSlot, PopulateTranslationOptions, TranslationProgress, Question, FieldOption, ValidationIssue, ValidationError, FormValues, TranslationStatus, CanonicalTranslationMetadata, FormValue, AnswerValidationResult, FormEngineTranslator, FormEngineMessages, TranslationMissingKeyEvent, FieldType } from '@form-engine-ts/core';
3
3
  export { FormSubmissionError, FormSubmissionSerializedError, QuestionType } from '@form-engine-ts/core';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, ComponentType, KeyboardEvent, MouseEvent, CSSProperties } from 'react';
@@ -125,6 +125,127 @@ interface UseFormBuilderResult extends FormBuilderResult {
125
125
  }
126
126
  declare function useFormBuilder({ schema, onChange, policy, idFactory, factories, fieldEditorMode, activeFieldId: controlledActiveFieldId, defaultActiveFieldId, onActiveFieldChange }: FormBuilderOptions): FormBuilderResult;
127
127
 
128
+ interface UseTranslationWorkspaceOptions {
129
+ readonly schema: FormSchema;
130
+ readonly onChange?: (schema: FormSchema) => void;
131
+ readonly sourceLocale?: string;
132
+ readonly targetLocale?: string;
133
+ readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
134
+ readonly signal?: AbortSignal;
135
+ readonly readOnly?: boolean;
136
+ readonly policy?: FormPolicy;
137
+ readonly availableLocales?: readonly (string | LocaleOption)[];
138
+ readonly onLocaleAdded?: (locale: string) => void;
139
+ readonly onLocaleRemoved?: (locale: string) => void;
140
+ readonly onLocaleChange?: (targetLocale: string) => void;
141
+ readonly beforeRemoveLocale?: (locale: string, context: {
142
+ readonly slotCount: number;
143
+ }) => Promise<boolean> | boolean;
144
+ readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
145
+ readonly slots?: Pick<TranslationWorkspaceSlots, "confirmRemoveLocale">;
146
+ readonly onTranslationStart?: (params: {
147
+ readonly targetLocale: string;
148
+ readonly mode: "manual" | "automatic";
149
+ }) => void;
150
+ readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
151
+ readonly onTranslationReport?: (report: TranslationReport) => void;
152
+ readonly onTranslationError?: (params: {
153
+ readonly targetLocale: string;
154
+ readonly error: TranslationWorkspaceError;
155
+ }) => void;
156
+ readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
157
+ readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
158
+ }
159
+ interface LocaleValidationContext {
160
+ readonly locale: string;
161
+ readonly defaultLocale: string;
162
+ readonly currentLocales: readonly string[];
163
+ readonly policy?: FormPolicy;
164
+ }
165
+ type CustomLocaleValidator = (locale: string, context: LocaleValidationContext) => LocaleValidationResult | boolean | string | undefined | void;
166
+ interface LocaleValidationResult {
167
+ readonly valid: boolean;
168
+ readonly error?: {
169
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
170
+ readonly message: string;
171
+ };
172
+ }
173
+ interface TranslationSummary {
174
+ readonly totalSlots: number;
175
+ readonly translatedCount: number;
176
+ readonly missingCount: number;
177
+ readonly staleCount: number;
178
+ readonly manualCount: number;
179
+ readonly completionPercentage: number;
180
+ }
181
+ type TranslationWorkspaceError = {
182
+ readonly type: "locale_not_allowed";
183
+ readonly locale: string;
184
+ } | {
185
+ readonly type: "locale_already_exists";
186
+ readonly locale: string;
187
+ } | {
188
+ readonly type: "invalid_locale_format";
189
+ readonly locale: string;
190
+ } | {
191
+ readonly type: "max_locales_exceeded";
192
+ readonly max: number;
193
+ readonly current: number;
194
+ } | {
195
+ readonly type: "read_only_mode";
196
+ } | {
197
+ readonly type: "adapter_not_configured";
198
+ } | {
199
+ readonly type: "target_locale_missing";
200
+ } | {
201
+ readonly type: "translation_failed";
202
+ readonly message: string;
203
+ readonly cause?: unknown;
204
+ } | {
205
+ readonly type: "partial_failure";
206
+ readonly succeeded: number;
207
+ readonly failed: number;
208
+ } | {
209
+ readonly type: "cancelled";
210
+ } | {
211
+ readonly type: "custom_validation_failed";
212
+ readonly message: string;
213
+ };
214
+ interface UseTranslationWorkspaceResult {
215
+ readonly sourceLocale: string;
216
+ readonly targetLocale: string;
217
+ readonly targetLocales: readonly string[];
218
+ readonly localeOptions: readonly LocaleOption[];
219
+ readonly setTargetLocale: (locale: string) => void;
220
+ readonly slots: readonly TranslationSlot[];
221
+ readonly summary: TranslationSummary;
222
+ readonly addLocale: (locale: string) => {
223
+ readonly success: boolean;
224
+ readonly error?: TranslationWorkspaceError;
225
+ };
226
+ readonly isAddLocaleAllowed: (locale: string) => boolean;
227
+ readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
228
+ readonly removeLocaleConfirmation?: ReactNode;
229
+ readonly setTranslation: (slot: TranslationSlot, text: string) => void;
230
+ readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
231
+ readonly success: boolean;
232
+ readonly report?: TranslationReport;
233
+ readonly error?: TranslationWorkspaceError;
234
+ }>;
235
+ readonly translateSlot: (slot: TranslationSlot, options?: {
236
+ readonly signal?: AbortSignal;
237
+ }) => Promise<{
238
+ readonly success: boolean;
239
+ readonly error?: TranslationWorkspaceError;
240
+ }>;
241
+ readonly cancelTranslation: () => void;
242
+ readonly isTranslating: boolean;
243
+ readonly progress?: TranslationProgress;
244
+ readonly error?: TranslationWorkspaceError;
245
+ }
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;
248
+
128
249
  interface SubmissionReceipt {
129
250
  readonly formId: string;
130
251
  readonly formVersion: number;
@@ -396,6 +517,8 @@ interface TranslationWorkspaceHeaderProps {
396
517
  readonly onTranslateAll: () => void;
397
518
  readonly isTranslating: boolean;
398
519
  readonly readOnly: boolean;
520
+ readonly progress?: TranslationProgress;
521
+ readonly onCancel?: () => void;
399
522
  }
400
523
  interface TranslationSlotRowProps {
401
524
  readonly slot: _form_engine_ts_core.TranslationSlot;
@@ -429,21 +552,35 @@ interface TranslationComparisonHeaderProps {
429
552
  readonly onTranslateAll: () => void;
430
553
  readonly isTranslating: boolean;
431
554
  readonly readOnly: boolean;
555
+ readonly report?: TranslationReport;
556
+ readonly progress?: TranslationProgress;
557
+ readonly onCancel?: () => void;
432
558
  }
433
559
  interface TranslationComparisonItemRowProps {
434
560
  readonly item: TranslationComparisonItem;
561
+ readonly questionIndex?: number;
562
+ readonly fieldType?: string;
563
+ readonly optionIndex?: number;
564
+ readonly sourceLocaleLabel?: string;
565
+ readonly targetLocaleLabel?: string;
435
566
  readonly readOnly: boolean;
436
567
  readonly onChange: (text: string) => void;
437
- readonly onTranslate: () => void;
568
+ readonly onTranslate: () => Promise<void>;
438
569
  }
439
570
  interface UseTranslationComparisonOptions {
440
571
  readonly schema: FormSchema;
441
572
  readonly sourceLocale?: string;
442
573
  readonly targetLocale: string;
443
- readonly translationAdapter?: TranslationAdapter;
574
+ readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
575
+ readonly signal?: AbortSignal;
444
576
  readonly readOnly?: boolean;
445
577
  readonly onChange?: (nextSchema: FormSchema) => void;
446
578
  readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
579
+ readonly onTranslationReport?: (report: TranslationReport) => void;
580
+ readonly onTranslationError?: (params: {
581
+ readonly targetLocale: string;
582
+ readonly error: TranslationWorkspaceError;
583
+ }) => void;
447
584
  }
448
585
  interface UseTranslationComparisonResult {
449
586
  readonly sourceLocale: string;
@@ -453,7 +590,11 @@ interface UseTranslationComparisonResult {
453
590
  readonly isTranslating: boolean;
454
591
  readonly updateTranslation: (path: string, text: string) => void;
455
592
  readonly translateSingle: (path: string) => Promise<void>;
456
- readonly translateAll: () => Promise<TranslationReport>;
593
+ readonly translateAll: (options?: PopulateTranslationOptions) => Promise<TranslationReport>;
594
+ readonly cancelTranslation: () => void;
595
+ readonly progress?: TranslationProgress;
596
+ readonly error?: TranslationWorkspaceError;
597
+ readonly report?: TranslationReport;
457
598
  }
458
599
  interface LocaleSelectorProps {
459
600
  readonly targetLocale: string;
@@ -496,6 +637,7 @@ interface ConfirmRemoveLocaleSlotProps {
496
637
  interface TranslationWorkspaceSlots {
497
638
  readonly renderHeader?: (props: TranslationWorkspaceHeaderProps) => ReactNode;
498
639
  readonly renderSlotRow?: (props: TranslationSlotRowProps) => ReactNode;
640
+ readonly renderItemRow?: (props: TranslationComparisonItemRowProps) => ReactNode;
499
641
  readonly renderLocaleSelector?: (props: LocaleSelectorProps) => ReactNode;
500
642
  readonly renderStatusBadge?: (props: {
501
643
  readonly status: _form_engine_ts_core.TranslationStatus;
@@ -820,116 +962,7 @@ interface FieldState {
820
962
  }
821
963
  declare function useField(fieldId: string): FieldState;
822
964
 
823
- declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
824
-
825
- interface UseTranslationWorkspaceOptions {
826
- readonly schema: FormSchema;
827
- readonly onChange?: (schema: FormSchema) => void;
828
- readonly sourceLocale?: string;
829
- readonly targetLocale?: string;
830
- readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
831
- readonly readOnly?: boolean;
832
- readonly policy?: FormPolicy;
833
- readonly availableLocales?: readonly (string | LocaleOption)[];
834
- readonly onLocaleAdded?: (locale: string) => void;
835
- readonly onLocaleRemoved?: (locale: string) => void;
836
- readonly onLocaleChange?: (targetLocale: string) => void;
837
- readonly beforeRemoveLocale?: (locale: string, context: {
838
- readonly slotCount: number;
839
- }) => Promise<boolean> | boolean;
840
- readonly confirmRemoveLocale?: (props: ConfirmRemoveLocaleSlotProps) => ReactNode;
841
- readonly slots?: Pick<TranslationWorkspaceSlots, "confirmRemoveLocale">;
842
- readonly onTranslationStart?: (params: {
843
- readonly targetLocale: string;
844
- readonly mode: "manual" | "automatic";
845
- }) => void;
846
- readonly onTranslationSuccess?: (payload: TranslationEventPayload) => void;
847
- readonly onTranslationError?: (params: {
848
- readonly targetLocale: string;
849
- readonly error: TranslationWorkspaceError;
850
- }) => void;
851
- readonly onTranslationChange?: (event: TranslationSlotChangeEvent) => void;
852
- readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
853
- }
854
- interface LocaleValidationContext {
855
- readonly locale: string;
856
- readonly defaultLocale: string;
857
- readonly currentLocales: readonly string[];
858
- readonly policy?: FormPolicy;
859
- }
860
- type CustomLocaleValidator = (locale: string, context: LocaleValidationContext) => LocaleValidationResult | boolean | string | undefined | void;
861
- interface LocaleValidationResult {
862
- readonly valid: boolean;
863
- readonly error?: {
864
- readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
865
- readonly message: string;
866
- };
867
- }
868
- interface TranslationSummary {
869
- readonly totalSlots: number;
870
- readonly translatedCount: number;
871
- readonly missingCount: number;
872
- readonly staleCount: number;
873
- readonly manualCount: number;
874
- readonly completionPercentage: number;
875
- }
876
- type TranslationWorkspaceError = {
877
- readonly type: "locale_not_allowed";
878
- readonly locale: string;
879
- } | {
880
- readonly type: "locale_already_exists";
881
- readonly locale: string;
882
- } | {
883
- readonly type: "invalid_locale_format";
884
- readonly locale: string;
885
- } | {
886
- readonly type: "max_locales_exceeded";
887
- readonly max: number;
888
- readonly current: number;
889
- } | {
890
- readonly type: "read_only_mode";
891
- } | {
892
- readonly type: "adapter_not_configured";
893
- } | {
894
- readonly type: "target_locale_missing";
895
- } | {
896
- readonly type: "translation_failed";
897
- readonly message: string;
898
- readonly cause?: unknown;
899
- } | {
900
- readonly type: "custom_validation_failed";
901
- readonly message: string;
902
- };
903
- interface UseTranslationWorkspaceResult {
904
- readonly sourceLocale: string;
905
- readonly targetLocale: string;
906
- readonly targetLocales: readonly string[];
907
- readonly localeOptions: readonly LocaleOption[];
908
- readonly setTargetLocale: (locale: string) => void;
909
- readonly slots: readonly TranslationSlot[];
910
- readonly summary: TranslationSummary;
911
- readonly addLocale: (locale: string) => {
912
- readonly success: boolean;
913
- readonly error?: TranslationWorkspaceError;
914
- };
915
- readonly isAddLocaleAllowed: (locale: string) => boolean;
916
- readonly removeLocale: (locale: string) => boolean | Promise<boolean>;
917
- readonly removeLocaleConfirmation?: ReactNode;
918
- readonly setTranslation: (slot: TranslationSlot, text: string) => void;
919
- readonly translateAll: (options?: PopulateTranslationOptions) => Promise<{
920
- readonly success: boolean;
921
- readonly report?: TranslationReport;
922
- readonly error?: TranslationWorkspaceError;
923
- }>;
924
- readonly translateSlot: (slot: TranslationSlot) => Promise<{
925
- readonly success: boolean;
926
- readonly error?: TranslationWorkspaceError;
927
- }>;
928
- readonly isTranslating: boolean;
929
- readonly error?: TranslationWorkspaceError;
930
- }
931
- declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator, availableLocales?: readonly (string | LocaleOption)[]) => LocaleValidationResult;
932
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, availableLocales, onLocaleAdded, onLocaleRemoved, onLocaleChange, beforeRemoveLocale, confirmRemoveLocale, slots: workspaceSlots, onTranslationStart, onTranslationSuccess, onTranslationError, onTranslationChange, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
965
+ declare function useTranslationComparison({ schema, sourceLocale, targetLocale, translationAdapter, readOnly, onChange, onTranslationChange, onTranslationReport, onTranslationError, signal }: UseTranslationComparisonOptions): UseTranslationComparisonResult;
933
966
 
934
967
  declare const BUILDER_TRANSLATION_KEYS: {
935
968
  readonly ADD_FIELD: "builder.actions.addField";
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 {
@@ -3465,6 +3465,7 @@ function useTranslationWorkspace({
3465
3465
  sourceLocale = schema.defaultLocale ?? "en",
3466
3466
  targetLocale,
3467
3467
  translationAdapter,
3468
+ signal,
3468
3469
  readOnly = false,
3469
3470
  policy,
3470
3471
  availableLocales,
@@ -3476,6 +3477,7 @@ function useTranslationWorkspace({
3476
3477
  slots: workspaceSlots,
3477
3478
  onTranslationStart,
3478
3479
  onTranslationSuccess,
3480
+ onTranslationReport,
3479
3481
  onTranslationError,
3480
3482
  onTranslationChange,
3481
3483
  validateLocale
@@ -3483,8 +3485,10 @@ function useTranslationWorkspace({
3483
3485
  const [draftSchema, setDraftSchema] = useState4(schema);
3484
3486
  const [selectedLocale, setSelectedLocale] = useState4(normalizeLocale(targetLocale ?? "") ?? targetLocale ?? "");
3485
3487
  const [isTranslating, setIsTranslating] = useState4(false);
3488
+ const [progress, setProgress] = useState4();
3486
3489
  const [error, setError] = useState4();
3487
3490
  const [pendingRemoval, setPendingRemoval] = useState4();
3491
+ const activeAbortController = useRef2(void 0);
3488
3492
  const pendingRemovalResolver = useRef2(void 0);
3489
3493
  const currentSchema = onChange === void 0 ? draftSchema : schema;
3490
3494
  const confirmRemoveLocaleRenderer = confirmRemoveLocale ?? workspaceSlots?.confirmRemoveLocale;
@@ -3724,7 +3728,15 @@ function useTranslationWorkspace({
3724
3728
  }
3725
3729
  setIsTranslating(true);
3726
3730
  setError(void 0);
3731
+ setProgress(void 0);
3727
3732
  onTranslationStart?.({ targetLocale: activeLocale, mode: "automatic" });
3733
+ const controller = new AbortController();
3734
+ activeAbortController.current = controller;
3735
+ const operationSignal = options.signal ?? signal;
3736
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3737
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3738
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3739
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3728
3740
  try {
3729
3741
  const populated = await populateSchemaTranslations2(
3730
3742
  currentSchema,
@@ -3733,13 +3745,36 @@ function useTranslationWorkspace({
3733
3745
  {
3734
3746
  overwrite: "stale-and-missing",
3735
3747
  preserveManualTranslations: true,
3736
- ...options
3748
+ ...options,
3749
+ continueOnError: true,
3750
+ signal: controller.signal,
3751
+ onProgress: (nextProgress) => {
3752
+ setProgress(nextProgress);
3753
+ options.onProgress?.(nextProgress);
3754
+ }
3737
3755
  }
3738
3756
  );
3739
3757
  commit(populated.schema);
3758
+ onTranslationReport?.(populated.report);
3740
3759
  const missingSlotsCount = collectTranslationSlots(populated.schema, activeLocale).filter(
3741
3760
  (slot) => slot.status === "missing"
3742
3761
  ).length;
3762
+ if (populated.report.cancelled === true) {
3763
+ const workspaceError = { type: "cancelled" };
3764
+ setError(workspaceError);
3765
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3766
+ return { success: false, report: populated.report, error: workspaceError };
3767
+ }
3768
+ if ((populated.report.failed ?? 0) > 0) {
3769
+ const workspaceError = {
3770
+ type: "partial_failure",
3771
+ succeeded: populated.report.succeeded ?? populated.report.updatedSlots.length,
3772
+ failed: populated.report.failed ?? 0
3773
+ };
3774
+ setError(workspaceError);
3775
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3776
+ return { success: false, report: populated.report, error: workspaceError };
3777
+ }
3743
3778
  onTranslationSuccess?.({
3744
3779
  sourceLocale,
3745
3780
  targetLocale: activeLocale,
@@ -3750,6 +3785,12 @@ function useTranslationWorkspace({
3750
3785
  });
3751
3786
  return { success: true, report: populated.report };
3752
3787
  } catch (cause) {
3788
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3789
+ const workspaceError2 = { type: "cancelled" };
3790
+ setError(workspaceError2);
3791
+ onTranslationError?.({ targetLocale: activeLocale, error: workspaceError2 });
3792
+ return { success: false, error: workspaceError2 };
3793
+ }
3753
3794
  const workspaceError = {
3754
3795
  type: "translation_failed",
3755
3796
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3759,6 +3800,11 @@ function useTranslationWorkspace({
3759
3800
  onTranslationError?.({ targetLocale: activeLocale, error: workspaceError });
3760
3801
  return { success: false, error: workspaceError };
3761
3802
  } finally {
3803
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3804
+ const listener = abortListener;
3805
+ operationSignal.removeEventListener("abort", listener);
3806
+ }
3807
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3762
3808
  setIsTranslating(false);
3763
3809
  }
3764
3810
  },
@@ -3770,14 +3816,19 @@ function useTranslationWorkspace({
3770
3816
  onTranslationError,
3771
3817
  onTranslationStart,
3772
3818
  onTranslationSuccess,
3819
+ onTranslationReport,
3773
3820
  readOnly,
3774
3821
  slots,
3775
3822
  sourceLocale,
3776
- translationAdapter
3823
+ translationAdapter,
3824
+ signal
3777
3825
  ]
3778
3826
  );
3827
+ const cancelTranslation = useCallback3(() => {
3828
+ activeAbortController.current?.abort(new DOMException("The translation was cancelled.", "AbortError"));
3829
+ }, []);
3779
3830
  const translateSlot = useCallback3(
3780
- async (slot) => {
3831
+ async (slot, options = {}) => {
3781
3832
  if (translationAdapter === void 0) {
3782
3833
  const workspaceError = { type: "adapter_not_configured" };
3783
3834
  setError(workspaceError);
@@ -3790,8 +3841,21 @@ function useTranslationWorkspace({
3790
3841
  }
3791
3842
  setIsTranslating(true);
3792
3843
  setError(void 0);
3844
+ setProgress({ total: 1, completed: 0, succeeded: 0, failed: 0, percentage: 0 });
3845
+ const controller = new AbortController();
3846
+ activeAbortController.current = controller;
3847
+ const operationSignal = options.signal ?? signal;
3848
+ const abortListener = operationSignal === void 0 ? void 0 : () => controller.abort(operationSignal.reason);
3849
+ if (operationSignal?.aborted) controller.abort(operationSignal.reason);
3850
+ else if (operationSignal !== void 0 && abortListener !== void 0)
3851
+ operationSignal.addEventListener("abort", abortListener, { once: true });
3793
3852
  try {
3794
- const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3853
+ const text = await asAsyncAdapter(translationAdapter).translateText(
3854
+ slot.sourceText,
3855
+ slot.locale,
3856
+ sourceLocale,
3857
+ controller.signal
3858
+ );
3795
3859
  const metadata = translationMetadata(slot.sourceText, sourceLocale, "automatic");
3796
3860
  commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale, "automatic"));
3797
3861
  onTranslationChange?.({
@@ -3801,8 +3865,15 @@ function useTranslationWorkspace({
3801
3865
  mode: "automatic",
3802
3866
  metadata
3803
3867
  });
3868
+ setProgress({ total: 1, completed: 1, succeeded: 1, failed: 0, percentage: 100 });
3804
3869
  return { success: true };
3805
3870
  } catch (cause) {
3871
+ if (controller.signal.aborted || cause instanceof DOMException && cause.name === "AbortError") {
3872
+ const workspaceError2 = { type: "cancelled" };
3873
+ setError(workspaceError2);
3874
+ onTranslationError?.({ targetLocale: slot.locale, error: workspaceError2 });
3875
+ return { success: false, error: workspaceError2 };
3876
+ }
3806
3877
  const workspaceError = {
3807
3878
  type: "translation_failed",
3808
3879
  message: cause instanceof Error ? cause.message : String(cause),
@@ -3811,10 +3882,15 @@ function useTranslationWorkspace({
3811
3882
  setError(workspaceError);
3812
3883
  return { success: false, error: workspaceError };
3813
3884
  } finally {
3885
+ if (operationSignal !== void 0 && abortListener !== void 0) {
3886
+ const listener = abortListener;
3887
+ operationSignal.removeEventListener("abort", listener);
3888
+ }
3889
+ if (activeAbortController.current === controller) activeAbortController.current = void 0;
3814
3890
  setIsTranslating(false);
3815
3891
  }
3816
3892
  },
3817
- [commit, currentSchema, onTranslationChange, readOnly, sourceLocale, translationAdapter]
3893
+ [commit, currentSchema, onTranslationChange, onTranslationError, readOnly, signal, sourceLocale, translationAdapter]
3818
3894
  );
3819
3895
  return {
3820
3896
  sourceLocale,
@@ -3835,7 +3911,9 @@ function useTranslationWorkspace({
3835
3911
  setTranslation,
3836
3912
  translateAll,
3837
3913
  translateSlot,
3914
+ cancelTranslation,
3838
3915
  isTranslating,
3916
+ ...progress === void 0 ? {} : { progress },
3839
3917
  ...error === void 0 ? {} : { error }
3840
3918
  };
3841
3919
  }
@@ -3845,6 +3923,7 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3845
3923
  if (translatedText === void 0 || translatedText.trim().length === 0) return void 0;
3846
3924
  if (metadata !== void 0 && typeof metadata.sourceLocale === "string" && typeof metadata.sourceTextHash === "string" && (metadata.translationSource === "automatic" || metadata.translationSource === "manual")) {
3847
3925
  return {
3926
+ ...metadata,
3848
3927
  sourceLocale: metadata.sourceLocale,
3849
3928
  sourceTextHash: metadata.sourceTextHash,
3850
3929
  translationSource: metadata.translationSource,
@@ -3852,7 +3931,10 @@ function canonicalMetadata(metadata, sourceText, sourceLocale, translatedText) {
3852
3931
  ...typeof metadata.editedAt === "string" ? { editedAt: metadata.editedAt } : {}
3853
3932
  };
3854
3933
  }
3934
+ const canonicalKeys = /* @__PURE__ */ new Set(["sourceLocale", "sourceTextHash", "translationSource", "translatedAt", "editedAt"]);
3935
+ const extensions = Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => !canonicalKeys.has(key)));
3855
3936
  return {
3937
+ ...extensions,
3856
3938
  sourceLocale: typeof metadata?.sourceLocale === "string" ? metadata.sourceLocale : sourceLocale,
3857
3939
  sourceTextHash: computeSourceTextHash2(sourceText),
3858
3940
  translationSource: metadata?.isManual === true || metadata?.isManuallyEdited === true || metadata?.translationSource === "manual" ? "manual" : "automatic"
@@ -3914,7 +3996,10 @@ function useTranslationComparison({
3914
3996
  translationAdapter,
3915
3997
  readOnly = false,
3916
3998
  onChange,
3917
- onTranslationChange
3999
+ onTranslationChange,
4000
+ onTranslationReport,
4001
+ onTranslationError,
4002
+ signal
3918
4003
  }) {
3919
4004
  const workspace = useTranslationWorkspace({
3920
4005
  schema,
@@ -3923,8 +4008,12 @@ function useTranslationComparison({
3923
4008
  ...translationAdapter === void 0 ? {} : { translationAdapter },
3924
4009
  readOnly,
3925
4010
  ...onChange === void 0 ? {} : { onChange },
3926
- ...onTranslationChange === void 0 ? {} : { onTranslationChange }
4011
+ ...onTranslationChange === void 0 ? {} : { onTranslationChange },
4012
+ ...onTranslationReport === void 0 ? {} : { onTranslationReport },
4013
+ ...onTranslationError === void 0 ? {} : { onTranslationError },
4014
+ ...signal === void 0 ? {} : { signal }
3927
4015
  });
4016
+ const [report, setReport] = useState5();
3928
4017
  const items = useMemo5(
3929
4018
  () => workspace.slots.map((slot) => comparisonItem(schema, slot, sourceLocale, translationAdapter !== void 0)),
3930
4019
  [schema, sourceLocale, translationAdapter, workspace.slots]
@@ -3946,11 +4035,17 @@ function useTranslationComparison({
3946
4035
  },
3947
4036
  [itemByPath, workspace]
3948
4037
  );
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]);
4038
+ const translateAll = useCallback4(
4039
+ async (options) => {
4040
+ const result = await workspace.translateAll(options);
4041
+ if (result.report !== void 0) {
4042
+ setReport(result.report);
4043
+ return result.report;
4044
+ }
4045
+ throw new Error(result.error?.type ?? "Translation failed.");
4046
+ },
4047
+ [workspace]
4048
+ );
3954
4049
  return {
3955
4050
  sourceLocale: workspace.sourceLocale,
3956
4051
  targetLocale: workspace.targetLocale,
@@ -3959,12 +4054,16 @@ function useTranslationComparison({
3959
4054
  isTranslating: workspace.isTranslating,
3960
4055
  updateTranslation,
3961
4056
  translateSingle,
3962
- translateAll
4057
+ translateAll,
4058
+ cancelTranslation: workspace.cancelTranslation,
4059
+ ...workspace.progress === void 0 ? {} : { progress: workspace.progress },
4060
+ ...workspace.error === void 0 ? {} : { error: workspace.error },
4061
+ ...report === void 0 ? {} : { report }
3963
4062
  };
3964
4063
  }
3965
4064
 
3966
4065
  // src/receipt.ts
3967
- import { useEffect as useEffect2, useMemo as useMemo6, useState as useState5 } from "react";
4066
+ import { useEffect as useEffect2, useMemo as useMemo6, useState as useState6 } from "react";
3968
4067
  function submissionReceiptQueryKey(formId, formVersion) {
3969
4068
  return `${formId}:v${formVersion}`;
3970
4069
  }
@@ -4042,7 +4141,7 @@ function useSubmissionReceipts(store, queries) {
4042
4141
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
4043
4142
  );
4044
4143
  }, [querySignature]);
4045
- const [state, setState] = useState5({
4144
+ const [state, setState] = useState6({
4046
4145
  receipts: /* @__PURE__ */ new Map(),
4047
4146
  isLoading: stableQueries.length > 0,
4048
4147
  error: null
@@ -4095,7 +4194,7 @@ import {
4095
4194
  useId,
4096
4195
  useMemo as useMemo7,
4097
4196
  useRef as useRef3,
4098
- useState as useState6
4197
+ useState as useState7
4099
4198
  } from "react";
4100
4199
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
4101
4200
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
@@ -4612,15 +4711,15 @@ function ContextFormRenderer({
4612
4711
  const prefix = useId().replace(/:/g, "");
4613
4712
  const formRef = useRef3(null);
4614
4713
  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);
4714
+ const [draftRestored, setDraftRestored] = useState7(false);
4715
+ const [currentPageIndex, setCurrentPageIndex] = useState7(0);
4716
+ const [focusFieldId, setFocusFieldId] = useState7(null);
4717
+ const [confirmation, setConfirmation] = useState7(null);
4718
+ const [guardMessage, setGuardMessage] = useState7(null);
4719
+ const [guardsPending, setGuardsPending] = useState7(false);
4720
+ const [receipt, setReceipt] = useState7(null);
4721
+ const [completionData, setCompletionData] = useState7(null);
4722
+ const [receiptLoaded, setReceiptLoaded] = useState7(receiptStore === void 0);
4624
4723
  const rendererSubmissionInFlight = useRef3(false);
4625
4724
  const fallbackAttemptId = useRef3(null);
4626
4725
  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.2.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.2.0",
46
+ "@form-engine-ts/privacy": "6.2.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",