@p6t/cli 0.1.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 +136 -0
- package/dist/index.js +1029 -0
- package/dist/lingui-VSOT4HY4.js +222 -0
- package/dist/react-intl-EIVHSW57.js +222 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { defineCommand as defineCommand5, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/commands/translate.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import { defineCommand } from "citty";
|
|
9
|
+
|
|
10
|
+
// src/config/loader.ts
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { ConfigSchema } from "@p6t/config";
|
|
14
|
+
var CONFIG_FILE = "i18n.json";
|
|
15
|
+
var ConfigError = class extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ConfigError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function findConfigFile(startDir) {
|
|
22
|
+
let dir = startDir;
|
|
23
|
+
while (true) {
|
|
24
|
+
const candidate = path.join(dir, CONFIG_FILE);
|
|
25
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
26
|
+
const parent = path.dirname(dir);
|
|
27
|
+
if (parent === dir) return null;
|
|
28
|
+
dir = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function loadConfig(cwd = process.cwd()) {
|
|
32
|
+
const filePath = findConfigFile(cwd);
|
|
33
|
+
if (!filePath) {
|
|
34
|
+
throw new ConfigError(
|
|
35
|
+
`No ${CONFIG_FILE} found. Run \`p6t init\` to create one.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
throw new ConfigError(`Failed to parse ${filePath}`);
|
|
43
|
+
}
|
|
44
|
+
const result = ConfigSchema.safeParse(raw);
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
47
|
+
throw new ConfigError(`Invalid ${CONFIG_FILE}:
|
|
48
|
+
${issues}`);
|
|
49
|
+
}
|
|
50
|
+
return result.data;
|
|
51
|
+
}
|
|
52
|
+
function getConfigDir(cwd = process.cwd()) {
|
|
53
|
+
const filePath = findConfigFile(cwd);
|
|
54
|
+
return filePath ? path.dirname(filePath) : cwd;
|
|
55
|
+
}
|
|
56
|
+
function resolveContextPath(template, locale, configDir) {
|
|
57
|
+
return path.resolve(configDir, template.replace("{locale}", locale));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/context/loader.ts
|
|
61
|
+
import fs2 from "fs";
|
|
62
|
+
function readFileSafe(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
return fs2.readFileSync(filePath, "utf8");
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function readGlossary(filePath) {
|
|
70
|
+
const raw = readFileSafe(filePath);
|
|
71
|
+
if (!raw) return {};
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
75
|
+
return Object.fromEntries(
|
|
76
|
+
Object.entries(parsed).flatMap(
|
|
77
|
+
([k, v]) => typeof v === "string" ? [[k, v]] : []
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
function loadContext(config, targetLocale, configDir) {
|
|
86
|
+
const descriptionPath = resolveContextPath(
|
|
87
|
+
config.context.description,
|
|
88
|
+
targetLocale,
|
|
89
|
+
configDir
|
|
90
|
+
);
|
|
91
|
+
const styleGuidePath = resolveContextPath(
|
|
92
|
+
config.context.styleGuide,
|
|
93
|
+
targetLocale,
|
|
94
|
+
configDir
|
|
95
|
+
);
|
|
96
|
+
const glossaryPath = resolveContextPath(
|
|
97
|
+
config.context.glossary,
|
|
98
|
+
targetLocale,
|
|
99
|
+
configDir
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
description: readFileSafe(descriptionPath),
|
|
103
|
+
styleGuide: readFileSafe(styleGuidePath),
|
|
104
|
+
glossary: readGlossary(glossaryPath)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function formatGlossary(glossary) {
|
|
108
|
+
return Object.entries(glossary).map(([source, target]) => `${source} \u2192 ${target}`).join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/catalog/adapter.ts
|
|
112
|
+
import fs3 from "fs";
|
|
113
|
+
import path2 from "path";
|
|
114
|
+
function detectFormat(configDir) {
|
|
115
|
+
const linguiConfigNames = [
|
|
116
|
+
"lingui.config.ts",
|
|
117
|
+
"lingui.config.js",
|
|
118
|
+
"lingui.config.mjs"
|
|
119
|
+
];
|
|
120
|
+
for (const name of linguiConfigNames) {
|
|
121
|
+
if (fs3.existsSync(path2.join(configDir, name))) return "lingui";
|
|
122
|
+
}
|
|
123
|
+
return "lingui";
|
|
124
|
+
}
|
|
125
|
+
async function createAdapter(config, configDir) {
|
|
126
|
+
const format = config.format ?? detectFormat(configDir);
|
|
127
|
+
switch (format) {
|
|
128
|
+
case "lingui": {
|
|
129
|
+
const { createLinguiAdapter } = await import("./lingui-VSOT4HY4.js");
|
|
130
|
+
return createLinguiAdapter(configDir, config.catalogs);
|
|
131
|
+
}
|
|
132
|
+
case "react-intl": {
|
|
133
|
+
if (!config.catalogs || config.catalogs.length === 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
'react-intl requires catalogs in i18n.json, e.g. [{ "path": "i18n/{locale}.json" }]'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const { createReactIntlAdapter } = await import("./react-intl-EIVHSW57.js");
|
|
139
|
+
return createReactIntlAdapter(configDir, config.catalogs);
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/llm/providers.ts
|
|
147
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
148
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
149
|
+
import { createMistral } from "@ai-sdk/mistral";
|
|
150
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
151
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
152
|
+
import { createOllama } from "ai-sdk-ollama";
|
|
153
|
+
var ProviderError = class extends Error {
|
|
154
|
+
constructor(message) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = "ProviderError";
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
function createModel(provider) {
|
|
160
|
+
switch (provider.id) {
|
|
161
|
+
case "openai": {
|
|
162
|
+
const apiKey = process.env["OPENAI_API_KEY"];
|
|
163
|
+
if (!apiKey) throw new ProviderError("OPENAI_API_KEY env var is not set");
|
|
164
|
+
return createOpenAI({
|
|
165
|
+
apiKey,
|
|
166
|
+
...provider.baseUrl !== void 0 ? { baseURL: provider.baseUrl } : {}
|
|
167
|
+
})(provider.model);
|
|
168
|
+
}
|
|
169
|
+
case "anthropic": {
|
|
170
|
+
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
171
|
+
if (!apiKey)
|
|
172
|
+
throw new ProviderError("ANTHROPIC_API_KEY env var is not set");
|
|
173
|
+
return createAnthropic({
|
|
174
|
+
apiKey,
|
|
175
|
+
...provider.baseUrl !== void 0 ? { baseURL: provider.baseUrl } : {}
|
|
176
|
+
})(provider.model);
|
|
177
|
+
}
|
|
178
|
+
case "google": {
|
|
179
|
+
const apiKey = process.env["GOOGLE_GENERATIVE_AI_API_KEY"] ?? process.env["GOOGLE_API_KEY"];
|
|
180
|
+
if (!apiKey)
|
|
181
|
+
throw new ProviderError(
|
|
182
|
+
"GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY) env var is not set"
|
|
183
|
+
);
|
|
184
|
+
return createGoogleGenerativeAI({
|
|
185
|
+
apiKey,
|
|
186
|
+
...provider.baseUrl !== void 0 ? { baseURL: provider.baseUrl } : {}
|
|
187
|
+
})(provider.model);
|
|
188
|
+
}
|
|
189
|
+
case "mistral": {
|
|
190
|
+
const apiKey = process.env["MISTRAL_API_KEY"];
|
|
191
|
+
if (!apiKey)
|
|
192
|
+
throw new ProviderError("MISTRAL_API_KEY env var is not set");
|
|
193
|
+
return createMistral({
|
|
194
|
+
apiKey,
|
|
195
|
+
...provider.baseUrl !== void 0 ? { baseURL: provider.baseUrl } : {}
|
|
196
|
+
})(provider.model);
|
|
197
|
+
}
|
|
198
|
+
case "ollama": {
|
|
199
|
+
return createOllama({
|
|
200
|
+
...provider.baseUrl !== void 0 ? { baseURL: provider.baseUrl } : {}
|
|
201
|
+
})(provider.model);
|
|
202
|
+
}
|
|
203
|
+
case "openrouter": {
|
|
204
|
+
const apiKey = process.env["OPENROUTER_API_KEY"];
|
|
205
|
+
if (!apiKey)
|
|
206
|
+
throw new ProviderError("OPENROUTER_API_KEY env var is not set");
|
|
207
|
+
return createOpenRouter({ apiKey })(provider.model);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/llm/translator.ts
|
|
213
|
+
import { generateText } from "ai";
|
|
214
|
+
|
|
215
|
+
// src/llm/format.ts
|
|
216
|
+
var BLOCK_SEP = "\n---\n";
|
|
217
|
+
function serialiseUnits(units) {
|
|
218
|
+
const blocks = units.map((unit) => {
|
|
219
|
+
const lines = [`id: ${unit.id}`, `source: ${unit.source}`];
|
|
220
|
+
if (unit.sourcePlural) lines.push(`source_plural: ${unit.sourcePlural}`);
|
|
221
|
+
if (unit.context) lines.push(`context: ${unit.context}`);
|
|
222
|
+
if (unit.references?.length) {
|
|
223
|
+
lines.push(`ref: ${unit.references.join(", ")}`);
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
});
|
|
227
|
+
return "---\n" + blocks.join(BLOCK_SEP) + "\n---";
|
|
228
|
+
}
|
|
229
|
+
function serialiseOrphans(orphans) {
|
|
230
|
+
const blocks = orphans.map((o) => {
|
|
231
|
+
const lines = [`id: ${o.id}`];
|
|
232
|
+
if (o.source) lines.push(`source: ${o.source}`);
|
|
233
|
+
lines.push(`translation: ${o.translation}`);
|
|
234
|
+
if (o.context) lines.push(`context: ${o.context}`);
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
});
|
|
237
|
+
return "---\n" + blocks.join(BLOCK_SEP) + "\n---";
|
|
238
|
+
}
|
|
239
|
+
function parseResults(raw) {
|
|
240
|
+
const blocks = raw.split(/\n?---\n?/).map((b) => b.trim()).filter(Boolean);
|
|
241
|
+
const results = [];
|
|
242
|
+
for (const block of blocks) {
|
|
243
|
+
let id;
|
|
244
|
+
const translationLines = [];
|
|
245
|
+
let inTranslation = false;
|
|
246
|
+
for (const line of block.split("\n")) {
|
|
247
|
+
if (!inTranslation) {
|
|
248
|
+
const idMatch = /^id:\s*(.+)$/.exec(line);
|
|
249
|
+
if (idMatch) {
|
|
250
|
+
id = idMatch[1].trim();
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const translationMatch = /^translation:\s*(.*)$/.exec(line) ?? /^target:\s*(.*)$/.exec(line);
|
|
254
|
+
if (translationMatch) {
|
|
255
|
+
translationLines.push(translationMatch[1] ?? "");
|
|
256
|
+
inTranslation = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
translationLines.push(line);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (id && translationLines.length > 0) {
|
|
264
|
+
results.push({ id, translation: translationLines.join("\n").trimEnd() });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/llm/translator.ts
|
|
271
|
+
var DEFAULT_BATCH_SIZE = 200;
|
|
272
|
+
var DEFAULT_MAX_ORPHANS = 50;
|
|
273
|
+
function buildSystemPrompt(sourceLocale, targetLocale, ctx, orphans, customPrompt) {
|
|
274
|
+
const sections = [];
|
|
275
|
+
if (customPrompt) {
|
|
276
|
+
sections.push(customPrompt);
|
|
277
|
+
} else {
|
|
278
|
+
sections.push(
|
|
279
|
+
`You are a professional translator. Translate from ${sourceLocale} to ${targetLocale}.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (ctx.description) {
|
|
283
|
+
sections.push(`## Project description
|
|
284
|
+
${ctx.description.trim()}`);
|
|
285
|
+
}
|
|
286
|
+
if (ctx.styleGuide) {
|
|
287
|
+
sections.push(`## Style guide (${targetLocale})
|
|
288
|
+
${ctx.styleGuide.trim()}`);
|
|
289
|
+
}
|
|
290
|
+
const glossaryText = formatGlossary(ctx.glossary);
|
|
291
|
+
if (glossaryText) {
|
|
292
|
+
sections.push(
|
|
293
|
+
`## Glossary \u2014 always use these exact translations
|
|
294
|
+
${glossaryText}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (orphans.length > 0) {
|
|
298
|
+
sections.push(
|
|
299
|
+
[
|
|
300
|
+
"## Previously translated strings (style reference)",
|
|
301
|
+
"These keys were removed or renamed from the source. Use them to maintain consistent tone and terminology:",
|
|
302
|
+
serialiseOrphans(orphans)
|
|
303
|
+
].join("\n\n")
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
sections.push(
|
|
307
|
+
[
|
|
308
|
+
"## Instructions",
|
|
309
|
+
"- Return translations in exactly the format shown below.",
|
|
310
|
+
"- Maintain ICU message syntax (plurals, select, variables) unchanged in structure.",
|
|
311
|
+
"- Produce the correct plural categories for the target locale.",
|
|
312
|
+
"- Never add, remove, or rename interpolation variables.",
|
|
313
|
+
"- Never translate the `id` field.",
|
|
314
|
+
"- Output one block per input string, delimited by `---`.",
|
|
315
|
+
"",
|
|
316
|
+
"## Expected output format",
|
|
317
|
+
"---",
|
|
318
|
+
"id: example_key",
|
|
319
|
+
"translation: Translated text goes here",
|
|
320
|
+
"---"
|
|
321
|
+
].join("\n")
|
|
322
|
+
);
|
|
323
|
+
return sections.join("\n\n");
|
|
324
|
+
}
|
|
325
|
+
function buildUserPrompt(group2, sourceLocale, targetLocale) {
|
|
326
|
+
const contextLine = group2.hasSourceRefs ? `These strings all appear in: ${group2.sourceFile}
|
|
327
|
+
|
|
328
|
+
` : "";
|
|
329
|
+
return `Translate the following strings from ${sourceLocale} to ${targetLocale}:
|
|
330
|
+
|
|
331
|
+
` + contextLine + serialiseUnits(group2.units);
|
|
332
|
+
}
|
|
333
|
+
var DefaultTranslator = class {
|
|
334
|
+
constructor(model, customPrompt, batchSize = DEFAULT_BATCH_SIZE, maxOrphans = DEFAULT_MAX_ORPHANS) {
|
|
335
|
+
this.model = model;
|
|
336
|
+
this.customPrompt = customPrompt;
|
|
337
|
+
this.batchSize = batchSize;
|
|
338
|
+
this.maxOrphans = maxOrphans;
|
|
339
|
+
}
|
|
340
|
+
async translate(group2, orphans, sourceLocale, targetLocale, ctx) {
|
|
341
|
+
if (group2.units.length === 0) {
|
|
342
|
+
return { results: [], missing: [], rawResponse: void 0 };
|
|
343
|
+
}
|
|
344
|
+
const trimmedOrphans = orphans.slice(0, this.maxOrphans);
|
|
345
|
+
const systemPrompt = buildSystemPrompt(
|
|
346
|
+
sourceLocale,
|
|
347
|
+
targetLocale,
|
|
348
|
+
ctx,
|
|
349
|
+
trimmedOrphans,
|
|
350
|
+
this.customPrompt
|
|
351
|
+
);
|
|
352
|
+
const results = [];
|
|
353
|
+
const missing = [];
|
|
354
|
+
let lastRawResponse;
|
|
355
|
+
for (let i = 0; i < group2.units.length; i += this.batchSize) {
|
|
356
|
+
const batchUnits = group2.units.slice(i, i + this.batchSize);
|
|
357
|
+
const batchGroup = { ...group2, units: batchUnits };
|
|
358
|
+
const userPrompt = buildUserPrompt(batchGroup, sourceLocale, targetLocale);
|
|
359
|
+
const { text: text2 } = await generateText({
|
|
360
|
+
model: this.model,
|
|
361
|
+
system: systemPrompt,
|
|
362
|
+
prompt: userPrompt
|
|
363
|
+
});
|
|
364
|
+
lastRawResponse = text2;
|
|
365
|
+
const batchResults = parseResults(text2);
|
|
366
|
+
const returned = new Set(batchResults.map((r) => r.id));
|
|
367
|
+
for (const unit of batchUnits) {
|
|
368
|
+
if (!returned.has(unit.id)) {
|
|
369
|
+
missing.push({ id: unit.id, source: unit.source });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
results.push(...batchResults);
|
|
373
|
+
}
|
|
374
|
+
return { results, missing, rawResponse: lastRawResponse };
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/commands/translate.ts
|
|
379
|
+
var translateCommand = defineCommand({
|
|
380
|
+
meta: {
|
|
381
|
+
name: "translate",
|
|
382
|
+
description: "Translate missing strings in catalogs using an LLM"
|
|
383
|
+
},
|
|
384
|
+
args: {
|
|
385
|
+
locale: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "Only translate a specific target locale",
|
|
388
|
+
alias: "l"
|
|
389
|
+
},
|
|
390
|
+
verbose: {
|
|
391
|
+
type: "boolean",
|
|
392
|
+
description: "Print raw LLM responses for debugging",
|
|
393
|
+
default: false
|
|
394
|
+
},
|
|
395
|
+
"max-orphans": {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Maximum number of orphaned (removed) translations to include as style context in each prompt",
|
|
398
|
+
default: "50"
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
async run({ args }) {
|
|
402
|
+
p.intro("p6t translate");
|
|
403
|
+
let config;
|
|
404
|
+
try {
|
|
405
|
+
config = loadConfig();
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err instanceof ConfigError) {
|
|
408
|
+
p.cancel(err.message);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
const configDir = getConfigDir();
|
|
414
|
+
const targetLocales = args.locale ? [args.locale] : config.locale.targets;
|
|
415
|
+
const maxOrphans = parseInt(args["max-orphans"], 10);
|
|
416
|
+
if (Number.isNaN(maxOrphans) || maxOrphans < 0) {
|
|
417
|
+
p.cancel("Invalid --max-orphans value. Must be a non-negative integer.");
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
let model;
|
|
421
|
+
try {
|
|
422
|
+
model = createModel(config.provider);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (err instanceof ProviderError) {
|
|
425
|
+
p.cancel(err.message);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
const translator = new DefaultTranslator(
|
|
431
|
+
model,
|
|
432
|
+
config.provider.prompt,
|
|
433
|
+
void 0,
|
|
434
|
+
maxOrphans
|
|
435
|
+
);
|
|
436
|
+
let adapter;
|
|
437
|
+
try {
|
|
438
|
+
adapter = await createAdapter(config, configDir);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
p.cancel(err instanceof Error ? err.message : String(err));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
for (const targetLocale of targetLocales) {
|
|
444
|
+
const ctx = loadContext(config, targetLocale, configDir);
|
|
445
|
+
const catalogPaths = adapter.getCatalogPathsForLocale(targetLocale);
|
|
446
|
+
const sourcePaths = adapter.getCatalogPathsForLocale(
|
|
447
|
+
config.locale.source
|
|
448
|
+
);
|
|
449
|
+
for (let i = 0; i < catalogPaths.length; i++) {
|
|
450
|
+
const targetPath = catalogPaths[i];
|
|
451
|
+
const sourcePath = sourcePaths[i];
|
|
452
|
+
if (!targetPath || !sourcePath) continue;
|
|
453
|
+
const orphans = adapter.removeStaleKeys(targetPath, sourcePath);
|
|
454
|
+
if (orphans.length > 0) {
|
|
455
|
+
p.log.info(
|
|
456
|
+
`Removed ${orphans.length} stale string${orphans.length === 1 ? "" : "s"} from ${targetLocale}`
|
|
457
|
+
);
|
|
458
|
+
for (const orphan of orphans) {
|
|
459
|
+
p.log.info(` \u2022 ${orphan.id}: "${orphan.source}"`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const pending = adapter.readPendingTranslations(targetPath, sourcePath);
|
|
463
|
+
const totalUnits = pending.groups.reduce(
|
|
464
|
+
(sum, g) => sum + g.units.length,
|
|
465
|
+
0
|
|
466
|
+
);
|
|
467
|
+
if (pending.groups.some((g) => g.hasSourceRefs === false && g.units.length > 0)) {
|
|
468
|
+
p.log.warn(
|
|
469
|
+
"Source file references not found in catalog. Translations will be grouped by catalog file instead of by component. For better results, run `formatjs extract` with `--extract-source-location`."
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (totalUnits === 0) {
|
|
473
|
+
if (orphans.length === 0) {
|
|
474
|
+
p.log.info(`${targetLocale}: already up to date`);
|
|
475
|
+
}
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const allResults = [];
|
|
479
|
+
const allMissing = [];
|
|
480
|
+
let lastRawResponse;
|
|
481
|
+
for (const group2 of pending.groups) {
|
|
482
|
+
if (group2.units.length === 0) continue;
|
|
483
|
+
const contextLabel = group2.sourceFile;
|
|
484
|
+
const spinner3 = p.spinner();
|
|
485
|
+
spinner3.start(
|
|
486
|
+
`Translating ${group2.units.length} string${group2.units.length === 1 ? "" : "s"} in ${contextLabel} \u2192 ${targetLocale}`
|
|
487
|
+
);
|
|
488
|
+
try {
|
|
489
|
+
const batch = await translator.translate(
|
|
490
|
+
group2,
|
|
491
|
+
orphans,
|
|
492
|
+
config.locale.source,
|
|
493
|
+
targetLocale,
|
|
494
|
+
ctx
|
|
495
|
+
);
|
|
496
|
+
spinner3.stop(
|
|
497
|
+
`Translated ${batch.results.length}/${group2.units.length} strings in ${contextLabel} \u2192 ${targetLocale}`
|
|
498
|
+
);
|
|
499
|
+
allResults.push(...batch.results);
|
|
500
|
+
allMissing.push(...batch.missing);
|
|
501
|
+
if (batch.rawResponse) {
|
|
502
|
+
lastRawResponse = batch.rawResponse;
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
spinner3.stop(
|
|
506
|
+
`Failed to translate ${contextLabel} \u2192 ${targetLocale}`
|
|
507
|
+
);
|
|
508
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (allResults.length > 0) {
|
|
512
|
+
adapter.writeTranslations(targetPath, sourcePath, allResults);
|
|
513
|
+
}
|
|
514
|
+
if (allMissing.length > 0) {
|
|
515
|
+
p.log.warn(
|
|
516
|
+
`${allMissing.length} string${allMissing.length === 1 ? "" : "s"} not returned by the LLM:`
|
|
517
|
+
);
|
|
518
|
+
for (const unit of allMissing) {
|
|
519
|
+
const preview = unit.source.length > 60 ? unit.source.slice(0, 60) + "\u2026" : unit.source;
|
|
520
|
+
p.log.warn(` \u2022 ${unit.id}: "${preview}"`);
|
|
521
|
+
}
|
|
522
|
+
if (args.verbose && lastRawResponse) {
|
|
523
|
+
p.log.info("Raw LLM response:");
|
|
524
|
+
process.stdout.write(lastRawResponse + "\n");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
p.outro("Done");
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// src/commands/init.ts
|
|
534
|
+
import fs4 from "fs";
|
|
535
|
+
import path3 from "path";
|
|
536
|
+
import * as p2 from "@clack/prompts";
|
|
537
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
538
|
+
import { generateText as generateText2 } from "ai";
|
|
539
|
+
var CONFIG_FILE2 = "i18n.json";
|
|
540
|
+
function writeFileSafe(filePath, content) {
|
|
541
|
+
fs4.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
542
|
+
fs4.writeFileSync(filePath, content, "utf8");
|
|
543
|
+
}
|
|
544
|
+
function readFileSafe2(filePath) {
|
|
545
|
+
try {
|
|
546
|
+
return fs4.readFileSync(filePath, "utf8");
|
|
547
|
+
} catch {
|
|
548
|
+
return void 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function resolveContextPath2(template, locale, cwd) {
|
|
552
|
+
return path3.resolve(cwd, template.replace("{locale}", locale));
|
|
553
|
+
}
|
|
554
|
+
function detectFormatFromFiles(cwd) {
|
|
555
|
+
for (const dir of ["locale", "locales", "src/locales", "src/locale"]) {
|
|
556
|
+
const full = path3.join(cwd, dir);
|
|
557
|
+
if (!fs4.existsSync(full)) continue;
|
|
558
|
+
for (const entry of fs4.readdirSync(full, { recursive: true })) {
|
|
559
|
+
if (entry.endsWith(".po")) return "lingui";
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
for (const dir of [
|
|
563
|
+
"lang",
|
|
564
|
+
"src/lang",
|
|
565
|
+
"messages",
|
|
566
|
+
"src/messages",
|
|
567
|
+
"i18n",
|
|
568
|
+
"locales",
|
|
569
|
+
"src/locales",
|
|
570
|
+
"src/i18n"
|
|
571
|
+
]) {
|
|
572
|
+
const full = path3.join(cwd, dir);
|
|
573
|
+
if (!fs4.existsSync(full)) continue;
|
|
574
|
+
for (const entry of fs4.readdirSync(full, { recursive: true })) {
|
|
575
|
+
if (!entry.endsWith(".json")) continue;
|
|
576
|
+
const raw = readFileSafe2(path3.join(full, entry));
|
|
577
|
+
if (!raw) continue;
|
|
578
|
+
try {
|
|
579
|
+
const parsed = JSON.parse(raw);
|
|
580
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
581
|
+
const values = Object.values(parsed);
|
|
582
|
+
if (values.some(
|
|
583
|
+
(v) => typeof v === "object" && v !== null && "defaultMessage" in v
|
|
584
|
+
)) {
|
|
585
|
+
return "react-intl";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
async function draftDescription(readme, sourceStrings, model) {
|
|
595
|
+
const sample = sourceStrings.join("\n");
|
|
596
|
+
const prompt = [
|
|
597
|
+
"Write a concise project description (2-4 paragraphs) for a professional translator.",
|
|
598
|
+
"It should explain: what this product is, who uses it, its domain/industry, and any important vocabulary notes.",
|
|
599
|
+
"Base it on the README and sample UI strings below.",
|
|
600
|
+
readme ? `
|
|
601
|
+
## README
|
|
602
|
+
${readme.slice(0, 1e5)}` : "",
|
|
603
|
+
sample ? `
|
|
604
|
+
## Sample UI strings
|
|
605
|
+
${sample}` : ""
|
|
606
|
+
].filter(Boolean).join("\n");
|
|
607
|
+
const { text: text2 } = await generateText2({
|
|
608
|
+
model,
|
|
609
|
+
system: "You are a localization consultant writing a project brief for professional translators. Be clear and practical.",
|
|
610
|
+
prompt
|
|
611
|
+
});
|
|
612
|
+
return text2.trim();
|
|
613
|
+
}
|
|
614
|
+
async function draftStyleGuide(locale, projectDescription, translatedPairs, model) {
|
|
615
|
+
const examplesSection = translatedPairs.length > 0 ? `
|
|
616
|
+
## Existing translations (observe the established style)
|
|
617
|
+
${translatedPairs.map(
|
|
618
|
+
({ source, translation }) => `source: ${source}
|
|
619
|
+
translation: ${translation}`
|
|
620
|
+
).join("\n\n")}` : "";
|
|
621
|
+
const { text: text2 } = await generateText2({
|
|
622
|
+
model,
|
|
623
|
+
system: "You are a localization expert. Write a translation style guide in markdown.",
|
|
624
|
+
prompt: [
|
|
625
|
+
`Write a style guide for translating into ${locale}.`,
|
|
626
|
+
"Cover: formality level (formal/informal), tone, brand voice, punctuation conventions, number/date formats, and any locale-specific considerations.",
|
|
627
|
+
"Keep it practical \u2014 1-2 sentences per rule. Use markdown headings.",
|
|
628
|
+
"If existing translations are provided, infer the style from them rather than guessing.",
|
|
629
|
+
`
|
|
630
|
+
## Project context
|
|
631
|
+
${projectDescription}`,
|
|
632
|
+
examplesSection
|
|
633
|
+
].filter(Boolean).join("\n")
|
|
634
|
+
});
|
|
635
|
+
return text2.trim();
|
|
636
|
+
}
|
|
637
|
+
async function draftGlossary(locale, sourceStrings, projectDescription, translatedPairs, styleGuide, model) {
|
|
638
|
+
const sample = sourceStrings.join("\n");
|
|
639
|
+
const existingSection = translatedPairs.length > 0 ? `
|
|
640
|
+
## Existing translations (extract consistent term mappings from these)
|
|
641
|
+
${translatedPairs.map(
|
|
642
|
+
({ source, translation }) => `source: ${source}
|
|
643
|
+
translation: ${translation}`
|
|
644
|
+
).join("\n")}` : "";
|
|
645
|
+
const { text: text2 } = await generateText2({
|
|
646
|
+
model,
|
|
647
|
+
system: "You are a localization expert. Respond with valid JSON only \u2014 a flat object mapping English terms to their preferred translations. No markdown, no explanation.",
|
|
648
|
+
prompt: [
|
|
649
|
+
`Identify the most important domain-specific terms and provide their preferred ${locale} translations. This just needs to be enough for a smart translator to be able to consistenly translate the UI \u2014 shorter is better.`,
|
|
650
|
+
"If existing translations are provided, extract term mappings directly from them \u2014 prefer attested translations over guesses.",
|
|
651
|
+
`
|
|
652
|
+
## Project context
|
|
653
|
+
${projectDescription}`,
|
|
654
|
+
`
|
|
655
|
+
## Style guide
|
|
656
|
+
${styleGuide}`,
|
|
657
|
+
sample ? `
|
|
658
|
+
## Source UI strings
|
|
659
|
+
${sample}` : "",
|
|
660
|
+
existingSection
|
|
661
|
+
].filter(Boolean).join("\n")
|
|
662
|
+
});
|
|
663
|
+
try {
|
|
664
|
+
const cleaned = text2.replace(/^```[a-z]*\n?/m, "").replace(/```$/m, "").trim();
|
|
665
|
+
const parsed = JSON.parse(cleaned);
|
|
666
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
667
|
+
return Object.fromEntries(
|
|
668
|
+
Object.entries(parsed).flatMap(
|
|
669
|
+
([k, v]) => typeof v === "string" ? [[k, v]] : []
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
}
|
|
675
|
+
return {};
|
|
676
|
+
}
|
|
677
|
+
var initCommand = defineCommand2({
|
|
678
|
+
meta: {
|
|
679
|
+
name: "init",
|
|
680
|
+
description: "Scaffold i18n.json and translation context files"
|
|
681
|
+
},
|
|
682
|
+
async run() {
|
|
683
|
+
p2.intro("p6t init");
|
|
684
|
+
const cwd = process.cwd();
|
|
685
|
+
if (fs4.existsSync(path3.join(cwd, CONFIG_FILE2))) {
|
|
686
|
+
const overwrite = await p2.confirm({
|
|
687
|
+
message: `${CONFIG_FILE2} already exists. Overwrite?`,
|
|
688
|
+
initialValue: false
|
|
689
|
+
});
|
|
690
|
+
if (p2.isCancel(overwrite) || !overwrite) {
|
|
691
|
+
p2.cancel("Aborted.");
|
|
692
|
+
process.exit(0);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const detectedFormat = detectFormatFromFiles(cwd);
|
|
696
|
+
const answers = await p2.group(
|
|
697
|
+
{
|
|
698
|
+
sourceLocale: () => p2.text({
|
|
699
|
+
message: "Source locale (language your strings are written in)",
|
|
700
|
+
placeholder: "en",
|
|
701
|
+
defaultValue: "en"
|
|
702
|
+
}),
|
|
703
|
+
targetLocales: () => p2.text({
|
|
704
|
+
message: "Target locales (comma-separated)",
|
|
705
|
+
placeholder: "de, fr, es",
|
|
706
|
+
validate: (v) => (v ?? "").trim() ? void 0 : "At least one target locale is required"
|
|
707
|
+
}),
|
|
708
|
+
providerId: () => p2.select({
|
|
709
|
+
message: "LLM provider",
|
|
710
|
+
options: [
|
|
711
|
+
{ value: "openai", label: "OpenAI" },
|
|
712
|
+
{ value: "anthropic", label: "Anthropic" },
|
|
713
|
+
{ value: "google", label: "Google" },
|
|
714
|
+
{ value: "mistral", label: "Mistral" },
|
|
715
|
+
{ value: "ollama", label: "Ollama (local)" },
|
|
716
|
+
{ value: "openrouter", label: "OpenRouter" }
|
|
717
|
+
]
|
|
718
|
+
}),
|
|
719
|
+
model: () => p2.text({
|
|
720
|
+
message: "Model name",
|
|
721
|
+
placeholder: "gpt-4o",
|
|
722
|
+
validate: (v) => (v ?? "").trim() ? void 0 : "Model name is required"
|
|
723
|
+
}),
|
|
724
|
+
format: () => p2.select({
|
|
725
|
+
message: "Translation library / catalog format",
|
|
726
|
+
initialValue: detectedFormat,
|
|
727
|
+
options: [
|
|
728
|
+
{ value: "lingui", label: "Lingui (.po)" },
|
|
729
|
+
{ value: "react-intl", label: "react-intl / FormatJS (.json)" }
|
|
730
|
+
]
|
|
731
|
+
}),
|
|
732
|
+
catalogPath: ({ results }) => {
|
|
733
|
+
if (results.format !== "react-intl") return void 0;
|
|
734
|
+
return p2.text({
|
|
735
|
+
message: "Catalog path template (use {locale} placeholder)",
|
|
736
|
+
placeholder: "i18n/{locale}.json",
|
|
737
|
+
validate: (v) => {
|
|
738
|
+
if (!(v ?? "").trim()) return "Catalog path is required";
|
|
739
|
+
if (!v.includes("{locale}"))
|
|
740
|
+
return "Path must contain {locale}";
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
},
|
|
745
|
+
draftContext: () => p2.confirm({
|
|
746
|
+
message: "Use the LLM to draft description, style guides, and glossaries? (requires API key)",
|
|
747
|
+
initialValue: true
|
|
748
|
+
})
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
onCancel: () => {
|
|
752
|
+
p2.cancel("Aborted.");
|
|
753
|
+
process.exit(0);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
);
|
|
757
|
+
const targetLocales = answers.targetLocales.split(",").map((l) => l.trim()).filter(Boolean);
|
|
758
|
+
const providerId = answers.providerId;
|
|
759
|
+
const model = answers.model;
|
|
760
|
+
const sourceLocale = answers.sourceLocale;
|
|
761
|
+
const format = answers.format;
|
|
762
|
+
const draftContext = answers.draftContext;
|
|
763
|
+
const catalogPath = answers.catalogPath;
|
|
764
|
+
const config = {
|
|
765
|
+
version: 1,
|
|
766
|
+
format,
|
|
767
|
+
locale: { source: sourceLocale, targets: targetLocales },
|
|
768
|
+
provider: { id: providerId, model },
|
|
769
|
+
context: {
|
|
770
|
+
description: "i18n/description.md",
|
|
771
|
+
styleGuide: "i18n/{locale}/style-guide.md",
|
|
772
|
+
glossary: "i18n/{locale}/glossary.json"
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
if (format === "react-intl" && catalogPath) {
|
|
776
|
+
config.catalogs = [{ path: catalogPath }];
|
|
777
|
+
}
|
|
778
|
+
writeFileSafe(
|
|
779
|
+
path3.join(cwd, CONFIG_FILE2),
|
|
780
|
+
JSON.stringify(
|
|
781
|
+
{ $schema: "https://p6t.dev/schema/i18n.json", ...config },
|
|
782
|
+
null,
|
|
783
|
+
2
|
|
784
|
+
) + "\n"
|
|
785
|
+
);
|
|
786
|
+
p2.log.success(`Created ${CONFIG_FILE2}`);
|
|
787
|
+
let adapter;
|
|
788
|
+
try {
|
|
789
|
+
adapter = await createAdapter(config, cwd);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
p2.log.warn(
|
|
792
|
+
`Could not load catalog adapter (${err instanceof Error ? err.message : String(err)}) \u2014 skipping LLM drafting.`
|
|
793
|
+
);
|
|
794
|
+
writeContextStubs(cwd, config, targetLocales);
|
|
795
|
+
p2.outro("Done (stubs created)");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (draftContext) {
|
|
799
|
+
let llmModel;
|
|
800
|
+
try {
|
|
801
|
+
llmModel = createModel({ id: providerId, model });
|
|
802
|
+
} catch (err) {
|
|
803
|
+
if (err instanceof ProviderError) {
|
|
804
|
+
p2.log.warn(
|
|
805
|
+
`${err.message} \u2014 skipping LLM drafting. Edit context files manually.`
|
|
806
|
+
);
|
|
807
|
+
writeContextStubs(cwd, config, targetLocales);
|
|
808
|
+
p2.outro("Done (stubs created)");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
throw err;
|
|
812
|
+
}
|
|
813
|
+
const spinner3 = p2.spinner();
|
|
814
|
+
const readme = readFileSafe2(path3.join(cwd, "README.md"));
|
|
815
|
+
const allCatalogs = adapter.discoverCatalogs(cwd);
|
|
816
|
+
const sourceCatalogs = allCatalogs.filter(
|
|
817
|
+
(f) => f.includes(`/${sourceLocale}/`) || path3.basename(f, path3.extname(f)) === sourceLocale
|
|
818
|
+
);
|
|
819
|
+
const sourceStrings = sourceCatalogs.flatMap(
|
|
820
|
+
(p5) => adapter.extractSourceStrings(p5)
|
|
821
|
+
);
|
|
822
|
+
spinner3.start("Drafting project description\u2026");
|
|
823
|
+
let description;
|
|
824
|
+
try {
|
|
825
|
+
description = await draftDescription(readme, sourceStrings, llmModel);
|
|
826
|
+
writeFileSafe(
|
|
827
|
+
path3.join(cwd, config.context.description),
|
|
828
|
+
description + "\n"
|
|
829
|
+
);
|
|
830
|
+
spinner3.stop(`Created ${config.context.description}`);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
p2.log.warn(
|
|
833
|
+
`LLM description drafting failed (${err instanceof Error ? err.message : String(err)}) \u2014 writing stubs.`
|
|
834
|
+
);
|
|
835
|
+
writeContextStubs(cwd, config, targetLocales);
|
|
836
|
+
p2.outro("Done (stubs created)");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
for (const locale of targetLocales) {
|
|
840
|
+
spinner3.start(`Drafting style guide + glossary for ${locale}\u2026`);
|
|
841
|
+
const targetCatalogs = adapter.getCatalogPathsForLocale(locale);
|
|
842
|
+
const sourcePaths = adapter.getCatalogPathsForLocale(sourceLocale);
|
|
843
|
+
const translatedPairs = [];
|
|
844
|
+
for (let i = 0; i < targetCatalogs.length; i++) {
|
|
845
|
+
if (!fs4.existsSync(targetCatalogs[i])) continue;
|
|
846
|
+
const pairs = adapter.extractTranslatedPairs(
|
|
847
|
+
targetCatalogs[i],
|
|
848
|
+
sourcePaths[i]
|
|
849
|
+
);
|
|
850
|
+
translatedPairs.push(...pairs);
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
const styleGuide = await draftStyleGuide(
|
|
854
|
+
locale,
|
|
855
|
+
description,
|
|
856
|
+
translatedPairs,
|
|
857
|
+
llmModel
|
|
858
|
+
);
|
|
859
|
+
const glossary = await draftGlossary(
|
|
860
|
+
locale,
|
|
861
|
+
sourceStrings,
|
|
862
|
+
description,
|
|
863
|
+
translatedPairs,
|
|
864
|
+
styleGuide,
|
|
865
|
+
llmModel
|
|
866
|
+
);
|
|
867
|
+
const styleGuidePath = resolveContextPath2(
|
|
868
|
+
config.context.styleGuide,
|
|
869
|
+
locale,
|
|
870
|
+
cwd
|
|
871
|
+
);
|
|
872
|
+
const glossaryPath = resolveContextPath2(
|
|
873
|
+
config.context.glossary,
|
|
874
|
+
locale,
|
|
875
|
+
cwd
|
|
876
|
+
);
|
|
877
|
+
writeFileSafe(styleGuidePath, styleGuide + "\n");
|
|
878
|
+
writeFileSafe(glossaryPath, JSON.stringify(glossary, null, 2) + "\n");
|
|
879
|
+
spinner3.stop(`Created context files for ${locale}`);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
p2.log.warn(
|
|
882
|
+
`LLM drafting failed for ${locale} (${err instanceof Error ? err.message : String(err)}) \u2014 writing stubs.`
|
|
883
|
+
);
|
|
884
|
+
const styleGuidePath = resolveContextPath2(
|
|
885
|
+
config.context.styleGuide,
|
|
886
|
+
locale,
|
|
887
|
+
cwd
|
|
888
|
+
);
|
|
889
|
+
const glossaryPath = resolveContextPath2(
|
|
890
|
+
config.context.glossary,
|
|
891
|
+
locale,
|
|
892
|
+
cwd
|
|
893
|
+
);
|
|
894
|
+
if (!fs4.existsSync(styleGuidePath)) {
|
|
895
|
+
writeFileSafe(
|
|
896
|
+
styleGuidePath,
|
|
897
|
+
[
|
|
898
|
+
`# Style guide \u2014 ${locale}`,
|
|
899
|
+
"",
|
|
900
|
+
"## Tone and formality",
|
|
901
|
+
"<!-- Formal or informal? -->",
|
|
902
|
+
"",
|
|
903
|
+
"## Brand voice",
|
|
904
|
+
"<!-- How should the brand sound? -->",
|
|
905
|
+
"",
|
|
906
|
+
"## Punctuation and formatting",
|
|
907
|
+
"<!-- Any locale-specific rules? -->"
|
|
908
|
+
].join("\n") + "\n"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
if (!fs4.existsSync(glossaryPath)) {
|
|
912
|
+
writeFileSafe(glossaryPath, "{}\n");
|
|
913
|
+
}
|
|
914
|
+
spinner3.stop(`Created stub context files for ${locale}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
writeContextStubs(cwd, config, targetLocales);
|
|
919
|
+
}
|
|
920
|
+
p2.note(
|
|
921
|
+
[
|
|
922
|
+
"Review and commit the generated files:",
|
|
923
|
+
` ${CONFIG_FILE2}`,
|
|
924
|
+
` i18n/description.md`,
|
|
925
|
+
...targetLocales.map((l) => ` i18n/${l}/style-guide.md`),
|
|
926
|
+
...targetLocales.map((l) => ` i18n/${l}/glossary.json`)
|
|
927
|
+
].join("\n"),
|
|
928
|
+
"Next steps"
|
|
929
|
+
);
|
|
930
|
+
p2.outro("Done \u2014 run `p6t translate` when ready");
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
function writeContextStubs(cwd, config, targetLocales) {
|
|
934
|
+
const descPath = path3.join(cwd, config.context.description);
|
|
935
|
+
if (!fs4.existsSync(descPath)) {
|
|
936
|
+
writeFileSafe(
|
|
937
|
+
descPath,
|
|
938
|
+
[
|
|
939
|
+
"# Project description",
|
|
940
|
+
"",
|
|
941
|
+
"Describe your project here for translators:",
|
|
942
|
+
"- What does it do?",
|
|
943
|
+
"- Who are the users?",
|
|
944
|
+
"- What industry/domain?",
|
|
945
|
+
"- Any important vocabulary notes?"
|
|
946
|
+
].join("\n") + "\n"
|
|
947
|
+
);
|
|
948
|
+
p2.log.success(`Created ${config.context.description}`);
|
|
949
|
+
}
|
|
950
|
+
for (const locale of targetLocales) {
|
|
951
|
+
const styleGuidePath = resolveContextPath2(
|
|
952
|
+
config.context.styleGuide,
|
|
953
|
+
locale,
|
|
954
|
+
cwd
|
|
955
|
+
);
|
|
956
|
+
const glossaryPath = resolveContextPath2(
|
|
957
|
+
config.context.glossary,
|
|
958
|
+
locale,
|
|
959
|
+
cwd
|
|
960
|
+
);
|
|
961
|
+
if (!fs4.existsSync(styleGuidePath)) {
|
|
962
|
+
writeFileSafe(
|
|
963
|
+
styleGuidePath,
|
|
964
|
+
[
|
|
965
|
+
`# Style guide \u2014 ${locale}`,
|
|
966
|
+
"",
|
|
967
|
+
"## Tone and formality",
|
|
968
|
+
"<!-- Formal or informal? -->",
|
|
969
|
+
"",
|
|
970
|
+
"## Brand voice",
|
|
971
|
+
"<!-- How should the brand sound? -->",
|
|
972
|
+
"",
|
|
973
|
+
"## Punctuation and formatting",
|
|
974
|
+
"<!-- Any locale-specific rules? -->"
|
|
975
|
+
].join("\n") + "\n"
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (!fs4.existsSync(glossaryPath)) {
|
|
979
|
+
writeFileSafe(glossaryPath, "{}\n");
|
|
980
|
+
}
|
|
981
|
+
p2.log.success(`Created context files for ${locale}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/commands/push.ts
|
|
986
|
+
import * as p3 from "@clack/prompts";
|
|
987
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
988
|
+
var pushCommand = defineCommand3({
|
|
989
|
+
meta: {
|
|
990
|
+
name: "push",
|
|
991
|
+
description: "Push translations to p6t web for review (coming soon)"
|
|
992
|
+
},
|
|
993
|
+
async run() {
|
|
994
|
+
p3.intro("p6t push");
|
|
995
|
+
p3.log.warn("Web integration is not yet implemented.");
|
|
996
|
+
p3.outro("Nothing pushed.");
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// src/commands/pull.ts
|
|
1001
|
+
import * as p4 from "@clack/prompts";
|
|
1002
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
1003
|
+
var pullCommand = defineCommand4({
|
|
1004
|
+
meta: {
|
|
1005
|
+
name: "pull",
|
|
1006
|
+
description: "Pull confirmed translations from p6t web (coming soon)"
|
|
1007
|
+
},
|
|
1008
|
+
async run() {
|
|
1009
|
+
p4.intro("p6t pull");
|
|
1010
|
+
p4.log.warn("Web integration is not yet implemented.");
|
|
1011
|
+
p4.outro("Nothing pulled.");
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// src/index.ts
|
|
1016
|
+
var main = defineCommand5({
|
|
1017
|
+
meta: {
|
|
1018
|
+
name: "p6t",
|
|
1019
|
+
description: "p6t \u2014 AI-powered translation pipeline",
|
|
1020
|
+
version: "0.0.0"
|
|
1021
|
+
},
|
|
1022
|
+
subCommands: {
|
|
1023
|
+
translate: translateCommand,
|
|
1024
|
+
init: initCommand,
|
|
1025
|
+
push: pushCommand,
|
|
1026
|
+
pull: pullCommand
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
runMain(main);
|