diff --git a/shared/.claude/CLAUDE.md b/shared/.claude/CLAUDE.md index 4a9bd5e..7e27c64 100644 --- a/shared/.claude/CLAUDE.md +++ b/shared/.claude/CLAUDE.md @@ -9,6 +9,7 @@ to, so they have to be in context before the mistake, not after. | Rule | Applies when | |------|--------------| | [Reporting data and metrics](rules/reporting-data.md) | a number gets reported — a query, a benchmark, a count | +| [Localisation](rules/localisation.md) | user-facing text gets written — a label, an error, an email | | [Attribution](rules/attribution.md) | anything leaves the machine or gets committed | | [Commit messages](rules/commit-messages.md) | writing a commit | | [Git history](rules/git-history.md) | amend, squash, rebase, force-push | diff --git a/shared/.claude/rules/localisation.md b/shared/.claude/rules/localisation.md new file mode 100644 index 0000000..6331de7 --- /dev/null +++ b/shared/.claude/rules/localisation.md @@ -0,0 +1,46 @@ +# Localisation + +User-facing text goes through a localisation key, never into the source as a +literal. The reason is that the second language is what makes the first one +expensive: a hardcoded string is invisible until someone has to find all of +them, and by then they are spread across templates, validators and error paths. + +Use what the project already has, and never introduce a solution without asking +which one. "Already has" usually means `messages.properties` through Spring +Boot's `MessageSource`, `ResourceBundle` on plain Java or Kotlin, or i18next, +Lingui, react-intl, Angular i18n or plain JSON files on the frontend. An +existing library is not replaced as a side effect of adding a string. + +For a new string: + +- Add the key, and a translation in every language the app already ships. If a + translation cannot be written reliably, add the key with a placeholder and say + which languages are still missing — a quietly English row reads as done. +- Reuse a key before adding a near-duplicate. +- Keys carry no spaces, follow whatever convention the project already uses, and + name the thing rather than its position: `user.profile.saveButton`, + `error.invalidCredentials`, not `label1`. +- Localised text is for display only — never an identifier, a config value, an + API field, a database value, or a branch in business logic. That turns + translating a word into a behaviour change. + +## Email templates + +Same rule, and the subject line is the one that gets missed. Localise the +subject, the headings, the body, the button labels, the footer. Keep dynamic +values — names, order numbers, dates, links — as placeholders in the template +instead of concatenating around a localised fragment, because the word order is +not the same in every language. Dates, numbers, currencies and plurals go +through the framework's locale-aware formatting. + +Whether that is keys or a template per language is a decision the project has +already made; follow it. + +Templates share one base layout — header, footer, branding, styling — and hold +only their own content. Copy-pasted markup between two templates is a footer +that gets fixed in one of them. + +Text baked into an image is not translatable, not selectable and not readable by +a screen reader, so use HTML text. If a banner or a logo genuinely has to carry +words, either ship a variant per language or say up front that the localised +assets are missing.