diff --git a/.gitignore b/.gitignore index 9ab5d28..62be9ee 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,3 @@ server/data/ .vite/ coverage/ -# Worktrees used by parallel agents; never part of a commit. -.claude/worktrees/ diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index fa5133b..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,59 +0,0 @@ -# Notes for Claude - -**Scope: translations and verifying UI work. Nothing else.** - -This file covers exactly two things — the nine translation catalogues, and -what it takes to confirm a visible change actually works. Do not add sections -for anything else: no build, deploy, architecture, style or workflow notes, -however useful they seem. `CONTRIBUTING.md` is the style guide, and everything -else belongs in the code, the docs, or a commit message. - -## Translations - -Nine languages ship alongside English: German, Spanish, French, Dutch, -Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, in -`web/src/locales/`. A missing key renders its English source rather than -failing, so an untranslated string is invisible until somebody reading that -language finds it. - -**Any change that adds or alters a user-visible string adds work in all nine -catalogues.** Say so explicitly when reporting the change — how many keys, and -the fallback count before and after — and say so just as explicitly when a -change adds none, so it is never left to be inferred. - -### The catalogue key for a plural is the `other` form - -`plural()` looks the entry up by `forms.other`, so a call site written as - -```ts -plural(n, { one: "Deleted {n} contact", other: "Deleted {n} contacts" }) -``` - -is keyed on **`"Deleted {n} contacts"`**. Keying the catalogue on the `one` -form type-checks, builds, passes every test, and silently falls back to English -in all nine languages. Nothing errors. The only signal is the fallback count -going up, so read it: - -```sh -npm run i18n:check # literals wrapped, and catalogue health -node scripts/i18n-catalog-check.mjs # per-language: translated / used / falling back -``` - -Compare the "falling back to English" number against `main` before and after. -It should not rise. Do not read the percentage instead — adding keys moves the -denominator, so it can hold steady while new strings go untranslated. - -Plural forms are per language, from `Intl.PluralRules`: `one`/`other` for most, -`one`/`few`/`many`/`other` for Russian and Ukrainian, `other` alone for Japanese -and Chinese. Supplying a form a language does not draw is inventing a -distinction, not being thorough. - -## Verifying UI work - -Store tests do not exercise the component. At least one bug in this repo's -history — a shift-click range measured inside a `setState` updater, which React -runs after the anchor ref has already moved — passed every store assertion and -failed the moment the built app was driven. If a change is visible on screen, -run it: `npm run dev:mock` (mock Stalwart, credentials printed on start), then -drive the real thing. Add a component test for what you find; there are -examples in `web/src/views/*/__tests__/`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55bfcbc..4d63912 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,12 +48,10 @@ For larger changes, please open an issue to discuss the approach **before** subm - Related issue number(s), if any - Screenshots/GIFs for UI changes - Any manual testing you performed -8. **Add translations** for any new user-visible string. Nine languages ship - alongside English in `web/src/locales/`, and a missing key renders its - English source rather than failing — so an untranslated string is invisible - until somebody reading that language finds it. `npm run i18n:check` and - `node scripts/i18n-catalog-check.mjs` report where you stand; the catalogue - key for a plural is the `other` form. See [CLAUDE.md](CLAUDE.md). +8. **Add translations** for any new user-visible string — see + [Translations](#translations) below — and **drive the built app** for any + change that is visible on screen, as described in + [Verifying UI work](#verifying-ui-work). `main` is protected. A change reaches it through a pull request whose **build** check has passed — not afterwards — and the branch cannot be force-pushed or @@ -67,6 +65,56 @@ waiting for one. - Prefer clarity over cleverness — this is a mail client people rely on for their inbox. - Comment non-obvious JMAP interactions, especially around state/`changes` handling, since JMAP's delta-sync model can be easy to get subtly wrong. +### Translations + +Nine languages ship alongside English: German, Spanish, French, Dutch, +Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, in +`web/src/locales/`. A missing key renders its English source rather than +failing, so an untranslated string is invisible until somebody reading that +language finds it. + +**Any change that adds or alters a user-visible string adds work in all nine +catalogues.** Say so explicitly in the PR — how many keys, and the fallback +count before and after — and say so just as explicitly when a change adds none, +so it is never left to be inferred. + +#### The catalogue key for a plural is the `other` form + +`plural()` looks the entry up by `forms.other`, so a call site written as + +```ts +plural(n, { one: "Deleted {n} contact", other: "Deleted {n} contacts" }) +``` + +is keyed on **`"Deleted {n} contacts"`**. Keying the catalogue on the `one` +form type-checks, builds, passes every test, and silently falls back to English +in all nine languages. Nothing errors. The only signal is the fallback count +going up, so read it: + +```sh +npm run i18n:check # literals wrapped, and catalogue health +node scripts/i18n-catalog-check.mjs # per-language: translated / used / falling back +``` + +Compare the "falling back to English" number against `main` before and after. +It should not rise. Do not read the percentage instead — adding keys moves the +denominator, so it can hold steady while new strings go untranslated. + +Plural forms are per language, from `Intl.PluralRules`: `one`/`other` for most, +`one`/`few`/`many`/`other` for Russian and Ukrainian, `other` alone for Japanese +and Chinese. Supplying a form a language does not draw is inventing a +distinction, not being thorough. + +### Verifying UI work + +Store tests do not exercise the component. At least one bug in this repo's +history — a shift-click range measured inside a `setState` updater, which React +runs after the anchor ref has already moved — passed every store assertion and +failed the moment the built app was driven. If a change is visible on screen, +run it: `npm run dev:mock` (mock Stalwart, credentials printed on start), then +drive the real thing. Add a component test for what you find; there are +examples in `web/src/views/*/__tests__/`. + ### Development Setup 1. Clone your fork: