@form-engine-ts/react 4.5.0 → 4.6.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
@@ -79,6 +79,11 @@ single-slot or batch translation with stale/manual status handling. The MUI pack
79
79
  `setActiveFieldId` to keep one field editor open at a time. Set `submissionSettingsOptions={{ enabled: true }}` to expose
80
80
  schema-driven pre-submit confirmation controls in the builder.
81
81
 
82
+ `useTranslationWorkspace` validates added locales against `policy.allowedLocales` and `policy.maxLocales` and returns
83
+ a structured `{ success, error }` result from `addLocale`. Use `validateLocale` for application-specific BCP 47 or
84
+ tenant rules, and `isAddLocaleAllowed` to disable locale controls before submission. Removing a locale also clears its
85
+ localized values and metadata; the default locale remains protected.
86
+
82
87
  ## Headless builder and renderer lifecycle
83
88
 
84
89
  `useFormBuilder({ schema, onChange, policy, idFactory, factories })` exposes controlled field, option, page, condition,
package/dist/index.cjs CHANGED
@@ -3281,6 +3281,38 @@ function asAsyncAdapter(adapter) {
3281
3281
  )
3282
3282
  };
3283
3283
  }
3284
+ function validateLocaleByPolicy(locale, currentLocales, policy) {
3285
+ if (locale.length === 0) {
3286
+ return {
3287
+ valid: false,
3288
+ error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3289
+ };
3290
+ }
3291
+ try {
3292
+ Intl.getCanonicalLocales(locale);
3293
+ } catch {
3294
+ return {
3295
+ valid: false,
3296
+ error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3297
+ };
3298
+ }
3299
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3300
+ return {
3301
+ valid: false,
3302
+ error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3303
+ };
3304
+ }
3305
+ if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3306
+ return {
3307
+ valid: false,
3308
+ error: {
3309
+ type: "max_locales_exceeded",
3310
+ message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3311
+ }
3312
+ };
3313
+ }
3314
+ return { valid: true };
3315
+ }
3284
3316
  function manualMetadata(sourceText, sourceLocale) {
3285
3317
  return {
3286
3318
  sourceLocale,
@@ -3348,7 +3380,9 @@ function useTranslationWorkspace({
3348
3380
  sourceLocale = schema.defaultLocale ?? "en",
3349
3381
  targetLocale,
3350
3382
  translationAdapter,
3351
- readOnly = false
3383
+ readOnly = false,
3384
+ policy,
3385
+ validateLocale
3352
3386
  }) {
3353
3387
  const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
3354
3388
  const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
@@ -3401,23 +3435,50 @@ function useTranslationWorkspace({
3401
3435
  );
3402
3436
  const addLocale = (0, import_react4.useCallback)(
3403
3437
  (locale) => {
3404
- if (readOnly || locale.trim().length === 0) return;
3438
+ if (readOnly) return { success: false, error: "Workspace is read-only." };
3405
3439
  const normalized = locale.trim();
3440
+ const currentLocales = [
3441
+ .../* @__PURE__ */ new Set([
3442
+ ...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
3443
+ sourceLocale,
3444
+ ...(0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales
3445
+ ])
3446
+ ];
3447
+ const validation = validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy);
3448
+ if (!validation.valid) return { success: false, error: validation.error?.message ?? "Locale is not valid." };
3449
+ const alreadyRegistered = normalized === sourceLocale || normalized === currentSchema.defaultLocale || (currentSchema.supportedLocales ?? []).includes(normalized);
3450
+ if (alreadyRegistered) {
3451
+ setSelectedLocale(normalized);
3452
+ return { success: true };
3453
+ }
3406
3454
  commit({
3407
3455
  ...currentSchema,
3408
3456
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3409
3457
  });
3410
3458
  setSelectedLocale(normalized);
3459
+ return { success: true };
3460
+ },
3461
+ [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3462
+ );
3463
+ const isAddLocaleAllowed = (0, import_react4.useCallback)(
3464
+ (locale) => {
3465
+ if (readOnly) return false;
3466
+ const normalized = locale.trim();
3467
+ const currentLocales = [
3468
+ .../* @__PURE__ */ new Set([
3469
+ ...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
3470
+ sourceLocale,
3471
+ ...(0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales
3472
+ ])
3473
+ ];
3474
+ return (validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy)).valid;
3411
3475
  },
3412
- [commit, currentSchema, readOnly]
3476
+ [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3413
3477
  );
3414
3478
  const removeLocale = (0, import_react4.useCallback)(
3415
3479
  (locale) => {
3416
- if (readOnly || locale === sourceLocale) return;
3417
- commit({
3418
- ...currentSchema,
3419
- supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
3420
- });
3480
+ if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return;
3481
+ commit((0, import_core4.removeLocaleFromSchema)(currentSchema, locale));
3421
3482
  if (activeLocale === locale) setSelectedLocale("");
3422
3483
  },
3423
3484
  [activeLocale, commit, currentSchema, readOnly, sourceLocale]
@@ -3478,6 +3539,7 @@ function useTranslationWorkspace({
3478
3539
  slots,
3479
3540
  summary,
3480
3541
  addLocale,
3542
+ isAddLocaleAllowed,
3481
3543
  removeLocale,
3482
3544
  setTranslation,
3483
3545
  translateAll,
package/dist/index.d.cts CHANGED
@@ -707,6 +707,15 @@ interface UseTranslationWorkspaceOptions {
707
707
  readonly targetLocale?: string;
708
708
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
709
709
  readonly readOnly?: boolean;
710
+ readonly policy?: FormPolicy;
711
+ readonly validateLocale?: (locale: string, currentLocales: readonly string[]) => LocaleValidationResult;
712
+ }
713
+ interface LocaleValidationResult {
714
+ readonly valid: boolean;
715
+ readonly error?: {
716
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format";
717
+ readonly message: string;
718
+ };
710
719
  }
711
720
  interface TranslationSummary {
712
721
  readonly totalSlots: number;
@@ -723,7 +732,11 @@ interface UseTranslationWorkspaceResult {
723
732
  readonly setTargetLocale: (locale: string) => void;
724
733
  readonly slots: readonly TranslationSlot[];
725
734
  readonly summary: TranslationSummary;
726
- readonly addLocale: (locale: string) => void;
735
+ readonly addLocale: (locale: string) => {
736
+ readonly success: boolean;
737
+ readonly error?: string;
738
+ };
739
+ readonly isAddLocaleAllowed: (locale: string) => boolean;
727
740
  readonly removeLocale: (locale: string) => void;
728
741
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
729
742
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<TranslationReport>;
@@ -731,7 +744,7 @@ interface UseTranslationWorkspaceResult {
731
744
  readonly isTranslating: boolean;
732
745
  readonly error?: string;
733
746
  }
734
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
747
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
735
748
 
736
749
  declare const BUILDER_TRANSLATION_KEYS: {
737
750
  readonly ADD_FIELD: "builder.actions.addField";
@@ -805,4 +818,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
805
818
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
806
819
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
807
820
 
808
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationSummary, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts, useTranslationWorkspace };
821
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocaleValidationResult, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationSummary, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts, useTranslationWorkspace };
package/dist/index.d.ts CHANGED
@@ -707,6 +707,15 @@ interface UseTranslationWorkspaceOptions {
707
707
  readonly targetLocale?: string;
708
708
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
709
709
  readonly readOnly?: boolean;
710
+ readonly policy?: FormPolicy;
711
+ readonly validateLocale?: (locale: string, currentLocales: readonly string[]) => LocaleValidationResult;
712
+ }
713
+ interface LocaleValidationResult {
714
+ readonly valid: boolean;
715
+ readonly error?: {
716
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format";
717
+ readonly message: string;
718
+ };
710
719
  }
711
720
  interface TranslationSummary {
712
721
  readonly totalSlots: number;
@@ -723,7 +732,11 @@ interface UseTranslationWorkspaceResult {
723
732
  readonly setTargetLocale: (locale: string) => void;
724
733
  readonly slots: readonly TranslationSlot[];
725
734
  readonly summary: TranslationSummary;
726
- readonly addLocale: (locale: string) => void;
735
+ readonly addLocale: (locale: string) => {
736
+ readonly success: boolean;
737
+ readonly error?: string;
738
+ };
739
+ readonly isAddLocaleAllowed: (locale: string) => boolean;
727
740
  readonly removeLocale: (locale: string) => void;
728
741
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
729
742
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<TranslationReport>;
@@ -731,7 +744,7 @@ interface UseTranslationWorkspaceResult {
731
744
  readonly isTranslating: boolean;
732
745
  readonly error?: string;
733
746
  }
734
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
747
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
735
748
 
736
749
  declare const BUILDER_TRANSLATION_KEYS: {
737
750
  readonly ADD_FIELD: "builder.actions.addField";
@@ -805,4 +818,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
805
818
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
806
819
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
807
820
 
808
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationSummary, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts, useTranslationWorkspace };
821
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ChoiceFieldLayoutMode, type ChoiceFieldTypeLayoutMap, type ChoiceGroupSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldEditorMode, type FieldError, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormBuilderSubmissionSettingsOptions, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererAppearance, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlotProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocaleValidationResult, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationOptions, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type TranslationSummary, type UseFormBuilderOptions, type UseFormBuilderResult, type UseSubmissionReceiptsResult, type UseTranslationWorkspaceOptions, type UseTranslationWorkspaceResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveChoiceFieldLayout, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts, useTranslationWorkspace };
package/dist/index.js CHANGED
@@ -3238,7 +3238,8 @@ import {
3238
3238
  collectSchemaLocales as collectSchemaLocales2,
3239
3239
  collectTranslationSlots,
3240
3240
  computeSourceTextHash,
3241
- populateSchemaTranslations as populateSchemaTranslations2
3241
+ populateSchemaTranslations as populateSchemaTranslations2,
3242
+ removeLocaleFromSchema
3242
3243
  } from "@form-engine-ts/core";
3243
3244
  import { useCallback as useCallback3, useMemo as useMemo3, useState as useState4 } from "react";
3244
3245
  function updateTranslationMap(translations, locale, property, text) {
@@ -3256,6 +3257,38 @@ function asAsyncAdapter(adapter) {
3256
3257
  )
3257
3258
  };
3258
3259
  }
3260
+ function validateLocaleByPolicy(locale, currentLocales, policy) {
3261
+ if (locale.length === 0) {
3262
+ return {
3263
+ valid: false,
3264
+ error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3265
+ };
3266
+ }
3267
+ try {
3268
+ Intl.getCanonicalLocales(locale);
3269
+ } catch {
3270
+ return {
3271
+ valid: false,
3272
+ error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3273
+ };
3274
+ }
3275
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3276
+ return {
3277
+ valid: false,
3278
+ error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3279
+ };
3280
+ }
3281
+ if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3282
+ return {
3283
+ valid: false,
3284
+ error: {
3285
+ type: "max_locales_exceeded",
3286
+ message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3287
+ }
3288
+ };
3289
+ }
3290
+ return { valid: true };
3291
+ }
3259
3292
  function manualMetadata(sourceText, sourceLocale) {
3260
3293
  return {
3261
3294
  sourceLocale,
@@ -3323,7 +3356,9 @@ function useTranslationWorkspace({
3323
3356
  sourceLocale = schema.defaultLocale ?? "en",
3324
3357
  targetLocale,
3325
3358
  translationAdapter,
3326
- readOnly = false
3359
+ readOnly = false,
3360
+ policy,
3361
+ validateLocale
3327
3362
  }) {
3328
3363
  const [draftSchema, setDraftSchema] = useState4(schema);
3329
3364
  const [selectedLocale, setSelectedLocale] = useState4(targetLocale ?? "");
@@ -3376,23 +3411,50 @@ function useTranslationWorkspace({
3376
3411
  );
3377
3412
  const addLocale = useCallback3(
3378
3413
  (locale) => {
3379
- if (readOnly || locale.trim().length === 0) return;
3414
+ if (readOnly) return { success: false, error: "Workspace is read-only." };
3380
3415
  const normalized = locale.trim();
3416
+ const currentLocales = [
3417
+ .../* @__PURE__ */ new Set([
3418
+ ...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
3419
+ sourceLocale,
3420
+ ...collectSchemaLocales2(currentSchema).allUniqueLocales
3421
+ ])
3422
+ ];
3423
+ const validation = validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy);
3424
+ if (!validation.valid) return { success: false, error: validation.error?.message ?? "Locale is not valid." };
3425
+ const alreadyRegistered = normalized === sourceLocale || normalized === currentSchema.defaultLocale || (currentSchema.supportedLocales ?? []).includes(normalized);
3426
+ if (alreadyRegistered) {
3427
+ setSelectedLocale(normalized);
3428
+ return { success: true };
3429
+ }
3381
3430
  commit({
3382
3431
  ...currentSchema,
3383
3432
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3384
3433
  });
3385
3434
  setSelectedLocale(normalized);
3435
+ return { success: true };
3436
+ },
3437
+ [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3438
+ );
3439
+ const isAddLocaleAllowed = useCallback3(
3440
+ (locale) => {
3441
+ if (readOnly) return false;
3442
+ const normalized = locale.trim();
3443
+ const currentLocales = [
3444
+ .../* @__PURE__ */ new Set([
3445
+ ...currentSchema.defaultLocale === void 0 ? [] : [currentSchema.defaultLocale],
3446
+ sourceLocale,
3447
+ ...collectSchemaLocales2(currentSchema).allUniqueLocales
3448
+ ])
3449
+ ];
3450
+ return (validateLocale?.(normalized, currentLocales) ?? validateLocaleByPolicy(normalized, currentLocales, policy)).valid;
3386
3451
  },
3387
- [commit, currentSchema, readOnly]
3452
+ [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3388
3453
  );
3389
3454
  const removeLocale = useCallback3(
3390
3455
  (locale) => {
3391
- if (readOnly || locale === sourceLocale) return;
3392
- commit({
3393
- ...currentSchema,
3394
- supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
3395
- });
3456
+ if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return;
3457
+ commit(removeLocaleFromSchema(currentSchema, locale));
3396
3458
  if (activeLocale === locale) setSelectedLocale("");
3397
3459
  },
3398
3460
  [activeLocale, commit, currentSchema, readOnly, sourceLocale]
@@ -3453,6 +3515,7 @@ function useTranslationWorkspace({
3453
3515
  slots,
3454
3516
  summary,
3455
3517
  addLocale,
3518
+ isAddLocaleAllowed,
3456
3519
  removeLocale,
3457
3520
  setTranslation,
3458
3521
  translateAll,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "4.5.0",
3
+ "version": "4.6.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": "4.5.0",
46
- "@form-engine-ts/privacy": "4.5.0"
45
+ "@form-engine-ts/core": "4.6.0",
46
+ "@form-engine-ts/privacy": "4.6.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",