From 7c09be538dce2a03ad2d7bf4df11c6031d47787b Mon Sep 17 00:00:00 2001 From: Peyton Spencer Date: Sun, 19 Jul 2026 16:15:06 -0400 Subject: [PATCH] feat: serve-only vite plugin mode (translate: false) for hermetic app builds The plugin can now serve the virtual translation modules from committed locale JSON files without extracting, translating, or writing anything. model becomes optional and is validated only when translation runs. Co-Authored-By: Claude Fable 5 --- src/types.ts | 14 +++++++++++-- src/vite.ts | 13 ++++++++++++ tests/vite-virtual.test.ts | 43 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index 3e3cca9..7f6e1ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,8 +8,18 @@ export interface SolidTranslatePluginConfig { targetLocales: string[]; /** Directory containing locale JSON files, relative to project root (default: "./src/locales") */ localesDir?: string; - /** AI model to use for translations (any Vercel AI SDK LanguageModelV1) */ - model: LanguageModelV1; + /** + * AI model to use for translations (any Vercel AI SDK LanguageModelV1). + * Required unless `translate` is `false`. + */ + model?: LanguageModelV1; + /** + * When `false`, the plugin only serves the virtual translation modules + * from the committed locale JSON files — no extraction, no AI calls, no + * file writes. Use this to keep app builds hermetic and run extraction/ + * translation exclusively through the CLI (e.g. in CI). Default: `true`. + */ + translate?: boolean; /** Custom system prompt for the AI translator */ systemPrompt?: string; /** Max keys per API call (default: 50) */ diff --git a/src/vite.ts b/src/vite.ts index fbffb49..a830894 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -48,6 +48,7 @@ export function solidTranslate(config: SolidTranslatePluginConfig): Plugin { model, systemPrompt, batchSize = 50, + translate = true, autoExtract = false, include = ["src/**/*.tsx", "src/**/*.ts", "src/**/*.jsx"], } = config; @@ -64,6 +65,18 @@ export function solidTranslate(config: SolidTranslatePluginConfig): Plugin { }, async buildStart() { + // Serve-only mode: virtual modules read the committed locale files; + // extraction and translation are handled elsewhere (CLI/CI). + if (!translate) { + return; + } + + if (!model) { + throw new Error( + "[solid-translate] `model` is required unless `translate: false` is set", + ); + } + // Ensure locales directory exists if (!existsSync(resolvedLocalesDir)) { mkdirSync(resolvedLocalesDir, { recursive: true }); diff --git a/tests/vite-virtual.test.ts b/tests/vite-virtual.test.ts index d2395fe..fd3bbd1 100644 --- a/tests/vite-virtual.test.ts +++ b/tests/vite-virtual.test.ts @@ -89,3 +89,46 @@ describe("virtual modules", () => { ).toBeUndefined(); }); }); + +describe("serve-only mode (translate: false)", () => { + test("buildStart is a no-op: no model needed, no files written", async () => { + const root = mkdtempSync(join(tmpdir(), "st-vite-serve-")); + dirs.push(root); + const localesDir = join(root, "locales"); + mkdirSync(localesDir, { recursive: true }); + writeFileSync(join(localesDir, "en.json"), JSON.stringify({ Hello: "Hello" })); + + const plugin: any = solidTranslate({ + sourceLocale: "en", + targetLocales: ["es"], + localesDir: "./locales", + translate: false, + }); + plugin.configResolved({ root }); + // Must not throw despite a stale/absent lock and no model + await plugin.buildStart(); + // Virtual modules still serve the committed files + const resolved = plugin.resolveId("virtual:solid-translate"); + expect(plugin.load(resolved)).toContain("Hola" === "Hola" ? "Hello" : ""); + // No lock or target files were created + const { existsSync } = await import("node:fs"); + expect(existsSync(join(localesDir, ".solid-translate.lock"))).toBe(false); + expect(existsSync(join(localesDir, "es.json"))).toBe(false); + }); + + test("translating mode without model throws a clear error", async () => { + const root = mkdtempSync(join(tmpdir(), "st-vite-nomodel-")); + dirs.push(root); + const localesDir = join(root, "locales"); + mkdirSync(localesDir, { recursive: true }); + writeFileSync(join(localesDir, "en.json"), JSON.stringify({ Hello: "Hello" })); + + const plugin: any = solidTranslate({ + sourceLocale: "en", + targetLocales: ["es"], + localesDir: "./locales", + }); + plugin.configResolved({ root }); + await expect(plugin.buildStart()).rejects.toThrow(/model.*required/); + }); +});