solid-translate 1.1.1 → 1.3.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
+ };