@form-engine-ts/react 4.6.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
@@ -80,8 +80,9 @@ 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 locale, default locale, current locales, and policy.
85
+ Use `isAddLocaleAllowed` to disable locale controls before submission. Removing a locale also clears its
85
86
  localized values and metadata; the default locale remains protected.
86
87
 
87
88
  ## 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,87 @@ function asAsyncAdapter(adapter) {
3281
3282
  )
3282
3283
  };
3283
3284
  }
3284
- function validateLocaleByPolicy(locale, currentLocales, policy) {
3285
- if (locale.length === 0) {
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)) {
3286
3299
  return {
3287
3300
  valid: false,
3288
- error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3301
+ error: {
3302
+ type: "invalid_locale_format",
3303
+ message: `Invalid BCP 47 locale format: "${locale}"`
3304
+ }
3289
3305
  };
3290
3306
  }
3291
- try {
3292
- Intl.getCanonicalLocales(locale);
3293
- } catch {
3307
+ if (locale === defaultLocale || currentLocales.includes(locale)) {
3294
3308
  return {
3295
3309
  valid: false,
3296
- error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3310
+ error: {
3311
+ type: "locale_already_exists",
3312
+ message: `Locale "${locale}" is already registered.`
3313
+ }
3297
3314
  };
3298
3315
  }
3299
3316
  if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3300
3317
  return {
3301
3318
  valid: false,
3302
- error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3319
+ error: {
3320
+ type: "locale_not_allowed",
3321
+ message: `Locale "${locale}" is not allowed by policy.`
3322
+ }
3303
3323
  };
3304
3324
  }
3305
- if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3325
+ const totalLocalesCount = 1 + currentLocales.length;
3326
+ if (policy?.maxLocales !== void 0 && totalLocalesCount >= policy.maxLocales) {
3306
3327
  return {
3307
3328
  valid: false,
3308
3329
  error: {
3309
3330
  type: "max_locales_exceeded",
3310
- message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3331
+ message: `Cannot add locale: maximum allowed locales limit (${policy.maxLocales}) reached.`
3311
3332
  }
3312
3333
  };
3313
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
+ }
3314
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.";
3315
3366
  }
3316
3367
  function manualMetadata(sourceText, sourceLocale) {
3317
3368
  return {
@@ -3437,20 +3488,8 @@ function useTranslationWorkspace({
3437
3488
  (locale) => {
3438
3489
  if (readOnly) return { success: false, error: "Workspace is read-only." };
3439
3490
  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
- }
3491
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3492
+ if (!validation.valid) return { success: false, error: workspaceLocaleValidationMessage(validation) };
3454
3493
  commit({
3455
3494
  ...currentSchema,
3456
3495
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
@@ -3458,22 +3497,15 @@ function useTranslationWorkspace({
3458
3497
  setSelectedLocale(normalized);
3459
3498
  return { success: true };
3460
3499
  },
3461
- [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3500
+ [commit, currentSchema, policy, readOnly, validateLocale]
3462
3501
  );
3463
3502
  const isAddLocaleAllowed = (0, import_react4.useCallback)(
3464
3503
  (locale) => {
3465
3504
  if (readOnly) return false;
3466
3505
  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;
3506
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3475
3507
  },
3476
- [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3508
+ [currentSchema, policy, readOnly, validateLocale]
3477
3509
  );
3478
3510
  const removeLocale = (0, import_react4.useCallback)(
3479
3511
  (locale) => {
@@ -4846,5 +4878,6 @@ function FormRenderer(props) {
4846
4878
  useForm,
4847
4879
  useFormBuilder,
4848
4880
  useSubmissionReceipts,
4849
- useTranslationWorkspace
4881
+ useTranslationWorkspace,
4882
+ validateLocalePipeline
4850
4883
  });
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
@@ -3257,37 +3257,87 @@ function asAsyncAdapter(adapter) {
3257
3257
  )
3258
3258
  };
3259
3259
  }
3260
- function validateLocaleByPolicy(locale, currentLocales, policy) {
3261
- if (locale.length === 0) {
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)) {
3262
3274
  return {
3263
3275
  valid: false,
3264
- error: { type: "invalid_locale_format", message: "Locale must not be empty." }
3276
+ error: {
3277
+ type: "invalid_locale_format",
3278
+ message: `Invalid BCP 47 locale format: "${locale}"`
3279
+ }
3265
3280
  };
3266
3281
  }
3267
- try {
3268
- Intl.getCanonicalLocales(locale);
3269
- } catch {
3282
+ if (locale === defaultLocale || currentLocales.includes(locale)) {
3270
3283
  return {
3271
3284
  valid: false,
3272
- error: { type: "invalid_locale_format", message: `Locale "${locale}" is not a valid BCP 47 locale.` }
3285
+ error: {
3286
+ type: "locale_already_exists",
3287
+ message: `Locale "${locale}" is already registered.`
3288
+ }
3273
3289
  };
3274
3290
  }
3275
3291
  if (policy?.allowedLocales !== void 0 && !policy.allowedLocales.includes(locale)) {
3276
3292
  return {
3277
3293
  valid: false,
3278
- error: { type: "locale_not_allowed", message: `Locale "${locale}" is not allowed by the form policy.` }
3294
+ error: {
3295
+ type: "locale_not_allowed",
3296
+ message: `Locale "${locale}" is not allowed by policy.`
3297
+ }
3279
3298
  };
3280
3299
  }
3281
- if (policy?.maxLocales !== void 0 && !currentLocales.includes(locale) && currentLocales.length >= policy.maxLocales) {
3300
+ const totalLocalesCount = 1 + currentLocales.length;
3301
+ if (policy?.maxLocales !== void 0 && totalLocalesCount >= policy.maxLocales) {
3282
3302
  return {
3283
3303
  valid: false,
3284
3304
  error: {
3285
3305
  type: "max_locales_exceeded",
3286
- message: `At most ${policy.maxLocales} locales are allowed by the form policy.`
3306
+ message: `Cannot add locale: maximum allowed locales limit (${policy.maxLocales}) reached.`
3287
3307
  }
3288
3308
  };
3289
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
+ }
3290
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.";
3291
3341
  }
3292
3342
  function manualMetadata(sourceText, sourceLocale) {
3293
3343
  return {
@@ -3413,20 +3463,8 @@ function useTranslationWorkspace({
3413
3463
  (locale) => {
3414
3464
  if (readOnly) return { success: false, error: "Workspace is read-only." };
3415
3465
  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
- }
3466
+ const validation = validateLocalePipeline(normalized, currentSchema, policy, validateLocale);
3467
+ if (!validation.valid) return { success: false, error: workspaceLocaleValidationMessage(validation) };
3430
3468
  commit({
3431
3469
  ...currentSchema,
3432
3470
  supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
@@ -3434,22 +3472,15 @@ function useTranslationWorkspace({
3434
3472
  setSelectedLocale(normalized);
3435
3473
  return { success: true };
3436
3474
  },
3437
- [commit, currentSchema, policy, readOnly, sourceLocale, validateLocale]
3475
+ [commit, currentSchema, policy, readOnly, validateLocale]
3438
3476
  );
3439
3477
  const isAddLocaleAllowed = useCallback3(
3440
3478
  (locale) => {
3441
3479
  if (readOnly) return false;
3442
3480
  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;
3481
+ return validateLocalePipeline(normalized, currentSchema, policy, validateLocale).valid;
3451
3482
  },
3452
- [currentSchema, policy, readOnly, sourceLocale, validateLocale]
3483
+ [currentSchema, policy, readOnly, validateLocale]
3453
3484
  );
3454
3485
  const removeLocale = useCallback3(
3455
3486
  (locale) => {
@@ -4832,5 +4863,6 @@ export {
4832
4863
  useForm,
4833
4864
  useFormBuilder,
4834
4865
  useSubmissionReceipts,
4835
- useTranslationWorkspace
4866
+ useTranslationWorkspace,
4867
+ validateLocalePipeline
4836
4868
  };
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.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.6.0",
46
- "@form-engine-ts/privacy": "4.6.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",