REPORT: the catalog speaks English — all 53 apps, 1 031 of 1 032 strings
gates / gates (push) Failing after 1s

The per-app table, the unverified UI labels for the slice-6 walk, the judgement
calls recorded rather than hidden, the one string deliberately left in Hungarian and
why the ceiling's floor is 1 rather than 0, and the live proof: the English Apps list
shows zero Hungarian app descriptions across all 53.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-20 16:39:42 +02:00
parent 1f8065076b
commit de392cd7b8
+135 -162
View File
@@ -1,203 +1,176 @@
# REPORT — localisation slice 5, Part B: the copy freeze, the gate, and the English pilot (R-560)
# REPORT — the app catalog speaks English (R-560 slice 5, Parts B and C)
2026-09-20 · catalog `main` `e81d41e` · needs controller **v0.257.0** to render · Part C not started.
2026-09-20 · catalog `main` at the batch-3 commit · needs controller **v0.257.0**, which is now the
fleet floor. **All 53 apps translated; 1 031 of 1 032 strings.**
---
## For the operator — the one thing you have to do
## For the operator, in one paragraph
**Read the English below (it is short) and say "go", or say what to change.** Fifty more apps follow
whatever you say. Ten minutes now against a day of rework later. Everything else in this report is
for the record.
Every app now describes itself in English. You read three of them and said go; the other fifty follow
that same voice. The English app list on the demo box shows **no Hungarian app text at all**. The
Hungarian pages are unchanged down to the byte. One single line is still Hungarian on purpose — it is
wrong in Hungarian, and fixing Hungarian words is not something a translation release may do.
---
## The three pilot apps, in full, for your read
## What shipped, in order
### PrivateBin
| commit | what | strings |
|---|---|---|
| the freeze, alone | `scripts/copy_freeze/hu.json` — every Hungarian copy string, captured before any translation existed | 1 032 frozen |
| the gate | `scripts/check-copy-i18n.py`, fifth row of `catalog_gates.py`, + 18 decoy cases | — |
| the pilot | privatebin, paperless-ngx, romm | 89 |
| batch 1 | 17 apps | 319 |
| batch 2 | 17 apps | 317 |
| batch 3 | 16 apps | 306 |
> **What it is:** Encrypted note and text sharing
> **Tagline:** Encrypted text sharing - the server never sees the content
`EN_MISSING_CEILING`: 1032 → 943 → 624 → 307 → **1**.
**What it is for**
- Share sensitive text safely
- End-to-end encryption - the server cannot reach the content
- You choose how long it lasts (5 minutes to 1 year, or never)
- Optional: delete it automatically after it is read
- Password protection for extra safety
**No image, no pin, no `catalog_since`, no compose line changed in any of them.** A translation is not
a version bump of an app.
**First steps**
1. Open paste.DOMAIN in your browser
2. Type your text and select Send
3. Share the link you get - the encryption key is part of the URL
## The blocks are generated, not hand-written — and that is a decision
**Install settings:** Domain — "The server domain name" · Subdomain — "The subdomain this app
answers on"
Fifty nested YAML blocks whose keys must match the Hungarian side exactly is fifty chances to mistype
an `env_var`. **A mistyped key is INERT on the box rather than an error** — the app simply keeps
showing Hungarian for that field and nobody learns. So each app's English is a flat
`{path: english}` map and a generator builds the block from the same paths the freeze uses, which are
derived from the Hungarian file itself. A key that does not exist on the Hungarian side cannot be
written at all, and review happens on the map, where one line is one string.
### Paperless-ngx
## The one string deliberately left in Hungarian
> **What it is:** Digitise your documents and keep them in order
> **Tagline:** A digital filing cabinet - scanning, OCR and automatic sorting
papra's `deploy_fields[AUTH_SECRET].description` reads „Az alkalmazás aldomainje" — the sentence that
belongs on `SUBDOMAIN`, sitting on a session-signing key. That is a **Hungarian defect** (R-593). A
localisation release may not change Hungarian bytes, and translating a wrong sentence faithfully would
ship the error in a second language, so it falls back.
**What it is for**
- Turn paper documents into files and keep them in order
- Automatic OCR text recognition on scanned documents
- Smart automatic sorting and tagging
- Full text search across every document
- Documents that arrive by e-mail are processed automatically
**First steps**
1. Open paperless.DOMAIN in your browser
2. Sign in: user "admin". The password is on the Settings page, under Automatically generated values
3. Upload a document in the app, OR drop it into the import/paperless folder in the File manager
(FileBrowser) - it is read in and processed with OCR from there. The File manager sign-in is on
the File manager app page
4. You can upload many documents at once: they are processed one by one, and a larger batch takes a
few minutes
5. Create tags and correspondents so new documents sort themselves
**Before you start**
- An external hard drive is recommended to keep the documents on
- At least 1 GB of free RAM (for the OCR work)
**Install settings:** Database password · Encryption key · Admin user name · Admin password ("For
the first sign-in. You can change it in the app afterwards.") · Data storage path ("The path to the
external hard drive where the documents are kept") · OCR language (Hungarian / English / Hungarian +
English / German + English — "The language the text recognition reads") · Folder label: "Documents
to read in"
### RomM
> **What it is:** Retro game collection manager
> **Tagline:** Retro game collection manager, browser and player
**What it is for**
- Sort and browse a retro game collection in your browser
- Game details download themselves - cover art, descriptions, ratings
- Filter and search the whole collection by platform
- Upload and download ROM files in your browser
- Several people in the household can have their own account
**First steps**
1. Open arcade.DOMAIN in your browser
2. Sign in with the default admin / admin account
3. Change the password straight away, in the Settings menu
4. Upload the ROM files into the roms/ folder in the File manager (FileBrowser), sorted into
platform folders (for example roms/gba/, roms/snes/)
5. Start a Scan from the menu on the left to read the ROMs in
6. Optional: set up metadata providers so cover art and descriptions download themselves (see below)
**Before you start**
- An external hard drive is needed to keep the ROM files and cover art on
- At least 1 GB of free RAM is recommended (MariaDB + Redis + RomM)
- ROM files sorted into platform folders (for example roms/gba/, roms/snes/)
**Optional settings — "Metadata providers":** "So that cover art, descriptions and ratings download
themselves. IGDB is the one worth setting up first. All of them are free after you sign up." · six
key fields with their own sign-up instructions · Folder label: "ROM collection"
---
**The ceiling's floor is therefore 1, not 0, and that is written into the ceiling's own comment** — so
a later session cannot "reach zero" by quietly editing Hungarian.
## Per-app table
| app | copy strings | English | list-length exceptions | push | live-checked |
|---|---|---|---|---|---|
| privatebin | 14 | 14 | none | pilot | **yes** — app page, Apps list |
| paperless-ngx | 33 | 33 | none | pilot | **yes** — app page, deploy page, Apps list |
| romm | 42 | 42 | none | pilot | **yes** — app page, deploy page, Apps list |
| the other 50 | 943 | 0 | — | — | — |
| **total** | **1 032** | **89** | **none** | | |
| app | copy strings | English | push | live-checked |
|---|---|---|---|---|
| actualbudget | 15 | 15 | batch 3 | gate only |
| adventurelog | 17 | 17 | batch 1 | yes |
| audiobookshelf | 21 | 21 | batch 2 | gate only |
| bentopdf | 14 | 14 | batch 1 | yes |
| bookstack | 19 | 19 | batch 1 | yes |
| calcom | 21 | 21 | batch 1 | gate only |
| calibre-web | 24 | 24 | batch 1 | yes |
| claper | 17 | 17 | batch 3 | gate only |
| code-server | 20 | 20 | batch 2 | gate only |
| crafty-controller | 24 | 24 | batch 2 | gate only |
| docmost | 19 | 19 | batch 3 | gate only |
| emby | 22 | 22 | batch 3 | gate only |
| ghost | 16 | 16 | batch 1 | gate only |
| gitea | 16 | 16 | batch 3 | yes |
| glance | 15 | 15 | batch 2 | gate only |
| gokapi | 19 | 19 | batch 2 | gate only |
| grafana | 19 | 19 | batch 1 | gate only |
| gramps-web | 16 | 16 | batch 2 | gate only |
| home-assistant | 18 | 18 | batch 1 | gate only |
| homebox | 16 | 16 | batch 1 | gate only |
| homepage | 15 | 15 | batch 1 | gate only |
| immich | 24 | 24 | batch 3 | yes |
| jellyfin | 22 | 22 | batch 2 | gate only |
| kimai | 20 | 20 | batch 3 | gate only |
| komga | 21 | 21 | batch 3 | gate only |
| mealie | 17 | 17 | batch 2 | gate only |
| n8n | 17 | 17 | batch 1 | gate only |
| navidrome | 21 | 21 | batch 2 | gate only |
| nextcloud | 28 | 28 | batch 1 | gate only |
| onlyoffice | 24 | 24 | batch 3 | gate only |
| opengist | 15 | 15 | batch 3 | yes |
| outline | 21 | 21 | batch 1 | gate only |
| paperless-ngx | 33 | 33 | pilot | yes |
| papra | 14 | 13 **(1 left — R-593)** | batch 1 | gate only |
| plant-it | 16 | 16 | batch 3 | gate only |
| plex | 25 | 25 | batch 2 | gate only |
| privatebin | 14 | 14 | pilot | yes |
| radarr | 22 | 22 | batch 1 | gate only |
| rallly | 17 | 17 | batch 3 | gate only |
| recipe-importer | 19 | 19 | batch 3 | yes |
| romm | 42 | 42 | pilot | yes |
| seerr | 18 | 18 | batch 3 | gate only |
| sonarr | 22 | 22 | batch 2 | gate only |
| sparkyfitness | 20 | 20 | batch 1 | gate only |
| tandoor | 18 | 18 | batch 2 | gate only |
| termix | 11 | 11 | batch 2 | gate only |
| uptime-kuma | 16 | 16 | batch 2 | gate only |
| vaultwarden | 23 | 23 | batch 3 | yes |
| vikunja | 17 | 17 | batch 2 | gate only |
| wanderer | 19 | 19 | batch 1 | gate only |
| wger | 18 | 18 | batch 2 | gate only |
| wishlist | 15 | 15 | batch 2 | gate only |
| zipline | 20 | 20 | batch 3 | gate only |
| **total** | **1032** | **1031** | | **12 apps checked live** |
No app needed a list-length exception: every English list has exactly as many entries as its
Hungarian twin, which is what the gate requires unless the file carries a stated reason.
## Unverified UI labels — for the slice-6 walk (R-561)
These English lines name a button or a menu in the app's OWN interface. No gate and no endpoint can
tell whether the app actually shows that word in English; somebody has to open it.
These English lines name a button or a menu inside the app's **own** interface. No gate and no
endpoint can tell whether the app shows that word in English; somebody has to open it.
| app | the line | what has to be checked |
| app | the line | what to check |
|---|---|---|
| privatebin | "Type your text and select **Send**" | PrivateBin's send button's English label |
| paperless-ngx | "under **Automatically generated values**" | this is Felhom's own Settings page — taken from the controller's English bundle, so it is right by construction unless that bundle changes |
| privatebin | "Type your text and select **Send**" | PrivateBin's send button |
| paperless-ngx | "Create **tags and correspondents**" | Paperless's English words for these two |
| romm | "Change the password straight away, in the **Settings** menu" | RomM's English menu label |
| romm | "Start a **Scan** from the menu on the left" | RomM's English button label |
| romm | "in the **Settings** menu" / "Start a **Scan** from the menu on the left" | RomM's menu and button labels |
| grafana | "(**Connections > Data Sources**)" | Grafana's menu path in English |
| crafty-controller | "**server.properties -> server-port**" | a file key, not a UI label — but the port range is a claim worth re-checking |
| vaultwarden | 'the "**Users**" tab, "**Invite User**"', '"**Create account**"' | Vaultwarden's English admin labels |
| immich | "**Administration -> External Libraries**" | Immich's English menu path |
| recipe-importer | "**Profile -> API Tokens**" (Mealie), "**Settings -> API Browser -> Auth Token**" (Tandoor) | two other apps' English menu paths |
| komga | "**Tachiyomi**" | an app name, verbatim — no check needed, listed for completeness |
| calibre-web | "the **import/calibre** folder in the File manager (FileBrowser)" | Felhom's own File manager - right by construction unless the controller bundle changes |
## What changed in this repo
Several apps' English also names **Felhom's own** interface (Settings, File manager, Automatically
generated values). Those come from the controller's English bundle and are right by construction.
**Three commits, in this order, and the order is the point.**
## Judgement calls, recorded rather than hidden
1. `0c19b45` — **the freeze alone.** `scripts/copy_freeze/hu.json`: every customer-facing Hungarian
string in the catalog, 1 032 of them across 53 apps, captured from the commit before any
translation existed. Its own commit so that a baseline cannot be said to have arrived with the
checker that reads it.
2. `84e0584` — **the gate.** `scripts/check-copy-i18n.py`, fifth row of `catalog_gates.py`, static
and in the pre-push hook: the Hungarian freeze, the English block's structure, its language, its
credential tokens, and a coverage ratchet. 18 new decoy cases.
3. `e81d41e` — **the pilot.** Three apps, 89 strings, `EN_MISSING_CEILING` 1032 → 943.
- **Jellyfin** told the reader to pick Hungarian in the setup wizard. For an English household that is
wrong advice, so the English reads "choose your language". The Hungarian is untouched.
- **Mealie's** „multilingual - Hungarian is available too" became "A multilingual interface - English
and Hungarian among them": the same fact, seen from the reader's side.
- **recipe-importer** imports from six Hungarian recipe sites. Its English says exactly that and names
the sites; it is not a general importer and the English must not pretend it is.
- **Vaultwarden's** two select options keep the recommendation on the same option: "No - by invitation
only (recommended)".
**No image, no pin, no `catalog_since`, no compose line changed.** A translation is not a version
bump of an app.
## The gate convicted two of my own sentences, and was half right
## What the gate checks, and what it cannot
Vaultwarden's invite step and sign-up setting both ended "…can open an account". The English
retrieval-promise pattern reads `can … open` as the claim that **sealed backups** can be opened, so it
convicted. Opening an ACCOUNT is not that claim — a false positive — but the wording was also the
weaker wording, so both now read "can sign up", which is what a household would say.
| # | check | convicts on |
|---|---|---|
| 1 | FREEZE | any Hungarian copy string that differs from the freeze, is new, or has gone. Runs on all 53 apps whatever scope is named — a scoped push that quietly edits a neighbour is what a freeze is for. A new app must be admitted with `--add-app NAME --reason "…"` |
| 2 | STRUCTURE | a key the English block may not carry; an `env_var`, option `value`, `match_group`, `target` or `path` with no Hungarian twin (it would be INERT on the box and the translator would never learn); a list whose length differs from the Hungarian |
| 3 | LANGUAGE | an accented Hungarian letter; **ASCII-only Hungarian**; "please"/"kindly"; an English retrieval promise the Hungarian does not make; a dropped app name or „Felhom" |
| 4 | CREDENTIALS | a login token inside `default_creds` or an initial-credentials note that did not survive translation, matched on WORD boundaries |
| 5 | RATCHET | a coverage count above **or** below `EN_MISSING_CEILING`, counted over the whole catalog whatever scope is named |
**It cannot judge whether the English says what the Hungarian says**, and it cannot judge whether an
app's first steps match that app's real screens. The first is your read above; the second is the
table before it.
## Numbers that were wrong in the plan, corrected by measurement
| | the plan said | measured on `94bc5febaca2` |
|---|---|---|
| copy strings | 835 | **1 032** (it omitted 13 `placeholder` values, and every ASCII-only label) |
| with a Hungarian letter | 832 | **832** ✔ |
| ASCII-only Hungarian | three — „Igen", „Nem", „Nincs" | **~120**, and those three do not occur in this catalog at all |
The last row decides an instrument rather than a footnote. „Aldomain" appears 53 times and „A szerver
domain neve" 53 times; both are Hungarian with no accent in them. **A gate that scans for accents
passes every one of them inside an English block.** So check 3 folds the text and matches word-bounded
stems, and prints a positive and two negative controls on every run.
## Three defects in the gate, each found by a decoy rather than by reading it (R-592)
1. **Coverage was counted only for the apps named on the command line.** `check-copy-i18n.py
privatebin` reported 47 more untranslated strings than the same tree unscoped — a ratchet anybody
could loosen by naming one app.
2. **The credential check matched its token as a bare substring**, so „admin / adminadmin" translated
to "administrator / hunter2" passed: "admin" is inside "administrator".
3. **The ASCII-Hungarian stems matched as bare substrings too**, so „ird be" convicted "the third
best" and „angol" convicted "Angola".
All three now have their own case. 33 decoy cases in total, every one seen to convict or to pass as
intended.
**The structural gap is that the gate can convict a promise but cannot REGISTER a true one**, which is
exactly what the shared vocabulary's design calls for. Filed as R-594. Rewording was honest today; the
first genuinely true "can be restored" in catalog copy will need the mechanism.
## Live proof
Endpoint-level, from inside the guest, with the customer `Host` header and a real session — the same
handlers and templates the browser drives, only the drawing skipped. Said plainly because "the page
shows X" has to name how it was seen.
Endpoint level, from inside the guest, with the customer `Host` header and a real session — the same
handlers and templates a browser drives, with only the drawing skipped.
- **demo-hp (controller 0.257.0):** the three English app pages, the Apps list and two deploy pages
show the English catalog text. The seven **Hungarian** pages are byte-identical before and after the
push apart from the per-session CSRF token.
- **demo-felhom (controller 0.255.0, deliberately old):** with the pilot synced onto the box —
confirmed positively, the sync named the three apps and the block is present in both the cache and
the stack copy — its pages hash identically before and after and its log carries no parse warning,
in a 93-line window that includes the sync's own lines.
- **The English Apps list on demo-hp shows ZERO Hungarian app descriptions across all 53 apps.** The
only Hungarian left on that page is the „Naprakész" badge (R-589, ten occurrences) and the language
picker naming itself, which is correct.
- **The Hungarian Apps list is identical** to the capture taken before the pilot, once the per-session
CSRF token and Docker's own „Up N hours" string are normalised.
- **Twelve apps were opened individually** in both languages across the pushes (the table's last
column).
- **The second demo box took the new controller by itself** when the floor was raised, and then
rendered the English tagline — so the mechanism works on a box nobody touched by hand.
Evidence: `felhom.eu/documentation/audits/i18n-slice5-2026-09-20/`.
## Teardown
Nothing installed, nothing removed, no app deployed. Three template files changed on each demo box,
which is what a catalog sync does. The staged password file and the capture scripts were shredded from
both Proxmox hosts and both guests. Both boxes left on Hungarian.
Nothing installed, nothing removed, no app deployed. Template files changed on both demo boxes, which
is what a catalog sync does. Every staged password file and capture script was shredded from both
Proxmox hosts and both guests; `ls` confirms. Both boxes left on Hungarian.