Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 10-functional/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion 10-functional/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 |

---

Expand Down
11 changes: 8 additions & 3 deletions 10-functional/features/g-ux/g5-plain-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
158 changes: 158 additions & 0 deletions 10-functional/features/g-ux/g7-localisation.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion 20-architecture/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions 20-architecture/platform-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion 30-repos/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
Loading