The `rust-toolchain` ecosystem updates the `channel` in `rust-toolchain.toml`, which is now the only place the Rust version is written down. Weekly with the usual 14-day cooldown, so the `.1` point release has usually landed by the time the PR opens.
The builder stage starts from `buildpack-deps` (what the `rust:` image is built on anyway), installs a pinned `rustup-init` with `--default-toolchain none`, copies only `rust-toolchain.toml` and lets rustup install the toolchain it names. The Rust version is now written in exactly one place, shared with CI, so a toolchain bump needs no Dockerfile edit.
The toolchain layer is cached on the content of `rust-toolchain.toml` and the base image, and it is the same bytes we pulled as `rust:` image layers before, so build times are unchanged.
The registry cache mount also moves to `/usr/local/cargo/registry`: the `rust:` image already set `CARGO_HOME=/usr/local/cargo`, so the previous `/root/.cargo/registry` mount never held anything.
Until now only the clippy CI job and the Dockerfile named a Rust version (kept in sync by hand), while every other CI job, the release binaries and the docs build ran on whatever `stable` happened to be that day. Rust 1.98.0 landing on 2026-08-20 broke `build-binaries` overnight because of that (#5935).
`rust-toolchain.toml` pins 1.96.0 with the `minimal` profile plus clippy, and every `rustup toolchain install stable ...` in CI becomes a bare `rustup toolchain install`, which reads the file. Starting at 1.96.0 (the version clippy is already clean against) keeps this change free of lint churn; catching up to 1.98.0 is a follow-up.
rustfmt stays on nightly because `.rustfmt.toml` uses nightly-only options, so that job now invokes `cargo +nightly fmt` explicitly instead of setting a rustup directory override, which would silently take precedence over the toolchain file.
The file does not list the linux cross-compilation targets on purpose: that would make every developer and CI job download `rust-std` they never use. The two consumers that cross-compile add the targets themselves.
Now that the translations are lazily imported JS chunks rather than plain JSON
assets, `include_asset('locales/<lang>.json')` resolves to a manifest entry
flagged `isDynamicEntry`. Those must not be evaluated eagerly with a `<script>`
tag, so emit the same `modulepreload` hint we already use for imported chunks,
integrity included, and drop the now-unreachable top-level JSON preload.
`import.meta.glob` now generates lazy loaders for the locale JSON modules
rather than hashed asset URLs, so `Backend.read` awaits an `import()` instead
of doing a `fetch`. This puts translation loading under the `script-src` CSP
directive instead of `connect-src`, and lets the browser preload them as
modules, which the `<link rel="preload" as="fetch">` hint never really did
because of the credentials-mode mismatch.
Dynamic imports are cached for the lifetime of the page, so locale
hot-reloading now triggers a full page reload in dev instead of asking i18next
to re-read its resources.
The interactive playground page and its route/config option are gone; the GraphQL API itself (POST /graphql) is unaffected. External GraphQL usage is deprecated anyway in favour of the REST admin API.