Skip to content

Add a language setting, applied at the next launch - #639

Merged
zaerl merged 3 commits into
trunkfrom
feat/language-setting
Oct 6, 2026
Merged

zaerl merged 3 commits into
trunkfrom
feat/language-setting

Conversation

@zaerl

@zaerl zaerl commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Why

Second part of #559, on the dialog of #638. The app is translatable (#540) and picks its language from the operating system's; a contributor on a machine set to a language they do not read, or at a shared laptop, had no way to choose. This adds the language to the settings.

What changes

Language, on the General tab. Your system's language, which is the default, or English, or any language the build ships a catalog for, each named in its own language ("Deutsch", "Português (Brasil)") so that someone looking for theirs finds it whatever the app is showing. The list is what src/languages/ holds, so it grows as releases ship catalogs; today that is English.

Where it is read. Main puts the chosen language in front of the OS's languages and loads the first one with a catalog, as it walked the OS's before. In front, not in their place: a chosen language whose catalog a later release dropped falls back to the OS's language, not to English. The --lang switch still replaces the whole list; it is how the journeys pick a locale, and a flag typed at launch is a decision.

A change applies after a relaunch, since main applies the catalog as it starts, for its own strings and the window's. The dialog says so once what is set is no longer what the window started in, and offers Relaunch now: a quit and a start, so the quit sweep ends every child the app started, as it does on any quit. The new instance gets this one's arguments less a wpct:// address a cold start was given, which is not a second request for its ticket, and a --lang, which would outrank the language just chosen; on Linux it is started from the AppImage, since the mounted copy is gone once this one quits.

The first read of the store is now before the window exists. One that cannot be read is logged and counts as no choice, so the window still opens; every later read then fails as it did before.

Beside it: the catalogs' README and CONTRIBUTING.md say where the language comes from now; a journey can launch without --lang, as the app launches for a contributor.

Not in this pull request:

  • Switching the language without a relaunch. Main applies the catalog once, at startup, for its own strings; the window would need a second path, and the menu and the native dialogs a third.
  • The slugs no operating system reports (pirate, art-xemoji), which are not offered, as they could not be picked from the OS list either.
  • The rest of Settings dialog with what exists and all new settings #559: the PHP version and the debug flags, what starts when a site opens and what happens on quit, the theme (Add dark mode #560).

How to test this

Platforms: any. The journeys drive this on macOS and Windows. From the repository root, after npm ci and npm run build:once:

npx playwright test --project=journeys settings.spec i18n.spec
  • settings.spec.js, the language journey: the pseudo-locale set in the store, with no --lang, is the language the app starts in; the control shows it though the build has no catalog for it; English chosen is kept at once and the dialog offers the relaunch, which asks main; the system's language chosen is kept as none and the offer stays; the pseudo-locale chosen again is refused, since the build has no catalog for it; started again with English kept, the app is in English and the control says so with no offer.
  • i18n.spec.js: the dialog with the control, in the pseudo-locale.

I broke two of the journey's claims one at a time (main not reading the chosen language; the relaunch button asking nothing of main) and each turned it red. tests/unit/i18n.test.cjs holds the list, settings.test.cjs what is accepted, settings-view.test.cjs the control's entries and when the relaunch is offered, and ipc-wiring.test.cjs the order the languages are tried in, the store that cannot be read, and what the relaunch is given.

Worth a look by hand (any platform, the current head): Settings → General → Language: with no catalog shipped the list is your system's language and English. Choose English, press Relaunch now: the app quits and comes back, the control says English and offers nothing. Choose your system's language, press the cog's relaunch again: as before. With npm run i18n:download -- --locales=de run first (it needs the network), "Deutsch" is in the list, and chosen and relaunched, the app is in German where it is translated.

What must not have happened: a running server or build left behind by the relaunch (the quit sweep ends them, as on any quit); a wpct:// ticket opened again after a relaunch of an app that was started from a link (Windows and Linux); the app failing to open a window when settings.json is not JSON.

What I could not test: Linux, where the relaunch is started from the AppImage; the unit test pins what Electron is told, not that the image starts. And a real catalog in the dialog: the repository ships none, and I did not run the download.

Risks and limitations

  • Review: 2 passes, 4 findings fixed here, 1 follow-up taken here. Details below.
  • The language names are not translated, by design: each is in its own language.
  • A store that cannot be read lets the window open but every later read fails as it did before this change; the app does not say which file is at fault.
  • The relaunch is a quit: nothing asks first, as nothing asks on ⌘Q.

Related

Part of #559, which is part of #542. Follows #638. Translations: #540.


Design decisions and alternatives considered
  • A relaunch, not a live switch. Main applies the locale once, at startup, for the menu, the native dialogs and the sentences it sends, and the window reads its catalog before its first render; three paths to re-run for a setting changed a few times in the app's life. The dialog says what the relaunch costs.
  • The chosen language in front of the OS's, not in their place. A release drops a catalog that fell under the cut-off; the contributor who chose it should get their OS's language, as they did before choosing.
  • The control's entries with their tags as values, drawn by the dialog, and the list also handed over for the names. Handed the list alone, the design system's select takes each entry as its own value and tells them apart by identity, which a value kept in the store cannot match.
  • Entries made from the language the window started in, not the one set, so that a choice does not change them: an entry taken away under the control while it is choosing is reported as a second choice, of none. A language the window started in that the build has no catalog for is listed, so the control shows what is set, and refused if chosen again.
  • The quit sweep over app.exit. The relaunch goes through app.quit() so every child the app started is ended first.
Review outcome (required — see AGENTS.md)

Pass 1: 3 [fix here] · 0 [follow-up] · 4 style notes. Pass 2 (on the fix-up): 1 [fix here] · 1 [follow-up] · 1 style note. All 4 fixed, the follow-up taken here, every style note applied.

  • Review: completed — a separate agent context, read-only; head 82aac18 / base fe02e6b; the store read traced from whenReady, the relaunch against Electron's quit events and the single-instance lock, the language names checked on ICU for lowercase tags and Valencian, the select's value handling read in the design system's source, the journey read against the standard; ESLint, the unit files and the pot extraction run; 3 [fix here].
    • Fixed: a store that could not be read would have stopped the window opening, since the locale is resolved before it; a chosen language replaced the OS list, so one whose catalog was dropped gave English rather than the OS's language; the relaunch reused the launch's arguments, a wpct:// address and a --lang included.
    • Style notes, applied: a stale comment in the store module; a silent catch on the relaunch; the system entry's mapping as a tested function; the catalogs' README counting entries not offered; a journey comment.
  • Review: completed — a second separate agent context, read-only; head 7eab0b6 / base fe02e6b; the fallback order followed through resolveCatalog for each case, the relaunch arguments against Electron's documented default and a dev launch, the three changed wiring tests confirmed red on the first commit; ESLint and the unit files run; 1 [fix here], 1 [follow-up].
    • Fixed: the README still described the chosen language as used instead of the OS's rather than before them.
    • Follow-up, taken here: on Linux the relaunch would start from the AppImage's mount, which is gone once the app quits; the new instance is started from the image.
    • Style note, applied: one comment line left unwrapped.
  • Since review: 7eab0b6 → b68f7c2 is that finding, the follow-up and the rewrap, not reviewed again. On the head: npm run lint, npm test, npm run test:electron, the settings, i18n and create-site journeys and the packaged smoke pass; all 118 journeys passed on 82aac18.
Implementation notes
  • src/i18n.cjs: languageChoices(names). src/settings.cjs: locale. src/main.js: localeReply reads the store, languages(), i18n:languages, app:relaunch, relaunchArgs. src/preload.js: listLanguages, relaunch.
  • src/renderer/settings-view.cjs: languageItems, languageValue, languageChanged, SYSTEM_LANGUAGE. src/renderer/components/settings-dialog.jsx: LanguageControl. src/renderer/hooks/use-settings.jsx: loaded.
  • tests/e2e/helpers/app.cjs: lang: false drops the --lang switch.
Screenshots or recording

Not attached: I have no way to upload images from the command line. The language journey drives everything that changed.

🤖 Generated with Claude Code

zaerl and others added 3 commits October 6, 2026 11:49
The second of the settings (#559). The General tab offers the languages
the build ships a catalog for, each named in its own language, with the
system's language as the default; main reads the one set before the OS
list, as it does the --lang switch, which still wins over both. A change
shows after a relaunch, since main applies the catalog as it starts, and
the dialog says so and offers the relaunch, a quit and a start, so that
the quit sweep ends what the app started.

The entries of the control are made from the language the window began
in rather than the one set: an entry taken away under the control while
it is choosing is reported as a second choice, of none. A language the
window began in that the build has no catalog for is listed, so that the
control shows what is set, and is refused if chosen again.

A journey can now launch without --lang, as the app launches for a
contributor, and does: the pseudo-locale set in the store is the one the
app starts in, and the one kept is the one it restarts in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ly, and relaunch without the launch's link

From the review. A chosen language goes before the OS's languages and not
in their place, so one whose catalog a release has dropped falls back to
the OS's language and not to English. The store is read before there is
a window, and one that cannot be read is logged and counts as no choice,
where it would have stopped the window from opening. The relaunch hands
the new instance this one's arguments less a wpct:// address a cold start
was given, which is not a second request for its ticket, and a --lang
switch, which would outrank the language just chosen.

Also: the system's entry is kept as no choice through one tested function;
a relaunch that fails is said where it was asked for; a comment in the
store module and one in a journey said what is no longer so, and the
catalogs' README counted entries that are not offered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nch from the AppImage on Linux

From the second review. The catalogs' README said the chosen language
is used instead of the OS's, where it is tried before them. On Linux the
app is an AppImage, mounted while it runs and gone once it quits, so the
new instance is started from the image itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a256f064-9147-4ddb-8fc8-e976ffb2dea4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zaerl
zaerl merged commit 77751a1 into trunk Oct 6, 2026
10 checks passed
@zaerl
zaerl deleted the feat/language-setting branch October 6, 2026 10:35
zaerl added a commit that referenced this pull request Oct 6, 2026
…he next server start (#640)

## Why

Third part of #559, on the dialog of #638. Every site's development
server ran on the PHP the bundled Playground chooses and with
WordPress's debug constants fixed on; a contributor whose ticket is
about PHP 7.4, or about what a site does with `WP_DEBUG` off, had no way
to see it. This adds the PHP version and the two debug constants that
can be turned off to the settings, and shows what a server will start
with in the open site's details.

## What changes

**A Sites tab: Development server.** The PHP version, one of the seven
the bundled Playground has (7.4 to 8.5), 8.3 unless another is chosen;
and two switches, **Report notices and deprecations (WP_DEBUG)** and
**Use unminified scripts (SCRIPT_DEBUG)**. The rest of the constants a
server is booted with (the log, the display, the disabled fatal handler
and updater) stay as they were and are not anyone's to change.

**Applied at the next start.** Main reads the three at every server
start and hands them to the runner in its config; a server that is
running keeps what it started with until it is stopped and started, and
the tab says so. A version a release has taken out of the bundle is
passed over for 8.3, with a line in the app log, and the control shows
8.3 as chosen with a note saying why.

**The details say what a server will start with**, as the prototype has
them: "WordPress Core · PHP 8.3" on the Checkout row, and a Debugging
row with the constants that are on, or "Off".

**What WP_DEBUG off does, said for what it is.** It takes away notices
and deprecations. Warnings and errors still reach `debug.log` and the
browser, and `error_log()` calls still reach `debug.log`, because
Playground's own PHP keeps logging and display on whatever WP_DEBUG
says. The switch, the guide and the constants' module say so.

**The guide's pictures are retaken.** The details changed here, and the
footer's cog from #638 had never been retaken. The three pictures of a
site's page are taken in a window 900 high, so the details' Build watch
section is whole.

**Not in this pull request:**

- The PHP version a *running* server is on, in the Server section, where
the details show the version the next start is given. Follow-up.
- Per-site values: these are global, like the other settings. A per-site
override can go in `siteMeta` if one is ever wanted.
- The remaining constants (`WP_DEBUG_LOG`, `WP_DEBUG_DISPLAY`,
`WP_DISABLE_FATAL_ERROR_HANDLER`, `AUTOMATIC_UPDATER_DISABLED`), on
purpose: `src/wp-debug-constants.js` says why each is on.
- The rest of #559: what starts when a site opens and what happens on
quit; the theme (#560).

## How to test this

Platforms: any for the dialog and the details; macOS for the real
server, below. The journeys drive the dialog and the details on macOS
and Windows. From the repository root, after `npm ci` and `npm run
build:once`:

```
npx playwright test --project=journeys settings.spec site-header.spec i18n.spec
```

- `settings.spec.js`, the Sites journey: the fallbacks (8.3, both
constants on); 8.4 chosen is kept, WP_DEBUG off is kept and SCRIPT_DEBUG
left as it was; the open site's details then say "WordPress Core · PHP
8.4" and "SCRIPT_DEBUG"; restarted, the tab shows what was kept.
- `site-header.spec.js`: the details name the project with the PHP a
server starts on and which constants are on.
- `i18n.spec.js`: the tab in the pseudo-locale.

I broke two claims one at a time (main not reading the settings at a
start; the switch not writing) and each was caught, the first by
`ipc-wiring.test.cjs`, the second by the journey.
`tests/unit/runner-wiring.test.cjs` proves the runner hands Playground
the version and turns the two constants off in the blueprint;
`wp-debug-constants.test.cjs` that nothing else in the set moves;
`settings.test.cjs` and `settings-view.test.cjs` what is accepted and
what the control shows.

**The real server, run by me on this Mac (Apple Silicon, the current
head), as TESTING.md asks for a change to `src/server-runner.js`.** The
app was driven through Playwright with a throwaway profile listing a
built `wordpress-develop` checkout; **Start development server** was
pressed; the served WordPress was logged in to with the app's own
credentials and asked, on Site Health's Info tab, which PHP it runs on:

- PHP version set to **8.4**: the server came up, Site Health reported
**PHP 8.4.25**, wp-admin served, Stop ended it.
- PHP version left at the fallback, **8.3**: Site Health reported **PHP
8.3.33**.

**Worth a look by hand** (any platform, the current head): Settings →
Sites, choose 7.4, close: the details say "PHP 7.4". Start the server:
wp-admin → Tools → Site Health → Info → Server says PHP 7.4. Turn
WP_DEBUG off, stop and start the server: a page with a deprecated call
shows nothing for it, and a page that calls a missing function still
shows the fatal.

**What must not have happened:** a running server changing PHP or
constants under the contributor; a change applied to a site other than
the next start; `WP_DEBUG_LOG` or the fatal handler's constant moving
with the switches; the details saying a version the server will not
start on.

**What I could not test:** Windows, and a real server with WP_DEBUG off
(I checked what the code sends, not a page's output).

## Risks and limitations

- Review: 2 passes, 8 findings fixed here, 1 follow-up taken here (the
real boot), 2 left. Details below.
- **The details show the PHP of the next start, not of a server already
running.** Change the version while a server runs and "PHP 8.4" sits
above "Server running" on 8.3. Follow-up: the Server section should name
the running server's PHP.
- **Main requires `@php-wasm/universal`** for the list of versions. It
is not a declared dependency; it is `@wp-playground/cli`'s, at whatever
version that pins, and the comment says so. Declaring it pinned is a
`package.json` decision I have left to the maintainer.
- **The Electron unit suite failed one test once** in a run of 2004 and
passed on three runs after; I could not see which test it was the first
time.
- A `WP_DEBUG` switch that is off still leaves `WP_DEBUG_LOG` on, by
design, so the debug.log tab keeps showing warnings and errors.

## Related

Part of #559, which is part of #542. Follows
#639.

---

<details>
<summary>Design decisions and alternatives considered</summary>

- **Global, applied at the next start, and read by main at each start.**
The prototype's "New sites" tab suggests values copied into a site at
creation. Here the version and the constants are what the server is
booted with, so reading them at each start is simpler and also reaches
existing sites.
- **An explicit fallback, always passed.** The CLI's own default is 8.3
today; passing it ourselves keeps the behaviour stable across a
Playground bump, and a stored version the bump took out falls back to it
rather than to whatever the CLI then chooses.
- **The list from the bundle, not written down.** `SupportedPHPVersions`
from `@php-wasm/universal` is what the runner can serve; a list of our
own would drift.
- **Only two constants have a switch.** The others exist to make a fatal
show as itself, the log exist for the tab and the updater stay out of a
checkout; a switch for each would be one more thing to get wrong at a
Contributor Day.
- **One decision for which version is shown and used**,
`phpVersionChoice` in `settings-view.cjs`: the dialog, the details and
the start handler all agree on the fallback.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

**Pass 1: 3 [fix here] · 3 [follow-up]. Pass 2 (on the fix-up and the
details): 5 [fix here] · 1 [follow-up]. All 8 fixed, the real-boot
follow-up taken here, 2 left.**

- **Review:** completed — a separate agent context, read-only; head
`9131ed7` / base `77751a1`; the runner's options checked against the
CLI's `runCLI` signature and its persistent `start` command, WordPress's
`wp_debug_mode()` and Playground's php.ini and mu-plugin read for what
WP_DEBUG off does, the design system's switch and toggle props
confirmed; ESLint and the unit files run; 3 [fix here], 3 [follow-up].
- Fixed: WP_DEBUG off was said to empty the log and the browser of
errors, where it takes away notices and deprecations; which versions the
control offers was decided in the component; the real boot TESTING.md
asks for was not recorded.
- Follow-ups: the undeclared require (a comment, left to the maintainer
to declare); a stored version the bundle no longer has was shown as
chosen but not used (taken here); the journey pins 8.4 (left).
- **Review:** completed — a second separate agent context, read-only;
head `ca84255` / base `77751a1`; the four rewordings checked against
WordPress and Playground again, the details' new rows and their
journeys, the retaken pictures opened against their alt texts; ESLint
and the unit files run; 5 [fix here], 1 [follow-up].
- Fixed: the details showed the version set where the dialog and the
start used the fallback; `error_log()` was said to reach the browser;
three pictures cut the details' Build watch section off under the
footer; the require had no comment on whose dependency it rides; a
failed read of the versions left an enabled, empty control.
- Follow-up left: the Server section naming the PHP a running server is
on.
- **Since review:** `ca84255 → ba66103` is those five fixes and two
style notes, not reviewed again. The commit after it changes one
journey: `site-header.spec.js`'s stickiness test failed on both runners,
since the Debugging row left the details too tall to fit over the tray
at its smallest on their screens; its seeded site now has no creation
date and no trunk date, two rows fewer, and the journey was run at
windows 700, 680 and 640 high and is red at 640 with the app's rule
broken. On the head: `npm run lint`, `npm test` (2002), `npm run
test:electron` (2003), all 119 journeys, the packaged smoke and the docs
build pass. The twelve pictures were taken four times before the last
change and two of them moved by 12 and 30 pixels on a corner between
takes, the known noise; the three retaken after it were taken once.

</details>

<details>
<summary>Implementation notes</summary>

- `src/settings.cjs`: `phpVersion`, `wpDebug`, `scriptDebug`,
`acceptSwitch`. `src/wp-debug-constants.js`: `debugConstants(debug)`.
`src/server-runner.js`: `php` and the constants from the config.
- `src/main.js`: `phpVersions()`, `playground:php-versions`,
`playground:start` reads the store at each start. `src/preload.js`:
`listPhpVersions`.
- `src/renderer/settings-view.cjs`: `phpVersionChoice`.
`src/renderer/hooks/use-settings.jsx`: `php`.
`src/renderer/components/settings-dialog.jsx`: `SitesTab`.
`src/renderer/site-details.cjs`: `phpVersion` and `debug` in the rows.
- `scripts/screenshots/shots.cjs`: the three site views at 1200×900.

</details>

<details>
<summary>Screenshots or recording</summary>

The retaken images are in the diff: `docs/public/screenshots/` shows the
details before and after.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@zaerl zaerl added this to the v2.0.0 milestone Oct 7, 2026
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