Skip to content

Back office: switching the language leaves nothing in the old language until a reload - #36

Open
rorychatt wants to merge 29 commits into
mainfrom
fix/language-switch
Open

rorychatt wants to merge 29 commits into
mainfrom
fix/language-switch

Conversation

@rorychatt

@rorychatt rorychatt commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Switching the language in the back office without a reload left app names (and their order), the top bar, the sidebar header, the command palette and a few other texts in the old language; "Use the instance default" was missing until a reload. This fixes the causes, each confirmed in the code and in the running app, and adds tests that fail on main.

This description was rewritten after the review of 911a975 (round 2) and corrected again after the reviews of c8131b4 (round 3) and 9a9aa91 (round 4). Sections "Round 2" to "Round 4" say what changed; the rest is corrected where an earlier version was wrong.

What was wrong and what changed

# Cause (confirmed in code) Change
1 App.tsx apps = useMemo(buildApps(...), [modules, settings, isAdmin]) had no language in its deps; buildApps calls appTitle and compareText, so names and order stayed old lang, locale from useI18n() added to the deps
2 store.ts setLanguage called activate() (which emits) before lsSet/writeCookie; useI18n() reads personalLang() while rendering activate(lang, persist?) runs persist after the catalogue has loaded and before emit(). A catalogue that fails to load throws before persist, so nothing is saved. A pseudo-locale is still never saved
3 The name field of Customize > Apps was uncontrolled (defaultValue); a browser keeps what React first put into a field, so after change 1 the field kept the old-language name while a.name changed, and a plain focus + blur saved it as a custom name for everyone The field is controlled: it shows what is typed and not yet saved, else the app's name (Customize.tsx). (Round 1 keyed the input by a.name, which made the field again when its own rename was saved and lost keystrokes typed at that moment.)
4a RecordPanel.tsx related memo had [d.models, model.name] d.lang added
4b Every command (call in api.ts) carries ?lang= and Accept-Language; in the body, search() adds the lang that picks the collation only when the search has an order, and aggregate() always adds it. So a sorted list or a sorted board column depends on the locale; an aggregate does too in principle, but only select, ref, bool and date fields can be grouped (GROUPABLE) and the views put the keys in order on the client The locale is part of the query key of useServerRows and of the Kanban columns only when there is a sort
5 Sidebar tooltip title={m.description} was English in every language title={describeFor(m)}. Measured (see below): every model of the 23 bundled packages and of the 8 built-in modules has a description in de, fr, pl, fi, sv, es. A module without label tables falls back to its English description, as everywhere else describeFor is used

Round 2 (after the review of 911a975)

  • Memos the guard found, fixed: the dashboard lists (DashboardsApp.tsx, two memos that sort equal positions by title) and the widget card (resolve writes the detail sentences of an invalid tile, such as "A sum needs a measure field"; the titles "This tile is not set up correctly" and "No access" are written where the card draws). Each is one dependency.
  • The guard (i18nGuard.ts + i18nDeps.test.ts) no longer checks against a list of names. It reads every file of the apps and of packages/ui, resolves imports and re-exports, and takes as translators the functions that use a reader of the i18n store, directly or through other functions: called, handed on (.sort(byPosition)), used as a JSX tag, or a member such as d.recordLabel and d.refText. At 911a975 it caught 6 of the 14 memos that carry the language in their dependencies; a test takes the language out of each in memory and now catches all of them (17 today). A second test pins the set (a missing link in the call graph fails it) and a third fails when a function of the store that reads the language is in neither the display list nor the not-display list.
  • The record panel keeps the server's own words and which fields failed their check, and words them when it draws them (it stays open through a switch). moveRecord returns the raw message next to the sentence. InlineEdit has the same shape but is unreachable on a switch (leaving the cell closes it); the screens that still keep finished sentences are in docs/ISSUES.md D9.
  • An open record stays open while the sorted list around it asks again: after a switch the record can fall off the first page, useRecord had nothing for a render, ModelPage dropped the panel and the draft with it. useRecord keeps the last row for the id until the get answers. (This was a regression of round 1's locale in the query key.)
  • Query keys: the locale is in the key only for a request that carries the body lang with a sort; the useAggregate change is reverted (every consumer builds its own order from the groups, so it asked again for nothing); the Kanban change is kept, with a test.
  • Tests: the e2e spy that checked that no setting is written matched an address that the app never sends (it adds ?lang=), so it could not fail. It matches the path now, and a write on blur makes the test fail (mutation Mblur).

Round 3 (after the review of c8131b4)

  • The list-plan memo in ModelPage.tsx does not need the language. Rounds 1 and 2 said it did ("toServer reads range bounds with the decimal separator of the region") and added locale. That was wrong: rangeNum and rangeDecimal take a dot or a comma in every locale (trim().replace(",", ".")), and the only reader of the i18n store in toServer is fmt.exponent(), the digits of the instance currency, which a language or region change does not change. The dependency is out again and the memo carries // i18n-memo-ok: <that reason>. (Side note, not changed: the currency of the instance is not in that memo's dependencies either; a currency change while a list is open is the same case.)
  • The guard shadows a name only where the code around the hook declares it, not when any callback of the component names it; a ; inside an object type no longer cuts a declaration short; the exemption marker counts in a comment directly above the statement or inside the call and needs three word characters of reason; a member translator is followed on d, lk and lookup only (hash.update(x) is not d.update(x)); the floors of the tree test are 58 hooks seen and 16 carrying the language (there are 60 and 17). Round 3 also changed how the dependencies of a memo are told from its function, and which names shadow in a function of the call graph; round 4 corrects both (below).
  • useRecord forgot the remembered row when the get answered null. That did not cover a record deleted from its panel (no get had run), and the comment called every null "no such record"; both are corrected in round 4.
  • Comments and docs: the I18nProvider comment said never to use memo, the docs say a memo component that calls useI18n() is fine, and now both say the same; D9, D13 and D14 were extended. Round 3 said "?lang= and Accept-Language on every request"; that was wrong (round 4).

Round 4 (after the review of 9a9aa91)

  • F1, the guard shadowed the import with the names in a function type. scopeAt took every (a, b) => in the parameter list of the component, so { onPick }: { onPick: (t: string) => void } (the shape of brand/Iconography.tsx) put t into the shadow set and a t(...) in a memo there was not seen. It now takes the names the header binds (its parameters, the variable of a for (const x of ...)).
  • F2, the same for the translator graph. A function in the graph shadowed from its whole text, so a callback parameter named like a translator hid it for the function: CodeBlock in packages/ui (it calls t("Copied") and maps tokens with (t, j) =>) was not a translator and useMemo(() => <CodeBlock .../>, [..]) passed. A function now shadows with its own parameters and the constants at the level of its body, the rule of the hook. CodeBlock is in the pinned set.
  • F3, dependencies that are not a list were read as "no dependencies". Round 3 took the tail after the last comma for the dependencies only when it started with [ or was a plain name, so deps!, cond ? [a] : [b] and getDeps() passed. What follows the last top-level comma is now the dependencies, unless the text before it is not yet a whole argument (the comma is in type arguments such as Map<string, string>), and a tail that is not a list is refused with "the dependencies are not a list the guard can read".
  • F4, a trailing block comment exempted the next hook. const a = useMemo(..); /* i18n-memo-ok: x */ followed by another hook exempted the second. A /* */ counts as a marker above a statement only when its lines are its own, as // lines already did.
  • Other guard items: fixtures for the length of the reason (two characters refused, three and four accepted), a const t in a sibling callback or an if block above the memo, t => { ... }, three-line markers and a real negative for Object.create; the test header and the guard header describe the marker and the shadow rule as they are now. The limits the review probed and that stay are in D14: a marker above the first property of an object or the first of several declarators reaches the later ones, parameters of an expression-bodied hook or an anonymous default export are not read, <T extends (x: number) => void>(a: T) => is not seen as a function, and a constant in an inner block of a graph function does not shadow.
  • useRecord and Back. A record deleted from its panel closed the panel, last still held its row, and Back to its address opened the panel with the old data until the get answered (a record opened by address kept its fetched row the same way). The rule is a small module, lastRecord.ts, with unit tests: the row is kept while the get is on its way, and forgotten when the route holds no record or when the get does not return the record. Any get that does not answer ok (an error reply, which for a missing record is a 400, a failure after the retries, an approval reply) counts as "not returned", not only "no such record". One thing stays: a record fetched by its address, left for one that is on the page and opened again while it is not, shows the row fetched before until its get answers.
  • The unsorted-board test looked for an aggregate command that is not on the wire (an aggregate is a search with an aggregate key in its body), so two of its assertions could not fail. It classifies the requests by body now and lists any that the sort does not explain.
  • Docs. D9 said modules/PlanDialog.tsx is not affected; its install and uninstall dialogs keep serverMessage(r.message) in state (only the preview error is raw), and connectors/Catalog.tsx and Sections.tsx were missing from the list. D10: the sample fields are one useState, only their descriptions are translated. D13: what Git Bash gives (C:/Program Files/Git/admin/, measured). The statement about what a request carries is now exact (4b above and docs/brand/WORKFLOWS-LISTS-DASHBOARDS.md).

Tests

  • (a) web/packages/ui/src/i18n/switch.test.ts (5 tests, unchanged): a subscribe() listener that reads personalLang() sees the chosen language after await setLanguage(...); storage and cookie are written before the subscribers run; nothing is saved with persist=false, for a pseudo-locale, or when the catalogue fails to load.
  • (b) web/packages/e2e/tests/language-switch.spec.ts, 16 tests, no reload between the switch and the checks: sv to en on the language instance (default Swedish) and en to sv on the main one (home tiles and their order against a reload, top bar, launcher, palette, sidebar header, aria-label, tooltip, "Use the instance default"); the Apps tab; the related-record buttons; a sorted list and a list without a sort (no request on a switch); a sorted board and a board without a sort (no aggregate and no unsorted search on a switch); a field that failed its check; the server's refusal of a status step, of a move made with the Move to menu, of an action button and of a field it named, each worded in the new language (Swedish, English, Swedish); a record opened from the list stays open with its draft when the sorted list no longer has it; a record that is not on the page yet does not show the row of the one before it; a record deleted from its panel does not come back with Back. App names come from /api/meta as the page builds them; there is no fixed wait.
  • (c) web/apps/backoffice/src/i18nDeps.test.ts: 30 tests (the reader on fixtures including a fake design system, transitive translators across files, aliases and export *, identifier uses, member translators and their receivers, scope at the hook and in the graph, typed helpers, type arguments, dependencies that are not a list, the exemption marker, memo; the real tree). web/apps/backoffice/src/lastRecord.test.ts: 5 tests.

What I ran

Windows 11, system Chrome, SQLite, cargo build of this branch (Rust is not touched), OBOP_BIN=target/debug/obop.exe.

  • vp test: packages/ui 8 files, 172 tests; apps/backoffice 22 files, 339 tests (was 330: 4 more in i18nDeps.test.ts, 5 in the new lastRecord.test.ts); apps/platform 1 file, 7 tests. All passed.
  • vp check passes in apps/backoffice (126 files linted), packages/ui (33), packages/e2e (54; vp run typecheck exits 0) and apps/platform (7); vp run i18n:check in packages/ui: "catalogues ok"; vp build of backoffice (dist, and dist-admin with MSYS_NO_PATHCONV=1), ui and platform.
  • The guard tests (30) with each of the eleven round 4 guard fixes undone, one at a time; the five unit mutations of lastRecord.ts (tables below).
  • The language-switch spec (16 tests): the first run with test 16 passed 15 and failed 1 (the get of a deleted record answers 400, which the console guard counts; the test now declares it with allowHttpErrors(400)); test 16 after the rewrite 3 of 3; the whole spec with --repeat-each=3: 48 passed (vp exec playwright test tests/language-switch.spec.ts; the exit status is 1 on Windows only because global-teardown.ts hits EPERM, D13).
  • The whole e2e suite once (vp exec playwright test, 7.4 min): 242 passed, 16 skipped (the QA sweep, off unless OBOP_QA=1), 0 failed.
  • Six e2e mutations with a build each (tables below), all killed by the test meant for them.
  • CI on the pushed head f72739a: web, e2e and lockfiles passed on the first attempt; rust failed there in limits_tests::an_agent_is_not_told_how_long_a_value_it_only_holds_as_a_token_is (limits_tests.rs:943), a test this branch does not touch (it asserts !e.contains("40") on a message that begins with the random module prefix), and passed on the second attempt, with all. The same test failed once before on the previous head and passed on a re-run.

Mutations

Each one is a change to the source, vp build (for e2e), the spec or the unit tests, then reverted.

Rounds 1 and 2 (run against the 10-test spec of that time; the test numbers are those of today's 16-test spec, since tests were only appended; the rows were not repeated after rounds 3 and 4, which changed none of the code they touch except comments and useRecord, whose mutations M19 and M26 are redone in round 4 below):

Mutation Killed by
M1 store.ts as on main (subscribers told before the choice is saved) switch.test.ts ("is saved before the subscribers run, so they see the choice", "is seen by a person who had no choice and one who is changing it"); e2e 1, 2
M2 lang, locale out of the apps deps in App.tsx e2e 1, 2, 3; the guard names App.tsx:176
M3 sidebar tooltip back to m.description e2e 2 (English to Swedish; test 1 goes to English, where the English description is right)
M4 d.lang out of the related memo of the record panel e2e 4; the guard names RecordPanel.tsx:290
M5 no locale in the useServerRows key e2e 5 (sorted list) and 11
M5b the locale in that key for every query (round 1) e2e 6 (an unsorted list asked again)
M6 no locale in the Kanban key e2e 7 (sorted board)
M7a key={a.name} on the name field (round 1) e2e 3 ("the field was made again")
M7b defaultValue and no key (main) e2e 3
Mblur the name field saves on every blur e2e 3 (a second write before the rename)
M17a record panel and moveRecord as before round 2 (finished sentences in state) e2e 9 and 10
M17b the banner keeps the finished sentence (out.message) e2e 10
M19 useRecord without the remembered row e2e 11 (the panel comes back with an empty draft)
G1 to G4 locale out of the two dashboard memos, d.lang out of resolve, locale out of the list plan (round 2, when it had it) i18nDeps.test.ts "has no memo that translates, sorts or formats without the language", naming DashboardsApp.tsx:202, :210, WidgetCard.tsx:25
G5 to G9 lang out of the palette, control panel, module builder and widget form memos; the exemption comment of modules/view.ts removed the same test, naming Chrome.tsx:238, ControlPanel.tsx:47, ModuleBuilder.tsx:195, WidgetForm.tsx:60, modules/view.ts:107
R1 getLang out of the seeds of the reader 4 tests of i18nDeps.test.ts
R2 formatLocale out of the seeds 5 tests
R3 a bare i18n-memo-ok: accepted as a reason "lets a hook that is right say so, with a reason"
R4 memo components not read "holds a memo component to using the language hook"

Round 3, e2e: five source changes applied together to the committed head (mutate.sh: change, vp build, the 15-test spec, git checkout). Each failing test shows the signature of its own mutation. The other ten tests passed.

Mutation Source change Killed by (and how it fails)
M3a2 the Move to menu keeps the finished sentence RecordPanel.tsx GovernedValue onMove: setError({ raw: out.raw }) to out.message e2e 12: in English the banner still reads "Phase måste vara Utkast för att göra detta, det är Review"
M3a3 an action button keeps the finished sentence RecordPanel.tsx act(): setError({ raw: r.message }) to { raw: message } e2e 13: "Status måste vara Ny för att göra detta, det är Pågår" in English
M3b a refused field keeps the finished sentence RecordPanel.tsx failure(): [f.name]: message to serverMessage(message) e2e 14: expected the server's English ..._note.code must be at most 10 characters; this has 11, received "Kod får vara högst 10 tecken långt; nu är det 11"
M2d the unsorted board's key carries the locale KanbanView.tsx baseSig: order.length ? locale : null to locale e2e 8: a request after the switch has no order
M26 the remembered row is shown for any id of the model useServer.ts useRecord: drop && last.current.id === id e2e 15: the panel is still open (count 1, expected 0) while the get of the other record is held

Round 3, guard: the old behaviour of one fix restored at a time in i18nGuard.ts (gmut.sh: sed, vp test src/i18nDeps.test.ts, git checkout).

Mutation Source change Killed by
B1 shadow by the whole component outer = scopeAt(...) to localsOf( the whole declaration ) "shadows only what is in scope at the memo, not every callback of the component"
B1n a nested function shadows its own name no more if (!nested) shadow.delete(d.name) to shadow.delete(d.name) the same test (the constant t above the memo)
DG2 ; ends a declaration the arrow test back to /=>/.test(init.split(";")[0]) "is a helper whose parameters or result are typed with an object" and "finds the functions that translate by what they call"
PG deps are the second argument commas[commas.length - 1] to commas[0] "reads the dependencies of a memo whose function has type arguments with a comma" (both the reader test and the translating test)
DG4 the marker anywhere on the line before or in the call, any reason marked(...) back to the first version's regex "lets a hook that is right say so, with a reason", "marks the statement under the comment, not the one after it", and the tree test
DG3 a member translator on any receiver RECEIVER to any identifier "follows a member on the data context and on a lookup, not on any receiver"
UT6 the reader loses every seventh hook if (line % 7 !== 0) found.push(...) the tree test (floor of 58) and "explains every memo that names the language" (floor of 16)
the exemption of the list-plan memo removed // i18n-memo-ok: out of the comment in ModelPage.tsx the tree test, naming ModelPage.tsx:115 (toServer)

Not mutated: the useRecord forget-on-null change (DB-2) has no test; the comment and docs changes.

Two earlier mutations had survived the first version of their test and the tests were fixed: M19 (the record had been fetched at load by its address; the test opens it from the list now) and M17b (the English wording of a rule is close to the server's English, so a kept English sentence was worded in Swedish again; the tests provoke the refusal in Swedish and switch to English and back). The new refusal tests use the same Swedish, English, Swedish order, and the field test relies on serverMessage returning the server's own words unchanged in English.

Survivors among the hunks that stay: none known. Protected only by the guard (no behavioural test): the dashboard, widget card and palette memos, the control panel suggestions, the module builder targets and the widget form. Two of them are modal (the palette, the widget form), so the language cannot be switched while they are open; the control panel suggestions and the module builder targets could be driven and no test does. No test: useRecord forgetting the row on "no such record".

Round 4, guard: the old behaviour of one fix restored at a time in i18nGuard.ts (gmut4.sh: sed, vp test src/i18nDeps.test.ts, git checkout). Each fails exactly the tests named; the other tests pass.

Mutation Source change Killed by
F1 the header gives every (a) => in it declared(header) back to localsOf(header) in scopeAt "takes only the parameters of a function for a shadow, not the names in a function type"
F2 a graph function shadows from its whole text parametersOf(...) back to localsOf(text) in own() "is a function that calls one and has a callback or a function type that names a parameter like it" and the pinned set (CodeBlock)
F2b a constant of an inner block shadows in the graph drop the depth - base <= 1 filter in own() the same "callback or function type" test (a const t inside a callback)
F3 deps only when the tail is a list or a name the argument test back to the tail's shape "refuses dependencies it cannot read as a list, and does not take a type argument for them"
F3b deps always the tail after the last comma the argument test replaced by true "reads the dependencies of a memo whose function has type arguments with a comma"
F4 a trailing block comment is a marker drop the startsWith("/*") check in marked() "marks the statement under the comment, not the one after it"
H1a a reason of one character is enough {3} to {1} in REASON "lets a hook that is right say so, with a reason"
H1b a reason needs five {3} to {5} the same test (the three-character why)
H2 a constant of a sibling block shadows at the hook drop the depth filter in scopeAt "shadows by a constant in a block that holds the memo, not in one beside it"
SINGLE t => { ... } gives no parameter drop the single-parameter branch in scopeAt "shadows only what is in scope at the memo, not every callback of the component"
RECV Object as a receiver of members RECEIVER to (?:d|lk|lookup|Object) "follows a member on the data context and on a lookup, not on any receiver" (Object.create(null) is the only line it can change)

Round 4, e2e: one source change at a time (mutate4.sh: change, vp build, the spec restricted to the test with -g, git checkout).

Mutation Source change Killed by (and how it fails)
R3D2a useRecord as in round 3 the effect back to if (row) last = ...; else if (row === null) last = null test 16: the panel is open (count 1, expected 0) while the get is held; the remembered row is what it shows
R3D2b the fetched row stays when the route holds no record drop setFetched(null) test 16: the same, from the fetched row
M2d the unsorted board's key carries the locale KanbanView.tsx baseSig: order.length ? locale : null to locale test 8: "requests the switch made that the sort does not explain", entries of kind "search without a sort" with no lang
M19b useRecord shows no remembered row lastRecord.ts remembered: return undefined test 11: the panel comes back with an empty draft (expected "not saved yet", received "")
M26b the remembered row of any id of the model remembered: drop last.id === id test 15: the panel is open (count 1, expected 0) while the get of the other record is held
AGG the aggregate's key carries the language useAggregate.ts: d.lang added to the key test 8: the same message, with an entry of kind "aggregate" and lang: "sv-SE"

Round 4, unit (lastRecord.ts, vp test src/lastRecord.test.ts, git checkout):

Mutation Source change Killed by
NULL a null get keeps the row return null to return last for row === null "forgets it when the get does not return the record"
EMPTY an empty route keeps the row !now.id || now.id === "new" to !now.id, returning last "forgets it when the route holds no record" (the new id)
ID the row of any id of the model drop last.id === id "gives the row back only for the record, and the model, it was seen for"
MODEL the row of any model drop last.model === model the same test
NEWER a newer row is not taken return last ?? {...} "keeps the last row seen, and takes a newer one"

Not mutated: the comments and docs; the i18nGuard.ts header.

Measured: model descriptions in six languages

An instance with the 23 packages/*.toml installed (obop modules install), a user with every group, GET /api/meta: 23 packages, 73 models, and the 8 built-in modules, 23 models; every one has a non-empty descriptions[lang] different from the English one in de, fr, pl, fi, sv and es (0 missing, 0 identical, per language).

Corrections to earlier descriptions

  • "All five useCallback hits are lazy" was wrong: four are; SecurityAccount.tsx load (about line 433) stores translated text in state (problem). It is in docs/ISSUES.md D9. The guard still does not read useCallback (D14).
  • "Nothing is left as a follow-up" was wrong. docs/ISSUES.md D8 to D14 list: toasts and the confirm dialog keep their text (D8); error texts kept in state on the sign-in, second-factor, dashboard, upload, update, install, connector and device screens (D9); the module builder's seeded sample words (D10); a decimal typed before a switch is read at Save with the separators of the language active then (D11); the catalogue fetched twice at load (D12); the Windows e2e harness (D13); the blind spots of the guard, including that lang and locale are accepted for each other (D14).
  • Round 2 said the list-plan memo reads range bounds with the decimal separator of the region. It does not (round 3).
  • Round 3 said D9 modules/PlanDialog.tsx is not affected, that ?lang= and Accept-Language are on every request, and that useRecord forgot the row when the get answered "no such record". The first is wrong for the install and uninstall dialogs, the second for /api/meta, sign-in, uploads and the account endpoints, and the third left the deleted-then-Back path open (round 4).

Not verified

  • A real sv-SE browser (Chrome en-US with the app's own language choice), a real phone; Safari and Firefox.
  • PostgreSQL, HTTPS, a reverse proxy; mobile width; sign-out and sign-in after a switch (the existing default-language spec covers the sign-out path).
  • Region switches (locale) are covered by the dependencies and the guard, not by a test that changes the region alone. At least five memos name only lang and sort with the collation (D14), so a region change alone probably leaves their order; read from the code, not driven.
  • D8 to D12 are read from the code, not driven in a browser.

🤖 Generated with Claude Code

rorychatt and others added 29 commits October 31, 2026 11:37
setLanguage called activate(), which emits, and wrote the choice to localStorage afterwards.
useI18n() reads personalLang() while it renders, so right after a switch the person had no
choice of their own and "Use the instance default" was missing until a reload. activate()
now runs a persist callback after the catalogue has loaded and before it emits; a catalogue
that fails to load still saves nothing.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… changes

The apps (names, tile order, top bar, sidebar header, launcher, command palette) came from a
useMemo that listed the modules, the settings and isAdmin but not the language, so after a live
switch they kept the old language until a reload. Add lang and locale to its dependencies.

The name field of the Apps tab in Customize is uncontrolled (defaultValue) and a browser keeps
what a field was made with, so with the list rebuilt a plain focus and blur saved the old
name for everyone as a custom name. key={a.name} makes the field again when the name changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…n record

The memo of the smart buttons (titles of the related models, tooltips) listed d.models and the
model name, so a switch with the account menu on an open record kept the old titles.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…r dependencies

The command palette, the search suggestions of the control panel, the model choices of the
module builder and the readable models of the widget form build translated texts in a useMemo.
Only the module builder's could show the old language (its memo depends on the modules alone, so
a switch with the account menu on that tab kept the old names); the others were saved by an
input that changes on every render, or by the app list, which now changes with the language.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The server sorts text in the collation of the locale it is told (api.ts sends it at fetch time),
but the keys of useServerRows, useAggregate and the Kanban columns did not hold the locale, so a
list sorted on a text column kept the old order after a switch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… the language

title={m.description} was English in every language; describeFor(m) picks the description of
the active language (all bundled models have one in de, fr, pl, fi, sv and es) and falls back
to the English one.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…age in its dependencies

Reads the source of the apps and the design system, finds every useMemo and memo, and fails
when the function it is given calls t, tx, a label helper, compareText or a fmt formatter and
the dependencies name neither lang nor locale. A memo that is right although it looks wrong
(modules/view.ts: the texts are made by the handlers it returns) says why in an
i18n-memo-ok comment. On main it flags seven memos: the app list and the smart buttons (the
bug), the four fixed in the previous commits, and the exempt one.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d language

Swedish to English on an instance whose default is Swedish and English to Swedish: app names
and tile order equal those of the reloaded page, launcher, command palette, sidebar header,
aria label and tooltip, top bar, and "Use the instance default" at once. With the account
menu: the Apps tab follows and leaving a name field writes no setting (a real rename still
saves), the related-record buttons of an open record and a list sorted on the server follow.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d parse

The dashboard lists sort equal positions by title with the collation of the
region, the widget card writes its "not set up correctly" and "no access"
texts in resolve(), and the list plan reads range bounds with the decimal
separator of the region. All three were memoised without the language, so a
switch without a reload kept the old order and the old text.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…call graph

The guard checked a memo against a hand-written list of names, so it missed
every function that translates only through another one (byPosition sorts with
compareText, resolve writes texts with t) and a function handed on without a
call (.sort(byPosition)). Of the fourteen memos that named the language in
their dependencies at 911a975, eight would have passed with it removed.

i18nGuard.ts now reads every file of the apps and of packages/ui, resolves
imports and re-exports, and takes as translators the functions that use a
reader of the i18n store, directly or through others: by call, by reference, as
a JSX tag, or as a member such as d.recordLabel and d.refText. i18nDeps.test.ts
checks the reader with fixtures, pins the set of translators on the real tree
(a missing link fails the test), fails when a store function that reads the
language is not classified, and takes the language out of each of the eighteen
memos that name it now to see that every one is then flagged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…yped next

The name field of Customize > Apps was keyed by the app's name, so it was made
again when its own rename was saved; keystrokes typed after refocusing in that
moment were lost. It is a controlled field now: it shows what is typed and not
yet saved, else the app's name, so it follows a change of language, a reset of
the launcher and a rename saved elsewhere without being made again.

The e2e spec for the Apps tab gets stricter in the same change:
- the request spy matched an address that ends in /api/setting/update, which
  the app never sends (it adds ?lang=..), so "no setting was written" could not
  fail; it matches the path now, and the single write of the spec is the rename,
  so a write on blur would show as a second one;
- the Swedish app name comes from /api/meta (the module's label) as the page
  builds it, not from a word typed into the test;
- no fixed wait before a negative assertion; the order of the writes decides;
- the field is the same element after its own rename is saved, and a reset of
  the launcher puts the name of the language back.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…the request carries the language

api.ts puts lang on a search only when it has an order, so a list or a board
column without a sort gets the same answer in every language, and the locale in
the query keys of useServerRows and the Kanban columns made it fetch the same
page again for nothing. The locale is part of those keys only when there is a
sort.

useAggregate asked again on a switch too, but every consumer builds its own
order from the groups (the pivot, the graph, the board totals and the widgets
all sort or index them), so the change did nothing; it is back to the
version on main.

Two e2e tests: a list without a sort sends no request on a switch (the next one
is the sorted one, in the new language), and a sorted board puts the cards of a
column in the order of the new language.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ks again

After a language switch a sorted list asks again, and the open record can be
missing from the new first page. useRecord then had nothing for a render (the
get that fetches it has not answered), ModelPage dropped the record panel, and
whatever was typed in it was lost. useRecord remembers the last row it returned
for the id and keeps returning it until the get answers; a record that is gone
(the get says so) still closes the panel.

The e2e test opens a record that is on the first page in English and on the
second in Swedish, types in it, switches the language and checks the panel and
the draft are still there.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The panel kept finished sentences in state: the text of a field that failed its
check or was refused by the server, and the banner for a refused move or
action. The panel stays open through a language switch, so they stayed in the
language they were written in.

It keeps what the sentence is made of instead: which fields failed the check
(the sentence is made again from the value, by the same check, when drawn), the
server's own words for a refused field and for the banner (serverMessage words
them when drawn). moveRecord returns the server's words as raw next to the
sentence for its callers that show it at once.

Two e2e tests: a field that failed its check and a refused move are in the
language of the page after a switch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…h still leaves

The language bullet of WORKFLOWS-LISTS-DASHBOARDS now states the two rules
(a memo that translates names the language; state does not hold a finished
sentence), what i18nDeps.test.ts reads and what it does not, and that a query
without a sort is not asked again.

ISSUES D8 to D14: toasts and the confirm dialog keep their text; error texts
kept in state on the sign-in, dashboards and device screens; the module
builder's seeded sample words; a decimal draft read with the separators of the
language active at Save (same at 32a0758, scenario measured, not fixed); the
double catalogue fetch at load; the Windows end-to-end harness; the blind spots
of the guard.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Opened by the address, the record is fetched on its own at load and kept, so it
survives leaving the list's page whether or not useRecord remembers the last
row; the test passed with that change taken out. It opens the record from the
list now, as a person does, so only the list's page knows it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The test took its handle on the field after the rename was saved, by the label
that follows the new name, so it always found the new element and could not see
a field that was made again. It takes the handle first and checks it is still
attached afterwards; with the key of the first version of the fix
(key={a.name}) it fails there.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…glish

The server's English is what the message rules match, and the English wording of
a rule is close to it, so a sentence kept in English was worded in Swedish again
by serverMessage and the test passed with the banner storing finished text. A
sentence kept in Swedish is not worded in English by anything: the test now
provokes the refusal in Swedish, switches to English, and back.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… is read the same everywhere)

The comment claimed a range bound is read with the decimal separator of
the region. rangeNum and rangeDecimal take a dot or a comma in every
locale, and the only reader of the i18n store is fmt.exponent() (the
digits of the instance currency), so the memo is right without the
language. Drop the unused locale dependency and mark the memo with the
true reason.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ion, member receivers

- B-1: a name is shadowed only when the code around the hook declares it
  (constants above it in the same blocks, the parameters of the function
  that holds it), not when any callback of the component names it. A
  function declared inside another no longer counts its own name as a use
  of the import of the same name.
- DG-2: a declaration is an arrow function when its => sits at the depth
  of the declaration, so a ; in an object type does not cut it short.
- P-generic: the deps of a memo are the tail after the last top-level
  comma, so new Map<string, string>() in the body is not read as deps.
- DG-4: the marker counts in a comment directly above the statement or
  inside the call, and needs three word characters of reason.
- DG-3: a member translator is followed on d, lk and lookup only, so
  hash.update(x) is not d.update(x).
- UT-6: the floors are 58 hooks seen and 16 carrying the language.
- B-4: the comment on exported() no longer claims export default is
  followed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…gone

The last row seen is kept so an open record panel is not unmounted while
the sorted list asks again. When the get answers "no such record"
(deleted, or no access) nothing is left to keep, and the old row must not
come back while the same id is fetched again.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…o and useI18n, known limits

- DB-4: every request carries ?lang= and Accept-Language; an unsorted
  search lacks the body lang that picks the collation (useServer,
  KanbanView, WORKFLOWS-LISTS-DASHBOARDS).
- DB-5: resolve() writes the detail sentences of an invalid tile, not
  the titles drawn by the card.
- I18nProvider comment: a memo component is fine if it calls useI18n();
  the old text said never to use memo.
- ISSUES D9: more files with finished sentences in state, and a refusal
  the server already worded; D13: Git Bash path conversion; D14: the
  shapes found missed, the receiver rule, the marker rule, the lang and
  locale equivalence with the memos that show it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…another record

New cases in language-switch.spec (15 tests, was 10):
- the refusal of a move made with the Move to menu (GovernedValue),
  of an action button (act()) and of a field the server named (the
  refused map) are each shown in Swedish, then English, then Swedish
  again, so a sentence stored in the language it was worded in fails;
- a board with no sort asks for nothing when the language changes;
- a record that is not on the page yet does not show the remembered row
  of the one before it while its get is held back.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
An apostrophe in JSX text blanks the rest of the line, and a slash right
after a closing brace is read as the start of a regular expression, so a
t() after either on the same line is not seen by the guard.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ument, block comment markers

- F1: scopeAt takes the parameters a header binds (and the variable of a
  for-of), not every (a) => in it, so a function type in an annotation
  ({ f }: { f: (t: string) => void }) no longer hides the import.
- F2: a declaration of the translator graph shadows with its own
  parameters and the constants at the level of its body, the same rule;
  a callback's parameter or a function type no longer hides t (the
  CodeBlock of packages/ui is a translator again).
- F3: what follows the last top-level comma is the dependencies, unless
  the text before it is not yet a whole argument (type arguments), so
  deps, deps! and a conditional are refused as "not a list" again.
- F4: a block comment is a marker above the statement only when it has
  its lines to itself; a trailing one belongs to the line it follows.
- Fixtures for each, for the 3-character reason rule, for a const in a
  sibling callback or an if block, for t => { ... }, and a real negative
  for Object.create.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…get returns nothing

The remembered row of 9a9aa91 was forgotten only after a get had answered
null. A record deleted from its panel was never fetched again, so on Back
to its address the panel opened with the old data until the get answered
(and a record opened by address kept its fetched row the same way).

The rule is now a small pure module, lastRecord.ts: the row is kept while
the get is on its way, replaced by a newer one, and forgotten when the
route holds no record or when the get does not return the record (gone,
no access, or failed). useRecord also drops its fetched row when the
route holds no record. Unit tests for the module.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…oes not come back with Back

- The unsorted-board test looked for an aggregate command that is not on
  the wire (an aggregate is a search with an aggregate key in the body).
  Requests are now classified by body, and anything the sort does not
  explain is listed in the failure.
- A record deleted from its panel and opened again by Back (its get held
  back) shows no panel; the record is off the first page and opened by
  address, so both the remembered row and the fetched one are exercised.
- The held-get test (the record of another id) waits for the get before it
  looks, so it does not depend on when the render happens.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…D13, D14

- WORKFLOWS-LISTS-DASHBOARDS and the useServer and KanbanView comments:
  every command (call in api.ts) carries ?lang= and Accept-Language; the
  body lang is added by search() with an order and always by aggregate(),
  which does not matter while only select, ref, bool and date fields can
  be grouped, because the views order the keys on the client.
- D9: PlanDialog keeps the finished sentence in its install and uninstall
  dialogs (only the preview error is raw); connectors/Catalog.tsx and
  Sections.tsx added; the search is described as including helpers that
  return a sentence.
- D10: the sample fields are one useState, only their descriptions are
  translated. D13: the value Git Bash gives, and what the build writes.
- D14: the limits probed in round 4 (statement start and commas,
  parameters of expression bodies, type parameters that hold a function
  type, constants in inner blocks of a graph function).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant