@better-translate/cli 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @better-translate/cli
2
2
 
3
- `@better-translate/cli` generates translated message files and localized markdown from one source locale. Use it when you want Better Translate to create or update locale files for you.
3
+ `@better-translate/cli` extracts marked source strings into your source locale file, generates translated message files, and localizes markdown. Use it when you want Better Translate to create or update locale files for you.
4
+
5
+ Install the CLI and the provider package you want to use in your own project:
6
+
7
+ ```sh
8
+ npm install -D @better-translate/cli @ai-sdk/openai
9
+ # or: npm install -D @better-translate/cli @ai-sdk/anthropic
10
+ # or: npm install -D @better-translate/cli @ai-sdk/moonshotai
11
+ ```
12
+
13
+ Then configure the CLI with a real AI SDK language model. The flow is the same for any provider package:
14
+
15
+ ```ts
16
+ import { openai } from "@ai-sdk/openai";
17
+ import { defineConfig } from "@better-translate/cli/config";
18
+
19
+ export default defineConfig({
20
+ sourceLocale: "en",
21
+ locales: ["es", "fr"],
22
+ model: openai("gpt-5"),
23
+ messages: {
24
+ entry: "./src/messages/en.json",
25
+ },
26
+ });
27
+ ```
28
+
29
+ ```ts
30
+ import { anthropic } from "@ai-sdk/anthropic";
31
+ import { defineConfig } from "@better-translate/cli/config";
32
+
33
+ export default defineConfig({
34
+ sourceLocale: "en",
35
+ locales: ["es", "fr"],
36
+ model: anthropic("claude-sonnet-4-5"),
37
+ messages: {
38
+ entry: "./src/messages/en.json",
39
+ },
40
+ });
41
+ ```
42
+
43
+ ```ts
44
+ import { moonshotai } from "@ai-sdk/moonshotai";
45
+ import { defineConfig } from "@better-translate/cli/config";
46
+
47
+ export default defineConfig({
48
+ sourceLocale: "en",
49
+ locales: ["es", "fr"],
50
+ model: moonshotai("kimi-k2-0905-preview"),
51
+ messages: {
52
+ entry: "./src/messages/en.json",
53
+ },
54
+ });
55
+ ```
56
+
57
+ If you need provider-specific settings, create the model in your app first and pass it through. Credentials and provider configuration stay entirely in the provider package setup:
58
+
59
+ ```ts
60
+ import { createOpenAI } from "@ai-sdk/openai";
61
+ import { defineConfig } from "@better-translate/cli/config";
62
+
63
+ const model = createOpenAI({
64
+ apiKey: process.env.OPENAI_API_KEY,
65
+ baseURL: process.env.OPENAI_BASE_URL,
66
+ })("gpt-5");
67
+
68
+ export default defineConfig({
69
+ sourceLocale: "en",
70
+ locales: ["es", "fr"],
71
+ model,
72
+ messages: {
73
+ entry: "./src/messages/en.json",
74
+ },
75
+ });
76
+ ```
4
77
 
5
78
  Full docs: [better-translate-placeholder.com/en/docs/cli](https://better-translate-placeholder.com/en/docs/cli)
@@ -0,0 +1,89 @@
1
+ // src/ai-sdk-generator.ts
2
+ function validateGeneratedValue(value, request) {
3
+ if (!request.validate) {
4
+ return value;
5
+ }
6
+ return request.validate(value);
7
+ }
8
+ function createOutputValidator(request) {
9
+ return {
10
+ validate(value) {
11
+ try {
12
+ return {
13
+ success: true,
14
+ value: validateGeneratedValue(value, request)
15
+ };
16
+ } catch (error) {
17
+ return {
18
+ error: error instanceof Error ? error : new Error(String(error)),
19
+ success: false
20
+ };
21
+ }
22
+ }
23
+ };
24
+ }
25
+ function isSchemaTooLargeError(error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ const normalizedMessage = message.toLowerCase();
28
+ return normalizedMessage.includes("compiled grammar is too large") || normalizedMessage.includes("reduce the number of strict tools") || normalizedMessage.includes("simplify your tool schemas") || normalizedMessage.includes("tool schemas");
29
+ }
30
+ function extractJsonPayload(text) {
31
+ const trimmed = text.trim();
32
+ const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
33
+ if (fencedMatch?.[1]) {
34
+ return fencedMatch[1].trim();
35
+ }
36
+ const firstBrace = trimmed.indexOf("{");
37
+ const lastBrace = trimmed.lastIndexOf("}");
38
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
39
+ return trimmed.slice(firstBrace, lastBrace + 1);
40
+ }
41
+ return trimmed;
42
+ }
43
+ function parseJsonText(text, request) {
44
+ const payload = extractJsonPayload(text);
45
+ try {
46
+ return validateGeneratedValue(JSON.parse(payload), request);
47
+ } catch (error) {
48
+ const reason = error instanceof Error ? error.message : String(error);
49
+ throw new Error(
50
+ `Fallback JSON parsing failed for "${request.sourcePath}": ${reason}`
51
+ );
52
+ }
53
+ }
54
+ async function generateWithAiSdk(model, request) {
55
+ const { Output, generateText, jsonSchema } = await import("ai");
56
+ const baseInput = {
57
+ model,
58
+ prompt: request.prompt,
59
+ system: request.system,
60
+ temperature: 0
61
+ };
62
+ try {
63
+ const result = await generateText({
64
+ ...baseInput,
65
+ experimental_output: Output.object({
66
+ schema: jsonSchema(request.schema, createOutputValidator(request))
67
+ })
68
+ });
69
+ return result.experimental_output;
70
+ } catch (error) {
71
+ if (!isSchemaTooLargeError(error)) {
72
+ throw error;
73
+ }
74
+ }
75
+ const fallbackResult = await generateText({
76
+ ...baseInput,
77
+ prompt: [
78
+ request.prompt,
79
+ "",
80
+ "Return only a valid JSON object that matches the required shape exactly.",
81
+ "Do not wrap the JSON in markdown fences."
82
+ ].join("\n"),
83
+ system: `${request.system} Return only valid JSON.`
84
+ });
85
+ return parseJsonText(fallbackResult.text, request);
86
+ }
87
+ export {
88
+ generateWithAiSdk
89
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-generator.d.ts","sourceRoot":"","sources":["../src/ai-sdk-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE9D,wBAAsB,iBAAiB,CAAC,OAAO,EAC7C,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,2BAA2B,CAAC,OAAO,CAAC,GAC5C,OAAO,CAAC,OAAO,CAAC,CAkClB"}
1
+ {"version":3,"file":"ai-sdk-generator.d.ts","sourceRoot":"","sources":["../src/ai-sdk-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAyF9D,wBAAsB,iBAAiB,CAAC,OAAO,EAC7C,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,2BAA2B,CAAC,OAAO,CAAC,GAC5C,OAAO,CAAC,OAAO,CAAC,CAoClB"}
package/dist/bin.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ extractProject,
3
4
  generateProject
4
- } from "./chunk-JFSWNLL6.js";
5
+ } from "./chunk-55TGASZJ.js";
5
6
 
6
7
  // src/cli.ts
7
8
  import pc2 from "picocolors";
@@ -69,7 +70,7 @@ ${pc.magenta("\u25C6")} locale: ${pc.bold(localeMatch[1])}`);
69
70
  spinner.stopAndPersist({ symbol: "\u25CC", text: pc.dim(message) });
70
71
  return;
71
72
  }
72
- if (message.startsWith("Using AI Gateway model:") || message.startsWith("Using built-in OpenAI provider model:") || message.startsWith("Source locale:") || message.startsWith("Target locales:")) {
73
+ if (message.startsWith("Using AI Gateway model:") || message.startsWith("Using configured provider model:") || message.startsWith("Source locale:") || message.startsWith("Target locales:")) {
73
74
  spinner.stop();
74
75
  console.log(pc.dim(message));
75
76
  return;
@@ -96,6 +97,33 @@ ${pc.magenta("\u25C6")} locale: ${pc.bold(localeMatch[1])}`);
96
97
  console.log(pc.dim(message));
97
98
  return;
98
99
  }
100
+ const rewriteMatch = message.match(/^rewrote (.+)/);
101
+ if (rewriteMatch) {
102
+ const shortPath = (rewriteMatch[1] ?? "").split("/").slice(-3).join("/");
103
+ spinner.succeed(`${pc.green("\u2713")} ${shortPath}`);
104
+ return;
105
+ }
106
+ const updatedMsgMatch = message.match(/^updated messages (.+)/);
107
+ if (updatedMsgMatch) {
108
+ const shortPath = (updatedMsgMatch[1] ?? "").split("/").slice(-3).join("/");
109
+ spinner.stopAndPersist({ symbol: pc.blue("\u25C6"), text: shortPath });
110
+ return;
111
+ }
112
+ if (message.startsWith("warn ")) {
113
+ spinner.stopAndPersist({ symbol: pc.yellow("\u26A0"), text: pc.yellow(message.slice(5)) });
114
+ return;
115
+ }
116
+ const extractSummaryMatch = message.match(
117
+ /^processed \d+ (?:file|files) and synced \d+ (?:key|keys)\./
118
+ );
119
+ if (extractSummaryMatch) {
120
+ spinner.stop();
121
+ console.log(
122
+ pc.bold(pc.green(`
123
+ \u2713 ${message.charAt(0).toUpperCase()}${message.slice(1)}`))
124
+ );
125
+ return;
126
+ }
99
127
  console.log(message);
100
128
  },
101
129
  error(message) {
@@ -108,12 +136,14 @@ ${pc.magenta("\u25C6")} locale: ${pc.bold(localeMatch[1])}`);
108
136
  function usage() {
109
137
  return [
110
138
  "Usage:",
139
+ " bt extract [--config ./better-translate.config.ts] [--dry-run] [--max-length 40]",
111
140
  " bt generate [--config ./better-translate.config.ts] [--dry-run]"
112
141
  ].join("\n");
113
142
  }
114
- function parseArgs(argv) {
143
+ function parseCommonArgs(argv) {
115
144
  let configPath;
116
145
  let dryRun = false;
146
+ let maxLength;
117
147
  for (let index = 0; index < argv.length; index += 1) {
118
148
  const arg = argv[index];
119
149
  if (arg === "--dry-run") {
@@ -129,11 +159,28 @@ function parseArgs(argv) {
129
159
  index += 1;
130
160
  continue;
131
161
  }
162
+ if (arg === "--max-length") {
163
+ const value = argv[index + 1];
164
+ if (!value) {
165
+ throw new Error("--max-length requires a number.");
166
+ }
167
+ if (!/^\d+$/.test(value)) {
168
+ throw new Error("--max-length must be a positive integer.");
169
+ }
170
+ const parsed = Number.parseInt(value, 10);
171
+ if (!Number.isInteger(parsed) || parsed <= 0) {
172
+ throw new Error("--max-length must be a positive integer.");
173
+ }
174
+ maxLength = parsed;
175
+ index += 1;
176
+ continue;
177
+ }
132
178
  throw new Error(`Unknown argument "${arg}".`);
133
179
  }
134
180
  return {
135
181
  configPath,
136
- dryRun
182
+ dryRun,
183
+ maxLength
137
184
  };
138
185
  }
139
186
  async function runCli(argv = process.argv.slice(2), options = {}) {
@@ -144,24 +191,39 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
144
191
  stdout(usage());
145
192
  return command ? 0 : 1;
146
193
  }
147
- if (command !== "generate") {
194
+ if (command !== "extract" && command !== "generate") {
148
195
  stderr(`Unknown command "${command}".
149
196
  ${usage()}`);
150
197
  return 1;
151
198
  }
152
199
  try {
153
- const parsed = parseArgs(args);
200
+ const parsed = parseCommonArgs(args);
201
+ if (command === "generate" && parsed.maxLength !== void 0) {
202
+ stderr(`--max-length is not valid for "generate".
203
+ ${usage()}`);
204
+ return 1;
205
+ }
154
206
  console.log(pc2.bold("\n better-translate\n"));
155
- await generateProject({
156
- configPath: parsed.configPath,
157
- cwd: options.cwd,
158
- dryRun: parsed.dryRun,
159
- logger: createSpinnerLogger()
160
- });
207
+ if (command === "extract") {
208
+ await extractProject({
209
+ configPath: parsed.configPath,
210
+ cwd: options.cwd,
211
+ dryRun: parsed.dryRun,
212
+ logger: createSpinnerLogger(),
213
+ maxLength: parsed.maxLength
214
+ });
215
+ } else {
216
+ await generateProject({
217
+ configPath: parsed.configPath,
218
+ cwd: options.cwd,
219
+ dryRun: parsed.dryRun,
220
+ logger: createSpinnerLogger()
221
+ });
222
+ }
161
223
  return 0;
162
224
  } catch (error) {
163
225
  stderr(
164
- `Better Translate generation failed: ${error instanceof Error ? error.message : String(error)}`
226
+ `Better Translate ${command} failed: ${error instanceof Error ? error.message : String(error)}`
165
227
  );
166
228
  return 1;
167
229
  }