Skip to content

fix(i18n): apply each user's language and keep the picker in step - #339

Merged
HugoFara merged 3 commits into
developfrom
fix/current-language-and-locale
Nov 4, 2026
Merged

HugoFara merged 3 commits into
developfrom
fix/current-language-and-locale

Conversation

@HugoFara

@HugoFara HugoFara commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Two settings silently did nothing on multi-user installs, which is what production runs. The suite mostly runs single-user, so neither showed up.

Interface language

TranslatorServiceProvider::boot() runs before the middleware restores the user, so it only saw the global default. Three production users have chosen a language that was never applied.

  • UserLocale::apply() runs in AuthMiddleware and AdminMiddleware once the user is known. A user's own choice beats a leftover lwt_lang cookie; a user with no choice keeps what boot settled on.
  • The preferences page reloads when app_language changes, since it is already rendered in the old language.

Language picker

getStringWithDb() and getIntWithDb() saved through Settings::save(), which writes the global row that a user's reads never see. So ?filterlang= was lost, and one user's value landed in the shared defaults.

  • Both now use savePerUser(), as does formatSetDefault(). Remembered sort orders are fixed by the same change.
  • CurrentLanguage::follow() moves the picker when a language is created, a text is opened, or a language is reviewed. It only accepts the user's own languages.

Behaviour to expect after deploying

  • Users who chose a language see the app switch to it. Persian also flips the layout to right-to-left.
  • Opening a text in another language now switches the current language, and it stays there.

Also

24-anki-live failed without Anki instead of skipping as its header says, because cy.request() fails the test on a refused connection. It now probes AnkiConnect from a Node task.

Testing

  • 11 new integration tests in multi-user mode. Each test guarding a fix fails with that fix removed.
  • Full login and API flow against a local multi-user server: picker followed create, ?filterlang=, reader and review; de and fa applied, with dir="rtl" for fa.
  • The review step was checked starting from another language, on a language with no terms. A review with due terms takes the same call further down the method; that path has no live run.
  • PHPUnit, with and without a database, Vitest, Psalm, PHPCS, typecheck and ESLint pass.
  • Cypress: 325 of 327, the one failure and one skip being 24-anki-live. After the fix, that spec skips both tests without Anki. The full suite was not rerun after that change.

On multi-user installs the translator boots before the user is restored,
so a saved interface language was never read. Apply it once the
middleware knows the user, and reload the preferences page when it
changes.

Values persisted from a request went to the global row, which a user's
reads never see, so ?filterlang= was lost and one user's language leaked
into the shared defaults. Save them per user. Creating a language,
opening a text and reviewing a language now also update the picker.
cy.request() fails the test on a refused connection, so the check meant
to skip the spec failed it instead. Probe AnkiConnect from a Node task.
The call ran before the request was validated, so a review that was
about to redirect could still switch the language, and without a
database it stopped the page before the rest of its work. Move it to
just before rendering, using the language the review resolved so
reviews started from a text are covered too.
@HugoFara
HugoFara merged commit a3390a8 into develop Nov 4, 2026
9 checks passed
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