`web/project.inlang/settings.json` pointed `modules` at
https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js —
inlang's stock `paraglide-js init` scaffolding, which nobody had a reason to
keep. It made `bun run codegen` need the network at build time, so in a
sandbox that has none (the Nix build; any air-gapped CI) the plugin import
failed and paraglide compiled ZERO messages.
That failure is silent by design: the inlang SDK logs a PluginImportError as a
WARNING, paraglide still prints "Successfully compiled" and exits 0, vite
bundles the empty message set, and the console only dies at request time inside
renderToReadableStream because every `m.foo()` is undefined. Reported from a
NixOS host running punktfunk-web-server.
The plugin is a normal npm package, so depend on it like one: add
@inlang/plugin-message-format as a devDependency and reference it by relative
path. `loadProjectFromDirectory` (which both `paraglide-js compile` and the
vite plugin use) imports non-`http` modules straight off disk, and the shipped
dist/index.js is a self-contained bundle with no imports of its own, so it
loads from the data: URL exactly as the CDN copy did. Compiles 268 messages
offline, with no project.inlang/cache round-trip.
Add tools/check-i18n.mjs, run at the end of `codegen`, so this can never ship
silently again: it fails the build on a remote `modules` entry, a module that
does not resolve, or a compiled message count below what messages/en.json
defines. Verified it catches all three (remote URL, missing plugin, corrupt
plugin that compiles to 0 messages) and passes a healthy build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Marketing/store screenshots of the console, captured from the built Storybook
with headless Chromium (web/tools/screenshots.mjs) — every Pages/* + Shell/*
story rendered at 1440x900@2x. The page stories render from fixtures, so no live
mgmt API, login, or GPU is needed (the web analogue of apple.yml's screenshots
job). Gated to stable release tags in a standalone best-effort workflow; PNGs
upload as a 30-day artifact, not committed.
- Add Stats + Pairing stories (the two pages that lacked them) with stats/pairing
fixtures typed against the generated models.
- Extract a pure PairingView (index.tsx -> view.tsx), matching the
Dashboard/Clients/Stats split, so the page renders host-free from mock state
instead of racing its polling queries. Container wiring is behaviour-identical.
- Playwright driver + a chromium-capable tag-gated job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>