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 scope | API and recommended location |
|---|---|
| Standalone Angular application | provideFormNodesConfig() in app.config.ts, passed to bootstrapApplication |
| NgModule application | provideFormNodesConfig() in AppModule.providers |
| Shared process-wide fallback, including nodes outside DI | configureGlobalFormNodes() in main.ts, before bootstrapping |
| Larger message catalog | Export 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:
- Validator-local
messageoption. - Closest form or array
validatorMessagescatalog. - Closest
createFormPrimitives()validator-message default. - Closest Angular
provideFormNodesConfig()catalog. - Process-wide
configureGlobalFormNodes()catalog. - 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.
| Key | Callback parameters |
|---|---|
required | none |
email | none |
url | none |
equalTo | none; 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.