diff --git a/10-functional/README.md b/10-functional/README.md index b87a741..b0d5594 100644 --- a/10-functional/README.md +++ b/10-functional/README.md @@ -7,7 +7,7 @@ check. | Page | Contents | |------|----------| -| [features/](features/) | Fifty-one features across seven areas, each with numbered requirements | +| [features/](features/) | Fifty-two features across seven areas, each with numbered requirements | | [journeys/](journeys/) | Seven end-to-end paths, each naming the features it exercises | ## The contract diff --git a/10-functional/features/README.md b/10-functional/features/README.md index c1cb9ab..2205cf8 100644 --- a/10-functional/features/README.md +++ b/10-functional/features/README.md @@ -2,7 +2,7 @@ **Status:** Accepted -Fifty-one features across seven areas. This catalogue is the **contract the +Fifty-two features across seven areas. This catalogue is the **contract the technical spec is written against** — every architecture and implementation decision must trace to a feature requirement here, not the other way round. @@ -155,6 +155,7 @@ exhibit**. G1 in particular is the one the whole product is judged on. | [G4](g-ux/g4-pwa.md) | Responsive and installable PWA | | [G5](g-ux/g5-plain-language.md) | Plain language and in-product help | | [G6](g-ux/g6-keyboard.md) | Keyboard and command palette | +| [G7](g-ux/g7-localisation.md) | Interface localisation and language selection | --- diff --git a/10-functional/features/g-ux/g5-plain-language.md b/10-functional/features/g-ux/g5-plain-language.md index 154bbe6..123ac2a 100644 --- a/10-functional/features/g-ux/g5-plain-language.md +++ b/10-functional/features/g-ux/g5-plain-language.md @@ -84,8 +84,12 @@ comment policy applies to code ### Translation -The product has a translated readme and no translated interface. Interface -translation is not currently in scope, and this page does not imply otherwise. +Interface localisation is specified separately in +[G7](g7-localisation.md) — supported languages, detection, the per-user +preference, and full string coverage all live there. What this page owns is the +honesty rule that outlives any single locale: a string shown in English because +its translation is missing is **English**, and the interface must not present a +partially translated locale as though it were complete. ## Acceptance criteria @@ -104,11 +108,12 @@ translation is not currently in scope, and this page does not imply otherwise. | **G5-R11** | The product MUST use one voice, British-leaning, calm and precise. | | **G5-R12** | The interface MUST NOT contain placeholders, coming-soon copy, or controls that do nothing. | | **G5-R13** | A setting reserved for future behaviour MUST say so explicitly. | -| **G5-R14** | Interface translation MUST NOT be implied where it does not exist. | +| **G5-R14** | A locale's translation coverage MUST NOT be overstated; a string shown by English fallback MUST read as English, not as evidence of a complete translation. See [G7](g7-localisation.md). | ## Related - [G1 Privacy stance](g1-privacy.md) — the honesty requirements this enforces - [G2 Error and remedy model](g2-error-model.md) +- [G7 Interface localisation](g7-localisation.md) — supported languages, detection, and the per-user preference - [60-brand/brand-rules.md](../../../60-brand/brand-rules.md) — the voice - [ADR-0011](../../../00-overview/decisions/0011-code-comment-policy.md) — the same discipline in code diff --git a/10-functional/features/g-ux/g7-localisation.md b/10-functional/features/g-ux/g7-localisation.md new file mode 100644 index 0000000..79d83f6 --- /dev/null +++ b/10-functional/features/g-ux/g7-localisation.md @@ -0,0 +1,158 @@ +# G7 — Interface localisation and language selection + +**Status:** Accepted · **Area:** G — Cross-cutting UX + +> **Forthcoming.** This feature is specified ahead of its implementation, to +> unblock the app-side work that introduces it. Nothing here ships in `v1.3.0`; +> every requirement below is therefore marked *(Open)* until the behaviour it +> describes lands. This is the same discipline [G5](g5-plain-language.md) applies +> to deferred behaviour — a specification may run ahead of the product, but it +> must say so plainly rather than describe an interface that does not yet exist. + +--- + +## Purpose + +The interface has always been English, and only English. The words the product +chooses are load-bearing — the whole of [G5](g5-plain-language.md) is about +saying the true thing in the user's own vocabulary — and that argument does not +hold if the user does not read English. + +This feature makes the interface translatable and lets a household choose the +language it runs in. It starts with English and Dutch, the two the product's own +statement vocabulary already spans, and is built so a third costs a translation +rather than a rewrite. + +It deliberately mirrors the existing theme preference +([F1](../f-platform/f1-desktop-shell.md)): a signal the environment can offer, a +per-user choice that overrides it, and a switch in Settings that sits in the same +family as Appearance. + +## Behaviour + +### Two languages to start, more without a rewrite + +The interface ships in English (`en`) and Dutch (`nl`). English is the **source +locale**: it is the language keys are authored in, and the one every other locale +is measured against. + +Adding a further language is a matter of registering its locale and supplying its +translations. The mechanism that detects, selects, and applies a language does +not change when a locale is added — nothing about switching is wired to the fact +that there happen to be two. + +### Detection before choice + +On a first visit, before the household has expressed any preference, the +interface picks its language from the environment: the browser or system +`Accept-Language` preference, resolved to the **best supported match**. A request +that prefers `nl` gets Dutch; one that prefers a language the product does not +carry falls through. + +Where nothing in the request matches a supported locale, the interface uses +**English**. English is the floor, not an error state — an unmatched preference +is an ordinary outcome, not a failure to report. + +This mirrors the theme signal exactly: the environment offers a preference, its +absence is itself a defined outcome, and neither is ever the last word. + +### Choice beats detection + +Language is a **per-user preference**, stored on the user record next to the +theme it mirrors ([data model](../../../20-architecture/data-model.md)). Once a +user has set it, that choice wins — on this session and every one after it, +regardless of what the environment reports. Detection applies only until a +preference exists; it never overrides one. + +The preference persists the way every other user preference does. A user who +chose Dutch last month opens the application in Dutch, with no re-detection and +no drift back to the environment's language. + +### The switch lives in Settings + +The language switcher sits in Settings, in the **same family as Appearance** — +the surface that already owns the theme choice. It is an ordinary setting a user +can find where they would look for the theme, not a separate onboarding step or a +hidden flag. Changing it takes effect across the interface immediately, without +losing the user's place or data. + +### Everything user-facing is translatable + +Every string a user can read is translatable. No user-facing copy is pinned to a +single language in a way a locale cannot reach — the same completeness discipline +[G4](g4-pwa.md) applies to responsive layout, applied to language. + +Where a key has no value in the active locale, the interface falls back to its +**English** value rather than showing a raw key, a blank, or a placeholder. A +partially translated locale therefore degrades to English in the gaps, never to +something broken — which is exactly the honesty [G5-R14](g5-plain-language.md) +requires: an untranslated string reads as English, not as a translated interface +that silently isn't. + +### The active language is announced + +The active locale is reflected in the document's language attribute, so a screen +reader announces content in the language it is actually written in +([G3](g3-accessibility.md)). A Dutch interface that a screen reader pronounces as +English is a defect, not a cosmetic one. + +### It stays on the machine + +Detection reads a request header the browser already sends and a preference the +user already stored; neither is sent anywhere. Language selection adds no +outbound call and no third-party translation service — consistent with the +privacy stance ([G1](g1-privacy.md)), translation is a local concern like every +other. + +## States + +The active locale for a request resolves in a fixed order, first match wins: + +| Order | Source | When it applies | +|-------|--------|-----------------| +| 1 | The user's stored language preference | Whenever the user has set one. | +| 2 | The environment's `Accept-Language`, best supported match | First visit, before a preference exists, when the request names a supported locale. | +| 3 | English (`en`) | Whenever neither above yields a supported locale. | + +Setting a preference moves a user permanently from rows 2–3 to row 1. There is no +transition back short of clearing the preference. + +## Edge cases + +| Situation | Behaviour | +|-----------|-----------| +| `Accept-Language` prefers an unsupported language | Falls through to English; no error. | +| `Accept-Language` lists several languages | The best supported match is taken in the header's own priority order. | +| A supported locale with a region subtag (`nl-BE`) | Resolved to its base supported locale (`nl`). | +| A preference set, then the environment language changes | The preference still wins; detection does not re-run. | +| A key missing in the active locale | The English value is shown. | +| A key missing in English as well | A genuine defect — English coverage is the source locale's contract, not a fallback that may itself have holes. | +| Language changed mid-session | Applies immediately, without a reload that discards the user's place. | + +## Acceptance criteria + +| ID | Requirement | +|----|-------------| +| **G7-R1** | *(Open)* The interface MUST support English (`en`) and Dutch (`nl`), with English as the source and fallback locale. | +| **G7-R2** | *(Open)* Adding a further locale MUST require only its registration and its translations; the detect–select–apply mechanism MUST NOT change per locale. | +| **G7-R3** | *(Open)* On a first visit, before any preference exists, the active locale MUST be the best supported match against the request's `Accept-Language` preference. | +| **G7-R4** | *(Open)* Where `Accept-Language` yields no supported locale, the active locale MUST default to English, and this MUST NOT be treated as an error. | +| **G7-R5** | *(Open)* A language preference MUST be stored per user on the user record, alongside the existing theme preference. | +| **G7-R6** | *(Open)* Once a user language preference is set, it MUST take precedence over detection on every subsequent request. | +| **G7-R7** | *(Open)* Detection MUST apply only while no user preference exists; it MUST NOT override a stored preference. | +| **G7-R8** | *(Open)* The language preference MUST persist across sessions without re-detection. | +| **G7-R9** | *(Open)* A language switcher MUST live in Settings, in the same family as the Appearance/theme setting. | +| **G7-R10** | *(Open)* Changing the language MUST take effect across the interface immediately, without discarding the user's place or data. | +| **G7-R11** | *(Open)* Every user-facing string MUST be translatable; no user-facing copy may be pinned to a single language beyond a locale's reach. | +| **G7-R12** | *(Open)* A key absent in the active locale MUST fall back to its English value, never to a raw key, a blank, or a placeholder. | +| **G7-R13** | *(Open)* The active locale MUST be reflected in the document's language attribute for assistive technology. | +| **G7-R14** | *(Open)* Language detection and selection MUST NOT add an outbound call or send the user's locale off the machine. | + +## Related + +- [G5 Plain language and in-product help](g5-plain-language.md) — the voice these translations carry, and the honesty rule the English fallback satisfies +- [G3 Accessibility](g3-accessibility.md) — why the active locale must be announced +- [G1 Privacy stance](g1-privacy.md) — why selection stays on the machine +- [F1 Desktop shell](../f-platform/f1-desktop-shell.md) — the theme preference this mirrors +- [20-architecture/data-model.md](../../../20-architecture/data-model.md) — where the per-user preference lives +- [20-architecture/platform-matrix.md](../../../20-architecture/platform-matrix.md) — the language signal per platform diff --git a/20-architecture/data-model.md b/20-architecture/data-model.md index dc2b1cf..1d9472c 100644 --- a/20-architecture/data-model.md +++ b/20-architecture/data-model.md @@ -30,7 +30,8 @@ elsewhere is wrong by definition. Users; sessions; system alerts; user preferences. The user record carries the owner and developer flags, the forced-change flag, -and the per-user settings the product accumulated: theme, currency view, period +and the per-user settings the product accumulated: theme, language +([G7](../10-functional/features/g-ux/g7-localisation.md)), currency view, period start day, detection windows, thresholds, drop-folder behaviour, close behaviour, community settings, tax country, and the activation and backfill anchors. diff --git a/20-architecture/platform-matrix.md b/20-architecture/platform-matrix.md index d20b677..06b2b6c 100644 --- a/20-architecture/platform-matrix.md +++ b/20-architecture/platform-matrix.md @@ -34,6 +34,7 @@ documentation says so plainly rather than shipping something that will not run. | File-open intake | Native or process arguments | Not applicable | Not applicable | | Notifications | Operating-system delivery | In-application only | Platform local notifications | | Theme signal | Reported by the shell | Browser preference | Reported by the shell | +| Language signal | `Accept-Language` from the webview | `Accept-Language` from the browser | `Accept-Language` from the webview | Storage paths resolve through a **single path authority** ([ARCH-R8](README.md#the-arch-r-namespace)), which is what makes the diff --git a/30-repos/spec.md b/30-repos/spec.md index 920b4c5..7110631 100644 --- a/30-repos/spec.md +++ b/30-repos/spec.md @@ -16,7 +16,7 @@ enforcement in one place. ```text 00-overview/ vision, glossary, roadmap, decisions/ -10-functional/ features/ (7 areas, 51 features), journeys/ (7) +10-functional/ features/ (7 areas, 52 features), journeys/ (7) 20-architecture/ system context, components, data flow, data model, platform matrix, contracts/ 30-repos/ one page per repository diff --git a/AGENTS.md b/AGENTS.md index 3d3200c..ada2430 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ documentation build. ```text 00-overview/ vision, glossary, roadmap, 20 decision records -10-functional/ 51 features across 7 areas, 7 journeys — WHAT and WHY +10-functional/ 52 features across 7 areas, 7 journeys — WHAT and WHY 20-architecture/ system context, components, data flow, data model, platform matrix, 4 contracts 30-repos/ per-repository specifications diff --git a/README.md b/README.md index 376fc5e..e175af5 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ freely. | Section | Contents | Read this if… | |---------|----------|---------------| | **[00-overview](00-overview/)** | Vision, glossary, roadmap, and twenty Architecture Decision Records | …you want the *why* behind any choice | -| **[10-functional](10-functional/)** | Fifty-one features across seven areas, and seven user journeys | …you are deciding what to build, or verifying it got built | +| **[10-functional](10-functional/)** | Fifty-two features across seven areas, and seven user journeys | …you are deciding what to build, or verifying it got built | | **[20-architecture](20-architecture/)** | System context, component model, data flow, data model, platform matrix, contracts | …you are implementing across a seam | | **[30-repos](30-repos/)** | Per-repository specifications | …you are working inside one repository | | **[40-quality](40-quality/)** | Code standards, comment policy, testing, CI/CD, security, definition of done | …you are writing or reviewing a pull request | @@ -53,7 +53,7 @@ freely. ### Fast paths - **"I want to understand the product"** → [vision](00-overview/vision.md) → [journeys](10-functional/journeys/) → [J1 First run](10-functional/journeys/j1-first-run.md) -- **"What does it actually do?"** → [the feature catalogue](10-functional/features/) — 51 features +- **"What does it actually do?"** → [the feature catalogue](10-functional/features/) — 52 features - **"What is shipped, and what is not?"** → [roadmap](00-overview/roadmap.md) — three buckets, kept strictly apart - **"How does sync work?"** → [E1](10-functional/features/e-sync/e1-change-capture.md) → [E3](10-functional/features/e-sync/e3-transport.md) → [ADR-0014](00-overview/decisions/0014-op-log-crdt-merge-engine.md) → [ADR-0015](00-overview/decisions/0015-multi-master-p2p-sync.md) - **"What does it send anywhere?"** → [G1 Privacy stance](10-functional/features/g-ux/g1-privacy.md) — the complete outbound surface @@ -107,7 +107,7 @@ committed to. | Section | Status | Contents | |---------|--------|----------| | 00-overview | Accepted | Vision, glossary, roadmap, 20 decision records | -| 10-functional | Accepted | 51 features across 7 areas, 7 journeys | +| 10-functional | Accepted | 52 features across 7 areas, 7 journeys | | 20-architecture | Accepted | System context, components, data flow, data model, platform matrix, 4 contracts | | 30-repos | Accepted | spec, beatrax, website, .github | | 40-quality | Accepted | Standards, comments, testing, CI/CD, security, done, tooling |