ADR-004: Generated Catalogs Use Executable Message Functions
Generated catalogs contain literal strings and executable message functions shared by Core, React, and Solid.
Status: Accepted
Last updated: 2026-10-16
Decision
Generated catalog modules export one branded message map:
- constant messages are string values
- dynamic messages are executable functions
This representation is the public runtime contract on both server and client. ICU parsing belongs to authoring, validation, and compilation, not application message rendering. Generated code must not defer unsupported or invalid ICU to a runtime parser. Invalid ICU and ICU constructs outside the supported compiler/runtime subset are fatal compilation errors in both development and production, for source messages and translations alike. Diagnostics identify the affected catalog, locale, and source message. No adapter option may downgrade these failures to successful compilation or emit a lazy-parser substitute.
A missing translation is not an invalid message. By default, compilation
resolves it through the configured fallback locale chain and ultimately the
source message, then compiles that result using the same message representation.
Variables, plurals, and rich text retain their compiled semantics without a
runtime parser. The missing target-locale translation remains observable in
diagnostics; failOnMissing may enforce translation completeness even when a
valid fallback exists. Invalid or unsupported fallback messages still fail
compilation under the rule above.
This build-time translation fallback is distinct from failure to deliver a compiled fragment at runtime. Failed delivery and unexpected missing compiled entries must reach host error handling rather than render substitute source text or internal keys, as specified in ADR-020. Concrete adapter integration and recovery mechanics are described in ADR-008 and the v2 migration guide.
Message functions receive the lookup values and a small renderer interface.
They call named operations such as value, number, plural, tag, and
join; they do not return framework-specific values directly. Plural and
select branch functions and branch tables are hoisted to module scope so they
are allocated once rather than once per render.
Core, React, and Solid provide renderer implementations for their result types. The same function therefore produces a string, React nodes, or Solid nodes. Generated modules contain no parallel string/AST maps.
The catalog brand identifies generated catalog maps, including their constant strings. Constant strings in a compiled catalog are already compiled literal messages; they are not ICU patterns to interpret at runtime. Applications must preserve the generated catalog contract when loading complete catalogs or fragments. Uncompiled ICU maps are not an alternative runtime input.
Ferrocat remains the Rust-side ICU parser. The Rust core lowers its AST into a host-neutral message program, and the Node host boundary renders safe JavaScript module source in accordance with ADR-019.
The native renderer is the single catalog-module generator. The public
TypeScript renderCatalogModule() compatibility helper delegates to that
renderer instead of parsing ICU and maintaining a second JavaScript generator.
Context
Raw ICU strings require parsing during execution. A parallel map of pre-parsed nodes would avoid that parse but duplicate dynamic message keys and retain an AST interpreter. One executable representation avoids both costs.
Palamedes needs one generated representation that handles plain strings, variables, formatters, plurals, selects, and rich-text tags across the Core, React, and Solid renderers. Palamedes' supported application model is message authoring through macros and supported extraction, translation in source catalogs, and compilation before execution. Runtime interpretation of uncompiled ICU catalogs is not a separate supported application model.
Alternatives Considered
1. Parallel string and pre-parsed-node maps
Rejected because it duplicates keys and dynamic message data, allocates AST objects during module evaluation, and retains an interpreter in every runtime renderer.
2. Replace message strings with serialized AST values
Rejected because it still requires runtime interpretation and makes even constant messages structured data.
3. Generate framework-specific functions
Rejected because it would duplicate compiler semantics across Core, React, and Solid. The renderer parameter keeps one compiler output portable across hosts.
4. Retain runtime support for uncompiled ICU catalogs
Rejected because it adds a second execution model outside Palamedes' intended extraction, translation, and compilation workflow. Small applications can load a complete compiled catalog without needing a runtime parser.
Consequences
- Generated dynamic messages execute directly without server or client ICU parsing or AST traversal.
- Generated catalog values are
string | CompiledMessage; consumers must not assume every generated value is JSON-serializable. - Runtime parser and parsed-node inspection APIs stay outside the public application runtime contract. Build-time tooling may still parse ICU and inspect its representation.
- The compiler owns safe JavaScript expression generation, including escaped literals, computed object keys, nested choices, and plural-pound semantics.
- Custom integrations loading generated modules must declare
@palamedes/coreas a direct runtime dependency because the module importsdefineCompiledCatalog()from itscompiledentrypoint. - Runtime and bundle benchmarks must cover both payload size and first-render execution when the generated representation changes.
- ADR-005 applies the parser-free contract to the complete public application runtime, including package-root imports.
Validation
The catalog-delivery evidence report records parser-free artifacts, browser delivery, server catalog reuse, and host failure checks. The v2 migration guide owns legacy API replacements; release status belongs in the release policy and tracker.