0012 - Escaped Message Arguments and Layered Language Files¶
Status¶
Accepted (1.0.0)
Context¶
Messages are MiniMessage templates with {0}-style arguments. Many
arguments are typed by people - ban reasons, report texts, staff chat, note
texts, filter input. Substituted verbatim, a reason like
<click:run_command:/op me> would become a clickable command in a message
another staff member sees. Separately, lang/*.yml is copied to the data
folder once; keys added in later versions were missing there and rendered
as [missing: ...] after an upgrade, and a single YAML typo in a language
file aborted plugin startup.
Decision¶
MessageService#getMiniMessage-escapes everyStringargument. Arguments that are trusted markup must be wrapped inMessageService.markup(...)explicitly. Numbers and other types are passed through.- Language resolution is layered: the server owner's
plugins/UniversalAdmin/lang/<locale>.ymloverrides the copy bundled in the jar (plus defaults registered by extensions), thenen_USin the same order, then a visible marker. A broken override file is logged and skipped./admin reloadre-reads the override files. (Since 1.7.1 the override files hold only the owner's texts - ADR 0026.)
Consequences¶
- Player-supplied text can never inject formatting or events, in any module or extension, without each call site having to remember it.
- Upgrades never lose texts; server owners only need to keep the keys they changed.
- Code that resolves a message and shows it with
Component.text(...)instead ofComponentMessages.render(...)would show escape characters - render resolved messages as MiniMessage.
Alternatives¶
- Escaping at each call site. Rejected: one forgotten call is a security hole.
- MiniMessage tag resolvers instead of
MessageFormat. Cleaner in theory, but would have changed every template and every call site; the escaping keeps both and gives the same protection.