solid-translate 1.1.1 → 1.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/README.md CHANGED
@@ -17,7 +17,10 @@ Write your app in one language. Wrap text in `<T>`. Get translations generated a
17
17
  - **Auto Locale Detection** — detects from `navigator.languages` when `locale` prop is omitted
18
18
  - **`msg()`** — mark strings for extraction outside of JSX
19
19
  - **CLI Tool** — translate JSON, Markdown, and MDX files from the command line
20
+ - **`check` Command** — CI freshness gate, no AI calls (exit 1 when translations are stale)
20
21
  - **Vite Plugin** — build-time translation with smart change detection
22
+ - **Lazy Locale Chunks** — `virtual:solid-translate/lazy` code-splits each locale, loads on demand without suspending
23
+ - **Locale Persistence** — opt-in `persistLocale` remembers the user's choice in `localStorage`
21
24
  - **GitHub Action** — `omniaura/solid-translate@v1` for CI/CD translation automation
22
25
  - **BYOK** — use any [Vercel AI SDK](https://ai-sdk.dev/) provider (OpenRouter, OpenAI, Anthropic, Google, etc.)
23
26
 
@@ -251,8 +254,10 @@ Root provider. Wraps your app.
251
254
 
252
255
  ```tsx
253
256
  <TranslationProvider
254
- translations={translations} // Translation dictionaries
257
+ translations={translations} // Translation dictionaries or lazy manifest
255
258
  sourceLocale="en" // Source locale (default: "en")
259
+ persistLocale // Optional: persist locale to localStorage
260
+ // persistLocale={{ key: "my-app:locale" }} // ...with a custom storage key
256
261
  // locale="es" // Optional: explicit locale
257
262
  // // If omitted, auto-detects from navigator.languages
258
263
  >
@@ -260,6 +265,56 @@ Root provider. Wraps your app.
260
265
  </TranslationProvider>
261
266
  ```
262
267
 
268
+ `translations` accepts either the eager record from `virtual:solid-translate`
269
+ or the lazy manifest from `virtual:solid-translate/lazy` (see
270
+ [Lazy per-locale loading](#lazy-per-locale-loading)).
271
+
272
+ With `persistLocale` enabled, the initial locale is read from `localStorage`
273
+ (when it's still a valid locale) before falling back to browser detection,
274
+ and `setLocale` writes the choice back. Storage access is guarded, so SSR
275
+ and storage-disabled environments degrade gracefully. An explicit `locale`
276
+ prop always wins.
277
+
278
+ ### Lazy per-locale loading
279
+
280
+ By default `virtual:solid-translate` inlines every locale dictionary into
281
+ your main bundle. For large apps (thousands of strings × many locales) use
282
+ `virtual:solid-translate/lazy` instead — each locale becomes its own chunk,
283
+ fetched on demand via dynamic import:
284
+
285
+ ```tsx
286
+ import { TranslationProvider } from "solid-translate";
287
+ import translations from "virtual:solid-translate/lazy";
288
+
289
+ <TranslationProvider translations={translations} persistLocale>
290
+ <App />
291
+ </TranslationProvider>;
292
+ ```
293
+
294
+ The lazy manifest has this shape:
295
+
296
+ ```ts
297
+ {
298
+ sourceLocale: string;
299
+ locales: string[]; // source + target locales
300
+ loaders: Record<string, () => Promise<Record<string, string>>>;
301
+ }
302
+ ```
303
+
304
+ Loading is fully non-blocking: it never throws and never triggers
305
+ `<Suspense>`. While a locale's dictionary is in flight (or if its chunk
306
+ fails to load), `t()` and `<T>` render the source-language text; the UI
307
+ swaps to the translated text reactively once the loader resolves. Loaded
308
+ dictionaries are cached for the session, and `availableLocales()` derives
309
+ from `locales` in the manifest.
310
+
311
+ You can also import a single locale's dictionary directly:
312
+
313
+ ```ts
314
+ const es = await import("virtual:solid-translate/locale/es");
315
+ es.default; // Record<string, string>
316
+ ```
317
+
263
318
  ### `msg()` — Shared Strings
264
319
 
265
320
  Mark strings for extraction outside of JSX. At build time, the Vite plugin extracts them. At runtime, use `t()` to translate.
@@ -311,8 +366,19 @@ npx solid-translate extract
311
366
 
312
367
  # Translate everything
313
368
  npx solid-translate translate
369
+
370
+ # Verify translations are fresh (CI primitive — no AI calls, no writes)
371
+ npx solid-translate check
372
+ npx solid-translate check --json # machine-readable report
314
373
  ```
315
374
 
375
+ `check` re-extracts source strings and compares keys, content hashes, and
376
+ `context` hints against `.solid-translate.lock`, then verifies every target
377
+ locale file contains every key. Exit code 0 means everything is fresh; exit
378
+ code 1 means stale, with a report of missing/changed/orphaned keys per
379
+ locale. It needs no API key and never modifies files, so it's safe to run
380
+ on every pull request.
381
+
316
382
  ### CLI Config (`solid-translate.config.json`)
317
383
 
318
384
  ```json
@@ -409,10 +475,11 @@ jobs:
409
475
 
410
476
  | Input | Default | Description |
411
477
  |-------|---------|-------------|
412
- | `command` | `both` | `extract`, `translate`, or `both` |
478
+ | `command` | `both` | `extract`, `translate`, `check`, or `both` |
413
479
  | `working-directory` | `.` | Working directory |
414
480
  | `commit` | `false` | Auto-commit updated translation files |
415
481
  | `commit-message` | `chore: update translations` | Commit message |
482
+ | `fail-on-stale` | `true` | With `command: check` — fail the step when translations are stale |
416
483
  | `node-version` | `22` | Node.js version |
417
484
  | `package-manager` | `npm` | `npm`, `bun`, `pnpm`, or `yarn` |
418
485
 
@@ -422,11 +489,87 @@ jobs:
422
489
  |--------|-------------|
423
490
  | `changed` | `true` if translation files were updated |
424
491
  | `files` | Newline-separated list of changed translation files |
492
+ | `stale` | With `command: check` — `true` if translations are out of date |
425
493
 
426
494
  The action reports and commits changes for JSON, Markdown, MDX, and `.solid-translate.lock` files only. Package manifests and package manager lockfiles are intentionally excluded.
427
495
 
428
496
  #### Examples
429
497
 
498
+ **PR gate — fail CI when translations are stale (no API key needed):**
499
+
500
+ ```yaml
501
+ # .github/workflows/i18n-check.yml
502
+ name: i18n check
503
+
504
+ on:
505
+ pull_request:
506
+
507
+ jobs:
508
+ check:
509
+ runs-on: ubuntu-latest
510
+ steps:
511
+ - uses: actions/checkout@v4
512
+
513
+ - uses: omniaura/solid-translate@v1
514
+ with:
515
+ command: check
516
+ package-manager: bun
517
+ ```
518
+
519
+ `check` runs extraction only — no AI provider, no writes — so it's a fast,
520
+ deterministic gate. Set `fail-on-stale: "false"` to keep the step green and
521
+ branch on the `stale` output instead:
522
+
523
+ ```yaml
524
+ - uses: omniaura/solid-translate@v1
525
+ id: i18n
526
+ with:
527
+ command: check
528
+ fail-on-stale: "false"
529
+
530
+ - name: Comment on stale translations
531
+ if: steps.i18n.outputs.stale == 'true'
532
+ run: echo "Translations are stale — run 'solid-translate translate'"
533
+ ```
534
+
535
+ **Translate on main and open a PR with the updates:**
536
+
537
+ ```yaml
538
+ # .github/workflows/translate.yml
539
+ name: Translate
540
+
541
+ on:
542
+ push:
543
+ branches: [main]
544
+
545
+ jobs:
546
+ translate:
547
+ runs-on: ubuntu-latest
548
+ permissions:
549
+ contents: write
550
+ pull-requests: write
551
+ steps:
552
+ - uses: actions/checkout@v4
553
+
554
+ - uses: omniaura/solid-translate@v1
555
+ id: translate
556
+ env:
557
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
558
+
559
+ - name: Open PR with translation updates
560
+ if: steps.translate.outputs.changed == 'true'
561
+ uses: peter-evans/create-pull-request@v7
562
+ with:
563
+ branch: chore/update-translations
564
+ title: "chore: update translations"
565
+ commit-message: "chore: update translations"
566
+ body: |
567
+ Automated translation update.
568
+
569
+ Changed files:
570
+ ${{ steps.translate.outputs.files }}
571
+ ```
572
+
430
573
  **Translate on push and commit back:**
431
574
 
432
575
  Use auto-commit only on trusted refs where `GITHUB_TOKEN` can push to the checked-out branch, such as `push` events on your own repository. For pull requests, leave `commit` disabled and use the `changed`/`files` outputs to decide whether to fail CI or open a separate update PR.
@@ -518,15 +661,27 @@ This means you can safely check in all translation files. Rebuilds are free unle
518
661
 
519
662
  ## TypeScript
520
663
 
521
- For the virtual module import, add to your `env.d.ts` or `vite-env.d.ts`:
664
+ Types for all the virtual modules (`virtual:solid-translate`,
665
+ `virtual:solid-translate/lazy`, and `virtual:solid-translate/locale/*`)
666
+ ship with the package. Reference them once in your `env.d.ts` or
667
+ `vite-env.d.ts`:
522
668
 
523
669
  ```ts
524
- declare module "virtual:solid-translate" {
525
- const translations: Record<string, Record<string, string>>;
526
- export default translations;
670
+ /// <reference types="solid-translate/virtual" />
671
+ ```
672
+
673
+ or add them to `tsconfig.json`:
674
+
675
+ ```json
676
+ {
677
+ "compilerOptions": {
678
+ "types": ["solid-translate/virtual"]
679
+ }
527
680
  }
528
681
  ```
529
682
 
683
+ No hand-written `declare module` blocks needed.
684
+
530
685
  ## Comparison with General Translation (gt-react)
531
686
 
532
687
  | Feature | gt-react | solid-translate |
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/translate.ts
4
+ import { z } from "zod";
5
+ async function loadGenerateObject() {
6
+ const { generateObject } = await import("ai");
7
+ return generateObject;
8
+ }
9
+ async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
10
+ const keys = Object.keys(entries);
11
+ if (keys.length === 0) return {};
12
+ const defaultSystem = [
13
+ `You are a professional translator specializing in software localization.`,
14
+ `Translate text from "${sourceLocale}" to "${targetLocale}".`,
15
+ `Rules:`,
16
+ `- Preserve the original tone and meaning`,
17
+ `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,
18
+ `- Keep HTML tags unchanged`,
19
+ `- Do not add or remove content`,
20
+ `- Return natural, idiomatic translations`
21
+ ].join("\n");
22
+ let contextSection = "";
23
+ if (contexts && Object.keys(contexts).length > 0) {
24
+ const contextLines = Object.entries(contexts).filter(([key]) => key in entries).map(([key, ctx]) => ` "${key}": ${ctx}`);
25
+ if (contextLines.length > 0) {
26
+ contextSection = [
27
+ ``,
28
+ `Context hints for disambiguation:`,
29
+ ...contextLines,
30
+ ``
31
+ ].join("\n");
32
+ }
33
+ }
34
+ const generateObject = await loadGenerateObject();
35
+ const { object } = await generateObject({
36
+ model,
37
+ schema: z.object({
38
+ translations: z.record(z.string(), z.string())
39
+ }),
40
+ system: systemPrompt || defaultSystem,
41
+ prompt: [
42
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
43
+ `Return a JSON object with the exact same keys and the translated values.`,
44
+ contextSection,
45
+ JSON.stringify(entries, null, 2)
46
+ ].join("\n")
47
+ });
48
+ return object.translations;
49
+ }
50
+ async function translateMarkdown(model, content, targetLocale, sourceLocale, systemPrompt) {
51
+ const defaultSystem = [
52
+ `You are a professional translator specializing in documentation.`,
53
+ `Translate Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
54
+ `Rules:`,
55
+ `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,
56
+ `- Preserve code blocks and inline code unchanged`,
57
+ `- Preserve frontmatter YAML keys (only translate values)`,
58
+ `- Preserve MDX component syntax and JSX expressions`,
59
+ `- Preserve URLs and file paths unchanged`,
60
+ `- Return natural, idiomatic translations`
61
+ ].join("\n");
62
+ const generateObject = await loadGenerateObject();
63
+ const { object } = await generateObject({
64
+ model,
65
+ schema: z.object({
66
+ translated: z.string()
67
+ }),
68
+ system: systemPrompt || defaultSystem,
69
+ prompt: [
70
+ `Translate this Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
71
+ `Return the complete translated document.`,
72
+ ``,
73
+ content
74
+ ].join("\n")
75
+ });
76
+ return object.translated;
77
+ }
78
+
79
+ export {
80
+ translateBatch,
81
+ translateMarkdown
82
+ };
package/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ translateBatch
4
+ } from "./chunk-2BKJUY37.js";
2
5
  import "./chunk-FYS2JH42.js";
3
6
 
4
7
  // src/cli.ts
@@ -10,80 +13,10 @@ import {
10
13
  } from "fs";
11
14
  import { resolve, join as join2, dirname, relative, basename } from "path";
12
15
 
13
- // src/translate.ts
14
- import { z } from "zod";
15
- async function loadGenerateObject() {
16
- const { generateObject } = await import("ai");
17
- return generateObject;
18
- }
19
- async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
20
- const keys = Object.keys(entries);
21
- if (keys.length === 0) return {};
22
- const defaultSystem = [
23
- `You are a professional translator specializing in software localization.`,
24
- `Translate text from "${sourceLocale}" to "${targetLocale}".`,
25
- `Rules:`,
26
- `- Preserve the original tone and meaning`,
27
- `- Keep placeholders like {{variable}}, {variable}, {0}, {1} unchanged`,
28
- `- Keep HTML tags unchanged`,
29
- `- Do not add or remove content`,
30
- `- Return natural, idiomatic translations`
31
- ].join("\n");
32
- let contextSection = "";
33
- if (contexts && Object.keys(contexts).length > 0) {
34
- const contextLines = Object.entries(contexts).filter(([key]) => key in entries).map(([key, ctx]) => ` "${key}": ${ctx}`);
35
- if (contextLines.length > 0) {
36
- contextSection = [
37
- ``,
38
- `Context hints for disambiguation:`,
39
- ...contextLines,
40
- ``
41
- ].join("\n");
42
- }
43
- }
44
- const generateObject = await loadGenerateObject();
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
- async function translateMarkdown(model, content, targetLocale, sourceLocale, systemPrompt) {
61
- const defaultSystem = [
62
- `You are a professional translator specializing in documentation.`,
63
- `Translate Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
64
- `Rules:`,
65
- `- Preserve all Markdown formatting (headers, lists, bold, italic, links, etc.)`,
66
- `- Preserve code blocks and inline code unchanged`,
67
- `- Preserve frontmatter YAML keys (only translate values)`,
68
- `- Preserve MDX component syntax and JSX expressions`,
69
- `- Preserve URLs and file paths unchanged`,
70
- `- Return natural, idiomatic translations`
71
- ].join("\n");
72
- const generateObject = await loadGenerateObject();
73
- const { object } = await generateObject({
74
- model,
75
- schema: z.object({
76
- translated: z.string()
77
- }),
78
- system: systemPrompt || defaultSystem,
79
- prompt: [
80
- `Translate this Markdown/MDX content from "${sourceLocale}" to "${targetLocale}".`,
81
- `Return the complete translated document.`,
82
- ``,
83
- content
84
- ].join("\n")
85
- });
86
- return object.translated;
16
+ // src/hash.ts
17
+ import { createHash } from "crypto";
18
+ function hashContent(content) {
19
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
87
20
  }
88
21
 
89
22
  // src/extract.ts
@@ -128,14 +61,6 @@ function extractStringsFromSource(code, filePath) {
128
61
  // src/lock.ts
129
62
  import { readFileSync, writeFileSync, existsSync } from "fs";
130
63
  import { join } from "path";
131
-
132
- // src/hash.ts
133
- import { createHash } from "crypto";
134
- function hashContent(content) {
135
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
136
- }
137
-
138
- // src/lock.ts
139
64
  function diffLock(sourceDict, lock, contexts) {
140
65
  const changedKeys = {};
141
66
  const pendingEntries = {};
@@ -313,6 +238,10 @@ async function main() {
313
238
  await runTranslate();
314
239
  return;
315
240
  }
241
+ if (command === "check") {
242
+ await runCheck(args.includes("--json"));
243
+ return;
244
+ }
316
245
  console.error(`Unknown command: ${command}`);
317
246
  printUsage();
318
247
  process.exit(1);
@@ -325,6 +254,8 @@ Usage:
325
254
  solid-translate init Create a config file
326
255
  solid-translate extract Extract strings from source files
327
256
  solid-translate translate Translate source strings + files to target locales
257
+ solid-translate check Verify translations are up to date (no AI calls)
258
+ Exit 0 = fresh, 1 = stale. Use --json for machine output
328
259
 
329
260
  Config: solid-translate.config.json (or .js/.ts)
330
261
 
@@ -474,7 +405,152 @@ async function runExtract() {
474
405
  `Extracted ${total} strings (${newKeys} new) \u2192 ${relative(root, sourceFilePath)}`
475
406
  );
476
407
  }
408
+ async function runCheck(jsonOutput) {
409
+ const config = await loadConfig();
410
+ const root = process.cwd();
411
+ const sourceLocale = config.sourceLocale || "en";
412
+ const targetLocales = config.targetLocales || [];
413
+ const localesDir = resolve(config.localesDir || "./src/locales");
414
+ const patterns = config.include || [
415
+ "src/**/*.tsx",
416
+ "src/**/*.ts",
417
+ "src/**/*.jsx"
418
+ ];
419
+ const { glob } = await import("glob");
420
+ const extracted = {};
421
+ const contexts = {};
422
+ for (const pattern of patterns) {
423
+ const files = await glob(pattern, { cwd: root, absolute: true });
424
+ for (const file of files) {
425
+ try {
426
+ const code = readFileSync2(file, "utf-8");
427
+ const entries = extractStringsFromSource(
428
+ code,
429
+ relative(root, file)
430
+ );
431
+ for (const entry of entries) {
432
+ extracted[entry.key] = entry.source;
433
+ if (entry.context) {
434
+ contexts[entry.key] = entry.context;
435
+ }
436
+ }
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ const sourceFilePath = join2(localesDir, `${sourceLocale}.json`);
442
+ const sourceDict = {};
443
+ if (existsSync2(sourceFilePath)) {
444
+ try {
445
+ Object.assign(
446
+ sourceDict,
447
+ JSON.parse(readFileSync2(sourceFilePath, "utf-8"))
448
+ );
449
+ } catch {
450
+ }
451
+ }
452
+ for (const [key, value] of Object.entries(extracted)) {
453
+ if (!(key in sourceDict)) {
454
+ sourceDict[key] = value;
455
+ }
456
+ }
457
+ const lockFilePath = join2(localesDir, ".solid-translate.lock");
458
+ let lock = { version: 1, sourceLocale, keys: {} };
459
+ if (existsSync2(lockFilePath)) {
460
+ try {
461
+ lock = JSON.parse(readFileSync2(lockFilePath, "utf-8"));
462
+ } catch {
463
+ }
464
+ }
465
+ const report = {
466
+ fresh: true,
467
+ lock: { missing: [], changed: [], orphaned: [] },
468
+ locales: {}
469
+ };
470
+ for (const [key, value] of Object.entries(sourceDict)) {
471
+ const entry = lock.keys[key];
472
+ if (!entry) {
473
+ report.lock.missing.push(key);
474
+ } else if (entry.hash !== hashContent(value) || (entry.context ?? void 0) !== (contexts[key] ?? void 0)) {
475
+ report.lock.changed.push(key);
476
+ }
477
+ }
478
+ for (const key of Object.keys(lock.keys)) {
479
+ if (!(key in sourceDict)) {
480
+ report.lock.orphaned.push(key);
481
+ }
482
+ }
483
+ const sourceKeys = Object.keys(sourceDict);
484
+ for (const targetLocale of targetLocales) {
485
+ const targetFilePath = join2(localesDir, `${targetLocale}.json`);
486
+ let dict = {};
487
+ let fileExists = existsSync2(targetFilePath);
488
+ if (fileExists) {
489
+ try {
490
+ dict = JSON.parse(readFileSync2(targetFilePath, "utf-8"));
491
+ } catch {
492
+ fileExists = false;
493
+ }
494
+ }
495
+ const localeReport = {
496
+ missing: sourceKeys.filter((key) => !(key in dict)),
497
+ orphaned: Object.keys(dict).filter((key) => !(key in sourceDict)),
498
+ fileExists
499
+ };
500
+ report.locales[targetLocale] = localeReport;
501
+ }
502
+ report.fresh = report.lock.missing.length === 0 && report.lock.changed.length === 0 && report.lock.orphaned.length === 0 && Object.values(report.locales).every(
503
+ (l) => l.missing.length === 0 && l.orphaned.length === 0
504
+ );
505
+ if (jsonOutput) {
506
+ console.log(JSON.stringify(report, null, 2));
507
+ } else {
508
+ printCheckReport(report, sourceLocale);
509
+ }
510
+ process.exit(report.fresh ? 0 : 1);
511
+ }
512
+ function printCheckReport(report, sourceLocale) {
513
+ if (report.fresh) {
514
+ console.log("Translations are up to date.");
515
+ return;
516
+ }
517
+ console.log("Translations are stale:\n");
518
+ const { missing, changed, orphaned } = report.lock;
519
+ if (missing.length || changed.length || orphaned.length) {
520
+ console.log(".solid-translate.lock:");
521
+ for (const key of missing) {
522
+ console.log(` missing: ${JSON.stringify(key)} (never translated)`);
523
+ }
524
+ for (const key of changed) {
525
+ console.log(` changed: ${JSON.stringify(key)} (text or context changed)`);
526
+ }
527
+ for (const key of orphaned) {
528
+ console.log(` orphaned: ${JSON.stringify(key)} (no longer in source)`);
529
+ }
530
+ }
531
+ for (const [locale, localeReport] of Object.entries(report.locales)) {
532
+ if (!localeReport.missing.length && !localeReport.orphaned.length) {
533
+ continue;
534
+ }
535
+ if (!localeReport.fileExists) {
536
+ console.log(`${locale}.json: (file missing)`);
537
+ } else {
538
+ console.log(`${locale}.json:`);
539
+ }
540
+ for (const key of localeReport.missing) {
541
+ console.log(` missing: ${JSON.stringify(key)}`);
542
+ }
543
+ for (const key of localeReport.orphaned) {
544
+ console.log(` orphaned: ${JSON.stringify(key)}`);
545
+ }
546
+ }
547
+ console.log(
548
+ `
549
+ Run \`solid-translate translate\` to refresh ${sourceLocale} \u2192 targets.`
550
+ );
551
+ }
477
552
  async function runTranslate() {
553
+ const { translateBatch: translateBatch2, translateMarkdown: translateMarkdown2 } = await import("./translate-M737VQHG.js");
478
554
  const config = await loadConfig();
479
555
  const root = process.cwd();
480
556
  const sourceLocale = config.sourceLocale || "en";
@@ -527,7 +603,7 @@ async function runTranslate() {
527
603
  if (format === "json") {
528
604
  try {
529
605
  const sourceDict = JSON.parse(content);
530
- const translated = await translateBatch(
606
+ const translated = await translateBatch2(
531
607
  model,
532
608
  sourceDict,
533
609
  targetLocale,
@@ -550,7 +626,7 @@ async function runTranslate() {
550
626
  }
551
627
  } else {
552
628
  try {
553
- const translated = await translateMarkdown(
629
+ const translated = await translateMarkdown2(
554
630
  model,
555
631
  content,
556
632
  targetLocale,
package/dist/index.d.ts CHANGED
@@ -31,6 +31,21 @@ interface SolidTranslatePluginConfig {
31
31
  type TranslationDictionary = Record<string, string>;
32
32
  /** All translations keyed by locale code */
33
33
  type Translations = Record<string, TranslationDictionary>;
34
+ /**
35
+ * Lazy translation manifest, as exported by `virtual:solid-translate/lazy`.
36
+ * Each loader dynamically imports one locale's dictionary so it becomes its
37
+ * own chunk instead of being inlined into the main bundle.
38
+ */
39
+ interface LazyTranslations {
40
+ /** Source locale code */
41
+ sourceLocale: string;
42
+ /** All available locale codes (source + targets) */
43
+ locales: string[];
44
+ /** Per-locale dictionary loaders (dynamic imports) */
45
+ loaders: Record<string, () => Promise<TranslationDictionary>>;
46
+ }
47
+ /** Either an eager translations record or a lazy manifest */
48
+ type TranslationsInput = Translations | LazyTranslations;
34
49
 
35
50
  interface TranslationContextValue {
36
51
  /** Current locale as a reactive signal */
@@ -43,8 +58,8 @@ interface TranslationContextValue {
43
58
  sourceLocale: string;
44
59
  /** All available locale codes (reactive) */
45
60
  availableLocales: () => string[];
46
- /** Raw translations object */
47
- translations: Translations;
61
+ /** Raw translations object (eager record or lazy manifest) */
62
+ translations: TranslationsInput;
48
63
  }
49
64
 
50
65
  interface VarProps {
@@ -193,8 +208,21 @@ interface TranslationProviderProps {
193
208
  locale?: string;
194
209
  /** Source locale code (default: "en") */
195
210
  sourceLocale?: string;
196
- /** Translation dictionaries keyed by locale */
197
- translations: Translations;
211
+ /**
212
+ * Translation dictionaries keyed by locale (from `virtual:solid-translate`),
213
+ * or a lazy manifest (from `virtual:solid-translate/lazy`) whose per-locale
214
+ * dictionaries are loaded on demand via dynamic import.
215
+ */
216
+ translations: TranslationsInput;
217
+ /**
218
+ * Persist the active locale to `localStorage` (default: false).
219
+ * When enabled, the initial locale is read from storage (if still valid)
220
+ * before falling back to browser detection, and `setLocale` writes through.
221
+ * Pass `{ key: "..." }` to customize the storage key.
222
+ */
223
+ persistLocale?: boolean | {
224
+ key?: string;
225
+ };
198
226
  children: JSX.Element;
199
227
  }
200
228
  declare function TranslationProvider(props: TranslationProviderProps): JSX.Element;
@@ -236,4 +264,4 @@ interface TProps {
236
264
  */
237
265
  declare function T(props: TProps): JSX.Element;
238
266
 
239
- export { Currency, type CurrencyProps, DateTime, type DateTimeProps, LocaleSelector, type LocaleSelectorProps, Num, type NumProps, Plural, type PluralProps, type SolidTranslatePluginConfig, T, type TProps, type TranslationContextValue, type TranslationDictionary, TranslationProvider, type TranslationProviderProps, type Translations, Var, type VarProps, detectLocale, msg, useLocale, useTranslation };
267
+ export { Currency, type CurrencyProps, DateTime, type DateTimeProps, type LazyTranslations, LocaleSelector, type LocaleSelectorProps, Num, type NumProps, Plural, type PluralProps, type SolidTranslatePluginConfig, T, type TProps, type TranslationContextValue, type TranslationDictionary, TranslationProvider, type TranslationProviderProps, type Translations, type TranslationsInput, Var, type VarProps, detectLocale, msg, useLocale, useTranslation };
package/dist/index.js CHANGED
@@ -121,15 +121,65 @@ function msg(text, _params) {
121
121
  }
122
122
 
123
123
  // src/index.ts
124
+ var DEFAULT_PERSIST_KEY = "solid-translate:locale";
125
+ function isLazyTranslations(input) {
126
+ return typeof input === "object" && input !== null && Array.isArray(input.locales) && typeof input.loaders === "object" && input.loaders !== null;
127
+ }
128
+ function readPersistedLocale(key) {
129
+ try {
130
+ if (typeof localStorage === "undefined") return void 0;
131
+ return localStorage.getItem(key) ?? void 0;
132
+ } catch {
133
+ return void 0;
134
+ }
135
+ }
136
+ function writePersistedLocale(key, locale) {
137
+ try {
138
+ if (typeof localStorage === "undefined") return;
139
+ localStorage.setItem(key, locale);
140
+ } catch {
141
+ }
142
+ }
124
143
  function TranslationProvider(props) {
125
- const sourceLocale = props.sourceLocale || "en";
126
- const availableLocales = createMemo2(() => Object.keys(props.translations));
127
- const initialLocale = props.locale || detectLocale(availableLocales()) || sourceLocale;
128
- const [locale, setLocale] = createSignal(initialLocale);
144
+ const lazy = isLazyTranslations(props.translations) ? props.translations : void 0;
145
+ const sourceLocale = props.sourceLocale || lazy?.sourceLocale || "en";
146
+ const availableLocales = createMemo2(
147
+ () => lazy ? lazy.locales : Object.keys(props.translations)
148
+ );
149
+ const persistKey = props.persistLocale ? (typeof props.persistLocale === "object" ? props.persistLocale.key : void 0) || DEFAULT_PERSIST_KEY : void 0;
150
+ const persisted = persistKey ? readPersistedLocale(persistKey) : void 0;
151
+ const persistedValid = persisted !== void 0 && (persisted === sourceLocale || availableLocales().includes(persisted));
152
+ const initialLocale = props.locale || (persistedValid ? persisted : void 0) || detectLocale(availableLocales()) || sourceLocale;
153
+ const [locale, setLocaleSignal] = createSignal(initialLocale);
154
+ const [loadedDicts, setLoadedDicts] = createSignal({});
155
+ const pendingLoads = /* @__PURE__ */ new Set();
156
+ const loadLocale = (target) => {
157
+ if (!lazy) return;
158
+ const loader = lazy.loaders[target];
159
+ if (!loader) return;
160
+ if (target in loadedDicts() || pendingLoads.has(target)) return;
161
+ pendingLoads.add(target);
162
+ loader().then((dict) => {
163
+ setLoadedDicts((prev) => ({ ...prev, [target]: dict }));
164
+ }).catch((err) => {
165
+ console.warn(
166
+ `[solid-translate] Failed to load locale "${target}":`,
167
+ err
168
+ );
169
+ }).finally(() => {
170
+ pendingLoads.delete(target);
171
+ });
172
+ };
173
+ const setLocale = (next) => {
174
+ loadLocale(next);
175
+ setLocaleSignal(next);
176
+ if (persistKey) writePersistedLocale(persistKey, next);
177
+ };
178
+ loadLocale(initialLocale);
129
179
  const t = (key, params) => {
130
180
  const cur = locale();
131
181
  let text = key;
132
- const dict = props.translations[cur];
182
+ const dict = lazy ? loadedDicts()[cur] : props.translations[cur];
133
183
  if (dict && key in dict) {
134
184
  text = dict[key];
135
185
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n children as resolveChildren,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type { TranslationDictionary, Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type { TranslationDictionary, Translations } from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /** Translation dictionaries keyed by locale */\n translations: Translations;\n children: JSX.Element;\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const sourceLocale = props.sourceLocale || \"en\";\n const availableLocales = createMemo(() => Object.keys(props.translations));\n\n // Auto-detect locale from browser if not explicitly provided\n const initialLocale =\n props.locale || detectLocale(availableLocales()) || sourceLocale;\n const [locale, setLocale] = createSignal(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = props.translations[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n const resolved = resolveChildren(() => props.children);\n\n return createMemo(() => {\n const kids = resolved.toArray();\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Simple case: single text child\n if (kids.length === 1 && typeof kids[0] === \"string\") {\n const key = props.id || (kids[0] as string);\n return ctx.t(key, props.params);\n }\n\n // Explicit id with non-text children — translate via id\n if (props.id) {\n const translated = ctx.t(props.id, props.params);\n\n // If translation is just text (no slot placeholders), return it\n if (!/{(\\d+)}/.test(translated)) return translated;\n\n // Collect non-text children (Var, Num, etc.) as ordered slots\n const slots: JSX.Element[] = [];\n for (const kid of kids) {\n if (typeof kid !== \"string\" && typeof kid !== \"number\") {\n slots.push(kid as JSX.Element);\n }\n }\n\n return interpolateSlots(translated, slots);\n }\n\n // Mixed children without explicit id — build a template key\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n const translated = ctx.t(template, props.params);\n if (slots.length === 0) return translated;\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { Translations } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object */\n translations: Translations;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Map normalized available locales back to their canonical casing so a\n // browser \"pt-br\" can match an available \"pt-BR\" (and return \"pt-BR\").\n const canonical = new Map<string, string>();\n for (const al of availableLocales) {\n const normalized = normalizeLocale(al);\n if (!canonical.has(normalized)) canonical.set(normalized, al);\n }\n\n // Exact match (case-insensitive)\n for (const bl of browserLocales) {\n const match = canonical.get(normalizeLocale(bl));\n if (match) return match;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = normalizeLocale(bl).split(\"-\")[0]!;\n const match = canonical.get(lang);\n if (match) return match;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other={`${count()} items`}\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n return forms[category] ?? props.other;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA,cAAAA;AAAA,EACA;AAAA,EACA,cAAAC;AAAA,EACA,YAAY;AAAA,OAEP;;;ACPP,SAAS,qBAAqB;AA0BvB,IAAM,qBAAqB,cAAuC;;;ACnBlE,SAAS,aAAa,kBAAqC;AAChE,MAAI,OAAO,cAAc,YAAa,QAAO;AAE7C,QAAM,iBAAiB,UAAU,YAC7B,CAAC,GAAG,UAAU,SAAS,IACvB,CAAC,UAAU,YAAY,IAAI;AAE/B,MAAI,CAAC,oBAAoB,iBAAiB,WAAW,GAAG;AACtD,WAAO,gBAAgB,eAAe,CAAC,KAAK,IAAI;AAAA,EAClD;AAIA,QAAM,YAAY,oBAAI,IAAoB;AAC1C,aAAW,MAAM,kBAAkB;AACjC,UAAM,aAAa,gBAAgB,EAAE;AACrC,QAAI,CAAC,UAAU,IAAI,UAAU,EAAG,WAAU,IAAI,YAAY,EAAE;AAAA,EAC9D;AAGA,aAAW,MAAM,gBAAgB;AAC/B,UAAM,QAAQ,UAAU,IAAI,gBAAgB,EAAE,CAAC;AAC/C,QAAI,MAAO,QAAO;AAAA,EACpB;AAGA,aAAW,MAAM,gBAAgB;AAC/B,UAAM,OAAO,gBAAgB,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7C,UAAM,QAAQ,UAAU,IAAI,IAAI;AAChC,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,SAAO,iBAAiB,CAAC,KAAK;AAChC;AAEA,SAAS,gBAAgB,QAAwB;AAC/C,SAAO,OAAO,YAAY,EAAE,QAAQ,KAAK,GAAG;AAC9C;;;AC5CA,SAAS,YAAsB,KAAK,kBAAkB;AAqB/C,SAAS,IAAI,OAA8B;AAChD,UAAQ,MAAM,MAAM;AACtB;AAGC,IAAY,WAAW;AAqBjB,SAAS,IAAI,OAA8B;AAChD,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,WAAO,IAAI,KAAK,aAAa,QAAQ,MAAM,OAAO,EAAE,OAAO,MAAM,QAAQ;AAAA,EAC3E,CAAC;AACH;AAuBO,SAAS,SAAS,OAAmC;AAC1D,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,WAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,MACnC,OAAO;AAAA,MACP,UAAU,MAAM;AAAA,MAChB,GAAG,MAAM;AAAA,IACX,CAAC,EAAE,OAAO,MAAM,QAAQ;AAAA,EAC1B,CAAC;AACH;AAqBO,SAAS,SAAS,OAAmC;AAC1D,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,UAAM,OACJ,MAAM,oBAAoB,OACtB,MAAM,WACN,IAAI,KAAK,MAAM,QAAQ;AAC7B,WAAO,IAAI,KAAK,eAAe,QAAQ,MAAM,OAAO,EAAE,OAAO,IAAI;AAAA,EACnE,CAAC;AACH;AAkCO,SAAS,OAAO,OAAiC;AACtD,QAAM,MAAM,WAAW,kBAAkB;AAEzC,SAAO,WAAW,MAAM;AACtB,UAAM,SAAS,KAAK,OAAO,KAAK;AAChC,UAAM,QAAQ,IAAI,KAAK,YAAY,MAAM;AACzC,UAAM,WAAW,MAAM,OAAO,MAAM,CAAC;AAErC,UAAM,QAAiD;AAAA,MACrD,MAAM,MAAM;AAAA,MACZ,KAAK,MAAM;AAAA,MACX,KAAK,MAAM;AAAA,MACX,KAAK,MAAM;AAAA,MACX,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,IACf;AAEA,WAAO,MAAM,QAAQ,KAAK,MAAM;AAAA,EAClC,CAAC;AACH;AAsBO,SAAS,eAAe,OAAyC;AACtE,QAAM,MAAM,WAAW,kBAAkB;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,WAAW,MAAM,MAAM,WAAW,IAAI,iBAAiB,CAAC;AAExE,QAAM,cAAc,CAAC,SAAyB;AAC5C,QAAI,MAAM,SAAS,IAAI,EAAG,QAAO,MAAM,OAAO,IAAI;AAClD,QAAI;AACF,YAAM,KAAK,IAAI,KAAK,aAAa,CAAC,IAAI,GAAG,EAAE,MAAM,WAAW,CAAC;AAC7D,aAAO,GAAG,GAAG,IAAI,KAAK;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SACE,CAAC;AAAA,IACC,OAAO,MAAM;AAAA,IACb,OAAO,IAAI,OAAO;AAAA,IAClB,UAAU,CAAC,MAAM,IAAI,UAAU,EAAE,cAAc,KAAK;AAAA,GACrD;AAAA,MACC,CAAC,IAAI,MAAM,QAAQ,GAAG;AAAA,SACnB,CAAC,SAAS,CAAC,OAAO,OAAO,OAAO,YAAY,IAAI,EAAE,EAAvC,QAAiD;AAAA,MAC/D,EAFC,IAEK;AAAA,IACR,EARC;AAUL;;;AC7MO,SAAS,IACd,MACA,SACQ;AACR,SAAO;AACT;;;AJyBO,SAAS,oBAAoB,OAAiC;AACnE,QAAM,eAAe,MAAM,gBAAgB;AAC3C,QAAM,mBAAmBC,YAAW,MAAM,OAAO,KAAK,MAAM,YAAY,CAAC;AAGzE,QAAM,gBACJ,MAAM,UAAU,aAAa,iBAAiB,CAAC,KAAK;AACtD,QAAM,CAAC,QAAQ,SAAS,IAAI,aAAa,aAAa;AAEtD,QAAM,IAAI,CACR,KACA,WACW;AACX,UAAM,MAAM,OAAO;AACnB,QAAI,OAAO;AAGX,UAAM,OAAO,MAAM,aAAa,GAAG;AACnC,QAAI,QAAQ,OAAO,MAAM;AACvB,aAAO,KAAK,GAAG;AAAA,IACjB;AAGA,QAAI,QAAQ;AACV,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,eAAO,KAAK;AAAA,UACV,IAAI,OAAO,SAAS,CAAC,aAAa,CAAC,OAAO,GAAG;AAAA,UAC7C,OAAO,CAAC;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,QAAiC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,MAAM;AAAA,EACtB;AAEA,SAAO,gBAAgB,mBAAmB,UAAU;AAAA,IAClD;AAAA,IACA,IAAI,WAAW;AACb,aAAO,MAAM;AAAA,IACf;AAAA,EACF,CAAC;AACH;AAOO,SAAS,iBAA0C;AACxD,QAAM,MAAMC,YAAW,kBAAkB;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,YAKd;AACA,QAAM,MAAM,eAAe;AAC3B,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,cAAc,IAAI;AAAA,IAClB,kBAAkB,IAAI;AAAA,EACxB;AACF;AAkCO,SAAS,EAAE,OAA4B;AAC5C,QAAM,MAAMA,YAAW,kBAAkB;AACzC,QAAM,WAAW,gBAAgB,MAAM,MAAM,QAAQ;AAErD,SAAOD,YAAW,MAAM;AACtB,UAAM,OAAO,SAAS,QAAQ;AAG9B,QAAI,CAAC,IAAK,QAAO,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI;AAG/C,QAAI,KAAK,WAAW,KAAK,OAAO,KAAK,CAAC,MAAM,UAAU;AACpD,YAAM,MAAM,MAAM,MAAO,KAAK,CAAC;AAC/B,aAAO,IAAI,EAAE,KAAK,MAAM,MAAM;AAAA,IAChC;AAGA,QAAI,MAAM,IAAI;AACZ,YAAME,cAAa,IAAI,EAAE,MAAM,IAAI,MAAM,MAAM;AAG/C,UAAI,CAAC,UAAU,KAAKA,WAAU,EAAG,QAAOA;AAGxC,YAAMC,SAAuB,CAAC;AAC9B,iBAAW,OAAO,MAAM;AACtB,YAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;AACtD,UAAAA,OAAM,KAAK,GAAkB;AAAA,QAC/B;AAAA,MACF;AAEA,aAAO,iBAAiBD,aAAYC,MAAK;AAAA,IAC3C;AAGA,UAAM,QAAuB,CAAC;AAC9B,QAAI,WAAW;AACf,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,QAAQ,UAAU;AAC3B,oBAAY;AAAA,MACd,WAAW,OAAO,QAAQ,UAAU;AAClC,oBAAY,OAAO,GAAG;AAAA,MACxB,OAAO;AACL,oBAAY,IAAI,MAAM,MAAM;AAC5B,cAAM,KAAK,GAAkB;AAAA,MAC/B;AAAA,IACF;AAEA,UAAM,aAAa,IAAI,EAAE,UAAU,MAAM,MAAM;AAC/C,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,WAAO,iBAAiB,YAAY,KAAK;AAAA,EAC3C,CAAC;AACH;AAOA,SAAS,iBACP,MACA,OAC0B;AAC1B,QAAM,QAAQ,KAAK,MAAM,WAAW;AACpC,QAAM,SAAmC,CAAC;AAC1C,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,IAAI,MAAM,GAAG;AACf,UAAI,MAAM,CAAC,EAAG,QAAO,KAAK,MAAM,CAAC,CAAE;AAAA,IACrC,OAAO;AACL,YAAM,MAAM,SAAS,MAAM,CAAC,GAAI,EAAE;AAClC,UAAI,MAAM,GAAG,MAAM,OAAW,QAAO,KAAK,MAAM,GAAG,CAAE;AAAA,IACvD;AAAA,EACF;AACA,SAAO;AACT;","names":["useContext","createMemo","createMemo","useContext","translated","slots"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/context.ts","../src/locale-detect.ts","../src/components.tsx","../src/msg.ts"],"sourcesContent":["import {\n createComponent,\n useContext,\n createSignal,\n createMemo,\n children as resolveChildren,\n type JSX,\n} from \"solid-js\";\nimport {\n TranslationContext,\n type TranslationContextValue,\n} from \"./context.js\";\nimport { detectLocale } from \"./locale-detect.js\";\nimport type {\n LazyTranslations,\n TranslationDictionary,\n Translations,\n TranslationsInput,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { TranslationContextValue } from \"./context.js\";\nexport type {\n LazyTranslations,\n TranslationDictionary,\n Translations,\n TranslationsInput,\n} from \"./types.js\";\nexport type { SolidTranslatePluginConfig } from \"./types.js\";\nexport { Var, Num, Currency, DateTime, Plural, LocaleSelector } from \"./components.js\";\nexport type {\n VarProps,\n NumProps,\n CurrencyProps,\n DateTimeProps,\n PluralProps,\n LocaleSelectorProps,\n} from \"./components.js\";\nexport { msg } from \"./msg.js\";\nexport { detectLocale } from \"./locale-detect.js\";\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface TranslationProviderProps {\n /**\n * Initial locale. If omitted, auto-detects from the browser's\n * `navigator.languages` header, falling back to `sourceLocale`.\n */\n locale?: string;\n /** Source locale code (default: \"en\") */\n sourceLocale?: string;\n /**\n * Translation dictionaries keyed by locale (from `virtual:solid-translate`),\n * or a lazy manifest (from `virtual:solid-translate/lazy`) whose per-locale\n * dictionaries are loaded on demand via dynamic import.\n */\n translations: TranslationsInput;\n /**\n * Persist the active locale to `localStorage` (default: false).\n * When enabled, the initial locale is read from storage (if still valid)\n * before falling back to browser detection, and `setLocale` writes through.\n * Pass `{ key: \"...\" }` to customize the storage key.\n */\n persistLocale?: boolean | { key?: string };\n children: JSX.Element;\n}\n\nconst DEFAULT_PERSIST_KEY = \"solid-translate:locale\";\n\nfunction isLazyTranslations(\n input: TranslationsInput,\n): input is LazyTranslations {\n return (\n typeof input === \"object\" &&\n input !== null &&\n Array.isArray((input as LazyTranslations).locales) &&\n typeof (input as LazyTranslations).loaders === \"object\" &&\n (input as LazyTranslations).loaders !== null\n );\n}\n\nfunction readPersistedLocale(key: string): string | undefined {\n try {\n if (typeof localStorage === \"undefined\") return undefined;\n return localStorage.getItem(key) ?? undefined;\n } catch {\n // SSR / storage disabled\n return undefined;\n }\n}\n\nfunction writePersistedLocale(key: string, locale: string): void {\n try {\n if (typeof localStorage === \"undefined\") return;\n localStorage.setItem(key, locale);\n } catch {\n // SSR / storage disabled / quota exceeded — ignore\n }\n}\n\nexport function TranslationProvider(props: TranslationProviderProps) {\n const lazy = isLazyTranslations(props.translations)\n ? props.translations\n : undefined;\n const sourceLocale = props.sourceLocale || lazy?.sourceLocale || \"en\";\n const availableLocales = createMemo(() =>\n lazy ? lazy.locales : Object.keys(props.translations),\n );\n\n const persistKey = props.persistLocale\n ? (typeof props.persistLocale === \"object\"\n ? props.persistLocale.key\n : undefined) || DEFAULT_PERSIST_KEY\n : undefined;\n\n // Initial locale: explicit prop > persisted value (if valid) > detection\n const persisted = persistKey ? readPersistedLocale(persistKey) : undefined;\n const persistedValid =\n persisted !== undefined &&\n (persisted === sourceLocale || availableLocales().includes(persisted));\n const initialLocale =\n props.locale ||\n (persistedValid ? persisted : undefined) ||\n detectLocale(availableLocales()) ||\n sourceLocale;\n const [locale, setLocaleSignal] = createSignal(initialLocale);\n\n // Lazily loaded dictionaries, keyed by locale (lazy manifest mode only).\n // Loading NEVER throws or suspends — while a dictionary is in flight,\n // t() falls back to the source text.\n const [loadedDicts, setLoadedDicts] = createSignal<Translations>({});\n const pendingLoads = new Set<string>();\n\n const loadLocale = (target: string): void => {\n if (!lazy) return;\n const loader = lazy.loaders[target];\n if (!loader) return;\n if (target in loadedDicts() || pendingLoads.has(target)) return;\n pendingLoads.add(target);\n loader()\n .then((dict) => {\n setLoadedDicts((prev) => ({ ...prev, [target]: dict }));\n })\n .catch((err) => {\n console.warn(\n `[solid-translate] Failed to load locale \"${target}\":`,\n err,\n );\n })\n .finally(() => {\n pendingLoads.delete(target);\n });\n };\n\n const setLocale = (next: string): void => {\n loadLocale(next);\n setLocaleSignal(next);\n if (persistKey) writePersistedLocale(persistKey, next);\n };\n\n // Kick off loading for the initial locale (no-op in eager mode, or when\n // the locale has no loader — e.g. the source locale without a dict).\n loadLocale(initialLocale);\n\n const t = (\n key: string,\n params?: Record<string, string | number>,\n ): string => {\n const cur = locale();\n let text = key;\n\n // Look up in translation dictionary (works for both source and target locales)\n const dict = lazy\n ? loadedDicts()[cur]\n : (props.translations as Translations)[cur];\n if (dict && key in dict) {\n text = dict[key]!;\n }\n\n // Interpolate {{variable}} and {variable} placeholders\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n text = text.replace(\n new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}|\\\\{${k}\\\\}`, \"g\"),\n String(v),\n );\n }\n }\n\n return text;\n };\n\n const value: TranslationContextValue = {\n locale,\n setLocale,\n t,\n sourceLocale,\n availableLocales,\n translations: props.translations,\n };\n\n return createComponent(TranslationContext.Provider, {\n value,\n get children() {\n return props.children;\n },\n });\n}\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\n/** Access the full translation context. Must be inside a TranslationProvider. */\nexport function useTranslation(): TranslationContextValue {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"useTranslation() must be used within a <TranslationProvider>\",\n );\n }\n return ctx;\n}\n\n/** Access just the current locale and setter. */\nexport function useLocale(): {\n locale: () => string;\n setLocale: (locale: string) => void;\n sourceLocale: string;\n availableLocales: () => string[];\n} {\n const ctx = useTranslation();\n return {\n locale: ctx.locale,\n setLocale: ctx.setLocale,\n sourceLocale: ctx.sourceLocale,\n availableLocales: ctx.availableLocales,\n };\n}\n\n// ---------------------------------------------------------------------------\n// <T> Component\n// ---------------------------------------------------------------------------\n\nexport interface TProps {\n /** Explicit translation key. If omitted, children text is used as the key. */\n id?: string;\n /** Interpolation parameters */\n params?: Record<string, string | number>;\n /**\n * AI context hint — tells the AI translator about the meaning of this text.\n * Only used at build time for disambiguation; has no runtime effect.\n *\n * ```tsx\n * <T context=\"Button to save a document, not save money\">Save</T>\n * ```\n */\n context?: string;\n /** Source text / JSX content */\n children?: JSX.Element;\n}\n\n/**\n * Translatable content component.\n *\n * ```tsx\n * <T>Hello world</T>\n * <T id=\"greeting\" params={{ name: \"Alice\" }}>Hello {{name}}</T>\n * <T context=\"the physical bank\">Bank</T>\n * <T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>\n * ```\n */\nexport function T(props: TProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n const resolved = resolveChildren(() => props.children);\n\n return createMemo(() => {\n const kids = resolved.toArray();\n\n // No context — just render children\n if (!ctx) return kids.length === 1 ? kids[0] : kids;\n\n // Simple case: single text child\n if (kids.length === 1 && typeof kids[0] === \"string\") {\n const key = props.id || (kids[0] as string);\n return ctx.t(key, props.params);\n }\n\n // Explicit id with non-text children — translate via id\n if (props.id) {\n const translated = ctx.t(props.id, props.params);\n\n // If translation is just text (no slot placeholders), return it\n if (!/{(\\d+)}/.test(translated)) return translated;\n\n // Collect non-text children (Var, Num, etc.) as ordered slots\n const slots: JSX.Element[] = [];\n for (const kid of kids) {\n if (typeof kid !== \"string\" && typeof kid !== \"number\") {\n slots.push(kid as JSX.Element);\n }\n }\n\n return interpolateSlots(translated, slots);\n }\n\n // Mixed children without explicit id — build a template key\n const slots: JSX.Element[] = [];\n let template = \"\";\n for (const kid of kids) {\n if (typeof kid === \"string\") {\n template += kid;\n } else if (typeof kid === \"number\") {\n template += String(kid);\n } else {\n template += `{${slots.length}}`;\n slots.push(kid as JSX.Element);\n }\n }\n\n const translated = ctx.t(template, props.params);\n if (slots.length === 0) return translated;\n return interpolateSlots(translated, slots);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Split a translated string by `{0}`, `{1}`, etc. and interleave with slots */\nfunction interpolateSlots(\n text: string,\n slots: JSX.Element[],\n): (string | JSX.Element)[] {\n const parts = text.split(/\\{(\\d+)\\}/);\n const result: (string | JSX.Element)[] = [];\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n if (parts[i]) result.push(parts[i]!);\n } else {\n const idx = parseInt(parts[i]!, 10);\n if (slots[idx] !== undefined) result.push(slots[idx]!);\n }\n }\n return result;\n}\n","import { createContext } from \"solid-js\";\nimport type { TranslationsInput } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Context value type\n// ---------------------------------------------------------------------------\n\nexport interface TranslationContextValue {\n /** Current locale as a reactive signal */\n locale: () => string;\n /** Switch to a different locale */\n setLocale: (locale: string) => void;\n /** Translate a key with optional interpolation params */\n t: (key: string, params?: Record<string, string | number>) => string;\n /** The source locale code */\n sourceLocale: string;\n /** All available locale codes (reactive) */\n availableLocales: () => string[];\n /** Raw translations object (eager record or lazy manifest) */\n translations: TranslationsInput;\n}\n\n// ---------------------------------------------------------------------------\n// Shared context instance\n// ---------------------------------------------------------------------------\n\nexport const TranslationContext = createContext<TranslationContextValue>();\n","/**\n * Detect the user's preferred locale from browser settings.\n *\n * Checks `navigator.languages` (and falls back to `navigator.language`)\n * then matches against the list of available locales. Tries exact match\n * first, then language-only match (e.g. \"en-US\" → \"en\").\n */\nexport function detectLocale(availableLocales?: string[]): string {\n if (typeof navigator === \"undefined\") return \"en\";\n\n const browserLocales = navigator.languages\n ? [...navigator.languages]\n : [navigator.language || \"en\"];\n\n if (!availableLocales || availableLocales.length === 0) {\n return normalizeLocale(browserLocales[0] || \"en\");\n }\n\n // Map normalized available locales back to their canonical casing so a\n // browser \"pt-br\" can match an available \"pt-BR\" (and return \"pt-BR\").\n const canonical = new Map<string, string>();\n for (const al of availableLocales) {\n const normalized = normalizeLocale(al);\n if (!canonical.has(normalized)) canonical.set(normalized, al);\n }\n\n // Exact match (case-insensitive)\n for (const bl of browserLocales) {\n const match = canonical.get(normalizeLocale(bl));\n if (match) return match;\n }\n\n // Language-only match (e.g. \"en-US\" → \"en\")\n for (const bl of browserLocales) {\n const lang = normalizeLocale(bl).split(\"-\")[0]!;\n const match = canonical.get(lang);\n if (match) return match;\n }\n\n return availableLocales[0] || \"en\";\n}\n\nfunction normalizeLocale(locale: string): string {\n return locale.toLowerCase().replace(\"_\", \"-\");\n}\n","import { useContext, type JSX, For, createMemo } from \"solid-js\";\nimport { TranslationContext } from \"./context.js\";\n\n// ---------------------------------------------------------------------------\n// <Var> — protect dynamic content from translation\n// ---------------------------------------------------------------------------\n\nexport interface VarProps {\n /** Optional name for the variable (used as placeholder in templates) */\n name?: string;\n children: JSX.Element;\n}\n\n/**\n * Marks content as untranslatable. When used inside `<T>`, the content\n * is preserved as-is while surrounding text is translated.\n *\n * ```tsx\n * <T>Hello <Var>{userName()}</Var>, welcome!</T>\n * ```\n */\nexport function Var(props: VarProps): JSX.Element {\n return (() => props.children) as unknown as JSX.Element;\n}\n\n// Mark Var for identification by T component\n(Var as any).__st_var = true;\n\n// ---------------------------------------------------------------------------\n// <Num> — locale-aware number formatting\n// ---------------------------------------------------------------------------\n\nexport interface NumProps {\n /** The number to format */\n children: number;\n /** Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number according to the current locale using `Intl.NumberFormat`.\n *\n * ```tsx\n * <Num>{1000000}</Num> // \"1,000,000\" in en, \"1.000.000\" in de\n * <Num options={{ style: \"percent\" }}>{0.42}</Num> // \"42%\"\n * ```\n */\nexport function Num(props: NumProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, props.options).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Currency> — locale-aware currency formatting\n// ---------------------------------------------------------------------------\n\nexport interface CurrencyProps {\n /** The numeric value */\n children: number;\n /** ISO 4217 currency code (e.g. \"USD\", \"EUR\") */\n currency: string;\n /** Additional Intl.NumberFormat options */\n options?: Intl.NumberFormatOptions;\n}\n\n/**\n * Formats a number as currency according to the current locale.\n *\n * ```tsx\n * <Currency currency=\"USD\">{29.99}</Currency> // \"$29.99\" in en-US\n * <Currency currency=\"EUR\">{29.99}</Currency> // \"29,99 €\" in de\n * ```\n */\nexport function Currency(props: CurrencyProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n return new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: props.currency,\n ...props.options,\n }).format(props.children);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <DateTime> — locale-aware date/time formatting\n// ---------------------------------------------------------------------------\n\nexport interface DateTimeProps {\n /** The date to format (Date object, timestamp, or ISO string) */\n children: Date | number | string;\n /** Intl.DateTimeFormat options */\n options?: Intl.DateTimeFormatOptions;\n}\n\n/**\n * Formats a date/time according to the current locale using `Intl.DateTimeFormat`.\n *\n * ```tsx\n * <DateTime>{new Date()}</DateTime>\n * <DateTime options={{ dateStyle: \"long\" }}>{new Date()}</DateTime>\n * ```\n */\nexport function DateTime(props: DateTimeProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const date =\n props.children instanceof Date\n ? props.children\n : new Date(props.children);\n return new Intl.DateTimeFormat(locale, props.options).format(date);\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <Plural> — CLDR plural rules\n// ---------------------------------------------------------------------------\n\nexport interface PluralProps {\n /** The count value to determine which plural form to use */\n n: number;\n /** Form for zero items */\n zero?: JSX.Element;\n /** Form for exactly one item */\n one?: JSX.Element;\n /** Form for exactly two items */\n two?: JSX.Element;\n /** Form for \"few\" items (language-dependent) */\n few?: JSX.Element;\n /** Form for \"many\" items (language-dependent) */\n many?: JSX.Element;\n /** Default/fallback form */\n other: JSX.Element;\n}\n\n/**\n * Renders the appropriate plural form based on CLDR plural rules for the current locale.\n *\n * ```tsx\n * <Plural n={count()}\n * zero=\"No items\"\n * one=\"1 item\"\n * other={`${count()} items`}\n * />\n * ```\n */\nexport function Plural(props: PluralProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n\n return createMemo(() => {\n const locale = ctx?.locale() || \"en\";\n const rules = new Intl.PluralRules(locale);\n const category = rules.select(props.n);\n\n const forms: Record<string, JSX.Element | undefined> = {\n zero: props.zero,\n one: props.one,\n two: props.two,\n few: props.few,\n many: props.many,\n other: props.other,\n };\n\n return forms[category] ?? props.other;\n }) as unknown as JSX.Element;\n}\n\n// ---------------------------------------------------------------------------\n// <LocaleSelector> — drop-in locale picker\n// ---------------------------------------------------------------------------\n\nexport interface LocaleSelectorProps {\n /** Override which locales to show (defaults to all available) */\n locales?: string[];\n /** Map locale codes to display names, e.g. { en: \"English\", es: \"Español\" } */\n labels?: Record<string, string>;\n /** Additional CSS class */\n class?: string;\n}\n\n/**\n * A ready-to-use locale selector dropdown.\n *\n * ```tsx\n * <LocaleSelector labels={{ en: \"English\", es: \"Español\", fr: \"Français\" }} />\n * ```\n */\nexport function LocaleSelector(props: LocaleSelectorProps): JSX.Element {\n const ctx = useContext(TranslationContext);\n if (!ctx) {\n throw new Error(\n \"<LocaleSelector> must be used within a <TranslationProvider>\",\n );\n }\n\n const locales = createMemo(() => props.locales || ctx.availableLocales());\n\n const displayName = (code: string): string => {\n if (props.labels?.[code]) return props.labels[code]!;\n try {\n const dn = new Intl.DisplayNames([code], { type: \"language\" });\n return dn.of(code) || code;\n } catch {\n return code;\n }\n };\n\n return (\n <select\n class={props.class}\n value={ctx.locale()}\n onChange={(e) => ctx.setLocale(e.currentTarget.value)}\n >\n <For each={locales()}>\n {(code) => <option value={code}>{displayName(code)}</option>}\n </For>\n </select>\n ) as JSX.Element;\n}\n","/**\n * Mark a string for translation extraction.\n *\n * At build time, the Vite plugin and CLI scan for `msg()` calls and add\n * the strings to the source locale file for AI translation.\n *\n * At runtime, `msg()` is a no-op — it returns the source text as-is.\n * Use `t()` from `useTranslation()` for runtime translation.\n *\n * ```ts\n * // Marks \"Save changes\" for extraction\n * const label = msg(\"Save changes\");\n *\n * // With interpolation template\n * const greeting = msg(\"Hello {{name}}\", { name: \"World\" });\n *\n * // In a component, translate at runtime:\n * const { t } = useTranslation();\n * <button>{t(label)}</button>\n * ```\n */\nexport function msg(\n text: string,\n _params?: Record<string, string | number>,\n): string {\n return text;\n}\n"],"mappings":"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","names":["useContext","createMemo","createMemo","useContext","translated","slots"]}
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ translateBatch,
4
+ translateMarkdown
5
+ } from "./chunk-2BKJUY37.js";
6
+ import "./chunk-FYS2JH42.js";
7
+ export {
8
+ translateBatch,
9
+ translateMarkdown
10
+ };
package/dist/vite.js CHANGED
@@ -6,7 +6,13 @@ import {
6
6
  mkdirSync,
7
7
  readdirSync
8
8
  } from "fs";
9
- import { resolve, join as join2, relative } from "path";
9
+ import { resolve, join as join2, relative, basename } 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
+ }
10
16
 
11
17
  // src/translate.ts
12
18
  import { z } from "zod";
@@ -98,14 +104,6 @@ function extractStringsFromSource(code, filePath) {
98
104
  // src/lock.ts
99
105
  import { readFileSync, writeFileSync, existsSync } from "fs";
100
106
  import { join } from "path";
101
-
102
- // src/hash.ts
103
- import { createHash } from "crypto";
104
- function hashContent(content) {
105
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
106
- }
107
-
108
- // src/lock.ts
109
107
  function diffLock(sourceDict, lock, contexts) {
110
108
  const changedKeys = {};
111
109
  const pendingEntries = {};
@@ -261,6 +259,11 @@ function writeTargetFile(targetFilePath, translations, sourceDict) {
261
259
  // src/vite.ts
262
260
  var VIRTUAL_MODULE_ID = "virtual:solid-translate";
263
261
  var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
262
+ var VIRTUAL_LAZY_MODULE_ID = "virtual:solid-translate/lazy";
263
+ var RESOLVED_VIRTUAL_LAZY_MODULE_ID = "\0" + VIRTUAL_LAZY_MODULE_ID;
264
+ var VIRTUAL_LOCALE_MODULE_PREFIX = "virtual:solid-translate/locale/";
265
+ var RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX = "\0" + VIRTUAL_LOCALE_MODULE_PREFIX;
266
+ var LOCALE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
264
267
  function solidTranslate(config) {
265
268
  const {
266
269
  sourceLocale = "en",
@@ -367,6 +370,15 @@ function solidTranslate(config) {
367
370
  if (id === VIRTUAL_MODULE_ID) {
368
371
  return RESOLVED_VIRTUAL_MODULE_ID;
369
372
  }
373
+ if (id === VIRTUAL_LAZY_MODULE_ID) {
374
+ return RESOLVED_VIRTUAL_LAZY_MODULE_ID;
375
+ }
376
+ if (id.startsWith(VIRTUAL_LOCALE_MODULE_PREFIX)) {
377
+ const locale = id.slice(VIRTUAL_LOCALE_MODULE_PREFIX.length);
378
+ if (LOCALE_ID_PATTERN.test(locale)) {
379
+ return RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale;
380
+ }
381
+ }
370
382
  },
371
383
  load(id) {
372
384
  if (id === RESOLVED_VIRTUAL_MODULE_ID) {
@@ -387,16 +399,57 @@ function solidTranslate(config) {
387
399
  }
388
400
  return `export default ${JSON.stringify(translations)};`;
389
401
  }
402
+ if (id === RESOLVED_VIRTUAL_LAZY_MODULE_ID) {
403
+ const locales = [sourceLocale, ...targetLocales].filter(
404
+ (locale, i, all) => all.indexOf(locale) === i
405
+ );
406
+ const loaderEntries = locales.map(
407
+ (locale) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(
408
+ VIRTUAL_LOCALE_MODULE_PREFIX + locale
409
+ )}).then((m) => m.default),`
410
+ ).join("\n");
411
+ return [
412
+ `export const sourceLocale = ${JSON.stringify(sourceLocale)};`,
413
+ `export const locales = ${JSON.stringify(locales)};`,
414
+ `export const loaders = {`,
415
+ loaderEntries,
416
+ `};`,
417
+ `export default { sourceLocale, locales, loaders };`
418
+ ].join("\n");
419
+ }
420
+ if (id.startsWith(RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX)) {
421
+ const locale = id.slice(
422
+ RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX.length
423
+ );
424
+ let dict = {};
425
+ const filePath = join2(resolvedLocalesDir, `${locale}.json`);
426
+ if (existsSync2(filePath)) {
427
+ try {
428
+ dict = JSON.parse(readFileSync2(filePath, "utf-8"));
429
+ } catch {
430
+ }
431
+ }
432
+ return `export default ${JSON.stringify(dict)};`;
433
+ }
390
434
  },
391
435
  // HMR: reload translations when locale files change
392
436
  handleHotUpdate({ file, server }) {
393
437
  if (file.startsWith(resolvedLocalesDir) && file.endsWith(".json")) {
394
- const mod = server.moduleGraph.getModuleById(
395
- RESOLVED_VIRTUAL_MODULE_ID
396
- );
397
- if (mod) {
398
- server.moduleGraph.invalidateModule(mod);
399
- return [mod];
438
+ const invalidated = [];
439
+ const locale = basename(file, ".json");
440
+ const ids = [
441
+ RESOLVED_VIRTUAL_MODULE_ID,
442
+ RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale
443
+ ];
444
+ for (const id of ids) {
445
+ const mod = server.moduleGraph.getModuleById(id);
446
+ if (mod) {
447
+ server.moduleGraph.invalidateModule(mod);
448
+ invalidated.push(mod);
449
+ }
450
+ }
451
+ if (invalidated.length > 0) {
452
+ return invalidated;
400
453
  }
401
454
  }
402
455
  }
package/dist/vite.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vite.ts","../src/translate.ts","../src/extract.ts","../src/lock.ts","../src/hash.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 { translateBatch } from \"./translate.js\";\nimport { extractStringsFromSource } from \"./extract.js\";\nimport { syncLocaleFiles, formatSyncFailures } from \"./lock.js\";\nimport type { SolidTranslatePluginConfig } 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\n return {\n name: \"solid-translate\",\n\n configResolved(resolvedConfig: ResolvedConfig) {\n root = resolvedConfig.root;\n resolvedLocalesDir = resolve(root, localesDir);\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 result = await syncLocaleFiles({\n localesDir: resolvedLocalesDir,\n sourceLocale,\n targetLocales,\n batchSize,\n // Only pass extraction contexts when autoExtract ran; otherwise\n // preserve the contexts already recorded in the lock file.\n contexts: autoExtract ? contexts : undefined,\n translate: (batch, targetLocale, changedContexts) =>\n translateBatch(\n model,\n batch,\n targetLocale,\n sourceLocale,\n systemPrompt,\n changedContexts,\n ),\n log: (message) => console.log(`[solid-translate] ${message}`),\n });\n\n if (result.failures.length > 0) {\n // Fail the build: successfully translated batches were written, but\n // failed keys were NOT recorded in the lock, so they retry next run.\n throw new Error(\n [\n \"[solid-translate] Translation failed for some batches:\",\n ...formatSyncFailures(result.failures).map((line) => ` ${line}`),\n \"Failed keys were not recorded in the lock file — fix the error and rebuild to retry them.\",\n ].join(\"\\n\"),\n );\n }\n\n if (result.status === \"synced\") {\n console.log(\"[solid-translate] Translation complete.\");\n }\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 { z } from \"zod\";\nimport type { LanguageModelV1 } from \"ai\";\n\n/**\n * Lazily import the `ai` package so that merely loading this module (e.g.\n * via the Vite plugin on extract-only or fresh-lock builds) does not require\n * `ai` to be installed. It is only needed when translation actually runs.\n */\nasync function loadGenerateObject() {\n const { generateObject } = await import(\"ai\");\n return generateObject;\n}\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 generateObject = await loadGenerateObject();\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 generateObject = await loadGenerateObject();\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","import { readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\nimport type { LockFile, LockFileEntry } from \"./types.js\";\n\n/**\n * Shared lock-file + locale-sync logic used by both the CLI and the Vite\n * plugin. Keeping this single-sourced guarantees the two entry points agree\n * on what the lock file means: an entry exists for a key if and only if that\n * key has been successfully translated to every target locale.\n */\n\n/** Result of comparing the source dictionary against the lock file */\nexport interface LockDiff {\n /** Keys that are new or changed and need translation */\n changedKeys: Record<string, string>;\n /**\n * Lock entries for the changed keys. These are *pending*: they must only\n * be committed to the lock after translation succeeds for all locales.\n */\n pendingEntries: Record<string, LockFileEntry>;\n /** Keys present in the lock but no longer in the source dictionary */\n deletedKeys: string[];\n}\n\n/**\n * Compare the source dictionary against the lock file.\n *\n * When `contexts` is provided (Vite auto-extraction), a context change also\n * marks a key as changed and the new context is recorded in the pending\n * entry. When `contexts` is omitted (CLI), existing lock contexts are\n * preserved unchanged so a CLI run never clobbers Vite-written contexts.\n */\nexport function diffLock(\n sourceDict: Record<string, string>,\n lock: LockFile,\n contexts?: Record<string, string>,\n): LockDiff {\n const changedKeys: Record<string, string> = {};\n const pendingEntries: Record<string, LockFileEntry> = {};\n\n for (const [key, value] of Object.entries(sourceDict)) {\n const hash = hashContent(value);\n const existing = lock.keys[key];\n const newContext = contexts ? contexts[key] : existing?.context;\n const contextChanged =\n contexts !== undefined && existing?.context !== contexts[key];\n\n // Re-translate if the key is new, content changed, or context changed\n if (!existing || existing.hash !== hash || contextChanged) {\n changedKeys[key] = value;\n pendingEntries[key] = { hash, source: value, context: newContext };\n }\n }\n\n const deletedKeys = Object.keys(lock.keys).filter(\n (key) => !(key in sourceDict),\n );\n\n return { changedKeys, pendingEntries, deletedKeys };\n}\n\n/** Translate one batch of changed keys for one target locale */\nexport type TranslateFn = (\n batch: Record<string, string>,\n targetLocale: string,\n contexts: Record<string, string>,\n) => Promise<Record<string, string>>;\n\n/** A translation batch that failed for a target locale */\nexport interface SyncFailure {\n locale: string;\n keys: string[];\n error: unknown;\n}\n\nexport interface SyncResult {\n status: \"no-source\" | \"no-changes\" | \"synced\";\n /** Keys translated successfully for ALL target locales (recorded in lock) */\n translatedKeys: string[];\n /** Keys removed from source and pruned from targets + lock */\n deletedKeys: string[];\n /** Failed batches. Non-empty means the run must be treated as failed. */\n failures: SyncFailure[];\n}\n\nexport interface SyncOptions {\n localesDir: string;\n sourceLocale: string;\n targetLocales: string[];\n batchSize: number;\n translate: TranslateFn;\n /**\n * Context hints from auto-extraction (Vite). Omit to preserve existing\n * lock contexts (CLI).\n */\n contexts?: Record<string, string>;\n log?: (message: string) => void;\n}\n\n/**\n * Sync source locale changes into target locale files and the lock file.\n *\n * Guarantees:\n * - Lock entries are committed only for keys whose batches succeeded for\n * every target locale — the lock never claims a key is translated when\n * it isn't. Failed keys stay \"changed\" and are retried on the next run.\n * - Successfully translated batches are still written even when other\n * batches fail; callers must surface `failures` (exit non-zero / throw).\n * - Deleted source keys are pruned from target files and the lock even\n * when there is nothing to translate (no AI calls needed).\n */\nexport async function syncLocaleFiles(\n options: SyncOptions,\n): Promise<SyncResult> {\n const {\n localesDir,\n sourceLocale,\n targetLocales,\n batchSize,\n translate,\n contexts,\n log = () => {},\n } = options;\n\n const sourceFilePath = join(localesDir, `${sourceLocale}.json`);\n if (!existsSync(sourceFilePath)) {\n return {\n status: \"no-source\",\n translatedKeys: [],\n deletedKeys: [],\n failures: [],\n };\n }\n\n const sourceDict: Record<string, string> = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n\n const lockFilePath = join(localesDir, \".solid-translate.lock\");\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 const { changedKeys, pendingEntries, deletedKeys } = diffLock(\n sourceDict,\n lock,\n contexts,\n );\n\n // Remove keys that no longer exist in source\n for (const key of deletedKeys) {\n delete lock.keys[key];\n }\n\n const changedCount = Object.keys(changedKeys).length;\n\n if (changedCount === 0 && deletedKeys.length === 0) {\n log(\"No changes detected in locale files.\");\n return {\n status: \"no-changes\",\n translatedKeys: [],\n deletedKeys: [],\n failures: [],\n };\n }\n\n if (changedCount === 0) {\n // Deletions only — prune target files and the lock, no AI calls needed\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(localesDir, `${targetLocale}.json`);\n const existing = readTargetFile(targetFilePath);\n writeTargetFile(targetFilePath, existing, sourceDict);\n log(` ${targetLocale}: pruned deleted keys`);\n }\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n log(\n `Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? \"s\" : \"\"} from target locales.`,\n );\n return { status: \"synced\", translatedKeys: [], deletedKeys, failures: [] };\n }\n\n log(\n `Translating ${changedCount} key${changedCount > 1 ? \"s\" : \"\"} to ${targetLocales.length} locale${targetLocales.length > 1 ? \"s\" : \"\"}...`,\n );\n\n // Context hints for the changed keys, passed to the translator\n const changedContexts: Record<string, string> = {};\n for (const key of Object.keys(changedKeys)) {\n const ctx = pendingEntries[key]?.context;\n if (ctx) changedContexts[key] = ctx;\n }\n\n const failures: SyncFailure[] = [];\n const failedKeys = new Set<string>();\n\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(localesDir, `${targetLocale}.json`);\n\n // Load existing translations to preserve unchanged keys\n const existing = readTargetFile(targetFilePath);\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(entries.slice(i, i + batchSize));\n try {\n const translated = await translate(\n batch,\n targetLocale,\n changedContexts,\n );\n Object.assign(existing, translated);\n } catch (err) {\n failures.push({\n locale: targetLocale,\n keys: Object.keys(batch),\n error: err,\n });\n for (const key of Object.keys(batch)) {\n failedKeys.add(key);\n }\n }\n }\n\n writeTargetFile(targetFilePath, existing, sourceDict);\n log(` ${targetLocale}: ${Object.keys(existing).length} keys`);\n }\n\n // Commit lock entries only for keys that succeeded for ALL target locales.\n // Failed keys keep their old entry (or none), so the next run retries them.\n const translatedKeys: string[] = [];\n for (const [key, entry] of Object.entries(pendingEntries)) {\n if (failedKeys.has(key)) continue;\n lock.keys[key] = entry;\n translatedKeys.push(key);\n }\n\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n\n return { status: \"synced\", translatedKeys, deletedKeys, failures };\n}\n\n/** Format sync failures into a human-readable, single-line-per-batch report */\nexport function formatSyncFailures(failures: SyncFailure[]): string[] {\n return failures.map((failure) => {\n const message =\n failure.error instanceof Error\n ? failure.error.message\n : String(failure.error);\n return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? \"s\" : \"\"} [${failure.keys.join(\", \")}] — ${message}`;\n });\n}\n\nfunction readTargetFile(targetFilePath: string): Record<string, string> {\n if (!existsSync(targetFilePath)) return {};\n try {\n return JSON.parse(readFileSync(targetFilePath, \"utf-8\"));\n } catch {\n // Corrupted file — regenerate\n return {};\n }\n}\n\nfunction writeTargetFile(\n targetFilePath: string,\n translations: Record<string, string>,\n sourceDict: Record<string, string>,\n): void {\n // Remove keys that no longer exist in source\n for (const key of Object.keys(translations)) {\n if (!(key in sourceDict)) {\n delete translations[key];\n }\n }\n\n // Sort keys for stable, diff-friendly output\n const sorted = Object.fromEntries(\n Object.entries(translations).sort(([a], [b]) => a.localeCompare(b)),\n );\n\n writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + \"\\n\");\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"],"mappings":"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","names":["readFileSync","writeFileSync","existsSync","join","existsSync","join","readFileSync","writeFileSync"]}
1
+ {"version":3,"sources":["../src/vite.ts","../src/hash.ts","../src/translate.ts","../src/extract.ts","../src/lock.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, basename } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\nimport { translateBatch } from \"./translate.js\";\nimport { extractStringsFromSource } from \"./extract.js\";\nimport { syncLocaleFiles, formatSyncFailures } from \"./lock.js\";\nimport type { SolidTranslatePluginConfig } 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\nconst VIRTUAL_LAZY_MODULE_ID = \"virtual:solid-translate/lazy\";\nconst RESOLVED_VIRTUAL_LAZY_MODULE_ID = \"\\0\" + VIRTUAL_LAZY_MODULE_ID;\n\nconst VIRTUAL_LOCALE_MODULE_PREFIX = \"virtual:solid-translate/locale/\";\nconst RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX =\n \"\\0\" + VIRTUAL_LOCALE_MODULE_PREFIX;\n\n/** Locale codes must be simple path-safe tokens (e.g. \"en\", \"pt-BR\", \"zh_Hant\") */\nconst LOCALE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;\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\n return {\n name: \"solid-translate\",\n\n configResolved(resolvedConfig: ResolvedConfig) {\n root = resolvedConfig.root;\n resolvedLocalesDir = resolve(root, localesDir);\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 result = await syncLocaleFiles({\n localesDir: resolvedLocalesDir,\n sourceLocale,\n targetLocales,\n batchSize,\n // Only pass extraction contexts when autoExtract ran; otherwise\n // preserve the contexts already recorded in the lock file.\n contexts: autoExtract ? contexts : undefined,\n translate: (batch, targetLocale, changedContexts) =>\n translateBatch(\n model,\n batch,\n targetLocale,\n sourceLocale,\n systemPrompt,\n changedContexts,\n ),\n log: (message) => console.log(`[solid-translate] ${message}`),\n });\n\n if (result.failures.length > 0) {\n // Fail the build: successfully translated batches were written, but\n // failed keys were NOT recorded in the lock, so they retry next run.\n throw new Error(\n [\n \"[solid-translate] Translation failed for some batches:\",\n ...formatSyncFailures(result.failures).map((line) => ` ${line}`),\n \"Failed keys were not recorded in the lock file — fix the error and rebuild to retry them.\",\n ].join(\"\\n\"),\n );\n }\n\n if (result.status === \"synced\") {\n console.log(\"[solid-translate] Translation complete.\");\n }\n },\n\n resolveId(id: string) {\n if (id === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n if (id === VIRTUAL_LAZY_MODULE_ID) {\n return RESOLVED_VIRTUAL_LAZY_MODULE_ID;\n }\n if (id.startsWith(VIRTUAL_LOCALE_MODULE_PREFIX)) {\n const locale = id.slice(VIRTUAL_LOCALE_MODULE_PREFIX.length);\n if (LOCALE_ID_PATTERN.test(locale)) {\n return RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale;\n }\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 if (id === RESOLVED_VIRTUAL_LAZY_MODULE_ID) {\n // Lazy manifest: per-locale dictionaries stay out of the main bundle\n // and are code-split into their own chunks via dynamic import.\n const locales = [sourceLocale, ...targetLocales].filter(\n (locale, i, all) => all.indexOf(locale) === i,\n );\n const loaderEntries = locales\n .map(\n (locale) =>\n ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(\n VIRTUAL_LOCALE_MODULE_PREFIX + locale,\n )}).then((m) => m.default),`,\n )\n .join(\"\\n\");\n return [\n `export const sourceLocale = ${JSON.stringify(sourceLocale)};`,\n `export const locales = ${JSON.stringify(locales)};`,\n `export const loaders = {`,\n loaderEntries,\n `};`,\n `export default { sourceLocale, locales, loaders };`,\n ].join(\"\\n\");\n }\n\n if (id.startsWith(RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX)) {\n // Single locale dictionary module\n const locale = id.slice(\n RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX.length,\n );\n let dict: Record<string, string> = {};\n const filePath = join(resolvedLocalesDir, `${locale}.json`);\n if (existsSync(filePath)) {\n try {\n dict = JSON.parse(readFileSync(filePath, \"utf-8\"));\n } catch {\n // Malformed file — serve empty dict\n }\n }\n return `export default ${JSON.stringify(dict)};`;\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 invalidated = [];\n const locale = basename(file, \".json\");\n const ids = [\n RESOLVED_VIRTUAL_MODULE_ID,\n RESOLVED_VIRTUAL_LOCALE_MODULE_PREFIX + locale,\n ];\n for (const id of ids) {\n const mod = server.moduleGraph.getModuleById(id);\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n invalidated.push(mod);\n }\n }\n if (invalidated.length > 0) {\n return invalidated;\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 { z } from \"zod\";\nimport type { LanguageModelV1 } from \"ai\";\n\n/**\n * Lazily import the `ai` package so that merely loading this module (e.g.\n * via the Vite plugin on extract-only or fresh-lock builds) does not require\n * `ai` to be installed. It is only needed when translation actually runs.\n */\nasync function loadGenerateObject() {\n const { generateObject } = await import(\"ai\");\n return generateObject;\n}\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 generateObject = await loadGenerateObject();\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 generateObject = await loadGenerateObject();\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","import { readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { hashContent } from \"./hash.js\";\nimport type { LockFile, LockFileEntry } from \"./types.js\";\n\n/**\n * Shared lock-file + locale-sync logic used by both the CLI and the Vite\n * plugin. Keeping this single-sourced guarantees the two entry points agree\n * on what the lock file means: an entry exists for a key if and only if that\n * key has been successfully translated to every target locale.\n */\n\n/** Result of comparing the source dictionary against the lock file */\nexport interface LockDiff {\n /** Keys that are new or changed and need translation */\n changedKeys: Record<string, string>;\n /**\n * Lock entries for the changed keys. These are *pending*: they must only\n * be committed to the lock after translation succeeds for all locales.\n */\n pendingEntries: Record<string, LockFileEntry>;\n /** Keys present in the lock but no longer in the source dictionary */\n deletedKeys: string[];\n}\n\n/**\n * Compare the source dictionary against the lock file.\n *\n * When `contexts` is provided (Vite auto-extraction), a context change also\n * marks a key as changed and the new context is recorded in the pending\n * entry. When `contexts` is omitted (CLI), existing lock contexts are\n * preserved unchanged so a CLI run never clobbers Vite-written contexts.\n */\nexport function diffLock(\n sourceDict: Record<string, string>,\n lock: LockFile,\n contexts?: Record<string, string>,\n): LockDiff {\n const changedKeys: Record<string, string> = {};\n const pendingEntries: Record<string, LockFileEntry> = {};\n\n for (const [key, value] of Object.entries(sourceDict)) {\n const hash = hashContent(value);\n const existing = lock.keys[key];\n const newContext = contexts ? contexts[key] : existing?.context;\n const contextChanged =\n contexts !== undefined && existing?.context !== contexts[key];\n\n // Re-translate if the key is new, content changed, or context changed\n if (!existing || existing.hash !== hash || contextChanged) {\n changedKeys[key] = value;\n pendingEntries[key] = { hash, source: value, context: newContext };\n }\n }\n\n const deletedKeys = Object.keys(lock.keys).filter(\n (key) => !(key in sourceDict),\n );\n\n return { changedKeys, pendingEntries, deletedKeys };\n}\n\n/** Translate one batch of changed keys for one target locale */\nexport type TranslateFn = (\n batch: Record<string, string>,\n targetLocale: string,\n contexts: Record<string, string>,\n) => Promise<Record<string, string>>;\n\n/** A translation batch that failed for a target locale */\nexport interface SyncFailure {\n locale: string;\n keys: string[];\n error: unknown;\n}\n\nexport interface SyncResult {\n status: \"no-source\" | \"no-changes\" | \"synced\";\n /** Keys translated successfully for ALL target locales (recorded in lock) */\n translatedKeys: string[];\n /** Keys removed from source and pruned from targets + lock */\n deletedKeys: string[];\n /** Failed batches. Non-empty means the run must be treated as failed. */\n failures: SyncFailure[];\n}\n\nexport interface SyncOptions {\n localesDir: string;\n sourceLocale: string;\n targetLocales: string[];\n batchSize: number;\n translate: TranslateFn;\n /**\n * Context hints from auto-extraction (Vite). Omit to preserve existing\n * lock contexts (CLI).\n */\n contexts?: Record<string, string>;\n log?: (message: string) => void;\n}\n\n/**\n * Sync source locale changes into target locale files and the lock file.\n *\n * Guarantees:\n * - Lock entries are committed only for keys whose batches succeeded for\n * every target locale — the lock never claims a key is translated when\n * it isn't. Failed keys stay \"changed\" and are retried on the next run.\n * - Successfully translated batches are still written even when other\n * batches fail; callers must surface `failures` (exit non-zero / throw).\n * - Deleted source keys are pruned from target files and the lock even\n * when there is nothing to translate (no AI calls needed).\n */\nexport async function syncLocaleFiles(\n options: SyncOptions,\n): Promise<SyncResult> {\n const {\n localesDir,\n sourceLocale,\n targetLocales,\n batchSize,\n translate,\n contexts,\n log = () => {},\n } = options;\n\n const sourceFilePath = join(localesDir, `${sourceLocale}.json`);\n if (!existsSync(sourceFilePath)) {\n return {\n status: \"no-source\",\n translatedKeys: [],\n deletedKeys: [],\n failures: [],\n };\n }\n\n const sourceDict: Record<string, string> = JSON.parse(\n readFileSync(sourceFilePath, \"utf-8\"),\n );\n\n const lockFilePath = join(localesDir, \".solid-translate.lock\");\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 const { changedKeys, pendingEntries, deletedKeys } = diffLock(\n sourceDict,\n lock,\n contexts,\n );\n\n // Remove keys that no longer exist in source\n for (const key of deletedKeys) {\n delete lock.keys[key];\n }\n\n const changedCount = Object.keys(changedKeys).length;\n\n if (changedCount === 0 && deletedKeys.length === 0) {\n log(\"No changes detected in locale files.\");\n return {\n status: \"no-changes\",\n translatedKeys: [],\n deletedKeys: [],\n failures: [],\n };\n }\n\n if (changedCount === 0) {\n // Deletions only — prune target files and the lock, no AI calls needed\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(localesDir, `${targetLocale}.json`);\n const existing = readTargetFile(targetFilePath);\n writeTargetFile(targetFilePath, existing, sourceDict);\n log(` ${targetLocale}: pruned deleted keys`);\n }\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n log(\n `Removed ${deletedKeys.length} deleted key${deletedKeys.length > 1 ? \"s\" : \"\"} from target locales.`,\n );\n return { status: \"synced\", translatedKeys: [], deletedKeys, failures: [] };\n }\n\n log(\n `Translating ${changedCount} key${changedCount > 1 ? \"s\" : \"\"} to ${targetLocales.length} locale${targetLocales.length > 1 ? \"s\" : \"\"}...`,\n );\n\n // Context hints for the changed keys, passed to the translator\n const changedContexts: Record<string, string> = {};\n for (const key of Object.keys(changedKeys)) {\n const ctx = pendingEntries[key]?.context;\n if (ctx) changedContexts[key] = ctx;\n }\n\n const failures: SyncFailure[] = [];\n const failedKeys = new Set<string>();\n\n for (const targetLocale of targetLocales) {\n const targetFilePath = join(localesDir, `${targetLocale}.json`);\n\n // Load existing translations to preserve unchanged keys\n const existing = readTargetFile(targetFilePath);\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(entries.slice(i, i + batchSize));\n try {\n const translated = await translate(\n batch,\n targetLocale,\n changedContexts,\n );\n Object.assign(existing, translated);\n } catch (err) {\n failures.push({\n locale: targetLocale,\n keys: Object.keys(batch),\n error: err,\n });\n for (const key of Object.keys(batch)) {\n failedKeys.add(key);\n }\n }\n }\n\n writeTargetFile(targetFilePath, existing, sourceDict);\n log(` ${targetLocale}: ${Object.keys(existing).length} keys`);\n }\n\n // Commit lock entries only for keys that succeeded for ALL target locales.\n // Failed keys keep their old entry (or none), so the next run retries them.\n const translatedKeys: string[] = [];\n for (const [key, entry] of Object.entries(pendingEntries)) {\n if (failedKeys.has(key)) continue;\n lock.keys[key] = entry;\n translatedKeys.push(key);\n }\n\n writeFileSync(lockFilePath, JSON.stringify(lock, null, 2) + \"\\n\");\n\n return { status: \"synced\", translatedKeys, deletedKeys, failures };\n}\n\n/** Format sync failures into a human-readable, single-line-per-batch report */\nexport function formatSyncFailures(failures: SyncFailure[]): string[] {\n return failures.map((failure) => {\n const message =\n failure.error instanceof Error\n ? failure.error.message\n : String(failure.error);\n return `${failure.locale}: ${failure.keys.length} key${failure.keys.length > 1 ? \"s\" : \"\"} [${failure.keys.join(\", \")}] — ${message}`;\n });\n}\n\nfunction readTargetFile(targetFilePath: string): Record<string, string> {\n if (!existsSync(targetFilePath)) return {};\n try {\n return JSON.parse(readFileSync(targetFilePath, \"utf-8\"));\n } catch {\n // Corrupted file — regenerate\n return {};\n }\n}\n\nfunction writeTargetFile(\n targetFilePath: string,\n translations: Record<string, string>,\n sourceDict: Record<string, string>,\n): void {\n // Remove keys that no longer exist in source\n for (const key of Object.keys(translations)) {\n if (!(key in sourceDict)) {\n delete translations[key];\n }\n }\n\n // Sort keys for stable, diff-friendly output\n const sorted = Object.fromEntries(\n Object.entries(translations).sort(([a], [b]) => a.localeCompare(b)),\n );\n\n writeFileSync(targetFilePath, JSON.stringify(sorted, null, 2) + \"\\n\");\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync","existsSync","join","existsSync","join","readFileSync","writeFileSync"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-translate",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,10 +17,14 @@
17
17
  "./vite": {
18
18
  "types": "./dist/vite.d.ts",
19
19
  "import": "./dist/vite.js"
20
+ },
21
+ "./virtual": {
22
+ "types": "./virtual.d.ts"
20
23
  }
21
24
  },
22
25
  "files": [
23
26
  "dist",
27
+ "virtual.d.ts",
24
28
  "README.md",
25
29
  "LICENSE"
26
30
  ],
package/virtual.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Ambient types for the solid-translate virtual modules.
3
+ *
4
+ * Add to your project via a triple-slash reference:
5
+ *
6
+ * ```ts
7
+ * /// <reference types="solid-translate/virtual" />
8
+ * ```
9
+ *
10
+ * or in `tsconfig.json`:
11
+ *
12
+ * ```json
13
+ * { "compilerOptions": { "types": ["solid-translate/virtual"] } }
14
+ * ```
15
+ */
16
+
17
+ declare module "virtual:solid-translate" {
18
+ /** All translations keyed by locale code (eager — inlined at build time) */
19
+ const translations: Record<string, Record<string, string>>;
20
+ export default translations;
21
+ }
22
+
23
+ declare module "virtual:solid-translate/lazy" {
24
+ /** Source locale code */
25
+ export const sourceLocale: string;
26
+ /** All available locale codes (source + targets) */
27
+ export const locales: string[];
28
+ /** Per-locale dictionary loaders (each is its own code-split chunk) */
29
+ export const loaders: Record<
30
+ string,
31
+ () => Promise<Record<string, string>>
32
+ >;
33
+ const manifest: {
34
+ sourceLocale: string;
35
+ locales: string[];
36
+ loaders: Record<string, () => Promise<Record<string, string>>>;
37
+ };
38
+ export default manifest;
39
+ }
40
+
41
+ declare module "virtual:solid-translate/locale/*" {
42
+ /** A single locale's translation dictionary */
43
+ const dictionary: Record<string, string>;
44
+ export default dictionary;
45
+ }