10. Centralized copy.json for All User-Facing Strings
Status
Accepted — 2026-02-17
Context
The site’s satirical voice lives in its copy. Taglines, nav labels, empty states, error messages, button text, and microcopy all carry the tone. When strings are inlined in templates and components, tone review requires touching every file, and drift is hard to detect. A reader scanning the site cold does not know which strings were written with intent and which were typed into a template to make the error go away. The ernest-sludge-style-guardian agent can only enforce voice on content it can find and read coherently.
Decision
All user-facing strings live in frontend/src/app/content/copy.json, a single nested JSON file keyed by surface and component. Components import the typed COPY object via the @app/content path alias and reference string paths at usage sites.
copy.tsre-exportscopy.jsonand derives anAppCopytype from it, so references are type-checked at compile time.- The file is hand-authored, not generated. Keys reflect the surface (
shell.header.taglines,landing.hero.headline,shell.footer.classification) rather than feature-internal identifiers. - Strings used in prerendered markdown (post bodies) are authored in markdown files under
content/and are outside this ADR’s scope. The pipeline handles those.
Consequences
- Every visible string is enumerable from one file. Tone audits by
ernestorashread a single source instead of grepping the codebase. - Type safety on string access. A typo in
COPY.shell.header.tagline(missings) fails the build rather than renderingundefined. - The structure resembles an i18n setup without being one. If internationalization is ever wanted, the nested-key shape is already compatible with most i18n tools; if it is not wanted, nothing from i18n tooling is paying its cost.
- Every new feature has a copy step. Components cannot ship default framework strings or placeholder text without first landing a key in
copy.json. This is the point, not a friction to engineer around. - A single file for all strings grows monotonically. At the current scale this is a benefit (single source of truth); at much larger scale it could warrant splitting by surface — but splitting would forfeit the single-file audit property.
- Alternatives-per-rotation (e.g., the
taglinesarray) live in the data, not in component logic. Randomization is a view concern; the content is still centrally reviewable.