@form-engine-ts/react 4.5.0 → 4.7.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,12 @@ 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`. Built-in BCP 47, duplicate, and policy checks always run
84
+ before the optional `validateLocale` callback, which receives the locale, default locale, current locales, and policy.
85
+ Use `isAddLocaleAllowed` to disable locale controls before submission. Removing a locale also clears its
86
+ localized values and metadata; the default locale remains protected.
87
+
82
88
  ## Headless builder and renderer lifecycle
83
89
 
84
90
  `useFormBuilder({ schema, onChange, policy, idFactory, factories })` exposes controlled field, option, page, condition,
package/dist/index.cjs CHANGED
@@ -39,7 +39,8 @@ __export(index_exports, {
39
39
  useForm: () => useForm,
40
40
  useFormBuilder: () => useFormBuilder,
41
41
  useSubmissionReceipts: () => useSubmissionReceipts,
42
- useTranslationWorkspace: () => useTranslationWorkspace
42
+ useTranslationWorkspace: () => useTranslationWorkspace,
43
+ validateLocalePipeline: () => validateLocalePipeline
43
44
  });
44
45
  module.exports = __toCommonJS(index_exports);
45
46
 
@@ -3281,6 +3282,88 @@ function asAsyncAdapter(adapter) {
3281
3282
  )
3282
3283
  };
3283
3284
  }
3285
+ function isValidBcp47Locale(locale) {
3286
+ const language = locale.split("-")[0];
3287
+ if (language === void 0 || !/^[a-z]{2,3}$/iu.test(language)) return false;
3288
+ try {
3289
+ Intl.getCanonicalLocales(locale);
3290
+ return true;
3291
+ } catch {
3292
+ return false;
3293
+ }
3294
+ }
3295
+ var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3296
+ const currentLocales = schema.supportedLocales ?? [];
3297
+ const defaultLocale = schema.defaultLocale ?? "";
3298
+ if (!isValidBcp47Locale(locale)) {
3299
+ return {
3300
+ valid: false,
3301
+ error: {
3302
+ type: "invalid_locale_format",
3303
+ message: `Invalid BCP 47 locale format: "${locale}"`
3304
+ }
3305
+ };
3306
+ }
3307
+ if (locale === defaultLocale || currentLocales.includes(locale)) {
3308
+ return {
3309
+ valid: false,
3310
+ error: {
3311
+ type: "locale_already_exists",
3312
+ message: `Locale "${locale}" is already registered.`
3313
+ }
3314
+ };
3315
+ }
3316
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3317
+ return {
3318
+ valid: false,
3319
+ error: {
3320
+ type: "locale_not_allowed",
3321
+ message: `Locale "${locale}" is not allowed by policy.`
3322
+ }
3323
+ };
3324
+ }
3325
+ const totalLocalesCount = 1 + currentLocales.length;
3326
+ if (policy?.maxLocales !== void 0 && totalLocalesCount >= policy.maxLocales) {
3327
+ return {
3328
+ valid: false,
3329
+ error: {
3330
+ type: "max_locales_exceeded",
3331
+ message: `Cannot add locale: maximum allowed locales limit (${policy.maxLocales}) reached.`
3332
+ }
3333
+ };
3334
+ }
3335
+ if (customValidator !== void 0) {
3336
+ const customContext = policy === void 0 ? Object.assign([...currentLocales], { locale, defaultLocale, currentLocales }) : Object.assign([...currentLocales], { locale, defaultLocale, currentLocales, policy });
3337
+ const customResult = customValidator(locale, customContext);
3338
+ if (customResult === false) {
3339
+ return {
3340
+ valid: false,
3341
+ error: {
3342
+ type: "custom_validation_failed",
3343
+ message: `Custom validation rejected locale "${locale}".`
3344
+ }
3345
+ };
3346
+ }
3347
+ if (typeof customResult === "string") {
3348
+ return {
3349
+ valid: false,
3350
+ error: { type: "custom_validation_failed", message: customResult }
3351
+ };
3352
+ }
3353
+ if (typeof customResult === "object" && customResult !== null && !customResult.valid) return customResult;
3354
+ }
3355
+ return { valid: true };
3356
+ };
3357
+ function workspaceLocaleValidationMessage(validation) {
3358
+ if (validation.error?.type === "locale_not_allowed") {
3359
+ return validation.error.message.replace(" is not allowed by policy.", " is not allowed by the form policy.");
3360
+ }
3361
+ if (validation.error?.type === "max_locales_exceeded") {
3362
+ const maximum = validation.error.message.match(/\((\d+)\)/u)?.[1];
3363
+ return maximum === void 0 ? validation.error.message : `At most ${maximum} locales are allowed by the form policy.`;
3364
+ }
3365
+ return validation.error?.message ?? "Locale is not valid.";
3366
+ }
3284
3367
  function manualMetadata(sourceText, sourceLocale) {
3285
3368
  return {
3286
3369
  sourceLocale,
@@ -3348,7 +3431,9 @@ function useTranslationWorkspace({
3348
3431
  sourceLocale = schema.defaultLocale ?? "en",
3349
3432
  targetLocale,
3350
3433
  translationAdapter,
3351
- readOnly = false
3434
+ readOnly = false,
3435
+ policy,
3436
+ validateLocale
3352
3437
  }) {
3353
3438
  const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
3354
3439
  const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
@@ -3401,23 +3486,31 @@ function useTranslationWorkspace({
3401
3486
  );
3402
3487
  const addLocale = (0, import_react4.useCallback)(
3403
3488
  (locale) => {
3404
- if (readOnly || locale.trim().length === 0) return;
3489
+ if (readOnly) return { success: false, error: "Workspace is read-only." };
3405
3490
  const normalized = locale.trim();
3491
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3492
+ if (!validation.valid) return { success: false, error: workspaceLocaleValidationMessage(validation) };
3406
3493
  commit({
3407
3494
  ...currentSchema,
3408
3495
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3409
3496
  });
3410
3497
  setSelectedLocale(normalized);
3498
+ return { success: true };
3499
+ },
3500
+ [commit, currentSchema, policy, readOnly, validateLocale]
3501
+ );
3502
+ const isAddLocaleAllowed = (0, import_react4.useCallback)(
3503
+ (locale) => {
3504
+ if (readOnly) return false;
3505
+ const normalized = locale.trim();
3506
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3411
3507
  },
3412
- [commit, currentSchema, readOnly]
3508
+ [currentSchema, policy, readOnly, validateLocale]
3413
3509
  );
3414
3510
  const removeLocale = (0, import_react4.useCallback)(
3415
3511
  (locale) => {
3416
- if (readOnly || locale === sourceLocale) return;
3417
- commit({
3418
- ...currentSchema,
3419
- supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
3420
- });
3512
+ if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return;
3513
+ commit((0, import_core4.removeLocaleFromSchema)(currentSchema, locale));
3421
3514
  if (activeLocale === locale) setSelectedLocale("");
3422
3515
  },
3423
3516
  [activeLocale, commit, currentSchema, readOnly, sourceLocale]
@@ -3478,6 +3571,7 @@ function useTranslationWorkspace({
3478
3571
  slots,
3479
3572
  summary,
3480
3573
  addLocale,
3574
+ isAddLocaleAllowed,
3481
3575
  removeLocale,
3482
3576
  setTranslation,
3483
3577
  translateAll,
@@ -4784,5 +4878,6 @@ function FormRenderer(props) {
4784
4878
  useForm,
4785
4879
  useFormBuilder,
4786
4880
  useSubmissionReceipts,
4787
- useTranslationWorkspace
4881
+ useTranslationWorkspace,
4882
+ validateLocalePipeline
4788
4883
  });
package/dist/index.d.cts CHANGED
@@ -707,6 +707,22 @@ 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) | CustomLocaleValidator;
712
+ }
713
+ interface LocaleValidationContext {
714
+ readonly locale: string;
715
+ readonly defaultLocale: string;
716
+ readonly currentLocales: readonly string[];
717
+ readonly policy?: FormPolicy;
718
+ }
719
+ type CustomLocaleValidator = (locale: string, context: LocaleValidationContext) => LocaleValidationResult | boolean | string | undefined | void;
720
+ interface LocaleValidationResult {
721
+ readonly valid: boolean;
722
+ readonly error?: {
723
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
724
+ readonly message: string;
725
+ };
710
726
  }
711
727
  interface TranslationSummary {
712
728
  readonly totalSlots: number;
@@ -723,7 +739,11 @@ interface UseTranslationWorkspaceResult {
723
739
  readonly setTargetLocale: (locale: string) => void;
724
740
  readonly slots: readonly TranslationSlot[];
725
741
  readonly summary: TranslationSummary;
726
- readonly addLocale: (locale: string) => void;
742
+ readonly addLocale: (locale: string) => {
743
+ readonly success: boolean;
744
+ readonly error?: string;
745
+ };
746
+ readonly isAddLocaleAllowed: (locale: string) => boolean;
727
747
  readonly removeLocale: (locale: string) => void;
728
748
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
729
749
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<TranslationReport>;
@@ -731,7 +751,8 @@ interface UseTranslationWorkspaceResult {
731
751
  readonly isTranslating: boolean;
732
752
  readonly error?: string;
733
753
  }
734
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
754
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator) => LocaleValidationResult;
755
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
735
756
 
736
757
  declare const BUILDER_TRANSLATION_KEYS: {
737
758
  readonly ADD_FIELD: "builder.actions.addField";
@@ -805,4 +826,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
805
826
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
806
827
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
807
828
 
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 };
829
+ 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 CustomLocaleValidator, 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 LocaleValidationContext, 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, validateLocalePipeline };
package/dist/index.d.ts CHANGED
@@ -707,6 +707,22 @@ 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) | CustomLocaleValidator;
712
+ }
713
+ interface LocaleValidationContext {
714
+ readonly locale: string;
715
+ readonly defaultLocale: string;
716
+ readonly currentLocales: readonly string[];
717
+ readonly policy?: FormPolicy;
718
+ }
719
+ type CustomLocaleValidator = (locale: string, context: LocaleValidationContext) => LocaleValidationResult | boolean | string | undefined | void;
720
+ interface LocaleValidationResult {
721
+ readonly valid: boolean;
722
+ readonly error?: {
723
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
724
+ readonly message: string;
725
+ };
710
726
  }
711
727
  interface TranslationSummary {
712
728
  readonly totalSlots: number;
@@ -723,7 +739,11 @@ interface UseTranslationWorkspaceResult {
723
739
  readonly setTargetLocale: (locale: string) => void;
724
740
  readonly slots: readonly TranslationSlot[];
725
741
  readonly summary: TranslationSummary;
726
- readonly addLocale: (locale: string) => void;
742
+ readonly addLocale: (locale: string) => {
743
+ readonly success: boolean;
744
+ readonly error?: string;
745
+ };
746
+ readonly isAddLocaleAllowed: (locale: string) => boolean;
727
747
  readonly removeLocale: (locale: string) => void;
728
748
  readonly setTranslation: (slot: TranslationSlot, text: string) => void;
729
749
  readonly translateAll: (options?: PopulateTranslationOptions) => Promise<TranslationReport>;
@@ -731,7 +751,8 @@ interface UseTranslationWorkspaceResult {
731
751
  readonly isTranslating: boolean;
732
752
  readonly error?: string;
733
753
  }
734
- declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
754
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator) => LocaleValidationResult;
755
+ declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
735
756
 
736
757
  declare const BUILDER_TRANSLATION_KEYS: {
737
758
  readonly ADD_FIELD: "builder.actions.addField";
@@ -805,4 +826,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
805
826
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
806
827
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
807
828
 
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 };
829
+ 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 CustomLocaleValidator, 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 LocaleValidationContext, 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, validateLocalePipeline };
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,88 @@ function asAsyncAdapter(adapter) {
3256
3257
  )
3257
3258
  };
3258
3259
  }
3260
+ function isValidBcp47Locale(locale) {
3261
+ const language = locale.split("-")[0];
3262
+ if (language === void 0 || !/^[a-z]{2,3}$/iu.test(language)) return false;
3263
+ try {
3264
+ Intl.getCanonicalLocales(locale);
3265
+ return true;
3266
+ } catch {
3267
+ return false;
3268
+ }
3269
+ }
3270
+ var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3271
+ const currentLocales = schema.supportedLocales ?? [];
3272
+ const defaultLocale = schema.defaultLocale ?? "";
3273
+ if (!isValidBcp47Locale(locale)) {
3274
+ return {
3275
+ valid: false,
3276
+ error: {
3277
+ type: "invalid_locale_format",
3278
+ message: `Invalid BCP 47 locale format: "${locale}"`
3279
+ }
3280
+ };
3281
+ }
3282
+ if (locale === defaultLocale || currentLocales.includes(locale)) {
3283
+ return {
3284
+ valid: false,
3285
+ error: {
3286
+ type: "locale_already_exists",
3287
+ message: `Locale "${locale}" is already registered.`
3288
+ }
3289
+ };
3290
+ }
3291
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3292
+ return {
3293
+ valid: false,
3294
+ error: {
3295
+ type: "locale_not_allowed",
3296
+ message: `Locale "${locale}" is not allowed by policy.`
3297
+ }
3298
+ };
3299
+ }
3300
+ const totalLocalesCount = 1 + currentLocales.length;
3301
+ if (policy?.maxLocales !== void 0 && totalLocalesCount >= policy.maxLocales) {
3302
+ return {
3303
+ valid: false,
3304
+ error: {
3305
+ type: "max_locales_exceeded",
3306
+ message: `Cannot add locale: maximum allowed locales limit (${policy.maxLocales}) reached.`
3307
+ }
3308
+ };
3309
+ }
3310
+ if (customValidator !== void 0) {
3311
+ const customContext = policy === void 0 ? Object.assign([...currentLocales], { locale, defaultLocale, currentLocales }) : Object.assign([...currentLocales], { locale, defaultLocale, currentLocales, policy });
3312
+ const customResult = customValidator(locale, customContext);
3313
+ if (customResult === false) {
3314
+ return {
3315
+ valid: false,
3316
+ error: {
3317
+ type: "custom_validation_failed",
3318
+ message: `Custom validation rejected locale "${locale}".`
3319
+ }
3320
+ };
3321
+ }
3322
+ if (typeof customResult === "string") {
3323
+ return {
3324
+ valid: false,
3325
+ error: { type: "custom_validation_failed", message: customResult }
3326
+ };
3327
+ }
3328
+ if (typeof customResult === "object" && customResult !== null && !customResult.valid) return customResult;
3329
+ }
3330
+ return { valid: true };
3331
+ };
3332
+ function workspaceLocaleValidationMessage(validation) {
3333
+ if (validation.error?.type === "locale_not_allowed") {
3334
+ return validation.error.message.replace(" is not allowed by policy.", " is not allowed by the form policy.");
3335
+ }
3336
+ if (validation.error?.type === "max_locales_exceeded") {
3337
+ const maximum = validation.error.message.match(/\((\d+)\)/u)?.[1];
3338
+ return maximum === void 0 ? validation.error.message : `At most ${maximum} locales are allowed by the form policy.`;
3339
+ }
3340
+ return validation.error?.message ?? "Locale is not valid.";
3341
+ }
3259
3342
  function manualMetadata(sourceText, sourceLocale) {
3260
3343
  return {
3261
3344
  sourceLocale,
@@ -3323,7 +3406,9 @@ function useTranslationWorkspace({
3323
3406
  sourceLocale = schema.defaultLocale ?? "en",
3324
3407
  targetLocale,
3325
3408
  translationAdapter,
3326
- readOnly = false
3409
+ readOnly = false,
3410
+ policy,
3411
+ validateLocale
3327
3412
  }) {
3328
3413
  const [draftSchema, setDraftSchema] = useState4(schema);
3329
3414
  const [selectedLocale, setSelectedLocale] = useState4(targetLocale ?? "");
@@ -3376,23 +3461,31 @@ function useTranslationWorkspace({
3376
3461
  );
3377
3462
  const addLocale = useCallback3(
3378
3463
  (locale) => {
3379
- if (readOnly || locale.trim().length === 0) return;
3464
+ if (readOnly) return { success: false, error: "Workspace is read-only." };
3380
3465
  const normalized = locale.trim();
3466
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3467
+ if (!validation.valid) return { success: false, error: workspaceLocaleValidationMessage(validation) };
3381
3468
  commit({
3382
3469
  ...currentSchema,
3383
3470
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3384
3471
  });
3385
3472
  setSelectedLocale(normalized);
3473
+ return { success: true };
3474
+ },
3475
+ [commit, currentSchema, policy, readOnly, validateLocale]
3476
+ );
3477
+ const isAddLocaleAllowed = useCallback3(
3478
+ (locale) => {
3479
+ if (readOnly) return false;
3480
+ const normalized = locale.trim();
3481
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3386
3482
  },
3387
- [commit, currentSchema, readOnly]
3483
+ [currentSchema, policy, readOnly, validateLocale]
3388
3484
  );
3389
3485
  const removeLocale = useCallback3(
3390
3486
  (locale) => {
3391
- if (readOnly || locale === sourceLocale) return;
3392
- commit({
3393
- ...currentSchema,
3394
- supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
3395
- });
3487
+ if (readOnly || locale === sourceLocale || locale === currentSchema.defaultLocale) return;
3488
+ commit(removeLocaleFromSchema(currentSchema, locale));
3396
3489
  if (activeLocale === locale) setSelectedLocale("");
3397
3490
  },
3398
3491
  [activeLocale, commit, currentSchema, readOnly, sourceLocale]
@@ -3453,6 +3546,7 @@ function useTranslationWorkspace({
3453
3546
  slots,
3454
3547
  summary,
3455
3548
  addLocale,
3549
+ isAddLocaleAllowed,
3456
3550
  removeLocale,
3457
3551
  setTranslation,
3458
3552
  translateAll,
@@ -4769,5 +4863,6 @@ export {
4769
4863
  useForm,
4770
4864
  useFormBuilder,
4771
4865
  useSubmissionReceipts,
4772
- useTranslationWorkspace
4866
+ useTranslationWorkspace,
4867
+ validateLocalePipeline
4773
4868
  };
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.7.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.7.0",
46
+ "@form-engine-ts/privacy": "4.7.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",