Skip to main content

Validator messages and i18n

Built-in validators include English fallback messages. Applications can override them through a configured primitive set, Angular dependency injection, one form tree, one validator, or a global fallback.

For all node and binding optionsβ€”not only messagesβ€”see the Configuration reference.

πŸ’¬ Where to configure messages​

Intended scopeAPI and recommended location
Standalone Angular applicationprovideFormNodesConfig() in app.config.ts, passed to bootstrapApplication
NgModule applicationprovideFormNodesConfig() in AppModule.providers
Shared process-wide fallback, including nodes outside DIconfigureGlobalFormNodes() in main.ts, before bootstrapping
Larger message catalogExport the data from a separate file and import it at the chosen configuration point

Prefer the Angular provider for application configuration, particularly when messages depend on injected translations or an SSR request. Use the global setter when a shared fallback is intended. Do not repeat global setup in component constructors or lifecycle hooks. A static catalog needs no application initializer or side-effect-only import.

See the complete global startup example and application provider example.

βš™οΈ Precedence​

The closest definition wins:

  1. Validator-local message option.
  2. Closest form or array validatorMessages catalog.
  3. Closest createFormPrimitives() validator-message default.
  4. Closest Angular provideFormNodesConfig() catalog.
  5. Process-wide configureGlobalFormNodes() catalog.
  6. Built-in English message.

Missing entries and message functions returning undefined continue through the fallback chain.

βš™οΈ Configured primitive defaults​

Use one isolated factory set when application forms import their primitives from a shared module:

export const { form, group, array, field } = createFormPrimitives({
validatorMessages: () => ({
required: translations().required,
min: ({ min, actual }) => translations().min({ min, actual }),
}),
});

This also covers standalone fields created by that field() factory. A closer form, group, or array catalog can override individual messages.

βš™οΈ Angular application configuration​

β—† Direct provider catalogs​

provideFormNodesConfig() also accepts a catalog object directly:

provideFormNodesConfig({
validatorMessages: {
required: 'Please complete this field.',
min: ({ min }) => `The minimum value is ${min}.`,
},
});

Use a factory when building the catalog requires inject(). Both forms support message callbacks that read signals; precedence and inherited configuration are the same.

Use validatorMessages: null to replace the inherited provider catalog with an empty one. This preserves node-local, form-tree, ancestor-node provider, global, and built-in fallbacks. Omitting the option or passing undefined instead inherits the injector catalog.

β—† Injectable catalogs​

Configure translated defaults once in an application, route, or environment injector:

import { ApplicationConfig, inject } from '@angular/core';
import { provideFormNodesConfig } from '@ngblocks/form-nodes';

export const appConfig: ApplicationConfig = {
providers: [
provideFormNodesConfig({
validatorMessages: () => {
const translations = inject(TranslationService);

return {
required: () => translations.translate('validation.required'),
min: ({ min, actual }) => translations.translate('validation.min', { min, actual }),
};
},
}),
],
};

Provider factories may call inject(). A closer route or environment injector supplies the complete catalog captured in that scope; Angular does not merge it with an outer provider catalog. Missing entries continue through any different catalog captured by an ancestor node, followed by the global and built-in fallbacks.

βš™οΈ Global configuration​

Use process-wide configuration outside Angular or for one shared application default. In an Angular browser entry point, call it in main.ts before bootstrapping. The startup example keeps the catalog active; the following fragment instead shows a temporary override:

const restore = configureGlobalFormNodes({
validatorMessages: {
required: 'This value is required.',
min: ({ min, actual }) => `${actual} must be at least ${min}.`,
},
});

// Restore the previous catalog when a temporary scope ends.
restore();

Global configuration is shared module state. Do not mutate it per request during SSR; use an Angular provider or form catalog for request-specific locales.

βš™οΈ Form-tree configuration​

const checkout = form({
quantity: field(0, [min(1)]),
}, {
validatorMessages: () => ({
min: ({ min }) => checkoutTranslations().minimumQuantity(min),
}),
});

The catalog applies to that form or array and all descendants. A nested catalog overrides only the keys it defines.

πŸ’¬ Validator-local messages​

const myForm = form({
age: field(16, [
min(18, {
message: () => locale() === 'es'
? 'Debes ser mayor de edad.'
: 'You must be an adult.',
}),
]),
});

Local message functions close over their dependencies and take no parameters. Catalog callbacks receive strongly typed constraint data; IntelliSense exposes the available keys and parameters.

KeyCallback parameters
requirednone
emailnone
urlnone
equalTonone; compared values are intentionally private
uniqueItems{ duplicateIndexes: readonly number[] }
between{ min: number; max: number; actual: number }
min{ min: number, actual: number }
max{ max: number, actual: number }
integer{ actual: number }
minLength{ minLength: number, actual: number }
maxLength{ maxLength: number, actual: number }
pattern{ pattern: RegExp, actual: string }
minDate{ minDate: Date, actual: Date }
maxDate{ maxDate: Date, actual: Date }
dateBetween{ minDate: Date, maxDate: Date, actual: Date }
oneOf{ options: readonly unknown[], actual: unknown }
minWords{ minWords: number, actual: number }
maxWords{ maxWords: number, actual: number }

The callback receives the resolved constraint, not its original signal or source function. For example, a reactive min(() => minimumAge()) supplies the current numeric min value.

πŸ’¬ Reactive locale changes​

Catalog sources and the selected message function run reactively while the validator is failing:

const locale = signal<'en' | 'es'>('en');

configureGlobalFormNodes({
validatorMessages: () => ({
required: () => locale() === 'es'
? 'Este campo es obligatorio.'
: 'This field is required.',
}),
});

Changing locale updates existing failing errors without recreating the form. This works inside and outside Angular dependency injection.

These catalogs apply only to built-in validators. Custom validators supply their own errors and messages.