transduck 0.6.6 → 0.6.8
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/dist/cli.js +110 -17
- package/dist/scanner.js +8 -8
- package/package.json +1 -1
- package/src/cli.ts +99 -17
- package/src/scanner.ts +8 -8
- package/tests/scanner.test.ts +59 -0
package/dist/cli.js
CHANGED
|
@@ -376,6 +376,19 @@ export async function runScan(opts) {
|
|
|
376
376
|
const store = new TranslationStore(cfg.storagePath);
|
|
377
377
|
await store.initialize();
|
|
378
378
|
const projectContextHash = hash(cfg.projectContext);
|
|
379
|
+
// Initialize shared Postgres store if configured
|
|
380
|
+
let shared = null;
|
|
381
|
+
if (cfg.sharedUrl) {
|
|
382
|
+
try {
|
|
383
|
+
shared = new SharedStore(cfg.sharedUrl);
|
|
384
|
+
await shared.initialize();
|
|
385
|
+
console.log(' Connected to shared Postgres store');
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
console.warn(` Warning: could not connect to shared store: ${err.message}`);
|
|
389
|
+
shared = null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
379
392
|
let translated = 0;
|
|
380
393
|
let skipped = 0;
|
|
381
394
|
let failed = 0;
|
|
@@ -386,26 +399,68 @@ export async function runScan(opts) {
|
|
|
386
399
|
const sourceKey = entry.one + '\x00' + entry.other;
|
|
387
400
|
const stringContextHash = hash(entry.context ?? '');
|
|
388
401
|
const label = (entry.one ?? '').slice(0, 40);
|
|
402
|
+
const lookupParams = {
|
|
403
|
+
sourceText: sourceKey, sourceLang: cfg.sourceLang,
|
|
404
|
+
projectContextHash, stringContextHash,
|
|
405
|
+
};
|
|
389
406
|
for (const lang of targetLangs) {
|
|
390
407
|
done++;
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
projectContextHash, stringContextHash,
|
|
394
|
-
});
|
|
408
|
+
const langLookup = { ...lookupParams, targetLang: lang };
|
|
409
|
+
const cachedForms = await store.lookupPlural(langLookup);
|
|
395
410
|
if (Object.keys(cachedForms).length > 0) {
|
|
411
|
+
// Ensure shared store has it too
|
|
412
|
+
if (shared) {
|
|
413
|
+
try {
|
|
414
|
+
const sharedForms = await shared.lookupPlural(langLookup);
|
|
415
|
+
if (Object.keys(sharedForms).length === 0) {
|
|
416
|
+
for (const [cat, text] of Object.entries(cachedForms)) {
|
|
417
|
+
await shared.insertPlural({
|
|
418
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
419
|
+
pluralCategory: cat, translatedText: text,
|
|
420
|
+
model: cfg.backendModel, status: 'translated',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch { /* ignore shared store errors */ }
|
|
426
|
+
}
|
|
396
427
|
skipped++;
|
|
397
428
|
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
398
429
|
continue;
|
|
399
430
|
}
|
|
431
|
+
// Check shared store before calling backend
|
|
432
|
+
if (shared) {
|
|
433
|
+
try {
|
|
434
|
+
const sharedForms = await shared.lookupPlural(langLookup);
|
|
435
|
+
if (Object.keys(sharedForms).length > 0) {
|
|
436
|
+
for (const [cat, text] of Object.entries(sharedForms)) {
|
|
437
|
+
await store.insertPlural({
|
|
438
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
439
|
+
pluralCategory: cat, translatedText: text,
|
|
440
|
+
model: cfg.backendModel, status: 'translated',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
skipped++;
|
|
444
|
+
console.log(` [${done}/${total}] ${lang} skipped (shared cache): ${label}`);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch { /* ignore */ }
|
|
449
|
+
}
|
|
400
450
|
try {
|
|
401
451
|
const forms = await backendTranslatePlural(entry.one, entry.other, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
|
|
402
452
|
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
projectContextHash, stringContextHash, stringContext: entry.context ?? '',
|
|
453
|
+
const insertParams = {
|
|
454
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
406
455
|
pluralCategory: cat, translatedText: translatedText,
|
|
407
456
|
model: cfg.backendModel, status: 'translated',
|
|
408
|
-
}
|
|
457
|
+
};
|
|
458
|
+
await store.insertPlural(insertParams);
|
|
459
|
+
if (shared)
|
|
460
|
+
try {
|
|
461
|
+
await shared.insertPlural(insertParams);
|
|
462
|
+
}
|
|
463
|
+
catch { /* ignore */ }
|
|
409
464
|
}
|
|
410
465
|
translated++;
|
|
411
466
|
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
@@ -419,25 +474,61 @@ export async function runScan(opts) {
|
|
|
419
474
|
else {
|
|
420
475
|
const stringContextHash = hash(entry.context ?? '');
|
|
421
476
|
const label = (entry.text ?? '').slice(0, 40);
|
|
477
|
+
const lookupParams = {
|
|
478
|
+
sourceText: entry.text, sourceLang: cfg.sourceLang,
|
|
479
|
+
projectContextHash, stringContextHash,
|
|
480
|
+
};
|
|
422
481
|
for (const lang of targetLangs) {
|
|
423
482
|
done++;
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
projectContextHash, stringContextHash,
|
|
427
|
-
});
|
|
483
|
+
const langLookup = { ...lookupParams, targetLang: lang };
|
|
484
|
+
const cached = await store.lookup(langLookup);
|
|
428
485
|
if (cached !== null) {
|
|
486
|
+
// Ensure shared store has it too
|
|
487
|
+
if (shared) {
|
|
488
|
+
try {
|
|
489
|
+
const sharedCached = await shared.lookup(langLookup);
|
|
490
|
+
if (sharedCached === null) {
|
|
491
|
+
await shared.insert({
|
|
492
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
493
|
+
translatedText: cached, model: cfg.backendModel, status: 'translated',
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch { /* ignore */ }
|
|
498
|
+
}
|
|
429
499
|
skipped++;
|
|
430
500
|
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
431
501
|
continue;
|
|
432
502
|
}
|
|
503
|
+
// Check shared store before calling backend
|
|
504
|
+
if (shared) {
|
|
505
|
+
try {
|
|
506
|
+
const sharedCached = await shared.lookup(langLookup);
|
|
507
|
+
if (sharedCached !== null) {
|
|
508
|
+
await store.insert({
|
|
509
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
510
|
+
translatedText: sharedCached, model: cfg.backendModel, status: 'translated',
|
|
511
|
+
});
|
|
512
|
+
skipped++;
|
|
513
|
+
console.log(` [${done}/${total}] ${lang} skipped (shared cache): ${label}`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch { /* ignore */ }
|
|
518
|
+
}
|
|
433
519
|
try {
|
|
434
520
|
const result = await backendTranslate(entry.text, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
|
|
435
521
|
if (validateTranslation(entry.text, result)) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
projectContextHash, stringContextHash, stringContext: entry.context ?? '',
|
|
522
|
+
const insertParams = {
|
|
523
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
439
524
|
translatedText: result, model: cfg.backendModel, status: 'translated',
|
|
440
|
-
}
|
|
525
|
+
};
|
|
526
|
+
await store.insert(insertParams);
|
|
527
|
+
if (shared)
|
|
528
|
+
try {
|
|
529
|
+
await shared.insert(insertParams);
|
|
530
|
+
}
|
|
531
|
+
catch { /* ignore */ }
|
|
441
532
|
translated++;
|
|
442
533
|
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
443
534
|
}
|
|
@@ -454,6 +545,8 @@ export async function runScan(opts) {
|
|
|
454
545
|
}
|
|
455
546
|
}
|
|
456
547
|
store.close();
|
|
548
|
+
if (shared)
|
|
549
|
+
await shared.close();
|
|
457
550
|
lines.push(`\nTranslated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`);
|
|
458
551
|
} // end if entries.length > 0
|
|
459
552
|
}
|
|
@@ -544,7 +637,7 @@ export async function runStats(opts) {
|
|
|
544
637
|
}
|
|
545
638
|
// CLI entry point
|
|
546
639
|
const program = new Command();
|
|
547
|
-
program.name('transduck').description('AI-native translation tool').version('0.6.
|
|
640
|
+
program.name('transduck').description('AI-native translation tool').version('0.6.8');
|
|
548
641
|
program.command('init')
|
|
549
642
|
.description('Initialize a new transduck project')
|
|
550
643
|
.action(async () => {
|
package/dist/scanner.js
CHANGED
|
@@ -10,10 +10,10 @@ const AIT_KEYWORD_CTX = /ait\s*\(\s*(['"])(.*?)\1(?:\s*,\s*context\s*=\s*(['"])(
|
|
|
10
10
|
const AIT_POSITIONAL_CTX = /ait\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
11
11
|
// ait_plural("one", "other") or aitPlural("one", "other")
|
|
12
12
|
const AIT_PLURAL = /ait(?:_p|P)lural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
|
|
13
|
-
// {% ait "text" %} or {% ait "text" context="ctx" %}
|
|
14
|
-
const DJANGO_TAG = /\{%\s*ait\s+(['"])(.*?)\1(?:\s+context=(['"])(.*?)\3)?\s*%\}/g;
|
|
15
|
-
// {% ait_plural "one" "other" %} or {% ait_plural "one" "other" context="ctx" %}
|
|
16
|
-
const DJANGO_PLURAL_TAG = /\{%\s*ait_plural\s+(['"])(.*?)\1\s+(['"])(.*?)\3(?:\s+context=(['"])(.*?)\5)?\s*%\}/g;
|
|
13
|
+
// {% ait "text" %} or {% ait "text" context="ctx" %} or {% ait "text" as var %}
|
|
14
|
+
const DJANGO_TAG = /\{%\s*ait\s+(['"])(.*?)\1(?:\s+context=(['"])(.*?)\3)?(?:\s+as\s+\w+)?\s*%\}/g;
|
|
15
|
+
// {% ait_plural "one" "other" %} or {% ait_plural "one" "other" context="ctx" %} or with as var
|
|
16
|
+
const DJANGO_PLURAL_TAG = /\{%\s*ait_plural\s+(['"])(.*?)\1\s+(['"])(.*?)\3(?:\s+context=(['"])(.*?)\5)?(?:\s+as\s+\w+)?\s*%\}/g;
|
|
17
17
|
// t("text") or t("text", "ctx") — only matched in files with transduck/react import
|
|
18
18
|
const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
19
19
|
// tPlural("one", "other") — only matched in files with transduck/react import
|
|
@@ -21,11 +21,11 @@ const T_PLURAL = /(?<![a-zA-Z_.$])tPlural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)
|
|
|
21
21
|
const HAS_TRANSDUCK_REACT = /from\s+['"]transduck\/react['"]/;
|
|
22
22
|
const HAS_AIT_IDENTIFIER = /\bait\b/;
|
|
23
23
|
// File extensions that use JS-style positional context
|
|
24
|
-
const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx']);
|
|
25
|
-
// File extensions that may contain Django template tags
|
|
26
|
-
const TEMPLATE_EXTENSIONS = new Set(['.html', '.jinja', '.jinja2']);
|
|
24
|
+
const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.vue', '.svelte', '.mdx']);
|
|
25
|
+
// File extensions that may contain Django/Nunjucks template tags
|
|
26
|
+
const TEMPLATE_EXTENSIONS = new Set(['.html', '.jinja', '.jinja2', '.njk', '.vue', '.svelte']);
|
|
27
27
|
// Supported file extensions for scanning
|
|
28
|
-
const SCAN_EXTENSIONS = new Set(['.py', '.js', '.ts', '.tsx', '.jsx', '.html', '.jinja', '.jinja2']);
|
|
28
|
+
const SCAN_EXTENSIONS = new Set(['.py', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.html', '.jinja', '.jinja2', '.njk', '.vue', '.svelte', '.mdx']);
|
|
29
29
|
// Directories to skip
|
|
30
30
|
const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', '__pycache__', '.git', 'dist', 'build', '.next', 'site-packages']);
|
|
31
31
|
function shouldSkipDir(dirname) {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -459,6 +459,19 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
459
459
|
await store.initialize();
|
|
460
460
|
const projectContextHash = hash(cfg.projectContext);
|
|
461
461
|
|
|
462
|
+
// Initialize shared Postgres store if configured
|
|
463
|
+
let shared: SharedStore | null = null;
|
|
464
|
+
if (cfg.sharedUrl) {
|
|
465
|
+
try {
|
|
466
|
+
shared = new SharedStore(cfg.sharedUrl);
|
|
467
|
+
await shared.initialize();
|
|
468
|
+
console.log(' Connected to shared Postgres store');
|
|
469
|
+
} catch (err) {
|
|
470
|
+
console.warn(` Warning: could not connect to shared store: ${(err as Error).message}`);
|
|
471
|
+
shared = null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
462
475
|
let translated = 0;
|
|
463
476
|
let skipped = 0;
|
|
464
477
|
let failed = 0;
|
|
@@ -470,20 +483,56 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
470
483
|
const sourceKey = entry.one + '\x00' + entry.other;
|
|
471
484
|
const stringContextHash = hash(entry.context ?? '');
|
|
472
485
|
const label = (entry.one ?? '').slice(0, 40);
|
|
486
|
+
const lookupParams = {
|
|
487
|
+
sourceText: sourceKey, sourceLang: cfg.sourceLang,
|
|
488
|
+
projectContextHash, stringContextHash,
|
|
489
|
+
};
|
|
473
490
|
|
|
474
491
|
for (const lang of targetLangs) {
|
|
475
492
|
done++;
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
projectContextHash, stringContextHash,
|
|
479
|
-
});
|
|
493
|
+
const langLookup = { ...lookupParams, targetLang: lang };
|
|
494
|
+
const cachedForms = await store.lookupPlural(langLookup);
|
|
480
495
|
|
|
481
496
|
if (Object.keys(cachedForms).length > 0) {
|
|
497
|
+
// Ensure shared store has it too
|
|
498
|
+
if (shared) {
|
|
499
|
+
try {
|
|
500
|
+
const sharedForms = await shared.lookupPlural(langLookup);
|
|
501
|
+
if (Object.keys(sharedForms).length === 0) {
|
|
502
|
+
for (const [cat, text] of Object.entries(cachedForms)) {
|
|
503
|
+
await shared.insertPlural({
|
|
504
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
505
|
+
pluralCategory: cat, translatedText: text,
|
|
506
|
+
model: cfg.backendModel, status: 'translated',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch { /* ignore shared store errors */ }
|
|
511
|
+
}
|
|
482
512
|
skipped++;
|
|
483
513
|
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
484
514
|
continue;
|
|
485
515
|
}
|
|
486
516
|
|
|
517
|
+
// Check shared store before calling backend
|
|
518
|
+
if (shared) {
|
|
519
|
+
try {
|
|
520
|
+
const sharedForms = await shared.lookupPlural(langLookup);
|
|
521
|
+
if (Object.keys(sharedForms).length > 0) {
|
|
522
|
+
for (const [cat, text] of Object.entries(sharedForms)) {
|
|
523
|
+
await store.insertPlural({
|
|
524
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
525
|
+
pluralCategory: cat, translatedText: text,
|
|
526
|
+
model: cfg.backendModel, status: 'translated',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
skipped++;
|
|
530
|
+
console.log(` [${done}/${total}] ${lang} skipped (shared cache): ${label}`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
} catch { /* ignore */ }
|
|
534
|
+
}
|
|
535
|
+
|
|
487
536
|
try {
|
|
488
537
|
const forms = await backendTranslatePlural(
|
|
489
538
|
entry.one!, entry.other!,
|
|
@@ -492,12 +541,13 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
492
541
|
);
|
|
493
542
|
|
|
494
543
|
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
projectContextHash, stringContextHash, stringContext: entry.context ?? '',
|
|
544
|
+
const insertParams = {
|
|
545
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
498
546
|
pluralCategory: cat, translatedText: translatedText as string,
|
|
499
547
|
model: cfg.backendModel, status: 'translated',
|
|
500
|
-
}
|
|
548
|
+
};
|
|
549
|
+
await store.insertPlural(insertParams);
|
|
550
|
+
if (shared) try { await shared.insertPlural(insertParams); } catch { /* ignore */ }
|
|
501
551
|
}
|
|
502
552
|
translated++;
|
|
503
553
|
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
@@ -509,20 +559,50 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
509
559
|
} else {
|
|
510
560
|
const stringContextHash = hash(entry.context ?? '');
|
|
511
561
|
const label = (entry.text ?? '').slice(0, 40);
|
|
562
|
+
const lookupParams = {
|
|
563
|
+
sourceText: entry.text!, sourceLang: cfg.sourceLang,
|
|
564
|
+
projectContextHash, stringContextHash,
|
|
565
|
+
};
|
|
512
566
|
|
|
513
567
|
for (const lang of targetLangs) {
|
|
514
568
|
done++;
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
projectContextHash, stringContextHash,
|
|
518
|
-
});
|
|
569
|
+
const langLookup = { ...lookupParams, targetLang: lang };
|
|
570
|
+
const cached = await store.lookup(langLookup);
|
|
519
571
|
|
|
520
572
|
if (cached !== null) {
|
|
573
|
+
// Ensure shared store has it too
|
|
574
|
+
if (shared) {
|
|
575
|
+
try {
|
|
576
|
+
const sharedCached = await shared.lookup(langLookup);
|
|
577
|
+
if (sharedCached === null) {
|
|
578
|
+
await shared.insert({
|
|
579
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
580
|
+
translatedText: cached, model: cfg.backendModel, status: 'translated',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
} catch { /* ignore */ }
|
|
584
|
+
}
|
|
521
585
|
skipped++;
|
|
522
586
|
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
523
587
|
continue;
|
|
524
588
|
}
|
|
525
589
|
|
|
590
|
+
// Check shared store before calling backend
|
|
591
|
+
if (shared) {
|
|
592
|
+
try {
|
|
593
|
+
const sharedCached = await shared.lookup(langLookup);
|
|
594
|
+
if (sharedCached !== null) {
|
|
595
|
+
await store.insert({
|
|
596
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
597
|
+
translatedText: sharedCached, model: cfg.backendModel, status: 'translated',
|
|
598
|
+
});
|
|
599
|
+
skipped++;
|
|
600
|
+
console.log(` [${done}/${total}] ${lang} skipped (shared cache): ${label}`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
} catch { /* ignore */ }
|
|
604
|
+
}
|
|
605
|
+
|
|
526
606
|
try {
|
|
527
607
|
const result = await backendTranslate(
|
|
528
608
|
entry.text!, cfg.sourceLang, lang,
|
|
@@ -530,11 +610,12 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
530
610
|
);
|
|
531
611
|
|
|
532
612
|
if (validateTranslation(entry.text!, result)) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
projectContextHash, stringContextHash, stringContext: entry.context ?? '',
|
|
613
|
+
const insertParams = {
|
|
614
|
+
...langLookup, stringContext: entry.context ?? '',
|
|
536
615
|
translatedText: result, model: cfg.backendModel, status: 'translated',
|
|
537
|
-
}
|
|
616
|
+
};
|
|
617
|
+
await store.insert(insertParams);
|
|
618
|
+
if (shared) try { await shared.insert(insertParams); } catch { /* ignore */ }
|
|
538
619
|
translated++;
|
|
539
620
|
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
540
621
|
} else {
|
|
@@ -550,6 +631,7 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
550
631
|
}
|
|
551
632
|
|
|
552
633
|
store.close();
|
|
634
|
+
if (shared) await shared.close();
|
|
553
635
|
lines.push(`\nTranslated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`);
|
|
554
636
|
} // end if entries.length > 0
|
|
555
637
|
}
|
|
@@ -661,7 +743,7 @@ export async function runStats(opts: StatsOptions): Promise<string> {
|
|
|
661
743
|
// CLI entry point
|
|
662
744
|
const program = new Command();
|
|
663
745
|
|
|
664
|
-
program.name('transduck').description('AI-native translation tool').version('0.6.
|
|
746
|
+
program.name('transduck').description('AI-native translation tool').version('0.6.8');
|
|
665
747
|
|
|
666
748
|
program.command('init')
|
|
667
749
|
.description('Initialize a new transduck project')
|
package/src/scanner.ts
CHANGED
|
@@ -28,11 +28,11 @@ const AIT_POSITIONAL_CTX = /ait\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
|
28
28
|
// ait_plural("one", "other") or aitPlural("one", "other")
|
|
29
29
|
const AIT_PLURAL = /ait(?:_p|P)lural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
|
|
30
30
|
|
|
31
|
-
// {% ait "text" %} or {% ait "text" context="ctx" %}
|
|
32
|
-
const DJANGO_TAG = /\{%\s*ait\s+(['"])(.*?)\1(?:\s+context=(['"])(.*?)\3)?\s*%\}/g;
|
|
31
|
+
// {% ait "text" %} or {% ait "text" context="ctx" %} or {% ait "text" as var %}
|
|
32
|
+
const DJANGO_TAG = /\{%\s*ait\s+(['"])(.*?)\1(?:\s+context=(['"])(.*?)\3)?(?:\s+as\s+\w+)?\s*%\}/g;
|
|
33
33
|
|
|
34
|
-
// {% ait_plural "one" "other" %} or {% ait_plural "one" "other" context="ctx" %}
|
|
35
|
-
const DJANGO_PLURAL_TAG = /\{%\s*ait_plural\s+(['"])(.*?)\1\s+(['"])(.*?)\3(?:\s+context=(['"])(.*?)\5)?\s*%\}/g;
|
|
34
|
+
// {% ait_plural "one" "other" %} or {% ait_plural "one" "other" context="ctx" %} or with as var
|
|
35
|
+
const DJANGO_PLURAL_TAG = /\{%\s*ait_plural\s+(['"])(.*?)\1\s+(['"])(.*?)\3(?:\s+context=(['"])(.*?)\5)?(?:\s+as\s+\w+)?\s*%\}/g;
|
|
36
36
|
|
|
37
37
|
// t("text") or t("text", "ctx") — only matched in files with transduck/react import
|
|
38
38
|
const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
@@ -44,13 +44,13 @@ const HAS_TRANSDUCK_REACT = /from\s+['"]transduck\/react['"]/;
|
|
|
44
44
|
const HAS_AIT_IDENTIFIER = /\bait\b/;
|
|
45
45
|
|
|
46
46
|
// File extensions that use JS-style positional context
|
|
47
|
-
const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx']);
|
|
47
|
+
const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.vue', '.svelte', '.mdx']);
|
|
48
48
|
|
|
49
|
-
// File extensions that may contain Django template tags
|
|
50
|
-
const TEMPLATE_EXTENSIONS = new Set(['.html', '.jinja', '.jinja2']);
|
|
49
|
+
// File extensions that may contain Django/Nunjucks template tags
|
|
50
|
+
const TEMPLATE_EXTENSIONS = new Set(['.html', '.jinja', '.jinja2', '.njk', '.vue', '.svelte']);
|
|
51
51
|
|
|
52
52
|
// Supported file extensions for scanning
|
|
53
|
-
const SCAN_EXTENSIONS = new Set(['.py', '.js', '.ts', '.tsx', '.jsx', '.html', '.jinja', '.jinja2']);
|
|
53
|
+
const SCAN_EXTENSIONS = new Set(['.py', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.html', '.jinja', '.jinja2', '.njk', '.vue', '.svelte', '.mdx']);
|
|
54
54
|
|
|
55
55
|
// Directories to skip
|
|
56
56
|
const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', '__pycache__', '.git', 'dist', 'build', '.next', 'site-packages']);
|
package/tests/scanner.test.ts
CHANGED
|
@@ -178,6 +178,26 @@ const headline = await t("Your property visits, organized.", "Landing page headl
|
|
|
178
178
|
expect(result[0].other).toBe('{count} long items description here');
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
it('extracts Django tag with as var', () => {
|
|
182
|
+
const result = extractStrings('{% ait "Welcome" as headline %}', 'test.html');
|
|
183
|
+
expect(result).toHaveLength(1);
|
|
184
|
+
expect(result[0].text).toBe('Welcome');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('extracts Django tag with context and as var', () => {
|
|
188
|
+
const result = extractStrings('{% ait "Book" context="Hotel booking" as btn %}', 'test.html');
|
|
189
|
+
expect(result).toHaveLength(1);
|
|
190
|
+
expect(result[0].text).toBe('Book');
|
|
191
|
+
expect(result[0].context).toBe('Hotel booking');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('extracts Django plural tag with as var', () => {
|
|
195
|
+
const result = extractStrings('{% ait_plural "{count} night" "{count} nights" as stay %}', 'test.html');
|
|
196
|
+
expect(result).toHaveLength(1);
|
|
197
|
+
expect(result[0].plural).toBe(true);
|
|
198
|
+
expect(result[0].one).toBe('{count} night');
|
|
199
|
+
});
|
|
200
|
+
|
|
181
201
|
it('extracts Django plural template tag', () => {
|
|
182
202
|
const result = extractStrings('{% ait_plural "{count} night" "{count} nights" %}', 'test.html');
|
|
183
203
|
expect(result).toHaveLength(1);
|
|
@@ -209,6 +229,45 @@ const headline = await t("Your property visits, organized.", "Landing page headl
|
|
|
209
229
|
expect(result).toHaveLength(0);
|
|
210
230
|
});
|
|
211
231
|
|
|
232
|
+
it('extracts from Vue SFC', () => {
|
|
233
|
+
const code = `<template>\n <h1>{% ait "Welcome" %}</h1>\n</template>\n<script setup>\nimport { ait } from 'transduck';\nconst msg = ait("Hello", "greeting");\n</script>`;
|
|
234
|
+
const result = extractStrings(code, 'test.vue');
|
|
235
|
+
const texts = result.filter(e => !e.plural).map(e => e.text);
|
|
236
|
+
expect(texts).toContain('Welcome');
|
|
237
|
+
expect(texts).toContain('Hello');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('extracts from Svelte', () => {
|
|
241
|
+
const code = `<script>\nimport { ait } from 'transduck';\n</script>\n<h1>{ait("Welcome")}</h1>`;
|
|
242
|
+
const result = extractStrings(code, 'test.svelte');
|
|
243
|
+
expect(result.some(e => e.text === 'Welcome')).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('extracts from .mjs', () => {
|
|
247
|
+
const result = extractStrings('import { ait } from "transduck";\nait("Hello");', 'test.mjs');
|
|
248
|
+
expect(result).toHaveLength(1);
|
|
249
|
+
expect(result[0].text).toBe('Hello');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('extracts from .cjs', () => {
|
|
253
|
+
const result = extractStrings('const { ait } = require("transduck");\nait("Hello");', 'test.cjs');
|
|
254
|
+
expect(result).toHaveLength(1);
|
|
255
|
+
expect(result[0].text).toBe('Hello');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('extracts from .mdx', () => {
|
|
259
|
+
const result = extractStrings('import { ait } from "transduck";\n\n# Title\n\n{ait("Welcome")}', 'test.mdx');
|
|
260
|
+
expect(result.some(e => e.text === 'Welcome')).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('extracts from .njk (Nunjucks)', () => {
|
|
264
|
+
const result = extractStrings('{% ait "Welcome" %}\n{% ait "Book" context="hotel" %}', 'test.njk');
|
|
265
|
+
expect(result).toHaveLength(2);
|
|
266
|
+
expect(result[0].text).toBe('Welcome');
|
|
267
|
+
expect(result[1].text).toBe('Book');
|
|
268
|
+
expect(result[1].context).toBe('hotel');
|
|
269
|
+
});
|
|
270
|
+
|
|
212
271
|
it('extracts multi-line single-quoted strings', () => {
|
|
213
272
|
const code = `ait(\n 'This is a long '\n 'paragraph'\n)`;
|
|
214
273
|
const result = extractStrings(code, 'test.py');
|