MessageId: build the translation-key completeness check the type's own docstring promises #44

Open
opened 2026-08-22 07:07:19 +00:00 by mph · 0 comments
Owner

MessageId's own header has declared this intent since the type was written, and no project has ever collected on it:

// core/shared/src/main/scala/works/iterative/core/MessageId.scala:6-9
/* MessageId is an opaque type to mark all keys to translations.
 * The intent is to use it during build to generate a list of all keys
 * and lately to check if we have all the translations we need
 */
opaque type MessageId = String

Why now

Found in MEDECA-407 (medeca-modul-poptavky), recorded as lesson L8. Three MessageIds reachable from the Žádost workflow engine had no entry in either catalogue, so a user who hit them saw the raw key:

  • error.unhandled.command — the message of every UnhandledCommand, i.e. the whole applicableWhen rejection path. UnhandledCommand.userMessage (AggregateError.scala:20-21) is UserMessage("error.unhandled.command", command.toString(), state.toString()), so the untranslated case was also rendering the entire aggregate into a user-facing string.
  • error.zadost.ukoncena — a terminal-state refusal.
  • error.document_matrix.missing — four payload-guard sites.

error.unhandled.effect was Czech-only, a fourth gap in the same family. All four were fixed in that project. The gap that produced them was not.

Why it was invisible: -Werror cannot see a missing catalogue entry, and the project had only narrow per-bundle parity specs, none covering guard messages. Nothing in iw-support or iw-project-support offers a general check — I searched both.

What is asked for

A reusable completeness check: every MessageId a project can emit resolves in every catalogue the project ships.

MessageCatalogue.get(id): Option[String] (MessageCatalogue.scala:29) is the single resolution point, with JsonMessageCatalogue (ScalaJS) and InMemoryMessageCatalogue (JVM) as implementations. So the check needs one input the library does not have today: the set of all keys.

The obstacle, stated plainly

MessageId is an opaque type over String and carries given Conversion[String, MessageId] (MessageId.scala:22-24). A bare string literal therefore becomes a MessageId with no syntactic marker at the call site. You cannot find the keys by searching for MessageId(...).

Four ways to get the key set, ranked

  1. A macro on MessageId.apply that writes each key to a resource at compile time. Complete and robust, but it misses the implicit-conversion path unless that becomes a macro too.
  2. A scan of the .semanticdb files the build already emits. These index call sites and their literal arguments, including conversions, with no source parsing. The data exists in every build today. Suggested first step.
  3. A source scan for string literals in UserMessage/MessageId positions. Simple and brittle — what the original comment imagined.
  4. A runtime registry. Needs every key declared up front, which no project does.

Suggested shape

  • Key extraction in iw-project-support as an SBT task (it has no i18n tooling today), or in iw-support as a test-time utility.
  • A ready-made spec projects can call, e.g. MessageCatalogueCompletenessSpec(catalogues, keys), so a project adds coverage in one line rather than writing its own parity spec per bundle — the pattern MEDECA-407 found four narrow copies of.
`MessageId`'s own header has declared this intent since the type was written, and no project has ever collected on it: ```scala // core/shared/src/main/scala/works/iterative/core/MessageId.scala:6-9 /* MessageId is an opaque type to mark all keys to translations. * The intent is to use it during build to generate a list of all keys * and lately to check if we have all the translations we need */ opaque type MessageId = String ``` ## Why now Found in MEDECA-407 (medeca-modul-poptavky), recorded as lesson L8. Three `MessageId`s reachable from the Žádost workflow engine had **no entry in either catalogue**, so a user who hit them saw the raw key: - `error.unhandled.command` — the message of **every** `UnhandledCommand`, i.e. the whole `applicableWhen` rejection path. `UnhandledCommand.userMessage` (`AggregateError.scala:20-21`) is `UserMessage("error.unhandled.command", command.toString(), state.toString())`, so the untranslated case was also rendering the entire aggregate into a user-facing string. - `error.zadost.ukoncena` — a terminal-state refusal. - `error.document_matrix.missing` — four payload-guard sites. `error.unhandled.effect` was Czech-only, a fourth gap in the same family. All four were fixed in that project. The gap that produced them was not. **Why it was invisible:** `-Werror` cannot see a missing catalogue entry, and the project had only narrow per-bundle parity specs, none covering guard messages. Nothing in `iw-support` or `iw-project-support` offers a general check — I searched both. ## What is asked for A reusable completeness check: every `MessageId` a project can emit resolves in **every** catalogue the project ships. `MessageCatalogue.get(id): Option[String]` (`MessageCatalogue.scala:29`) is the single resolution point, with `JsonMessageCatalogue` (ScalaJS) and `InMemoryMessageCatalogue` (JVM) as implementations. So the check needs one input the library does not have today: **the set of all keys**. ## The obstacle, stated plainly `MessageId` is an opaque type over `String` and carries `given Conversion[String, MessageId]` (`MessageId.scala:22-24`). A bare string literal therefore becomes a `MessageId` with no syntactic marker at the call site. You cannot find the keys by searching for `MessageId(...)`. ## Four ways to get the key set, ranked 1. **A macro on `MessageId.apply`** that writes each key to a resource at compile time. Complete and robust, but it misses the implicit-conversion path unless that becomes a macro too. 2. **A scan of the `.semanticdb` files the build already emits.** These index call sites and their literal arguments, including conversions, with no source parsing. The data exists in every build today. *Suggested first step.* 3. **A source scan** for string literals in `UserMessage`/`MessageId` positions. Simple and brittle — what the original comment imagined. 4. **A runtime registry.** Needs every key declared up front, which no project does. ## Suggested shape - Key extraction in `iw-project-support` as an SBT task (it has no i18n tooling today), or in `iw-support` as a test-time utility. - A ready-made spec projects can call, e.g. `MessageCatalogueCompletenessSpec(catalogues, keys)`, so a project adds coverage in one line rather than writing its own parity spec per bundle — the pattern MEDECA-407 found four narrow copies of.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/support#44
No description provided.