Ballot validation messages
These are the warnings shown while a voter is marking a ballot — "select one more candidate", "shorten your write-in by two characters". They are a different family from the cast-vote failures in Cast Vote Errors, which happen after the voter submits.
Where they come from
The Rust checker in sequent-core reports a problem as a message key plus a message_map. Every value
in that map is a string, and the map describes the state of the selection — numSelected, min, max —
rather than the number the sentence is actually about.
That distinction is the whole reason this layer exists. "Select 2 more candidates" needs min - numSelected;
the checker never sends that number, it sends the two operands.
Rendering them
getBallotErrorOptions bridges the two. Always render these messages through it:
import {getBallotErrorOptions} from "@sequentech/ui-core"
t(error.message || "", getBallotErrorOptions(error.message, error.message_map))
It does two things:
- Copies every
message_mapentry into the options, coercing numeric strings to numbers. So if the checker already sends acount, the message pluralises with no further work. - Derives
countfor the keys listed inCOUNT_DERIVATIONS, overriding whatever came in the map.
The coercion in step 1 matters: i18next skips pluralisation entirely when count is a string and silently
falls back to the unsuffixed key. Passing the raw message_map straight to t() is a bug for exactly that
reason.
Adding a new validation message
Adding the key in Rust and translating it is enough when the sentence's number is already in
message_map.
Add an entry to COUNT_DERIVATIONS in packages/ui-core/src/services/ballotErrorMessages.ts only when the
number has to be computed from the state fields:
| message | the sentence is about | derivation |
|---|---|---|
errors.implicit.selectedMin | how many more to pick | min - numSelected |
errors.implicit.underVote | how many more are allowed | max - numSelected |
errors.implicit.selectedMax | how many to unpick | numSelected - max |
errors.implicit.maxSelectionsPerType | how many to unpick from one candidate type | numSelected - max |
errors.implicit.overVoteDisabled | the maximum already reached | max, falling back to numSelected when the map carries no max |
That is the whole of COUNT_DERIVATIONS. Every other validation message is rendered from the message_map
as it arrives.
A derivation always wins over a count that arrived in the map, so do not add one when the checker already
sends the right number.
Pluralising them
A derived count is a number like any other, so these messages follow the ordinary plural rules — with one
wrinkle worth knowing: the derivations subtract, so they can produce values the checker never sends.
- Give the message the plural forms every target language reports, not just
_oneand_other. Spanish, Catalan and French also need_many; a missing form falls through tofallbackLngand renders the English sentence rather than the right one._manyis a verbatim copy of_other. - Adding a suffix to
en.tsmakes it a required key in every other language file, because they are typedTranslationType = typeof englishTranslation. overVoteDisabledis the one message whose count is a bound rather than a difference, so it is the one that can carry a large operator-configured number straight into the sentence.
See Add a New Language for the per-language rules.