Fourteen English sentences were built in the ViewModel and rendered
verbatim as fatal, Working.message and both error fields, so they
shipped untranslated to every locale. Three more sites passed through
whatever text the server, the repository or a Throwable produced.
Seventeen in all, and three of the fourteen were added by fixes earlier
on this branch -- which is the argument for a type rather than a rule.
This is the defect Outcome.Cause was introduced to fix, and it only ever
covered the address step. AddAccountMessage does the same for the rest:
a plain Kotlin sealed type in the app, with the resource mapping beside
Cause's in the screen. The state fields carry it, so a literal is now a
compile error rather than something review has to catch. Nothing lints
for this -- HardcodedText reads XML layout attributes and this app has
none -- so the type is the only guard there is.
The passthroughs get causes of their own, following the rule
Outcome.Cause already states: PollResult.Failed carries RATE_LIMITED,
MAINTENANCE or SERVER_ERROR, keeping the 429/503 distinction that
flattening to one message would lose, and CredentialFailed carries
KEYSTORE_REFUSED or NOT_SAVED. Their reason and detail stay for logs and
are never shown. A server's words are untranslatable and often a bare
status line; a Throwable's are worse.
Twenty keys, base locale only -- Weblate owns the rest and picks them up.
The four tests that asserted on English prose now assert on the message,
which for the cross-domain one is stricter: it pins the host into the
argument instead of anywhere in a sentence.