@prettifystudio/lyngo-cli 0.1.0 → 0.1.2
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/bin/lyngo +78 -3
- package/package.json +1 -1
package/bin/lyngo
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// lyngo whoami who the token belongs to
|
|
9
9
|
// lyngo init [--languages fr,de] [--primary en] [--workspace <name>] [--yes] [--github]
|
|
10
10
|
// connect this repository: project, scaffolding, languages, agent hook
|
|
11
|
-
// lyngo localize [--languages fr,de] [--dry]
|
|
11
|
+
// lyngo localize [--languages fr,de] [--no-translate] [--dry]
|
|
12
|
+
// localize (+ translate) the working tree's changes
|
|
12
13
|
// lyngo check <file> the user-facing strings of one file
|
|
13
14
|
// lyngo status the project as Lyngo knows it
|
|
14
15
|
//
|
|
@@ -386,9 +387,77 @@ async function init(opts) {
|
|
|
386
387
|
project = await chooseLanguages(project, opts);
|
|
387
388
|
await scaffold(project);
|
|
388
389
|
setUpClaudeCode(top, remote);
|
|
390
|
+
await fillMissingCatalogs(project, opts);
|
|
389
391
|
console.log('Done. Build as usual; lyngo localize runs when your agent finishes, or run it yourself before committing.');
|
|
390
392
|
}
|
|
391
393
|
|
|
394
|
+
// A language added today has no catalog in the repository yet; the first
|
|
395
|
+
// `lyngo localize` would create one with the keys of the changed files only
|
|
396
|
+
// (pingcrm, 2026-10-12: 11 of 103 keys). Say so, and fill it on request.
|
|
397
|
+
async function fillMissingCatalogs(project, opts) {
|
|
398
|
+
const folder = catalogFolder(project);
|
|
399
|
+
const primary = project.primary_language || 'en';
|
|
400
|
+
const primaryPath = path.join(folder, primary + '.json');
|
|
401
|
+
if (!readIfFile(primaryPath)) return;
|
|
402
|
+
const targets = (project.languages || []).filter((l) => l !== primary);
|
|
403
|
+
const missing = targets.filter((l) => !readIfFile(path.join(folder, l + '.json')));
|
|
404
|
+
if (!missing.length) return;
|
|
405
|
+
const keys = Object.keys(parseCatalog(primaryPath) || {}).length;
|
|
406
|
+
console.log(`No catalog yet for ${missing.join(', ')}: ${folder}/<code>.json is missing, and ${primary}.json holds ${keys} keys that need translating.`);
|
|
407
|
+
const go = opts.yes ? 'y' : await ask(`Translate ${missing.length === 1 ? 'it' : 'them'} now? [Y/n] `);
|
|
408
|
+
if (go !== null && go !== '' && !/^y/i.test(go)) { console.log(`Later: lyngo translate ${missing.join(' ')}`); return; }
|
|
409
|
+
if (go === null) { console.log(`Run: lyngo translate ${missing.join(' ')}`); return; }
|
|
410
|
+
for (const code of missing) await translateLanguage(code, project);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function catalogFolder(project) { return ((project.i18n && project.i18n.catalog_folder) || 'lang').replace(/\/+$/, ''); }
|
|
414
|
+
function parseCatalog(p) { try { return JSON.parse(readIfFile(p) || ''); } catch { return null; } }
|
|
415
|
+
|
|
416
|
+
// ── translate: fill a language's catalog from the primary one ─────────────
|
|
417
|
+
async function translate(opts) {
|
|
418
|
+
const top = root();
|
|
419
|
+
process.chdir(top);
|
|
420
|
+
readToken();
|
|
421
|
+
const codes = opts._;
|
|
422
|
+
if (!codes.length) fail('usage: lyngo translate <code> [<code>...] (fills every key the language lacks)');
|
|
423
|
+
const st = await call('status', {});
|
|
424
|
+
const project = { primary_language: st.primary_language, languages: [st.primary_language, ...(st.languages || [])], i18n: st.i18n };
|
|
425
|
+
for (const code of codes) {
|
|
426
|
+
if (!project.languages.includes(code)) fail(`${code} is not one of the project's languages (${project.languages.join(', ')}). Add it with lyngo init --languages.`);
|
|
427
|
+
await translateLanguage(code, project);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function translateLanguage(code, project) {
|
|
432
|
+
const folder = catalogFolder(project);
|
|
433
|
+
const primary = project.primary_language || 'en';
|
|
434
|
+
const primaryPath = path.join(folder, primary + '.json');
|
|
435
|
+
const targetPath = path.join(folder, code + '.json');
|
|
436
|
+
const source = parseCatalog(primaryPath);
|
|
437
|
+
if (!source) fail(`${primaryPath} is missing or not valid JSON.`);
|
|
438
|
+
const current = parseCatalog(targetPath) || {};
|
|
439
|
+
const missing = Object.keys(source).filter((k) => !(k in current) || String(current[k]).trim() === '');
|
|
440
|
+
if (!missing.length) { console.log(`${code}: complete, ${Object.keys(source).length} keys.`); return; }
|
|
441
|
+
let translated = 0, memory = 0, flagged = [];
|
|
442
|
+
// Chunks keep each request under the server's time; the returned catalog carries forward.
|
|
443
|
+
for (let i = 0; i < missing.length; i += 100) {
|
|
444
|
+
const r = await call('translate', {
|
|
445
|
+
keys: missing.slice(i, i + 100),
|
|
446
|
+
languages: [code],
|
|
447
|
+
catalogs: [{ path: primaryPath, content: readIfFile(primaryPath) }, { path: targetPath, content: readIfFile(targetPath) || '{}' }],
|
|
448
|
+
});
|
|
449
|
+
const out = (r.catalogs || []).find((c) => c.path === targetPath || c.path.endsWith('/' + code + '.json'));
|
|
450
|
+
if (!out) fail(`the server returned no catalog for ${code}.`);
|
|
451
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
452
|
+
fs.writeFileSync(targetPath, out.content);
|
|
453
|
+
const l = (r.report && r.report.languages && r.report.languages[code]) || {};
|
|
454
|
+
translated += l.translated || 0; memory += l.from_memory || 0; flagged.push(...(l.flagged || []));
|
|
455
|
+
process.stdout.write(`\r${code}: ${Math.min(i + 100, missing.length)}/${missing.length}`);
|
|
456
|
+
}
|
|
457
|
+
const flaggedText = flagged.length ? `; ${flagged.length} flagged for review: ${flagged.slice(0, 3).map((x) => JSON.stringify(x.key || x)).join(', ')}` : '';
|
|
458
|
+
console.log(`\r${code}: ${translated} translated, ${memory} from memory${flaggedText} -> ${targetPath}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
392
461
|
// ── localize, check, status ────────────────────────────────────────────────
|
|
393
462
|
|
|
394
463
|
function changedFiles(top) {
|
|
@@ -420,6 +489,8 @@ async function localize(opts) {
|
|
|
420
489
|
const payload = files.map((p) => ({ path: p, content: fs.readFileSync(p, 'utf8') }));
|
|
421
490
|
const args = { files: payload, catalogs: catalogs.map((c) => ({ path: c.path, content: fs.existsSync(c.path) ? fs.readFileSync(c.path, 'utf8') : c.content })), format: 'patch' };
|
|
422
491
|
if (opts.languages) args.languages = opts.languages.filter((l) => l !== status.primary_language);
|
|
492
|
+
// --no-translate: keys only, catalogs for translators. Without the flag the project's setting decides.
|
|
493
|
+
if (opts['no-translate']) args.translate = false;
|
|
423
494
|
const r = await call('localize', args);
|
|
424
495
|
if (r.patch && r.patch.trim()) {
|
|
425
496
|
if (opts.dry) { process.stdout.write(r.patch); }
|
|
@@ -444,6 +515,7 @@ async function localize(opts) {
|
|
|
444
515
|
const wrapped = report.files.reduce((n, f) => n + f.wrapped.length, 0);
|
|
445
516
|
console.log(`lyngo: ${wrapped} string${wrapped === 1 ? '' : 's'} localized in ${report.files.length} file${report.files.length === 1 ? '' : 's'}; ${report.added.length} new key${report.added.length === 1 ? '' : 's'}, ${report.reused.length} reused.`);
|
|
446
517
|
for (const f of report.files) console.log(` ${f.path}: ${f.wrapped.slice(0, 6).map((s) => JSON.stringify(s)).join(', ')}${f.wrapped.length > 6 ? ', …' : ''}`);
|
|
518
|
+
if (opts['no-translate'] || (status.translate_on_localize === false && !Object.keys(report.languages).length)) console.log(' catalogs: not translated (keys only).');
|
|
447
519
|
for (const [code, l] of Object.entries(report.languages)) {
|
|
448
520
|
const flagged = l.flagged.length ? `; ${l.flagged.length} flagged for review: ${l.flagged.slice(0, 3).map((x) => JSON.stringify(x.key || x)).join(', ')}` : '';
|
|
449
521
|
console.log(` ${code}: ${l.translated} translated, ${l.from_memory} from memory${flagged}`);
|
|
@@ -480,8 +552,10 @@ const HELP = `lyngo ${version()}: localization for your repository, from the ter
|
|
|
480
552
|
lyngo init [--languages fr,de] [--primary en] [--workspace <name>] [--yes] [--github]
|
|
481
553
|
connect this repository: project, i18n setup, languages, agent hook
|
|
482
554
|
(--languages are the target languages; the source stays en unless --primary)
|
|
483
|
-
lyngo localize [--languages fr,de] [--dry]
|
|
484
|
-
localize
|
|
555
|
+
lyngo localize [--languages fr,de] [--no-translate] [--dry]
|
|
556
|
+
localize what changed in the working tree, and translate it
|
|
557
|
+
(--no-translate: keys only; the project setting "Translate when localizing" is the default)
|
|
558
|
+
lyngo translate <code> [<code>…] fill every key a language's catalog lacks, from the primary catalog
|
|
485
559
|
lyngo check <file> the user-facing strings of one file
|
|
486
560
|
lyngo status the project as Lyngo knows it
|
|
487
561
|
|
|
@@ -521,6 +595,7 @@ function parse(argv) {
|
|
|
521
595
|
case 'whoami': await whoami(); break;
|
|
522
596
|
case 'init': await init(opts); break;
|
|
523
597
|
case 'localize': await localize(opts); break;
|
|
598
|
+
case 'translate': await translate(opts); break;
|
|
524
599
|
case 'check': await check(opts._[0]); break;
|
|
525
600
|
case 'status': await status(); break;
|
|
526
601
|
default: console.error(`lyngo: unknown command "${cmd}".\n\n${HELP}`); process.exitCode = 1;
|
package/package.json
CHANGED