fix(web): load the inlang plugin from node_modules, not a CDN
audit / bun-audit (push) Successful in 13s
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 56s
apple / swift (push) Successful in 1m19s
audit / cargo-audit (push) Successful in 3m6s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 55s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 10m14s
ci / bench (push) Successful in 10m59s
windows-host / package (push) Successful in 11m11s
deb / build-publish-host (push) Successful in 13m54s
android / android (push) Successful in 17m22s
arch / build-publish (push) Successful in 17m37s
ci / rust (push) Successful in 22m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m48s
docker / deploy-docs (push) Successful in 29s
audit / bun-audit (push) Successful in 13s
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 56s
apple / swift (push) Successful in 1m19s
audit / cargo-audit (push) Successful in 3m6s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 55s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 10m14s
ci / bench (push) Successful in 10m59s
windows-host / package (push) Successful in 11m11s
deb / build-publish-host (push) Successful in 13m54s
android / android (push) Successful in 17m22s
arch / build-publish (push) Successful in 17m37s
ci / rust (push) Successful in 22m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m48s
docker / deploy-docs (push) Successful in 29s
`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>
This commit is contained in:
@@ -313,7 +313,7 @@ in
|
|||||||
dontFixup = true;
|
dontFixup = true;
|
||||||
outputHashMode = "recursive";
|
outputHashMode = "recursive";
|
||||||
outputHashAlgo = "sha256";
|
outputHashAlgo = "sha256";
|
||||||
outputHash = "sha256-OA4NjwapsCV/z+0rftDCMAQJGWw63Mi/GARetmuy0QU="; # web/bun.lock deps (refresh on lockfile change; see README).
|
outputHash = "sha256-5oVZv65SMvq9i2REzHE8Pyn6qUZaV2FnPQdaouwcwoU="; # web/bun.lock deps (refresh on lockfile change; see README).
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
stdenvNoCC.mkDerivation {
|
stdenvNoCC.mkDerivation {
|
||||||
@@ -326,6 +326,14 @@ in
|
|||||||
|
|
||||||
# No cross-derivation dep cache: codegen + the vite build are fully offline (every input is in
|
# No cross-derivation dep cache: codegen + the vite build are fully offline (every input is in
|
||||||
# the vendored node_modules, the checked-in api/openapi.json, and web/project.inlang).
|
# the vendored node_modules, the checked-in api/openapi.json, and web/project.inlang).
|
||||||
|
#
|
||||||
|
# ⚠ "Offline" is load-bearing and NOT self-enforcing. inlang resolves the plugins in
|
||||||
|
# web/project.inlang/settings.json `modules`, and a failed import is only a WARNING there:
|
||||||
|
# paraglide then prints "Successfully compiled", exits 0, and emits ZERO messages, so the
|
||||||
|
# console builds fine and dies at SSR time with every `m.foo()` undefined. That is exactly
|
||||||
|
# what a CDN URL in `modules` did in this network-less sandbox. The plugin is now a normal
|
||||||
|
# devDependency referenced by path, and `bun run codegen` ends in tools/check-i18n.mjs, which
|
||||||
|
# fails the build on a remote module or a short message count. Keep both properties.
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
export HOME=$TMPDIR
|
export HOME=$TMPDIR
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.2",
|
"@biomejs/biome": "^2.5.2",
|
||||||
"@inlang/paraglide-js": "^2.20.2",
|
"@inlang/paraglide-js": "^2.20.2",
|
||||||
|
"@inlang/plugin-message-format": "^4.4.0",
|
||||||
"@storybook/react-vite": "^10.4.6",
|
"@storybook/react-vite": "^10.4.6",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
|
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
|
||||||
@@ -283,6 +284,8 @@
|
|||||||
|
|
||||||
"@inlang/paraglide-js": ["@inlang/paraglide-js@2.20.2", "", { "dependencies": { "@inlang/recommend-sherlock": "^0.2.1", "@inlang/sdk": "^2.10.0", "commander": "11.1.0", "consola": "3.4.0", "json5": "2.2.3", "unplugin": "^2.1.2", "urlpattern-polyfill": "^10.0.0" }, "peerDependencies": { "typescript": ">=5.6" }, "optionalPeers": ["typescript"], "bin": { "paraglide-js": "bin/run.js" } }, "sha512-V8iY3uu/vQU94gEag1bdC3glMJSp4Dg3XMwfnabZLBh1Dv0F++DvDYlMeniqv2+nHbnS/twB75AM140OmpHDEg=="],
|
"@inlang/paraglide-js": ["@inlang/paraglide-js@2.20.2", "", { "dependencies": { "@inlang/recommend-sherlock": "^0.2.1", "@inlang/sdk": "^2.10.0", "commander": "11.1.0", "consola": "3.4.0", "json5": "2.2.3", "unplugin": "^2.1.2", "urlpattern-polyfill": "^10.0.0" }, "peerDependencies": { "typescript": ">=5.6" }, "optionalPeers": ["typescript"], "bin": { "paraglide-js": "bin/run.js" } }, "sha512-V8iY3uu/vQU94gEag1bdC3glMJSp4Dg3XMwfnabZLBh1Dv0F++DvDYlMeniqv2+nHbnS/twB75AM140OmpHDEg=="],
|
||||||
|
|
||||||
|
"@inlang/plugin-message-format": ["@inlang/plugin-message-format@4.4.0", "", { "dependencies": { "flat": "^6.0.1" } }, "sha512-n4aXt6XVg5kxhKoLAhi9nMgZtCA9iS0QOaXte56VqxWHcfj9O4c4gOkyVQZH7H9D8h7OZufCrO1sZGYOypPwEA=="],
|
||||||
|
|
||||||
"@inlang/recommend-sherlock": ["@inlang/recommend-sherlock@0.2.1", "", { "dependencies": { "comment-json": "^4.2.3" } }, "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg=="],
|
"@inlang/recommend-sherlock": ["@inlang/recommend-sherlock@0.2.1", "", { "dependencies": { "comment-json": "^4.2.3" } }, "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg=="],
|
||||||
|
|
||||||
"@inlang/sdk": ["@inlang/sdk@2.10.2", "", { "dependencies": { "@lix-js/sdk": "0.4.10", "@sinclair/typebox": "^0.31.17", "kysely": "^0.28.12", "sqlite-wasm-kysely": "0.3.0", "uuid": "^14.0.0" } }, "sha512-O1ki72SNK6LPagaGrvlioBb1mWKvump7cO7P85hfGZjdFTmDdn3icI0A6MvaBsB3P9KQHAjzyubnN1OslGufTw=="],
|
"@inlang/sdk": ["@inlang/sdk@2.10.2", "", { "dependencies": { "@lix-js/sdk": "0.4.10", "@sinclair/typebox": "^0.31.17", "kysely": "^0.28.12", "sqlite-wasm-kysely": "0.3.0", "uuid": "^14.0.0" } }, "sha512-O1ki72SNK6LPagaGrvlioBb1mWKvump7cO7P85hfGZjdFTmDdn3icI0A6MvaBsB3P9KQHAjzyubnN1OslGufTw=="],
|
||||||
@@ -1383,6 +1386,8 @@
|
|||||||
|
|
||||||
"find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="],
|
"find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="],
|
||||||
|
|
||||||
|
"flat": ["flat@6.0.1", "", { "bin": { "flat": "cli.js" } }, "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw=="],
|
||||||
|
|
||||||
"focus-trap": ["focus-trap@7.5.4", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w=="],
|
"focus-trap": ["focus-trap@7.5.4", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,7 @@
|
|||||||
"description": "punktfunk management console — TanStack Start + React Query (orval) + @unom/ui + Paraglide i18n",
|
"description": "punktfunk management console — TanStack Start + React Query (orval) + @unom/ui + Paraglide i18n",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "bun run codegen",
|
"prepare": "bun run codegen",
|
||||||
"codegen": "orval --config orval.config.ts && paraglide-js compile --project ./project.inlang --outdir ./src/paraglide",
|
"codegen": "orval --config orval.config.ts && paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && node tools/check-i18n.mjs",
|
||||||
"predev": "orval --config orval.config.ts",
|
"predev": "orval --config orval.config.ts",
|
||||||
"dev": "vite dev --port 47992",
|
"dev": "vite dev --port 47992",
|
||||||
"prebuild": "orval --config orval.config.ts",
|
"prebuild": "orval --config orval.config.ts",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.2",
|
"@biomejs/biome": "^2.5.2",
|
||||||
"@inlang/paraglide-js": "^2.20.2",
|
"@inlang/paraglide-js": "^2.20.2",
|
||||||
|
"@inlang/plugin-message-format": "^4.4.0",
|
||||||
"@storybook/react-vite": "^10.4.6",
|
"@storybook/react-vite": "^10.4.6",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
|
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
"$schema": "https://inlang.com/schema/project-settings",
|
"$schema": "https://inlang.com/schema/project-settings",
|
||||||
"baseLocale": "en",
|
"baseLocale": "en",
|
||||||
"locales": ["en", "de"],
|
"locales": ["en", "de"],
|
||||||
"modules": [
|
"modules": ["./node_modules/@inlang/plugin-message-format/dist/index.js"],
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js"
|
|
||||||
],
|
|
||||||
"plugin.inlang.messageFormat": {
|
"plugin.inlang.messageFormat": {
|
||||||
"pathPattern": "./messages/{locale}.json"
|
"pathPattern": "./messages/{locale}.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Guard: assert paraglide actually compiled our translations.
|
||||||
|
//
|
||||||
|
// The inlang SDK treats a failed plugin import as a WARNING, not an error: if the
|
||||||
|
// message-format plugin can't be loaded, `paraglide-js compile` still prints
|
||||||
|
// "Successfully compiled" and exits 0 — having emitted an empty message set. vite
|
||||||
|
// then happily bundles that, and the console only dies at SSR time, deep inside
|
||||||
|
// renderToReadableStream, because every `m.foo()` is undefined. That shipped once
|
||||||
|
// (a Nix build, where the plugin was fetched from a CDN in a network-off sandbox),
|
||||||
|
// so the build now fails loudly instead.
|
||||||
|
//
|
||||||
|
// node tools/check-i18n.mjs # run from web/, after `paraglide-js compile`
|
||||||
|
//
|
||||||
|
// Env knobs: PARAGLIDE_OUTDIR (default ./src/paraglide), INLANG_PROJECT (default
|
||||||
|
// ./project.inlang).
|
||||||
|
|
||||||
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
const outdir = resolve(process.env.PARAGLIDE_OUTDIR ?? "./src/paraglide");
|
||||||
|
const project = resolve(process.env.INLANG_PROJECT ?? "./project.inlang");
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
console.error(`ERROR: i18n check failed — ${msg}`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsPath = join(project, "settings.json");
|
||||||
|
if (!existsSync(settingsPath)) fail(`no inlang settings at ${settingsPath}`);
|
||||||
|
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
||||||
|
|
||||||
|
// Every plugin must resolve offline. A remote module makes the build depend on a CDN
|
||||||
|
// at compile time, which silently yields zero messages in any sandbox without network
|
||||||
|
// (Nix, air-gapped CI) — see the header.
|
||||||
|
for (const mod of settings.modules ?? []) {
|
||||||
|
if (/^https?:/i.test(mod)) {
|
||||||
|
fail(
|
||||||
|
`inlang module "${mod}" is a remote URL — vendor it as a devDependency and reference it by path, ` +
|
||||||
|
`else offline builds compile zero messages`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const modPath = resolve(project, "..", mod);
|
||||||
|
if (!existsSync(modPath))
|
||||||
|
fail(
|
||||||
|
`inlang module "${mod}" does not exist at ${modPath} (run \`bun install\`?)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The base-locale source file is the source of truth for how many messages we expect.
|
||||||
|
const pathPattern = settings["plugin.inlang.messageFormat"]?.pathPattern;
|
||||||
|
if (!pathPattern)
|
||||||
|
fail("settings.json has no plugin.inlang.messageFormat.pathPattern");
|
||||||
|
const basePath = resolve(
|
||||||
|
project,
|
||||||
|
"..",
|
||||||
|
pathPattern.replace("{locale}", settings.baseLocale),
|
||||||
|
);
|
||||||
|
if (!existsSync(basePath)) fail(`base-locale messages missing at ${basePath}`);
|
||||||
|
const expected = Object.keys(JSON.parse(readFileSync(basePath, "utf8"))).filter(
|
||||||
|
(k) => !k.startsWith("$"),
|
||||||
|
).length;
|
||||||
|
if (expected === 0) fail(`base-locale messages at ${basePath} are empty`);
|
||||||
|
|
||||||
|
const messagesDir = join(outdir, "messages");
|
||||||
|
if (!existsSync(messagesDir))
|
||||||
|
fail(`paraglide emitted no messages dir at ${messagesDir}`);
|
||||||
|
// paraglide writes one module per message plus an `_index.js` barrel.
|
||||||
|
const emitted = readdirSync(messagesDir).filter(
|
||||||
|
(f) => f.endsWith(".js") && f !== "_index.js",
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (emitted < expected) {
|
||||||
|
fail(
|
||||||
|
`paraglide emitted ${emitted} messages but ${basePath} defines ${expected}. ` +
|
||||||
|
`The message-format plugin most likely failed to import — check the compile output for PluginImportError.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✔ i18n check: ${emitted} compiled messages for ${settings.locales.join(", ")}`,
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user