@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.
Files changed (2) hide show
  1. package/bin/lyngo +78 -3
  2. 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] localize + translate the working tree's changes
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 and translate what changed in the working tree
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prettifystudio/lyngo-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Localization like code: Lyngo wraps user-facing text, reuses your project's keys and translates the catalogs, from the terminal.",
5
5
  "bin": {
6
6
  "lyngo": "bin/lyngo"