Skip to content

Booker messages

A rule that turns a booking away has to say why, and the wording is the venue's: "at most four per person" is a house style as much as a requirement. A booker message is one such sentence, written once and pointed at from wherever it is shown.

  • BookerMessage is a config entity (yoyaku_message), none shipped. It holds a label, the text, and the usages it is written for.
  • A rule that shows one keeps only the id. Nothing stores prose.
  • The screens are at /admin/booking/message.

How it works

An operator writes a message, ticks the rules that may show it, and then picks it on those rules' own settings forms. Every rule offers the shipped sentence as well, which is what a site with nothing of its own to say uses.

Because the text is configuration in its own right, Config Translation covers it on the message's own Translate tab, and a booker is shown it in the language they are reading the site in.

Why not a box on the rule

Where prose is stored decides whether it can be translated, and a constraint policy hangs on four kinds of host that store their attachments differently:

Host Where its attachments live Reachable by a translation system
yoyaku_resource_type third_party_settings, so configuration Yes
yoyaku_resource the policies field, JSON in one column No
yoyaku_resource_tariff the same field No
yoyaku_slot_tariff the same field No

A sentence typed into an attachment on any of the last three would be single language for good: neither Config Translation nor Content Translation reads inside a serialized value. Moving the prose onto a config entity, and keeping only its id in the attachment, puts one translated sentence behind all four hosts and translates it once instead of once per attachment.

Where a message may be used

Not free text, and not a list this engine keeps. Each rule publishes a usage key, which is its plugin id, and the message form shows one checkbox per key an installed rule publishes. A module shipping a new rule widens the vocabulary by existing.

A message carries several usages, because one sentence is often the right answer in two places, and that is also how two rules come to share a sentence: tick both. A rule offers only the messages ticked for it, and a message ticked for nothing is offered nowhere.

A message ticked for a rule no longer installed keeps that tick, named by its bare key on the list and on the form, rather than losing it to the first unrelated save.

Placeholders

A rule declares what it substitutes, and the settings form names them under the choice. Anything else is left exactly as it is written, so an operator who types @limit where nothing fills it in would have a booker read @limit. The message form refuses that on save, naming the placeholder and the rule.

Rule Fills in
Limit per transaction @limit, @tariff
Per-booker limit @limit
Per-booker limit across several resources @limit, @shared_with
Places a booking may orphan @places, @minimum
Minimum booker age @age

A placeholder is a @, % or : at a word boundary followed by letters, digits or underscores. An email address is not one, since the @ in it follows a letter, so a message telling a refused booker where to write is fine.

Note what is not on the list: @count. The sentences a rule ships are plural-aware, built with formatPlural(), and a written one cannot be. That is one reason to leave the shipped wording alone where it already reads well.

What happens when a message goes

Three situations, answered identically, and the answer is always the sentence the rule ships:

  • nothing was ever chosen;
  • the id names a message that has been deleted;
  • the message is still there but no longer ticked for this rule.

That mirrors how the policy manager answers an attachment naming a plugin that no longer exists, except that there the safe direction is to refuse and here it is to speak: a rule that is enforcing correctly and wording itself in its own words has not failed at anything.

A resource type pointing at a message gains a real configuration dependency on it, so the delete screen tells an operator which types would be updated, and deleting the message empties the reference rather than taking the type with it. The three content hosts keep their attachments in a field the dependency system cannot see into, so there a message that has gone is simply read as none chosen.

Adding a message slot to your own rule

A ConstraintPolicy gets one by calling buildMessageElement() from its settings form and getMessageSettingValue() from its submit handler, then reading the answer through getViolationMessage(). Declare what it fills in on the attribute:

#[ConstraintPolicy(
  id: 'my_rule',
  label: new TranslatableMarkup('My rule'),
  messagePlaceholders: [
    '@limit' => new TranslatableMarkup('the number above'),
  ],
)]

Pass every declared placeholder on every path that words a refusal, not only the one that obviously needs it: what a rule declares, an operator may write, and what is not passed reaches the booker as written.