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.
BookerMessageis 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.