solid-translate 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.js ADDED
@@ -0,0 +1,339 @@
1
+ // src/vite.ts
2
+ import {
3
+ readFileSync,
4
+ writeFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync
8
+ } from "fs";
9
+ import { resolve, join, relative } from "path";
10
+
11
+ // src/hash.ts
12
+ import { createHash } from "crypto";
13
+ function hashContent(content) {
14
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
15
+ }
16
+
17
+ // src/translate.ts
18
+ import { generateObject } from "ai";
19
+ import { z } from "zod";
20
+ async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
21
+ const keys = Object.keys(entries);
22
+ if (keys.length === 0) return {};
23
+ const defaultSystem = [
24
+ `You are a professional translator specializing in software localization.`,
25
+ `Translate text from "${sourceLocale}" to "${targetLocale}".`,
26
+ `Rules:`,
27
+ `- Preserve the original tone and meaning`,
28
+ `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,
29
+ `- Keep HTML tags unchanged`,
30
+ `- Do not add or remove content`,
31
+ `- Return natural, idiomatic translations`
32
+ ].join("\n");
33
+ let contextSection = "";
34
+ if (contexts && Object.keys(contexts).length > 0) {
35
+ const contextLines = Object.entries(contexts).filter(([key]) => key in entries).map(([key, ctx]) => ` "${key}": ${ctx}`);
36
+ if (contextLines.length > 0) {
37
+ contextSection = [
38
+ ``,
39
+ `Context hints for disambiguation:`,
40
+ ...contextLines,
41
+ ``
42
+ ].join("\n");
43
+ }
44
+ }
45
+ const { object } = await generateObject({
46
+ model,
47
+ schema: z.object({
48
+ translations: z.record(z.string(), z.string())
49
+ }),
50
+ system: systemPrompt || defaultSystem,
51
+ prompt: [
52
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
53
+ `Return a JSON object with the exact same keys and the translated values.`,
54
+ contextSection,
55
+ JSON.stringify(entries, null, 2)
56
+ ].join("\n")
57
+ });
58
+ return object.translations;
59
+ }
60
+
61
+ // src/extract.ts
62
+ function extractStringsFromSource(code, filePath) {
63
+ const results = [];
64
+ const seen = /* @__PURE__ */ new Set();
65
+ const tComponentRegex = /<T(\s[^>]*)?>([^]*?)<\/T>/g;
66
+ let match;
67
+ while ((match = tComponentRegex.exec(code)) !== null) {
68
+ const attrs = match[1] || "";
69
+ const rawChildren = match[2];
70
+ const line = code.substring(0, match.index).split("\n").length;
71
+ const idMatch = attrs.match(/id=["']([^"']+)["']/);
72
+ const contextMatch = attrs.match(/context=["']([^"']+)["']/);
73
+ let slotIndex = 0;
74
+ const source = rawChildren.replace(
75
+ /<(?:Var|Num|Currency|DateTime)(?:\s[^>]*)?>([^]*?)<\/(?:Var|Num|Currency|DateTime)>/g,
76
+ () => `{${slotIndex++}}`
77
+ ).trim();
78
+ const key = idMatch ? idMatch[1] : source;
79
+ if (!key || seen.has(key)) continue;
80
+ seen.add(key);
81
+ results.push({
82
+ key,
83
+ source,
84
+ file: filePath,
85
+ line,
86
+ context: contextMatch ? contextMatch[1] : void 0
87
+ });
88
+ }
89
+ const msgRegex = /\bmsg\(\s*["']([^"']+)["']\s*(?:,\s*\{[^}]*\})?\s*\)/g;
90
+ while ((match = msgRegex.exec(code)) !== null) {
91
+ const source = match[1];
92
+ if (seen.has(source)) continue;
93
+ seen.add(source);
94
+ const line = code.substring(0, match.index).split("\n").length;
95
+ results.push({ key: source, source, file: filePath, line });
96
+ }
97
+ return results;
98
+ }
99
+
100
+ // src/vite.ts
101
+ var VIRTUAL_MODULE_ID = "virtual:solid-translate";
102
+ var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
103
+ function solidTranslate(config) {
104
+ const {
105
+ sourceLocale = "en",
106
+ targetLocales,
107
+ localesDir = "./src/locales",
108
+ model,
109
+ systemPrompt,
110
+ batchSize = 50,
111
+ autoExtract = false,
112
+ include = ["src/**/*.tsx", "src/**/*.ts", "src/**/*.jsx"]
113
+ } = config;
114
+ let root;
115
+ let resolvedLocalesDir;
116
+ let lockFilePath;
117
+ return {
118
+ name: "solid-translate",
119
+ configResolved(resolvedConfig) {
120
+ root = resolvedConfig.root;
121
+ resolvedLocalesDir = resolve(root, localesDir);
122
+ lockFilePath = join(resolvedLocalesDir, ".solid-translate.lock");
123
+ },
124
+ async buildStart() {
125
+ if (!existsSync(resolvedLocalesDir)) {
126
+ mkdirSync(resolvedLocalesDir, { recursive: true });
127
+ }
128
+ const sourceFilePath = join(
129
+ resolvedLocalesDir,
130
+ `${sourceLocale}.json`
131
+ );
132
+ let contexts = {};
133
+ if (autoExtract) {
134
+ const extracted = await autoExtractStrings(root, include);
135
+ contexts = extracted.contexts;
136
+ let existingSource = {};
137
+ if (existsSync(sourceFilePath)) {
138
+ try {
139
+ existingSource = JSON.parse(
140
+ readFileSync(sourceFilePath, "utf-8")
141
+ );
142
+ } catch {
143
+ }
144
+ }
145
+ let changed = false;
146
+ for (const [key, value] of Object.entries(extracted.strings)) {
147
+ if (!(key in existingSource)) {
148
+ existingSource[key] = value;
149
+ changed = true;
150
+ }
151
+ }
152
+ if (changed) {
153
+ const sorted = Object.fromEntries(
154
+ Object.entries(existingSource).sort(
155
+ ([a], [b]) => a.localeCompare(b)
156
+ )
157
+ );
158
+ writeFileSync(
159
+ sourceFilePath,
160
+ JSON.stringify(sorted, null, 2) + "\n"
161
+ );
162
+ console.log(
163
+ `[solid-translate] Auto-extracted ${Object.keys(extracted.strings).length} strings from source`
164
+ );
165
+ }
166
+ }
167
+ if (!existsSync(sourceFilePath)) {
168
+ console.warn(
169
+ `[solid-translate] Source locale file not found: ${relative(root, sourceFilePath)}`
170
+ );
171
+ console.warn(
172
+ `[solid-translate] Create it with your source strings, or enable autoExtract`
173
+ );
174
+ return;
175
+ }
176
+ const sourceDict = JSON.parse(
177
+ readFileSync(sourceFilePath, "utf-8")
178
+ );
179
+ let lock = { version: 1, sourceLocale, keys: {} };
180
+ if (existsSync(lockFilePath)) {
181
+ try {
182
+ lock = JSON.parse(readFileSync(lockFilePath, "utf-8"));
183
+ } catch {
184
+ }
185
+ }
186
+ const changedKeys = {};
187
+ for (const [key, value] of Object.entries(sourceDict)) {
188
+ const hash = hashContent(value);
189
+ const existing = lock.keys[key];
190
+ const existingContext = existing?.context;
191
+ const newContext = contexts[key];
192
+ if (!existing || existing.hash !== hash || existingContext !== newContext) {
193
+ changedKeys[key] = value;
194
+ lock.keys[key] = { hash, source: value, context: newContext };
195
+ }
196
+ }
197
+ for (const key of Object.keys(lock.keys)) {
198
+ if (!(key in sourceDict)) {
199
+ delete lock.keys[key];
200
+ }
201
+ }
202
+ if (Object.keys(changedKeys).length === 0) {
203
+ console.log(
204
+ "[solid-translate] No changes detected, skipping translation."
205
+ );
206
+ return;
207
+ }
208
+ const count = Object.keys(changedKeys).length;
209
+ console.log(
210
+ `[solid-translate] Translating ${count} key${count > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
211
+ );
212
+ const changedContexts = {};
213
+ for (const key of Object.keys(changedKeys)) {
214
+ const ctx = lock.keys[key]?.context;
215
+ if (ctx) changedContexts[key] = ctx;
216
+ }
217
+ for (const targetLocale of targetLocales) {
218
+ const targetFilePath = join(
219
+ resolvedLocalesDir,
220
+ `${targetLocale}.json`
221
+ );
222
+ let existing = {};
223
+ if (existsSync(targetFilePath)) {
224
+ try {
225
+ existing = JSON.parse(readFileSync(targetFilePath, "utf-8"));
226
+ } catch {
227
+ }
228
+ }
229
+ const entries = Object.entries(changedKeys);
230
+ for (let i = 0; i < entries.length; i += batchSize) {
231
+ const batch = Object.fromEntries(
232
+ entries.slice(i, i + batchSize)
233
+ );
234
+ try {
235
+ const translated = await translateBatch(
236
+ model,
237
+ batch,
238
+ targetLocale,
239
+ sourceLocale,
240
+ systemPrompt,
241
+ changedContexts
242
+ );
243
+ Object.assign(existing, translated);
244
+ } catch (err) {
245
+ console.error(
246
+ `[solid-translate] Failed to translate batch for ${targetLocale}:`,
247
+ err
248
+ );
249
+ }
250
+ }
251
+ for (const key of Object.keys(existing)) {
252
+ if (!(key in sourceDict)) {
253
+ delete existing[key];
254
+ }
255
+ }
256
+ const sorted = Object.fromEntries(
257
+ Object.entries(existing).sort(([a], [b]) => a.localeCompare(b))
258
+ );
259
+ writeFileSync(
260
+ targetFilePath,
261
+ JSON.stringify(sorted, null, 2) + "\n"
262
+ );
263
+ console.log(
264
+ `[solid-translate] ${targetLocale}: ${Object.keys(sorted).length} keys`
265
+ );
266
+ }
267
+ writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + "\n");
268
+ console.log("[solid-translate] Translation complete.");
269
+ },
270
+ resolveId(id) {
271
+ if (id === VIRTUAL_MODULE_ID) {
272
+ return RESOLVED_VIRTUAL_MODULE_ID;
273
+ }
274
+ },
275
+ load(id) {
276
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) {
277
+ const translations = {};
278
+ if (existsSync(resolvedLocalesDir)) {
279
+ for (const file of readdirSync(resolvedLocalesDir)) {
280
+ if (!file.endsWith(".json")) continue;
281
+ if (file.startsWith(".")) continue;
282
+ const locale = file.replace(".json", "");
283
+ const filePath = join(resolvedLocalesDir, file);
284
+ try {
285
+ translations[locale] = JSON.parse(
286
+ readFileSync(filePath, "utf-8")
287
+ );
288
+ } catch {
289
+ }
290
+ }
291
+ }
292
+ return `export default ${JSON.stringify(translations)};`;
293
+ }
294
+ },
295
+ // HMR: reload translations when locale files change
296
+ handleHotUpdate({ file, server }) {
297
+ if (file.startsWith(resolvedLocalesDir) && file.endsWith(".json")) {
298
+ const mod = server.moduleGraph.getModuleById(
299
+ RESOLVED_VIRTUAL_MODULE_ID
300
+ );
301
+ if (mod) {
302
+ server.moduleGraph.invalidateModule(mod);
303
+ return [mod];
304
+ }
305
+ }
306
+ }
307
+ };
308
+ }
309
+ var vite_default = solidTranslate;
310
+ async function autoExtractStrings(root, patterns) {
311
+ const strings = {};
312
+ const contexts = {};
313
+ const { glob } = await import("glob");
314
+ for (const pattern of patterns) {
315
+ const files = await glob(pattern, { cwd: root, absolute: true });
316
+ for (const file of files) {
317
+ try {
318
+ const code = readFileSync(file, "utf-8");
319
+ const extracted = extractStringsFromSource(
320
+ code,
321
+ relative(root, file)
322
+ );
323
+ for (const entry of extracted) {
324
+ strings[entry.key] = entry.source;
325
+ if (entry.context) {
326
+ contexts[entry.key] = entry.context;
327
+ }
328
+ }
329
+ } catch {
330
+ }
331
+ }
332
+ }
333
+ return { strings, contexts };
334
+ }
335
+ export {
336
+ vite_default as default,
337
+ solidTranslate
338
+ };
339
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts","../src/hash.ts","../src/translate.ts","../src/extract.ts"],"sourcesContent":["import type { Plugin, ResolvedConfig } from \"vite\";\nimport {\n readFileSync,\n writeFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n} from \"node:fs\";\nimport { resolve, join, relative } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\nimport { translateBatch } from \"./translate.js\";\nimport { extractStringsFromSource } from \"./extract.js\";\nimport type { SolidTranslatePluginConfig, LockFile } from \"./types.js\";\n\nexport type { SolidTranslatePluginConfig };\n\nconst VIRTUAL_MODULE_ID = \"virtual:solid-translate\";\nconst RESOLVED_VIRTUAL_MODULE_ID = \"\\0\" + VIRTUAL_MODULE_ID;\n\n/**\n * Vite plugin for solid-translate.\n *\n * Handles:\n * 1. Optional extraction of <T>, msg() strings from source files\n * 2. AI translation of source locale to target locales (with context support)\n * 3. Lock file management for efficient re-translation\n * 4. Virtual module serving translations at runtime\n */\nexport function solidTranslate(config: SolidTranslatePluginConfig): Plugin {\n const {\n sourceLocale = \"en\",\n targetLocales,\n localesDir = \"./src/locales\",\n model,\n systemPrompt,\n batchSize = 50,\n autoExtract = false,\n include = [\"src/**/*.tsx\", \"src/**/*.ts\", \"src/**/*.jsx\"],\n } = config;\n\n let root: string;\n let resolvedLocalesDir: string;\n let lockFilePath: string;\n\n return {\n name: \"solid-translate\",\n\n configResolved(resolvedConfig: ResolvedConfig) {\n root = resolvedConfig.root;\n resolvedLocalesDir = resolve(root, localesDir);\n lockFilePath = join(resolvedLocalesDir, \".solid-translate.lock\");\n },\n\n async buildStart() {\n // Ensure locales directory exists\n if (!existsSync(resolvedLocalesDir)) {\n mkdirSync(resolvedLocalesDir, { recursive: true });\n }\n\n const sourceFilePath = join(\n resolvedLocalesDir,\n `${sourceLocale}.json`,\n );\n\n // Auto-extraction: scan source files for <T> and msg() strings\n let contexts: Record<string, string> = {};\n if (autoExtract) {\n const extracted = await autoExtractStrings(root, include);\n contexts = extracted.contexts;\n\n // Merge into source locale file\n let existingSource: Record<string, string> = {};\n if (existsSync(sourceFilePath)) {\n try {\n existingSource = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n } catch {\n // start fresh\n }\n }\n\n let changed = false;\n for (const [key, value] of Object.entries(extracted.strings)) {\n if (!(key in existingSource)) {\n existingSource[key] = value;\n changed = true;\n }\n }\n\n if (changed) {\n const sorted = Object.fromEntries(\n Object.entries(existingSource).sort(([a], [b]) =>\n a.localeCompare(b),\n ),\n );\n writeFileSync(\n sourceFilePath,\n JSON.stringify(sorted, null, 2) + \"\\n\",\n );\n console.log(\n `[solid-translate] Auto-extracted ${Object.keys(extracted.strings).length} strings from source`,\n );\n }\n }\n\n // Read source locale file\n if (!existsSync(sourceFilePath)) {\n console.warn(\n `[solid-translate] Source locale file not found: ${relative(root, sourceFilePath)}`,\n );\n console.warn(\n `[solid-translate] Create it with your source strings, or enable autoExtract`,\n );\n return;\n }\n\n const sourceDict: Record<string, string> = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n\n // Read or initialize lock file\n let lock: LockFile = { version: 1, sourceLocale, keys: {} };\n if (existsSync(lockFilePath)) {\n try {\n lock = JSON.parse(readFileSync(lockFilePath, \"utf-8\"));\n } catch {\n // Corrupted lock file — start fresh\n }\n }\n\n // Determine which keys have changed or are new\n const changedKeys: Record<string, string> = {};\n for (const [key, value] of Object.entries(sourceDict)) {\n const hash = hashContent(value);\n const existing = lock.keys[key];\n const existingContext = existing?.context;\n const newContext = contexts[key];\n\n // Re-translate if content changed OR context changed\n if (\n !existing ||\n existing.hash !== hash ||\n existingContext !== newContext\n ) {\n changedKeys[key] = value;\n lock.keys[key] = { hash, source: value, context: newContext };\n }\n }\n\n // Remove keys that no longer exist in source\n for (const key of Object.keys(lock.keys)) {\n if (!(key in sourceDict)) {\n delete lock.keys[key];\n }\n }\n\n if (Object.keys(changedKeys).length === 0) {\n console.log(\n \"[solid-translate] No changes detected, skipping translation.\",\n );\n return;\n }\n\n const count = Object.keys(changedKeys).length;\n console.log(\n `[solid-translate] Translating ${count} key${count > 1 ? \"s\" : \"\"} to ${targetLocales.length} locale${targetLocales.length > 1 ? \"s\" : \"\"}...`,\n );\n\n // Build context map for changed keys\n const changedContexts: Record<string, string> = {};\n for (const key of Object.keys(changedKeys)) {\n const ctx = lock.keys[key]?.context;\n if (ctx) changedContexts[key] = ctx;\n }\n\n // Translate for each target locale\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(\n resolvedLocalesDir,\n `${targetLocale}.json`,\n );\n\n // Load existing translations to preserve unchanged keys\n let existing: Record<string, string> = {};\n if (existsSync(targetFilePath)) {\n try {\n existing = JSON.parse(readFileSync(targetFilePath, \"utf-8\"));\n } catch {\n // Corrupted file — regenerate\n }\n }\n\n // Batch translate changed keys\n const entries = Object.entries(changedKeys);\n for (let i = 0; i < entries.length; i += batchSize) {\n const batch = Object.fromEntries(\n entries.slice(i, i + batchSize),\n );\n try {\n const translated = await translateBatch(\n model,\n batch,\n targetLocale,\n sourceLocale,\n systemPrompt,\n changedContexts,\n );\n Object.assign(existing, translated);\n } catch (err) {\n console.error(\n `[solid-translate] Failed to translate batch for ${targetLocale}:`,\n err,\n );\n }\n }\n\n // Remove keys that no longer exist in source\n for (const key of Object.keys(existing)) {\n if (!(key in sourceDict)) {\n delete existing[key];\n }\n }\n\n // Sort keys for stable, diff-friendly output\n const sorted = Object.fromEntries(\n Object.entries(existing).sort(([a], [b]) => a.localeCompare(b)),\n );\n\n writeFileSync(\n targetFilePath,\n JSON.stringify(sorted, null, 2) + \"\\n\",\n );\n console.log(\n `[solid-translate] ${targetLocale}: ${Object.keys(sorted).length} keys`,\n );\n }\n\n // Write updated lock file\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n console.log(\"[solid-translate] Translation complete.\");\n },\n\n resolveId(id: string) {\n if (id === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id: string) {\n if (id === RESOLVED_VIRTUAL_MODULE_ID) {\n // Load all locale JSON files and export as a single object\n const translations: Record<string, Record<string, string>> = {};\n\n if (existsSync(resolvedLocalesDir)) {\n for (const file of readdirSync(resolvedLocalesDir)) {\n if (!file.endsWith(\".json\")) continue;\n if (file.startsWith(\".\")) continue;\n const locale = file.replace(\".json\", \"\");\n const filePath = join(resolvedLocalesDir, file);\n try {\n translations[locale] = JSON.parse(\n readFileSync(filePath, \"utf-8\"),\n );\n } catch {\n // Skip malformed files\n }\n }\n }\n\n return `export default ${JSON.stringify(translations)};`;\n }\n },\n\n // HMR: reload translations when locale files change\n handleHotUpdate({ file, server }) {\n if (\n file.startsWith(resolvedLocalesDir) &&\n file.endsWith(\".json\")\n ) {\n const mod = server.moduleGraph.getModuleById(\n RESOLVED_VIRTUAL_MODULE_ID,\n );\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n return [mod];\n }\n }\n },\n };\n}\n\nexport default solidTranslate;\n\n// ---------------------------------------------------------------------------\n// Auto-extraction helper\n// ---------------------------------------------------------------------------\n\nasync function autoExtractStrings(\n root: string,\n patterns: string[],\n): Promise<{ strings: Record<string, string>; contexts: Record<string, string> }> {\n const strings: Record<string, string> = {};\n const contexts: Record<string, string> = {};\n\n // Dynamically import glob for file matching\n const { glob } = await import(\"glob\");\n\n for (const pattern of patterns) {\n const files = await glob(pattern, { cwd: root, absolute: true });\n for (const file of files) {\n try {\n const code = readFileSync(file, \"utf-8\");\n const extracted = extractStringsFromSource(\n code,\n relative(root, file),\n );\n for (const entry of extracted) {\n strings[entry.key] = entry.source;\n if (entry.context) {\n contexts[entry.key] = entry.context;\n }\n }\n } catch {\n // Skip unreadable files\n }\n }\n }\n\n return { strings, contexts };\n}\n","import { createHash } from \"node:crypto\";\n\n/** Create a short content hash for change detection */\nexport function hashContent(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 16);\n}\n","import { generateObject } from \"ai\";\nimport { z } from \"zod\";\nimport type { LanguageModelV1 } from \"ai\";\n\n/**\n * Translate a batch of key-value pairs from one locale to another using AI.\n * Supports optional per-key context hints for disambiguation.\n */\nexport async function translateBatch(\n model: LanguageModelV1,\n entries: Record<string, string>,\n targetLocale: string,\n sourceLocale: string,\n systemPrompt?: string,\n contexts?: Record<string, string>,\n): Promise<Record<string, string>> {\n const keys = Object.keys(entries);\n if (keys.length === 0) return {};\n\n const defaultSystem = [\n `You are a professional translator specializing in software localization.`,\n `Translate text from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Rules:`,\n `- Preserve the original tone and meaning`,\n `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,\n `- Keep HTML tags unchanged`,\n `- Do not add or remove content`,\n `- Return natural, idiomatic translations`,\n ].join(\"\\n\");\n\n // Build context section if any keys have context hints\n let contextSection = \"\";\n if (contexts && Object.keys(contexts).length > 0) {\n const contextLines = Object.entries(contexts)\n .filter(([key]) => key in entries)\n .map(([key, ctx]) => ` \"${key}\": ${ctx}`);\n if (contextLines.length > 0) {\n contextSection = [\n ``,\n `Context hints for disambiguation:`,\n ...contextLines,\n ``,\n ].join(\"\\n\");\n }\n }\n\n const { object } = await generateObject({\n model,\n schema: z.object({\n translations: z.record(z.string(), z.string()),\n }),\n system: systemPrompt || defaultSystem,\n prompt: [\n `Translate each value in this JSON object from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Return a JSON object with the exact same keys and the translated values.`,\n contextSection,\n JSON.stringify(entries, null, 2),\n ].join(\"\\n\"),\n });\n\n return object.translations;\n}\n\n/**\n * Translate a markdown or MDX string from one locale to another.\n * Preserves code blocks, frontmatter, and MDX components.\n */\nexport async function translateMarkdown(\n model: LanguageModelV1,\n content: string,\n targetLocale: string,\n sourceLocale: string,\n systemPrompt?: string,\n): Promise<string> {\n const defaultSystem = [\n `You are a professional translator specializing in documentation.`,\n `Translate Markdown/MDX content from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Rules:`,\n `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,\n `- Preserve code blocks and inline code unchanged`,\n `- Preserve frontmatter YAML keys (only translate values)`,\n `- Preserve MDX component syntax and JSX expressions`,\n `- Preserve URLs and file paths unchanged`,\n `- Return natural, idiomatic translations`,\n ].join(\"\\n\");\n\n const { object } = await generateObject({\n model,\n schema: z.object({\n translated: z.string(),\n }),\n system: systemPrompt || defaultSystem,\n prompt: [\n `Translate this Markdown/MDX content from \"${sourceLocale}\" to \"${targetLocale}\".`,\n `Return the complete translated document.`,\n ``,\n content,\n ].join(\"\\n\"),\n });\n\n return object.translated;\n}\n","/** Extracted translatable string from source code */\nexport interface ExtractedString {\n key: string;\n source: string;\n file: string;\n line: number;\n /** AI context hint from the `context` prop */\n context?: string;\n}\n\n/**\n * Extract translatable strings from source code by finding:\n * - `<T>text</T>` — source text is used as the key\n * - `<T id=\"key\">fallback</T>` — explicit key\n * - `<T context=\"hint\">text</T>` — with AI context\n * - `<T id=\"key\" context=\"hint\">text</T>` — both\n * - `<T>text <Var>...</Var> more</T>` — builds template with {0} placeholders\n * - `msg(\"text\")` — shared string marker\n */\nexport function extractStringsFromSource(\n code: string,\n filePath: string,\n): ExtractedString[] {\n const results: ExtractedString[] = [];\n const seen = new Set<string>();\n\n // Match <T ...props>children</T>\n const tComponentRegex = /<T(\\s[^>]*)?>([^]*?)<\\/T>/g;\n let match: RegExpExecArray | null;\n\n while ((match = tComponentRegex.exec(code)) !== null) {\n const attrs = match[1] || \"\";\n const rawChildren = match[2]!;\n const line = code.substring(0, match.index).split(\"\\n\").length;\n\n // Parse id attribute\n const idMatch = attrs.match(/id=[\"']([^\"']+)[\"']/);\n // Parse context attribute\n const contextMatch = attrs.match(/context=[\"']([^\"']+)[\"']/);\n\n // Build source text: replace <Var>, <Num>, <Currency>, <DateTime> with {n} placeholders\n // Single-pass replacement to preserve document order\n let slotIndex = 0;\n const source = rawChildren\n .replace(\n /<(?:Var|Num|Currency|DateTime)(?:\\s[^>]*)?>([^]*?)<\\/(?:Var|Num|Currency|DateTime)>/g,\n () => `{${slotIndex++}}`,\n )\n .trim();\n\n const key = idMatch ? idMatch[1]! : source;\n if (!key || seen.has(key)) continue;\n seen.add(key);\n\n results.push({\n key,\n source,\n file: filePath,\n line,\n context: contextMatch ? contextMatch[1] : undefined,\n });\n }\n\n // Match msg(\"text\") and msg('text') calls\n const msgRegex = /\\bmsg\\(\\s*[\"']([^\"']+)[\"']\\s*(?:,\\s*\\{[^}]*\\})?\\s*\\)/g;\n while ((match = msgRegex.exec(code)) !== null) {\n const source = match[1]!;\n if (seen.has(source)) continue;\n seen.add(source);\n const line = code.substring(0, match.index).split(\"\\n\").length;\n results.push({ key: source, source, file: filePath, line });\n }\n\n return results;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "solid-translate",
3
+ "version": "0.2.0",
4
+ "description": "AI-powered build-time translations for SolidJS. Full i18n with <T>, <Var>, <Num>, <Currency>, <Plural>, <DateTime>, locale detection, and a CLI — all BYOK.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "solid-translate": "./dist/cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./vite": {
18
+ "types": "./dist/vite.d.ts",
19
+ "import": "./dist/vite.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "test": "bun test",
31
+ "prepublishOnly": "bun run build"
32
+ },
33
+ "keywords": [
34
+ "solid",
35
+ "solidjs",
36
+ "i18n",
37
+ "translate",
38
+ "translation",
39
+ "internationalization",
40
+ "localization",
41
+ "ai",
42
+ "vite",
43
+ "vite-plugin",
44
+ "build-time",
45
+ "openrouter",
46
+ "vercel-ai-sdk",
47
+ "general-translation",
48
+ "gt-react",
49
+ "pluralization",
50
+ "intl",
51
+ "locale",
52
+ "currency",
53
+ "datetime"
54
+ ],
55
+ "license": "MIT",
56
+ "author": "omniaura",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/omniaura/solid-translate.git"
60
+ },
61
+ "homepage": "https://github.com/omniaura/solid-translate#readme",
62
+ "bugs": {
63
+ "url": "https://github.com/omniaura/solid-translate/issues"
64
+ },
65
+ "peerDependencies": {
66
+ "solid-js": ">=1.7.0",
67
+ "vite": ">=4.0.0",
68
+ "ai": ">=3.0.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "vite": {
72
+ "optional": true
73
+ },
74
+ "ai": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "dependencies": {
79
+ "glob": "^11.0.0",
80
+ "zod": "^3.23.0"
81
+ },
82
+ "devDependencies": {
83
+ "@happy-dom/global-registrator": "^16.0.0",
84
+ "@types/bun": "latest",
85
+ "ai": "^4.0.0",
86
+ "semantic-release": "25",
87
+ "solid-js": "^1.8.0",
88
+ "tsup": "^8.0.0",
89
+ "typescript": "^5.4.0",
90
+ "vite": "^5.0.0"
91
+ }
92
+ }