@form-engine-ts/react 4.6.0 → 4.8.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
@@ -80,8 +80,12 @@ single-slot or batch translation with stale/manual status handling. The MUI pack
80
80
  schema-driven pre-submit confirmation controls in the builder.
81
81
 
82
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
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 canonical locale and a plain context object containing
85
+ the canonical default locale, current locales, and policy.
86
+ Locale input is normalized to its canonical BCP 47 form before duplicate, policy, custom-validation, and schema updates;
87
+ underscore-separated values such as `EN_us` are accepted as compatibility input.
88
+ Use `isAddLocaleAllowed` to disable locale controls before submission. Removing a locale also clears its
85
89
  localized values and metadata; the default locale remains protected.
86
90
 
87
91
  ## Headless builder and renderer lifecycle
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,37 +3282,83 @@ function asAsyncAdapter(adapter) {
3281
3282
  )
3282
3283
  };
3283
3284
  }
3284
- function validateLocaleByPolicy(locale, currentLocales, policy) {
3285
- if (locale.length === 0) {
3285
+ var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3286
+ const canonicalLocale = (0, import_core4.normalizeLocale)(locale);
3287
+ if (canonicalLocale === null) {
3286
3288
  return {
3287
3289
  valid: false,
3288
- error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3290
+ error: {
3291
+ type: "invalid_locale_format",
3292
+ message: `Invalid BCP 47 locale format: "${locale}"`
3293
+ }
3289
3294
  };
3290
3295
  }
3291
- try {
3292
- Intl.getCanonicalLocales(locale);
3293
- } catch {
3296
+ const currentLocales = (schema.supportedLocales ?? []).map((candidate) => (0, import_core4.normalizeLocale)(candidate) ?? candidate);
3297
+ const defaultLocale = schema.defaultLocale === void 0 ? "" : (0, import_core4.normalizeLocale)(schema.defaultLocale) ?? schema.defaultLocale;
3298
+ if (canonicalLocale === defaultLocale || currentLocales.includes(canonicalLocale)) {
3294
3299
  return {
3295
3300
  valid: false,
3296
- error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3301
+ error: {
3302
+ type: "locale_already_exists",
3303
+ message: `Locale "${canonicalLocale}" is already registered.`
3304
+ }
3297
3305
  };
3298
3306
  }
3299
- if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3307
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.some((candidate) => (0, import_core4.normalizeLocale)(candidate) === canonicalLocale)) {
3300
3308
  return {
3301
3309
  valid: false,
3302
- error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3310
+ error: {
3311
+ type: "locale_not_allowed",
3312
+ message: `Locale "${canonicalLocale}" is not allowed by policy.`
3313
+ }
3303
3314
  };
3304
3315
  }
3305
- if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3316
+ const totalLocalesCount = 1 + currentLocales.length;
3317
+ if (policy?.maxLocales !== void 0 && totalLocalesCount >= policy.maxLocales) {
3306
3318
  return {
3307
3319
  valid: false,
3308
3320
  error: {
3309
3321
  type: "max_locales_exceeded",
3310
- message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3322
+ message: `Cannot add locale: maximum allowed locales limit (${policy.maxLocales}) reached.`
3311
3323
  }
3312
3324
  };
3313
3325
  }
3326
+ if (customValidator !== void 0) {
3327
+ const context = {
3328
+ locale: canonicalLocale,
3329
+ defaultLocale,
3330
+ currentLocales: Object.freeze([...currentLocales]),
3331
+ ...policy === void 0 ? {} : { policy }
3332
+ };
3333
+ const customResult = Reflect.apply(customValidator, void 0, [canonicalLocale, context]);
3334
+ if (customResult === false) {
3335
+ return {
3336
+ valid: false,
3337
+ error: {
3338
+ type: "custom_validation_failed",
3339
+ message: `Custom validation rejected locale "${locale}".`
3340
+ }
3341
+ };
3342
+ }
3343
+ if (typeof customResult === "string") {
3344
+ return {
3345
+ valid: false,
3346
+ error: { type: "custom_validation_failed", message: customResult }
3347
+ };
3348
+ }
3349
+ if (typeof customResult === "object" && customResult !== null && !customResult.valid) return customResult;
3350
+ }
3314
3351
  return { valid: true };
3352
+ };
3353
+ function workspaceLocaleValidationMessage(validation) {
3354
+ if (validation.error?.type === "locale_not_allowed") {
3355
+ return validation.error.message.replace(" is not allowed by policy.", " is not allowed by the form policy.");
3356
+ }
3357
+ if (validation.error?.type === "max_locales_exceeded") {
3358
+ const maximum = validation.error.message.match(/\((\d+)\)/u)?.[1];
3359
+ return maximum === void 0 ? validation.error.message : `At most ${maximum} locales are allowed by the form policy.`;
3360
+ }
3361
+ return validation.error?.message ?? "Locale is not valid.";
3315
3362
  }
3316
3363
  function manualMetadata(sourceText, sourceLocale) {
3317
3364
  return {
@@ -3436,21 +3483,15 @@ function useTranslationWorkspace({
3436
3483
  const addLocale = (0, import_react4.useCallback)(
3437
3484
  (locale) => {
3438
3485
  if (readOnly) return { success: false, error: "Workspace is read-only." };
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 };
3486
+ const normalized = (0, import_core4.normalizeLocale)(locale);
3487
+ if (normalized === null) {
3488
+ return {
3489
+ success: false,
3490
+ error: workspaceLocaleValidationMessage(validateLocalePipeline(locale, currentSchema, policy, validateLocale))
3491
+ };
3453
3492
  }
3493
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3494
+ if (!validation.valid) return { success: false, error: workspaceLocaleValidationMessage(validation) };
3454
3495
  commit({
3455
3496
  ...currentSchema,
3456
3497
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
@@ -3458,22 +3499,16 @@ function useTranslationWorkspace({
3458
3499
  setSelectedLocale(normalized);
3459
3500
  return { success: true };
3460
3501
  },
3461
- [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3502
+ [commit, currentSchema, policy, readOnly, validateLocale]
3462
3503
  );
3463
3504
  const isAddLocaleAllowed = (0, import_react4.useCallback)(
3464
3505
  (locale) => {
3465
3506
  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;
3507
+ const normalized = (0, import_core4.normalizeLocale)(locale);
3508
+ if (normalized === null) return false;
3509
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3475
3510
  },
3476
- [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3511
+ [currentSchema, policy, readOnly, validateLocale]
3477
3512
  );
3478
3513
  const removeLocale = (0, import_react4.useCallback)(
3479
3514
  (locale) => {
@@ -4846,5 +4881,6 @@ function FormRenderer(props) {
4846
4881
  useForm,
4847
4882
  useFormBuilder,
4848
4883
  useSubmissionReceipts,
4849
- useTranslationWorkspace
4884
+ useTranslationWorkspace,
4885
+ validateLocalePipeline
4850
4886
  });
package/dist/index.d.cts CHANGED
@@ -708,12 +708,19 @@ interface UseTranslationWorkspaceOptions {
708
708
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
709
709
  readonly readOnly?: boolean;
710
710
  readonly policy?: FormPolicy;
711
- readonly validateLocale?: (locale: string, currentLocales: readonly string[]) => LocaleValidationResult;
711
+ readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
712
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;
713
720
  interface LocaleValidationResult {
714
721
  readonly valid: boolean;
715
722
  readonly error?: {
716
- readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format";
723
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
717
724
  readonly message: string;
718
725
  };
719
726
  }
@@ -744,6 +751,7 @@ interface UseTranslationWorkspaceResult {
744
751
  readonly isTranslating: boolean;
745
752
  readonly error?: string;
746
753
  }
754
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator) => LocaleValidationResult;
747
755
  declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
748
756
 
749
757
  declare const BUILDER_TRANSLATION_KEYS: {
@@ -818,4 +826,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
818
826
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
819
827
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
820
828
 
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 };
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
@@ -708,12 +708,19 @@ interface UseTranslationWorkspaceOptions {
708
708
  readonly translationAdapter?: TranslationAdapter | AsyncTranslationAdapter;
709
709
  readonly readOnly?: boolean;
710
710
  readonly policy?: FormPolicy;
711
- readonly validateLocale?: (locale: string, currentLocales: readonly string[]) => LocaleValidationResult;
711
+ readonly validateLocale?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator;
712
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;
713
720
  interface LocaleValidationResult {
714
721
  readonly valid: boolean;
715
722
  readonly error?: {
716
- readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format";
723
+ readonly type: "locale_not_allowed" | "max_locales_exceeded" | "invalid_locale_format" | "locale_already_exists" | "custom_validation_failed";
717
724
  readonly message: string;
718
725
  };
719
726
  }
@@ -744,6 +751,7 @@ interface UseTranslationWorkspaceResult {
744
751
  readonly isTranslating: boolean;
745
752
  readonly error?: string;
746
753
  }
754
+ declare const validateLocalePipeline: (locale: string, schema: FormSchema, policy?: FormPolicy, customValidator?: ((locale: string, currentLocales: readonly string[]) => LocaleValidationResult) | CustomLocaleValidator) => LocaleValidationResult;
747
755
  declare function useTranslationWorkspace({ schema, onChange, sourceLocale, targetLocale, translationAdapter, readOnly, policy, validateLocale }: UseTranslationWorkspaceOptions): UseTranslationWorkspaceResult;
748
756
 
749
757
  declare const BUILDER_TRANSLATION_KEYS: {
@@ -818,4 +826,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
818
826
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
819
827
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
820
828
 
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 };
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,6 +3238,7 @@ import {
3238
3238
  collectSchemaLocales as collectSchemaLocales2,
3239
3239
  collectTranslationSlots,
3240
3240
  computeSourceTextHash,
3241
+ normalizeLocale,
3241
3242
  populateSchemaTranslations as populateSchemaTranslations2,
3242
3243
  removeLocaleFromSchema
3243
3244
  } from "@form-engine-ts/core";
@@ -3257,37 +3258,83 @@ function asAsyncAdapter(adapter) {
3257
3258
  )
3258
3259
  };
3259
3260
  }
3260
- function validateLocaleByPolicy(locale, currentLocales, policy) {
3261
- if (locale.length === 0) {
3261
+ var validateLocalePipeline = (locale, schema, policy, customValidator) => {
3262
+ const canonicalLocale = normalizeLocale(locale);
3263
+ if (canonicalLocale === null) {
3262
3264
  return {
3263
3265
  valid: false,
3264
- error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3266
+ error: {
3267
+ type: "invalid_locale_format",
3268
+ message: `Invalid BCP 47 locale format: "${locale}"`
3269
+ }
3265
3270
  };
3266
3271
  }
3267
- try {
3268
- Intl.getCanonicalLocales(locale);
3269
- } catch {
3272
+ const currentLocales = (schema.supportedLocales ?? []).map((candidate) => normalizeLocale(candidate) ?? candidate);
3273
+ const defaultLocale = schema.defaultLocale === void 0 ? "" : normalizeLocale(schema.defaultLocale) ?? schema.defaultLocale;
3274
+ if (canonicalLocale === defaultLocale || currentLocales.includes(canonicalLocale)) {
3270
3275
  return {
3271
3276
  valid: false,
3272
- error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3277
+ error: {
3278
+ type: "locale_already_exists",
3279
+ message: `Locale "${canonicalLocale}" is already registered.`
3280
+ }
3273
3281
  };
3274
3282
  }
3275
- if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3283
+ if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.some((candidate) => normalizeLocale(candidate) === canonicalLocale)) {
3276
3284
  return {
3277
3285
  valid: false,
3278
- error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3286
+ error: {
3287
+ type: "locale_not_allowed",
3288
+ message: `Locale "${canonicalLocale}" is not allowed by policy.`
3289
+ }
3279
3290
  };
3280
3291
  }
3281
- if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3292
+ const totalLocalesCount = 1 + currentLocales.length;
3293
+ if (policy?.maxLocales !== void 0 && totalLocalesCount >= policy.maxLocales) {
3282
3294
  return {
3283
3295
  valid: false,
3284
3296
  error: {
3285
3297
  type: "max_locales_exceeded",
3286
- message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3298
+ message: `Cannot add locale: maximum allowed locales limit (${policy.maxLocales}) reached.`
3287
3299
  }
3288
3300
  };
3289
3301
  }
3302
+ if (customValidator !== void 0) {
3303
+ const context = {
3304
+ locale: canonicalLocale,
3305
+ defaultLocale,
3306
+ currentLocales: Object.freeze([...currentLocales]),
3307
+ ...policy === void 0 ? {} : { policy }
3308
+ };
3309
+ const customResult = Reflect.apply(customValidator, void 0, [canonicalLocale, context]);
3310
+ if (customResult === false) {
3311
+ return {
3312
+ valid: false,
3313
+ error: {
3314
+ type: "custom_validation_failed",
3315
+ message: `Custom validation rejected locale "${locale}".`
3316
+ }
3317
+ };
3318
+ }
3319
+ if (typeof customResult === "string") {
3320
+ return {
3321
+ valid: false,
3322
+ error: { type: "custom_validation_failed", message: customResult }
3323
+ };
3324
+ }
3325
+ if (typeof customResult === "object" && customResult !== null && !customResult.valid) return customResult;
3326
+ }
3290
3327
  return { valid: true };
3328
+ };
3329
+ function workspaceLocaleValidationMessage(validation) {
3330
+ if (validation.error?.type === "locale_not_allowed") {
3331
+ return validation.error.message.replace(" is not allowed by policy.", " is not allowed by the form policy.");
3332
+ }
3333
+ if (validation.error?.type === "max_locales_exceeded") {
3334
+ const maximum = validation.error.message.match(/\((\d+)\)/u)?.[1];
3335
+ return maximum === void 0 ? validation.error.message : `At most ${maximum} locales are allowed by the form policy.`;
3336
+ }
3337
+ return validation.error?.message ?? "Locale is not valid.";
3291
3338
  }
3292
3339
  function manualMetadata(sourceText, sourceLocale) {
3293
3340
  return {
@@ -3412,21 +3459,15 @@ function useTranslationWorkspace({
3412
3459
  const addLocale = useCallback3(
3413
3460
  (locale) => {
3414
3461
  if (readOnly) return { success: false, error: "Workspace is read-only." };
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 };
3462
+ const normalized = normalizeLocale(locale);
3463
+ if (normalized === null) {
3464
+ return {
3465
+ success: false,
3466
+ error: workspaceLocaleValidationMessage(validateLocalePipeline(locale, currentSchema, policy, validateLocale))
3467
+ };
3429
3468
  }
3469
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3470
+ if (!validation.valid) return { success: false, error: workspaceLocaleValidationMessage(validation) };
3430
3471
  commit({
3431
3472
  ...currentSchema,
3432
3473
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
@@ -3434,22 +3475,16 @@ function useTranslationWorkspace({
3434
3475
  setSelectedLocale(normalized);
3435
3476
  return { success: true };
3436
3477
  },
3437
- [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3478
+ [commit, currentSchema, policy, readOnly, validateLocale]
3438
3479
  );
3439
3480
  const isAddLocaleAllowed = useCallback3(
3440
3481
  (locale) => {
3441
3482
  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;
3483
+ const normalized = normalizeLocale(locale);
3484
+ if (normalized === null) return false;
3485
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3451
3486
  },
3452
- [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3487
+ [currentSchema, policy, readOnly, validateLocale]
3453
3488
  );
3454
3489
  const removeLocale = useCallback3(
3455
3490
  (locale) => {
@@ -4832,5 +4867,6 @@ export {
4832
4867
  useForm,
4833
4868
  useFormBuilder,
4834
4869
  useSubmissionReceipts,
4835
- useTranslationWorkspace
4870
+ useTranslationWorkspace,
4871
+ validateLocalePipeline
4836
4872
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "4.6.0",
3
+ "version": "4.8.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.6.0",
46
- "@form-engine-ts/privacy": "4.6.0"
45
+ "@form-engine-ts/privacy": "4.8.0",
46
+ "@form-engine-ts/core": "4.8.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",