@syedamirali/i18n-toolkit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/dist/cli.js +109 -74
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Requires **Node ≥ 20**. For `translate` you also need a running Ollama instanc
|
|
|
31
31
|
i18n-toolkit extract --src /path/to/your/app/src
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Output: `output/{timestamp}_translation_extractor.json` — sorted, deterministic.
|
|
34
|
+
Output: `i18n-toolkit/output/{timestamp}_translation_extractor.json` — sorted, deterministic.
|
|
35
35
|
|
|
36
36
|
**What it matches** (all AST-based, no regex):
|
|
37
37
|
|
|
@@ -48,8 +48,8 @@ Output: `output/{timestamp}_translation_extractor.json` — sorted, deterministi
|
|
|
48
48
|
```
|
|
49
49
|
--src <path> root to scan (default ".")
|
|
50
50
|
--out <path> explicit output file (overrides --output-dir)
|
|
51
|
-
--output-dir <path> default ./output
|
|
52
|
-
--log-dir <path> default ./logs
|
|
51
|
+
--output-dir <path> default ./i18n-toolkit/output
|
|
52
|
+
--log-dir <path> default ./i18n-toolkit/logs
|
|
53
53
|
--lang <code> top-level language key (default "en")
|
|
54
54
|
--fail-on-conflict exit 1 if any key has conflicting defaults
|
|
55
55
|
--help full options list
|
|
@@ -70,7 +70,7 @@ i18n-toolkit diff
|
|
|
70
70
|
`--src` accepts either a directory (runs extraction in-process) or a previously-extracted JSON file.
|
|
71
71
|
`--resource` auto-detects flat `{ key: value }` vs language-wrapped `{ "en": { … } }` and picks the `en` slice.
|
|
72
72
|
|
|
73
|
-
**Output:** `diff-output/{timestamp}/`
|
|
73
|
+
**Output:** `i18n-toolkit/diff-output/{timestamp}/`
|
|
74
74
|
|
|
75
75
|
| File | What's in it |
|
|
76
76
|
|---|---|
|
|
@@ -96,7 +96,7 @@ Interactive. Three steps:
|
|
|
96
96
|
|
|
97
97
|
The Ollama call streams with live thinking + response phases and a sticky status footer (elapsed / tokens / KB).
|
|
98
98
|
|
|
99
|
-
**Output:** `translate-output/{timestamp}/translation.json`
|
|
99
|
+
**Output:** `i18n-toolkit/translate-output/{timestamp}/translation.json`
|
|
100
100
|
|
|
101
101
|
```json
|
|
102
102
|
{
|
|
@@ -121,15 +121,17 @@ NO_COLOR disable ANSI colors
|
|
|
121
121
|
The three commands compose:
|
|
122
122
|
|
|
123
123
|
```
|
|
124
|
-
your-app/src/ ──extract──▶ output/…json
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
resource.json ──diff──▶ diff-output/…/missing_in_resource.json
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
translate ──▶
|
|
124
|
+
your-app/src/ ──extract──▶ i18n-toolkit/output/…json
|
|
125
|
+
│
|
|
126
|
+
▼
|
|
127
|
+
resource.json ──diff──▶ i18n-toolkit/diff-output/…/missing_in_resource.json
|
|
128
|
+
│
|
|
129
|
+
▼
|
|
130
|
+
translate ──▶ i18n-toolkit/translate-output/…/translation.json
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
All four output directories live under a single `i18n-toolkit/` workspace folder in your project root, so you only have **one** entry to ignore (`i18n-toolkit/`) — and the tool actually drops an auto-generated `i18n-toolkit/.gitignore` for you on first run, so you don't even have to do that.
|
|
134
|
+
|
|
133
135
|
Drop the final `translation.json` back into your app's translation resource and the cycle closes.
|
|
134
136
|
|
|
135
137
|
---
|
|
@@ -169,7 +171,7 @@ docs/plans/ design docs (one per command)
|
|
|
169
171
|
dist/ bundled output (generated; not in git)
|
|
170
172
|
```
|
|
171
173
|
|
|
172
|
-
|
|
174
|
+
All generated artifacts go under `i18n-toolkit/` (auto-gitignored): `output/`, `logs/`, `diff-output/`, `translate-output/`.
|
|
173
175
|
|
|
174
176
|
---
|
|
175
177
|
|
package/dist/cli.js
CHANGED
|
@@ -5,18 +5,49 @@ import { parse } from "@babel/parser";
|
|
|
5
5
|
import _traverse from "@babel/traverse";
|
|
6
6
|
import * as t from "@babel/types";
|
|
7
7
|
import fg from "fast-glob";
|
|
8
|
-
import
|
|
8
|
+
import fs2 from "fs/promises";
|
|
9
9
|
import { createWriteStream } from "fs";
|
|
10
10
|
import os from "os";
|
|
11
|
-
import
|
|
11
|
+
import path2 from "path";
|
|
12
12
|
import { format as formatInspect } from "util";
|
|
13
|
+
|
|
14
|
+
// src/lib/workspace.ts
|
|
15
|
+
import fs from "fs/promises";
|
|
16
|
+
import path from "path";
|
|
17
|
+
var WORKSPACE_DIR_NAME = "i18n-toolkit";
|
|
18
|
+
var DEFAULT_DIRS = {
|
|
19
|
+
output: `./${WORKSPACE_DIR_NAME}/output`,
|
|
20
|
+
logs: `./${WORKSPACE_DIR_NAME}/logs`,
|
|
21
|
+
diffOutput: `./${WORKSPACE_DIR_NAME}/diff-output`,
|
|
22
|
+
translateOutput: `./${WORKSPACE_DIR_NAME}/translate-output`
|
|
23
|
+
};
|
|
24
|
+
var GITIGNORE_BODY = `# Auto-generated by @syedamirali/i18n-toolkit.
|
|
25
|
+
# Safe to delete or commit. Everything inside is build output.
|
|
26
|
+
*
|
|
27
|
+
!.gitignore
|
|
28
|
+
`;
|
|
29
|
+
async function ensureWorkspaceGitignore(targetPath) {
|
|
30
|
+
const workspaceAbs = path.resolve(WORKSPACE_DIR_NAME);
|
|
31
|
+
const targetAbs = path.resolve(targetPath);
|
|
32
|
+
if (targetAbs !== workspaceAbs && !targetAbs.startsWith(workspaceAbs + path.sep)) return;
|
|
33
|
+
await fs.mkdir(workspaceAbs, { recursive: true });
|
|
34
|
+
const gitignorePath = path.join(workspaceAbs, ".gitignore");
|
|
35
|
+
try {
|
|
36
|
+
await fs.stat(gitignorePath);
|
|
37
|
+
return;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
await fs.writeFile(gitignorePath, GITIGNORE_BODY, "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/index.ts
|
|
13
44
|
var traverse = _traverse.default ?? _traverse;
|
|
14
45
|
function parseArgs(argv) {
|
|
15
46
|
const opts = {
|
|
16
47
|
src: ".",
|
|
17
48
|
out: null,
|
|
18
|
-
outputDir:
|
|
19
|
-
logDir:
|
|
49
|
+
outputDir: DEFAULT_DIRS.output,
|
|
50
|
+
logDir: DEFAULT_DIRS.logs,
|
|
20
51
|
noLog: false,
|
|
21
52
|
lang: "en",
|
|
22
53
|
includeDynamic: true,
|
|
@@ -101,8 +132,8 @@ Usage:
|
|
|
101
132
|
Options:
|
|
102
133
|
--src <path> Root directory to scan (default ".")
|
|
103
134
|
--out <path> Output JSON file (overrides --output-dir)
|
|
104
|
-
--output-dir <path> Directory for auto-named output (default "./output")
|
|
105
|
-
--log-dir <path> Directory for run logs (default "./logs")
|
|
135
|
+
--output-dir <path> Directory for auto-named output (default "./i18n-toolkit/output")
|
|
136
|
+
--log-dir <path> Directory for run logs (default "./i18n-toolkit/logs")
|
|
106
137
|
--no-log Disable writing the per-run log file
|
|
107
138
|
--lang <code> Top-level language key (default "en")
|
|
108
139
|
--include-dynamic <bool> Emit dynamic keys with "__dynamic__" value (default true)
|
|
@@ -473,8 +504,8 @@ function extractFromFile(filePath, source, opts) {
|
|
|
473
504
|
}
|
|
474
505
|
traverse(ast, {
|
|
475
506
|
// Handle `t("k") || "fallback"` BEFORE the inner CallExpression visitor sees it.
|
|
476
|
-
LogicalExpression(
|
|
477
|
-
const node =
|
|
507
|
+
LogicalExpression(path5) {
|
|
508
|
+
const node = path5.node;
|
|
478
509
|
if (node.operator !== "||") return;
|
|
479
510
|
if (!t.isCallExpression(node.left)) return;
|
|
480
511
|
if (!isTCall(node.left)) return;
|
|
@@ -483,14 +514,14 @@ function extractFromFile(filePath, source, opts) {
|
|
|
483
514
|
handleTCall(node.left, fallback);
|
|
484
515
|
handledCalls.add(node.left);
|
|
485
516
|
},
|
|
486
|
-
CallExpression(
|
|
487
|
-
const node =
|
|
517
|
+
CallExpression(path5) {
|
|
518
|
+
const node = path5.node;
|
|
488
519
|
if (handledCalls.has(node)) return;
|
|
489
520
|
if (!isTCall(node)) return;
|
|
490
521
|
handleTCall(node, null);
|
|
491
522
|
},
|
|
492
|
-
JSXOpeningElement(
|
|
493
|
-
const openingEl =
|
|
523
|
+
JSXOpeningElement(path5) {
|
|
524
|
+
const openingEl = path5.node;
|
|
494
525
|
let localKeyAttr = null;
|
|
495
526
|
for (const attr of openingEl.attributes) {
|
|
496
527
|
if (!t.isJSXAttribute(attr)) continue;
|
|
@@ -509,7 +540,7 @@ function extractFromFile(filePath, source, opts) {
|
|
|
509
540
|
}
|
|
510
541
|
const keyInfo = getKeyFromNode(keyNode);
|
|
511
542
|
if (!keyInfo) return;
|
|
512
|
-
const parent =
|
|
543
|
+
const parent = path5.parent;
|
|
513
544
|
const children = t.isJSXElement(parent) ? parent.children : [];
|
|
514
545
|
let value;
|
|
515
546
|
if (openingEl.selfClosing || children.length === 0) {
|
|
@@ -526,7 +557,7 @@ function extractFromFile(filePath, source, opts) {
|
|
|
526
557
|
return records;
|
|
527
558
|
}
|
|
528
559
|
async function runExtraction(opts) {
|
|
529
|
-
const srcAbs =
|
|
560
|
+
const srcAbs = path2.resolve(opts.src);
|
|
530
561
|
const includeDynamic = opts.includeDynamic !== false;
|
|
531
562
|
const includeMissing = opts.includeMissing !== false;
|
|
532
563
|
const debug = opts.debug === true;
|
|
@@ -557,7 +588,7 @@ async function runExtraction(opts) {
|
|
|
557
588
|
const file = files[i];
|
|
558
589
|
let size;
|
|
559
590
|
try {
|
|
560
|
-
const st = await
|
|
591
|
+
const st = await fs2.stat(file);
|
|
561
592
|
size = st.size;
|
|
562
593
|
} catch (err) {
|
|
563
594
|
if (debug) console.warn(`${c.warn("[stat-error]")} ${c.path(file)}: ${err.message}`);
|
|
@@ -570,7 +601,7 @@ async function runExtraction(opts) {
|
|
|
570
601
|
}
|
|
571
602
|
let source;
|
|
572
603
|
try {
|
|
573
|
-
source = await
|
|
604
|
+
source = await fs2.readFile(file, "utf8");
|
|
574
605
|
} catch (err) {
|
|
575
606
|
if (debug) console.warn(`${c.warn("[read-error]")} ${c.path(file)}: ${err.message}`);
|
|
576
607
|
skippedFiles++;
|
|
@@ -579,7 +610,7 @@ async function runExtraction(opts) {
|
|
|
579
610
|
try {
|
|
580
611
|
perFileRecords[i] = extractFromFile(file, source, fileOpts);
|
|
581
612
|
parsedFiles++;
|
|
582
|
-
if (debug) console.log(`${c.ok("[parsed]")} ${c.path(
|
|
613
|
+
if (debug) console.log(`${c.ok("[parsed]")} ${c.path(path2.relative(process.cwd(), file))}`);
|
|
583
614
|
} catch {
|
|
584
615
|
parseErrorFiles++;
|
|
585
616
|
}
|
|
@@ -611,21 +642,23 @@ async function main() {
|
|
|
611
642
|
const opts = parseArgs(process.argv.slice(2));
|
|
612
643
|
const startedAt = /* @__PURE__ */ new Date();
|
|
613
644
|
const timestamp = formatTimestamp(startedAt);
|
|
614
|
-
const srcAbs =
|
|
645
|
+
const srcAbs = path2.resolve(opts.src);
|
|
615
646
|
let outAbs;
|
|
616
647
|
if (opts.out !== null) {
|
|
617
|
-
outAbs =
|
|
648
|
+
outAbs = path2.resolve(opts.out);
|
|
618
649
|
} else {
|
|
619
|
-
const outDirAbs =
|
|
620
|
-
await
|
|
621
|
-
|
|
650
|
+
const outDirAbs = path2.resolve(opts.outputDir);
|
|
651
|
+
await ensureWorkspaceGitignore(outDirAbs);
|
|
652
|
+
await fs2.mkdir(outDirAbs, { recursive: true });
|
|
653
|
+
outAbs = path2.join(outDirAbs, `${timestamp}_translation_extractor.json`);
|
|
622
654
|
}
|
|
623
655
|
let logTee = null;
|
|
624
656
|
let logAbs = null;
|
|
625
657
|
if (!opts.noLog) {
|
|
626
|
-
const logDirAbs =
|
|
627
|
-
await
|
|
628
|
-
|
|
658
|
+
const logDirAbs = path2.resolve(opts.logDir);
|
|
659
|
+
await ensureWorkspaceGitignore(logDirAbs);
|
|
660
|
+
await fs2.mkdir(logDirAbs, { recursive: true });
|
|
661
|
+
logAbs = path2.join(logDirAbs, `${timestamp}_translation_extractor.log`);
|
|
629
662
|
const stream = createWriteStream(logAbs, { flags: "a" });
|
|
630
663
|
logTee = setupLogTee(stream);
|
|
631
664
|
const tag = c.head("[translation-extractor]");
|
|
@@ -651,8 +684,8 @@ async function main() {
|
|
|
651
684
|
for (const conf of result.conflicts) {
|
|
652
685
|
const keptAbs = conf.kept.location.file;
|
|
653
686
|
const seenAbs = conf.seen.location.file;
|
|
654
|
-
const keptRel =
|
|
655
|
-
const seenRel =
|
|
687
|
+
const keptRel = path2.relative(process.cwd(), keptAbs);
|
|
688
|
+
const seenRel = path2.relative(process.cwd(), seenAbs);
|
|
656
689
|
const keptLine = conf.kept.location.line;
|
|
657
690
|
const seenLine = conf.seen.location.line;
|
|
658
691
|
const keptVal = JSON.stringify(conf.kept.value);
|
|
@@ -677,8 +710,8 @@ async function main() {
|
|
|
677
710
|
for (const [k, v] of result.entries) langObj[k] = v;
|
|
678
711
|
const output2 = { [opts.lang]: langObj };
|
|
679
712
|
const json = opts.pretty ? JSON.stringify(output2, null, 2) + "\n" : JSON.stringify(output2);
|
|
680
|
-
await
|
|
681
|
-
await
|
|
713
|
+
await fs2.mkdir(path2.dirname(outAbs), { recursive: true });
|
|
714
|
+
await fs2.writeFile(outAbs, json, "utf8");
|
|
682
715
|
const skippedNum = result.skippedFiles > 0 ? c.warn(result.skippedFiles) : c.value(result.skippedFiles);
|
|
683
716
|
const skipNote = result.skippedFiles > 0 ? c.label(" (too large or unreadable; rerun with --debug to include)") : "";
|
|
684
717
|
const conflictsNum = result.conflicts.length > 0 ? c.warn(result.conflicts.length) : c.ok(result.conflicts.length);
|
|
@@ -699,8 +732,8 @@ async function main() {
|
|
|
699
732
|
)
|
|
700
733
|
);
|
|
701
734
|
console.log(stat("Conflicts", String(conflictsNum)));
|
|
702
|
-
console.log(stat("Output", c.path(
|
|
703
|
-
if (logAbs) console.log(stat("Log", c.path(
|
|
735
|
+
console.log(stat("Output", c.path(path2.relative(process.cwd(), outAbs))));
|
|
736
|
+
if (logAbs) console.log(stat("Log", c.path(path2.relative(process.cwd(), logAbs))));
|
|
704
737
|
if (logTee) await logTee.teardown();
|
|
705
738
|
if (opts.failOnConflict && result.conflicts.length > 0) {
|
|
706
739
|
process.exit(1);
|
|
@@ -717,12 +750,12 @@ async function runCommand(_argv) {
|
|
|
717
750
|
|
|
718
751
|
// src/diff.ts
|
|
719
752
|
import { createInterface } from "readline/promises";
|
|
720
|
-
import
|
|
753
|
+
import fs3 from "fs/promises";
|
|
721
754
|
import { createWriteStream as createWriteStream2 } from "fs";
|
|
722
|
-
import
|
|
755
|
+
import path3 from "path";
|
|
723
756
|
import { stdin as input, stdout as output } from "process";
|
|
724
757
|
var TARGET_LANG = "en";
|
|
725
|
-
var DIFF_OUTPUT_DIR =
|
|
758
|
+
var DIFF_OUTPUT_DIR = DEFAULT_DIRS.diffOutput;
|
|
726
759
|
function parseDiffArgs(argv) {
|
|
727
760
|
const opts = { src: null, resource: null };
|
|
728
761
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -753,9 +786,9 @@ function cleanPath(raw) {
|
|
|
753
786
|
}
|
|
754
787
|
async function resolveSourcePath(p) {
|
|
755
788
|
if (!p) return { error: "Empty input." };
|
|
756
|
-
const abs =
|
|
789
|
+
const abs = path3.resolve(p);
|
|
757
790
|
try {
|
|
758
|
-
const st = await
|
|
791
|
+
const st = await fs3.stat(abs);
|
|
759
792
|
if (st.isDirectory()) return { kind: "directory", absPath: abs };
|
|
760
793
|
if (st.isFile()) {
|
|
761
794
|
if (!abs.toLowerCase().endsWith(".json")) return { error: `Not a .json file: ${abs}` };
|
|
@@ -768,9 +801,9 @@ async function resolveSourcePath(p) {
|
|
|
768
801
|
}
|
|
769
802
|
async function resolveResourcePath(p) {
|
|
770
803
|
if (!p) return { error: "Empty input." };
|
|
771
|
-
const abs =
|
|
804
|
+
const abs = path3.resolve(p);
|
|
772
805
|
try {
|
|
773
|
-
const st = await
|
|
806
|
+
const st = await fs3.stat(abs);
|
|
774
807
|
if (!st.isFile()) return { error: `Not a file: ${abs}` };
|
|
775
808
|
if (!abs.toLowerCase().endsWith(".json")) return { error: `Not a .json file: ${abs}` };
|
|
776
809
|
return { absPath: abs };
|
|
@@ -810,7 +843,7 @@ async function promptForResource(rl) {
|
|
|
810
843
|
}
|
|
811
844
|
}
|
|
812
845
|
async function loadKeyMap(filePath, sourceLabel) {
|
|
813
|
-
const text = await
|
|
846
|
+
const text = await fs3.readFile(filePath, "utf8");
|
|
814
847
|
const json = JSON.parse(text);
|
|
815
848
|
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
816
849
|
throw new Error(`${sourceLabel}: expected a JSON object at the top level`);
|
|
@@ -848,11 +881,12 @@ async function loadKeyMap(filePath, sourceLabel) {
|
|
|
848
881
|
async function main2() {
|
|
849
882
|
const startedAt = /* @__PURE__ */ new Date();
|
|
850
883
|
const timestamp = formatTimestamp(startedAt);
|
|
851
|
-
const runDir =
|
|
852
|
-
await
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
const
|
|
884
|
+
const runDir = path3.resolve(DIFF_OUTPUT_DIR, timestamp);
|
|
885
|
+
await ensureWorkspaceGitignore(runDir);
|
|
886
|
+
await fs3.mkdir(runDir, { recursive: true });
|
|
887
|
+
const logAbs = path3.join(runDir, `${timestamp}_translation_diff.log`);
|
|
888
|
+
const missingInResourcePath = path3.join(runDir, "missing_in_resource.json");
|
|
889
|
+
const missingInExtractorPath = path3.join(runDir, "missing_in_extractor.json");
|
|
856
890
|
const stream = createWriteStream2(logAbs, { flags: "a" });
|
|
857
891
|
const logTee = setupLogTee(stream);
|
|
858
892
|
const tag = c.head("[translation-diff]");
|
|
@@ -983,8 +1017,8 @@ async function main2() {
|
|
|
983
1017
|
return out;
|
|
984
1018
|
};
|
|
985
1019
|
const wrap2 = (m) => ({ [TARGET_LANG]: sortedObject(m) });
|
|
986
|
-
await
|
|
987
|
-
await
|
|
1020
|
+
await fs3.writeFile(missingInResourcePath, JSON.stringify(wrap2(missingInResource), null, 2) + "\n", "utf8");
|
|
1021
|
+
await fs3.writeFile(missingInExtractorPath, JSON.stringify(wrap2(missingInExtractor), null, 2) + "\n", "utf8");
|
|
988
1022
|
const labels = ["Extractor keys", "Resource keys", "Dynamic skipped", "Missing in resource", "Missing in extractor", "Output dir"];
|
|
989
1023
|
const pad = Math.max(...labels.map((l) => l.length));
|
|
990
1024
|
const stat = (label, value) => `${c.label(label.padEnd(pad) + " :")} ${value}`;
|
|
@@ -1007,10 +1041,10 @@ async function main2() {
|
|
|
1007
1041
|
`${Object.keys(missingInExtractor).length > 0 ? c.warn(Object.keys(missingInExtractor).length) : c.ok(0)} ${c.label("(in resource, not in extractor)")}`
|
|
1008
1042
|
)
|
|
1009
1043
|
);
|
|
1010
|
-
console.log(stat("Output dir", c.path(
|
|
1044
|
+
console.log(stat("Output dir", c.path(path3.relative(process.cwd(), runDir))));
|
|
1011
1045
|
console.log(` ${c.label("\u251C\u2500")} ${c.path("missing_in_resource.json")}`);
|
|
1012
1046
|
console.log(` ${c.label("\u251C\u2500")} ${c.path("missing_in_extractor.json")}`);
|
|
1013
|
-
console.log(` ${c.label("\u2514\u2500")} ${c.path(
|
|
1047
|
+
console.log(` ${c.label("\u2514\u2500")} ${c.path(path3.basename(logAbs))}`);
|
|
1014
1048
|
await logTee.teardown();
|
|
1015
1049
|
}
|
|
1016
1050
|
async function runCommand2(_argv) {
|
|
@@ -1023,9 +1057,9 @@ async function runCommand2(_argv) {
|
|
|
1023
1057
|
}
|
|
1024
1058
|
|
|
1025
1059
|
// src/translate.ts
|
|
1026
|
-
import
|
|
1060
|
+
import fs4 from "fs/promises";
|
|
1027
1061
|
import { createWriteStream as createWriteStream3 } from "fs";
|
|
1028
|
-
import
|
|
1062
|
+
import path4 from "path";
|
|
1029
1063
|
import { fileURLToPath } from "url";
|
|
1030
1064
|
import { checkbox, select, input as inquirerInput, Separator } from "@inquirer/prompts";
|
|
1031
1065
|
|
|
@@ -1533,12 +1567,12 @@ function formatError(e) {
|
|
|
1533
1567
|
}
|
|
1534
1568
|
|
|
1535
1569
|
// src/translate.ts
|
|
1536
|
-
var SCRIPT_DIR =
|
|
1537
|
-
var
|
|
1538
|
-
var PROMPT_PATH =
|
|
1539
|
-
var LANGUAGES_JSON_PATH =
|
|
1540
|
-
var DIFF_OUTPUT_DIR2 =
|
|
1541
|
-
var TRANSLATE_OUTPUT_DIR =
|
|
1570
|
+
var SCRIPT_DIR = path4.dirname(fileURLToPath(import.meta.url));
|
|
1571
|
+
var PACKAGE_ROOT = path4.resolve(SCRIPT_DIR, "..");
|
|
1572
|
+
var PROMPT_PATH = path4.resolve(PACKAGE_ROOT, "prompt/translation-prompt.md");
|
|
1573
|
+
var LANGUAGES_JSON_PATH = path4.resolve(PACKAGE_ROOT, "data/languages.json");
|
|
1574
|
+
var DIFF_OUTPUT_DIR2 = path4.resolve(process.cwd(), DEFAULT_DIRS.diffOutput);
|
|
1575
|
+
var TRANSLATE_OUTPUT_DIR = path4.resolve(process.cwd(), DEFAULT_DIRS.translateOutput);
|
|
1542
1576
|
var PLACEHOLDER_CONTENT = "PASTE CONTENT HERE";
|
|
1543
1577
|
var PLACEHOLDER_LANGS = "LANGUAGES TO PRODUCE";
|
|
1544
1578
|
var SOURCE_LANG = "en";
|
|
@@ -1588,17 +1622,17 @@ function cleanPath2(raw) {
|
|
|
1588
1622
|
async function findRecentDiffOutputs(max = 5) {
|
|
1589
1623
|
let dirEntries = [];
|
|
1590
1624
|
try {
|
|
1591
|
-
dirEntries = await
|
|
1625
|
+
dirEntries = await fs4.readdir(DIFF_OUTPUT_DIR2);
|
|
1592
1626
|
} catch {
|
|
1593
1627
|
return [];
|
|
1594
1628
|
}
|
|
1595
1629
|
const candidates = [];
|
|
1596
1630
|
for (const dirName of dirEntries) {
|
|
1597
|
-
const filePath =
|
|
1631
|
+
const filePath = path4.join(DIFF_OUTPUT_DIR2, dirName, "missing_in_resource.json");
|
|
1598
1632
|
try {
|
|
1599
|
-
const st = await
|
|
1633
|
+
const st = await fs4.stat(filePath);
|
|
1600
1634
|
if (!st.isFile()) continue;
|
|
1601
|
-
const text = await
|
|
1635
|
+
const text = await fs4.readFile(filePath, "utf8");
|
|
1602
1636
|
const json = JSON.parse(text);
|
|
1603
1637
|
const enSlice = json[SOURCE_LANG] ?? json;
|
|
1604
1638
|
const keyCount = typeof enSlice === "object" && enSlice !== null ? Object.keys(enSlice).length : 0;
|
|
@@ -1609,7 +1643,7 @@ async function findRecentDiffOutputs(max = 5) {
|
|
|
1609
1643
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1610
1644
|
return candidates.slice(0, max).map((it) => ({
|
|
1611
1645
|
absPath: it.abs,
|
|
1612
|
-
relPath:
|
|
1646
|
+
relPath: path4.relative(process.cwd(), it.abs),
|
|
1613
1647
|
mtimeMs: it.mtimeMs,
|
|
1614
1648
|
keyCount: it.keyCount,
|
|
1615
1649
|
ageLabel: humanAge(Date.now() - it.mtimeMs)
|
|
@@ -1626,7 +1660,7 @@ function humanAge(ms) {
|
|
|
1626
1660
|
return `${d}d ago`;
|
|
1627
1661
|
}
|
|
1628
1662
|
async function loadLanguages() {
|
|
1629
|
-
const text = await
|
|
1663
|
+
const text = await fs4.readFile(LANGUAGES_JSON_PATH, "utf8");
|
|
1630
1664
|
const arr = JSON.parse(text);
|
|
1631
1665
|
return arr.filter((l) => l && l.active && l.code);
|
|
1632
1666
|
}
|
|
@@ -1716,7 +1750,7 @@ async function promptInput(recents) {
|
|
|
1716
1750
|
choices
|
|
1717
1751
|
});
|
|
1718
1752
|
if (pick.kind === "recent") {
|
|
1719
|
-
const text = await
|
|
1753
|
+
const text = await fs4.readFile(pick.file.absPath, "utf8");
|
|
1720
1754
|
return {
|
|
1721
1755
|
sourceLabel: pick.file.absPath,
|
|
1722
1756
|
keyMap: parseEnglishKeyMap(text, pick.file.absPath)
|
|
@@ -1728,15 +1762,15 @@ async function promptInput(recents) {
|
|
|
1728
1762
|
message: "Paste a JSON file path:",
|
|
1729
1763
|
validate: (v) => v.trim().length > 0 ? true : "Path required"
|
|
1730
1764
|
});
|
|
1731
|
-
const p =
|
|
1765
|
+
const p = path4.resolve(cleanPath2(raw));
|
|
1732
1766
|
try {
|
|
1733
|
-
const st = await
|
|
1767
|
+
const st = await fs4.stat(p);
|
|
1734
1768
|
if (!st.isFile() || !p.toLowerCase().endsWith(".json")) {
|
|
1735
1769
|
console.log(c.err(` Not a .json file: ${p}
|
|
1736
1770
|
`));
|
|
1737
1771
|
continue;
|
|
1738
1772
|
}
|
|
1739
|
-
const text = await
|
|
1773
|
+
const text = await fs4.readFile(p, "utf8");
|
|
1740
1774
|
return { sourceLabel: p, keyMap: parseEnglishKeyMap(text, p) };
|
|
1741
1775
|
} catch (err) {
|
|
1742
1776
|
console.log(c.err(` ${err.message}
|
|
@@ -1992,10 +2026,11 @@ function printHeader(args) {
|
|
|
1992
2026
|
async function main3() {
|
|
1993
2027
|
const startedAt = /* @__PURE__ */ new Date();
|
|
1994
2028
|
const timestamp = formatTimestamp(startedAt);
|
|
1995
|
-
const runDir =
|
|
1996
|
-
await
|
|
1997
|
-
|
|
1998
|
-
const
|
|
2029
|
+
const runDir = path4.join(TRANSLATE_OUTPUT_DIR, timestamp);
|
|
2030
|
+
await ensureWorkspaceGitignore(runDir);
|
|
2031
|
+
await fs4.mkdir(runDir, { recursive: true });
|
|
2032
|
+
const outputPath = path4.join(runDir, "translation.json");
|
|
2033
|
+
const logAbs = path4.join(runDir, `${timestamp}_translate.log`);
|
|
1999
2034
|
const stream = createWriteStream3(logAbs, { flags: "a" });
|
|
2000
2035
|
const logTee = setupLogTee(stream);
|
|
2001
2036
|
const tag = c.head("[translation-translate]");
|
|
@@ -2027,9 +2062,9 @@ async function main3() {
|
|
|
2027
2062
|
sourceLabel: picked.sourceLabel,
|
|
2028
2063
|
keyCount: Object.keys(picked.keyMap).length,
|
|
2029
2064
|
langs: selectedLangs,
|
|
2030
|
-
outputDir:
|
|
2065
|
+
outputDir: path4.relative(process.cwd(), runDir)
|
|
2031
2066
|
});
|
|
2032
|
-
const promptTemplate = await
|
|
2067
|
+
const promptTemplate = await fs4.readFile(PROMPT_PATH, "utf8");
|
|
2033
2068
|
const prompt = renderPrompt(promptTemplate, selectedLangs, picked.keyMap);
|
|
2034
2069
|
const t0 = Date.now();
|
|
2035
2070
|
const spinner = new Spinner();
|
|
@@ -2041,7 +2076,7 @@ async function main3() {
|
|
|
2041
2076
|
parsed = JSON.parse(jsonText);
|
|
2042
2077
|
} catch (e) {
|
|
2043
2078
|
const debugPath = `${outputPath}.raw.txt`;
|
|
2044
|
-
await
|
|
2079
|
+
await fs4.writeFile(debugPath, response, "utf8");
|
|
2045
2080
|
throw new Error(
|
|
2046
2081
|
`Failed to parse JSON from model output. Raw response saved to ${debugPath}.
|
|
2047
2082
|
${e.message}`
|
|
@@ -2051,7 +2086,7 @@ ${e.message}`
|
|
|
2051
2086
|
const expectedKeys = Object.keys(picked.keyMap);
|
|
2052
2087
|
const report = validateTranslation(parsed, expectedLangs, expectedKeys);
|
|
2053
2088
|
const sorted = sortedLangBundle(parsed, expectedLangs, expectedKeys);
|
|
2054
|
-
await
|
|
2089
|
+
await fs4.writeFile(outputPath, JSON.stringify(sorted, null, 2) + "\n", "utf8");
|
|
2055
2090
|
const responseRate = metrics.evalTokens && metrics.evalMs ? (metrics.evalTokens / metrics.evalMs * 1e3).toFixed(1) : "\u2014";
|
|
2056
2091
|
process.stdout.write(`
|
|
2057
2092
|
${rule()}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syedamirali/i18n-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AST-based CLI to extract translation keys from React/Next.js code, diff them against translation files, and translate missing keys via a local Ollama instance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Syed Amir Ali",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/SyedAmirAli/i18n-toolkit.git"
|
|
10
|
+
"url": "git+https://github.com/SyedAmirAli/i18n-toolkit.git"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://github.com/SyedAmirAli/i18n-toolkit#readme",
|
|
13
13
|
"bugs": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=20"
|
|
32
32
|
},
|
|
33
33
|
"bin": {
|
|
34
|
-
"i18n-toolkit": "
|
|
34
|
+
"i18n-toolkit": "dist/cli.js"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|