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 +161 -6
- package/dist/chunk-2BKJUY37.js +82 -0
- package/dist/cli.js +160 -84
- package/dist/index.d.ts +33 -5
- package/dist/index.js +55 -5
- package/dist/index.js.map +1 -1
- package/dist/translate-M737VQHG.js +10 -0
- package/dist/vite.js +68 -15
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
- 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
|
+
};
|
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/
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
/**
|
|
197
|
-
|
|
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
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
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"]}
|
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
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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.
|
|
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
|
+
}
|