Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
13 changes: 13 additions & 0 deletions src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 });
Expand Down
43 changes: 43 additions & 0 deletions tests/vite-virtual.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
});
Loading