Skip to content

Messages & locales

Validation messages are templates. When a validator fails it renders a template into a string, substituting tokens drawn from the record and the validator options. The template is chosen in this order:

  1. An explicit message option on the validator declaration.
  2. A template registered for the current locale (see Locales).
  3. The built-in default for that error type.

Message tokens

Every template is interpolated with the tokens below. An undefined token is left untouched, so a template that does not mention a token is unaffected.

Token Substituted with
{model} The model's class name (e.g. User)
{attribute} The attribute name, or the as label when one is set
{value} The bound value for the failing validator (the length, the threshold)
{count} The numeric bound for length, numericality, and comparison validators
any option Any extra option passed to errors.add (for example {count})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use ORM::ActiveRecord::Model;

class User is Model {
  submethod BUILD {
    self.validate: 'name', { length => { max => 8 }, message => '{model}.{attribute} allows {count} characters' }
  }
}

my $u = User.build({name => 'overlong'});
$u.is-valid;
say $u.errors.name[0];   # User.name allows 8 characters

Locales

ORM::ActiveRecord::Support::I18n holds per-locale message templates and the currently active locale. Register templates with store, then switch the active locale with set-locale.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use ORM::ActiveRecord::Support::I18n;

I18n.store('fr', {
  errors => {
    messages => {
      blank          => 'doit être rempli',
      'too-long'     => 'est trop long (maximum {count} caractères)',
      'greater-than' => 'doit être supérieur à {count}',
    },
  },
});

I18n.set-locale('fr');

Error types use the same hyphenated names the validators record: blank, too-long, too-short, wrong-length, greater-than, less-than, taken, accepted, confirmation, inclusion, exclusion, invalid, and the rest listed in The errors collection.

Lookup order

For a given model, attribute, and error type, the most specific registered template wins. The keys are searched in this order:

  1. activerecord.errors.models.MODEL.attributes.ATTRIBUTE.TYPE
  2. activerecord.errors.models.MODEL.TYPE
  3. activerecord.errors.messages.TYPE
  4. errors.attributes.ATTRIBUTE.TYPE
  5. errors.messages.TYPE

MODEL is the lower-cased class name and ATTRIBUTE is the column name. A model-and-attribute override lets one attribute read differently from the rest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
I18n.store('fr', {
  activerecord => {
    errors => {
      models => {
        user => {
          attributes => {
            email => { blank => "l'adresse e-mail est obligatoire" },
          },
        },
      },
    },
  },
});

When no template is registered for the active locale, the lookup retries under default-locale (also en by default), then falls back to the built-in defaults. Nothing breaks if a locale is partially translated.

Switching locale for a block

with-locale runs a block under a temporary locale and restores the previous one afterwards, even if the block dies.

1
2
3
4
5
6
7
my @messages;

I18n.with-locale('fr', sub {
  my $u = User.build({email => ''});
  $u.is-valid;
  @messages = $u.errors.full-messages;
});

Managing the registry

1
2
3
4
5
6
I18n.locale;               # current locale, default 'en'
I18n.set-locale('de');     # set the active locale
I18n.default-locale;       # fallback locale, default 'en'
I18n.set-default-locale('en');
I18n.available-locales;    # sorted list of registered locales
I18n.reset;                # clear all templates, reset to 'en'

errors.add resolves through the same locale store, so messages added by hand or from a custom validator are localised too:

1
2
3
4
5
I18n.set-locale('fr');

my $u = User.build({});
$u.errors.add('email', 'blank');
say $u.errors.email[0];   # doit être rempli