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/); + }); +});