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 +161 -6
- package/dist/chunk-2BKJUY37.js +82 -0
- package/dist/cli.js +15053 -117
- package/dist/index.d.ts +38 -6
- package/dist/index.js +78 -27
- package/dist/index.js.map +1 -1
- package/dist/translate-M737VQHG.js +10 -0
- package/dist/vite.js +14983 -47
- package/dist/vite.js.map +1 -1
- package/package.json +10 -3
- package/virtual.d.ts +45 -0
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
};
|