transduck 0.6.7 → 0.6.9
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/react/provider.d.ts +3 -2
- package/dist/react/provider.js +35 -8
- package/package.json +1 -1
- package/src/cli.ts +99 -17
- package/src/react/provider.tsx +37 -10
- package/tests/react-provider.test.tsx +131 -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.9');
|
|
548
641
|
program.command('init')
|
|
549
642
|
.description('Initialize a new transduck project')
|
|
550
643
|
.action(async () => {
|
package/dist/react/provider.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import { TranslationResult } from '../result.js';
|
|
2
3
|
interface ReactState {
|
|
3
4
|
language: string;
|
|
4
5
|
sourceLang: string;
|
|
@@ -29,11 +30,11 @@ export interface UseTransDuckReturn {
|
|
|
29
30
|
export declare function useTransDuck(): UseTransDuckReturn;
|
|
30
31
|
export declare function _resetReactState(): void;
|
|
31
32
|
export declare function _getReactState(): ReactState;
|
|
32
|
-
export declare function t(sourceText: string, context?: string, vars?: Record<string, string | number>):
|
|
33
|
+
export declare function t(sourceText: string, context?: string, vars?: Record<string, string | number>): TranslationResult;
|
|
33
34
|
export declare function tPlural(one: string, other: string, count: number, opts?: {
|
|
34
35
|
context?: string;
|
|
35
36
|
vars?: Record<string, string | number>;
|
|
36
|
-
}):
|
|
37
|
+
}): TranslationResult;
|
|
37
38
|
export declare const ait: typeof t;
|
|
38
39
|
export declare const aitPlural: typeof tPlural;
|
|
39
40
|
interface TransDuckProviderProps {
|
package/dist/react/provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { TranslationResult } from '../result.js';
|
|
4
5
|
let _state = {
|
|
5
6
|
language: '',
|
|
6
7
|
sourceLang: 'EN',
|
|
@@ -82,18 +83,31 @@ export function t(sourceText, context, vars) {
|
|
|
82
83
|
_state.knownKeys.add(key);
|
|
83
84
|
// Same language — return source
|
|
84
85
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
85
|
-
return interpolateVars(sourceText, vars)
|
|
86
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
87
|
+
pending: false,
|
|
88
|
+
sourceLang: _state.sourceLang,
|
|
89
|
+
lang: _state.language,
|
|
90
|
+
});
|
|
86
91
|
}
|
|
87
92
|
// Cache hit
|
|
88
93
|
const cached = _state.cache.get(key);
|
|
89
94
|
if (cached !== undefined) {
|
|
90
|
-
return interpolateVars(cached, vars)
|
|
95
|
+
return new TranslationResult(interpolateVars(cached, vars), {
|
|
96
|
+
pending: false,
|
|
97
|
+
sourceLang: _state.sourceLang,
|
|
98
|
+
lang: _state.language,
|
|
99
|
+
source: 'cache',
|
|
100
|
+
});
|
|
91
101
|
}
|
|
92
102
|
// Queue for fetch
|
|
93
103
|
_state.pendingStrings.add(key);
|
|
94
104
|
schedulePendingFlush();
|
|
95
|
-
// Return source text as fallback
|
|
96
|
-
return interpolateVars(sourceText, vars)
|
|
105
|
+
// Return source text as fallback, marked pending
|
|
106
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
107
|
+
pending: true,
|
|
108
|
+
sourceLang: _state.sourceLang,
|
|
109
|
+
lang: _state.language,
|
|
110
|
+
});
|
|
97
111
|
}
|
|
98
112
|
export function tPlural(one, other, count, opts) {
|
|
99
113
|
const context = opts?.context;
|
|
@@ -114,7 +128,11 @@ export function tPlural(one, other, count, opts) {
|
|
|
114
128
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
115
129
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
116
130
|
const form = rules.select(count) === 'one' ? one : other;
|
|
117
|
-
return interpolateVars(form, vars)
|
|
131
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
132
|
+
pending: false,
|
|
133
|
+
sourceLang: _state.sourceLang,
|
|
134
|
+
lang: _state.language,
|
|
135
|
+
});
|
|
118
136
|
}
|
|
119
137
|
// Cache hit
|
|
120
138
|
const cachedForms = _state.pluralCache.get(cacheKey);
|
|
@@ -122,15 +140,24 @@ export function tPlural(one, other, count, opts) {
|
|
|
122
140
|
const rules = new Intl.PluralRules(_state.language.toLowerCase());
|
|
123
141
|
const category = rules.select(count);
|
|
124
142
|
const form = cachedForms[category] ?? cachedForms['other'] ?? other;
|
|
125
|
-
return interpolateVars(form, vars)
|
|
143
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
144
|
+
pending: false,
|
|
145
|
+
sourceLang: _state.sourceLang,
|
|
146
|
+
lang: _state.language,
|
|
147
|
+
source: 'cache',
|
|
148
|
+
});
|
|
126
149
|
}
|
|
127
150
|
// Queue for fetch
|
|
128
151
|
_state.pendingPlurals.add(cacheKey);
|
|
129
152
|
schedulePendingFlush();
|
|
130
|
-
// Fallback to source form
|
|
153
|
+
// Fallback to source form, marked pending
|
|
131
154
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
132
155
|
const form = rules.select(count) === 'one' ? one : other;
|
|
133
|
-
return interpolateVars(form, vars)
|
|
156
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
157
|
+
pending: true,
|
|
158
|
+
sourceLang: _state.sourceLang,
|
|
159
|
+
lang: _state.language,
|
|
160
|
+
});
|
|
134
161
|
}
|
|
135
162
|
// Aliases
|
|
136
163
|
export const ait = t;
|
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.9');
|
|
665
747
|
|
|
666
748
|
program.command('init')
|
|
667
749
|
.description('Initialize a new transduck project')
|
package/src/react/provider.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react';
|
|
4
|
+
import { TranslationResult } from '../result.js';
|
|
4
5
|
|
|
5
6
|
// --- Module-level state (accessed by stable t()/ait() functions) ---
|
|
6
7
|
|
|
@@ -133,27 +134,40 @@ export function t(
|
|
|
133
134
|
sourceText: string,
|
|
134
135
|
context?: string,
|
|
135
136
|
vars?: Record<string, string | number>,
|
|
136
|
-
):
|
|
137
|
+
): TranslationResult {
|
|
137
138
|
const key = `${sourceText}||${context ?? ''}`;
|
|
138
139
|
_state.knownKeys.add(key);
|
|
139
140
|
|
|
140
141
|
// Same language — return source
|
|
141
142
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
142
|
-
return interpolateVars(sourceText, vars)
|
|
143
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
144
|
+
pending: false,
|
|
145
|
+
sourceLang: _state.sourceLang,
|
|
146
|
+
lang: _state.language,
|
|
147
|
+
});
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
// Cache hit
|
|
146
151
|
const cached = _state.cache.get(key);
|
|
147
152
|
if (cached !== undefined) {
|
|
148
|
-
return interpolateVars(cached, vars)
|
|
153
|
+
return new TranslationResult(interpolateVars(cached, vars), {
|
|
154
|
+
pending: false,
|
|
155
|
+
sourceLang: _state.sourceLang,
|
|
156
|
+
lang: _state.language,
|
|
157
|
+
source: 'cache',
|
|
158
|
+
});
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
// Queue for fetch
|
|
152
162
|
_state.pendingStrings.add(key);
|
|
153
163
|
schedulePendingFlush();
|
|
154
164
|
|
|
155
|
-
// Return source text as fallback
|
|
156
|
-
return interpolateVars(sourceText, vars)
|
|
165
|
+
// Return source text as fallback, marked pending
|
|
166
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
167
|
+
pending: true,
|
|
168
|
+
sourceLang: _state.sourceLang,
|
|
169
|
+
lang: _state.language,
|
|
170
|
+
});
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
export function tPlural(
|
|
@@ -161,7 +175,7 @@ export function tPlural(
|
|
|
161
175
|
other: string,
|
|
162
176
|
count: number,
|
|
163
177
|
opts?: { context?: string; vars?: Record<string, string | number> },
|
|
164
|
-
):
|
|
178
|
+
): TranslationResult {
|
|
165
179
|
const context = opts?.context;
|
|
166
180
|
let vars: Record<string, string | number>;
|
|
167
181
|
if (!opts?.vars) {
|
|
@@ -180,7 +194,11 @@ export function tPlural(
|
|
|
180
194
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
181
195
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
182
196
|
const form = rules.select(count) === 'one' ? one : other;
|
|
183
|
-
return interpolateVars(form, vars)
|
|
197
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
198
|
+
pending: false,
|
|
199
|
+
sourceLang: _state.sourceLang,
|
|
200
|
+
lang: _state.language,
|
|
201
|
+
});
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
// Cache hit
|
|
@@ -189,17 +207,26 @@ export function tPlural(
|
|
|
189
207
|
const rules = new Intl.PluralRules(_state.language.toLowerCase());
|
|
190
208
|
const category = rules.select(count);
|
|
191
209
|
const form = cachedForms[category] ?? cachedForms['other'] ?? other;
|
|
192
|
-
return interpolateVars(form, vars)
|
|
210
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
211
|
+
pending: false,
|
|
212
|
+
sourceLang: _state.sourceLang,
|
|
213
|
+
lang: _state.language,
|
|
214
|
+
source: 'cache',
|
|
215
|
+
});
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
// Queue for fetch
|
|
196
219
|
_state.pendingPlurals.add(cacheKey);
|
|
197
220
|
schedulePendingFlush();
|
|
198
221
|
|
|
199
|
-
// Fallback to source form
|
|
222
|
+
// Fallback to source form, marked pending
|
|
200
223
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
201
224
|
const form = rules.select(count) === 'one' ? one : other;
|
|
202
|
-
return interpolateVars(form, vars)
|
|
225
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
226
|
+
pending: true,
|
|
227
|
+
sourceLang: _state.sourceLang,
|
|
228
|
+
lang: _state.language,
|
|
229
|
+
});
|
|
203
230
|
}
|
|
204
231
|
|
|
205
232
|
// Aliases
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { render, screen, cleanup, waitFor, act } from '@testing-library/react';
|
|
4
4
|
import { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState, _getReactState } from '../src/react/index.js';
|
|
5
5
|
import type { UseTransDuckReturn } from '../src/react/index.js';
|
|
6
|
+
import { TranslationResult } from '../src/result.js';
|
|
6
7
|
|
|
7
8
|
// Mock fetch globally
|
|
8
9
|
const mockFetch = vi.fn();
|
|
@@ -499,6 +500,136 @@ describe('TransDuckProvider + t()', () => {
|
|
|
499
500
|
});
|
|
500
501
|
});
|
|
501
502
|
|
|
503
|
+
it('t() returns a TranslationResult with pending=true on cache miss', () => {
|
|
504
|
+
mockFetch.mockResolvedValue({
|
|
505
|
+
ok: true,
|
|
506
|
+
json: async () => ({ translations: { 'Hello||': 'Hallo' }, plurals: {} }),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
let captured: ReturnType<typeof t> | null = null;
|
|
510
|
+
function TestComp() {
|
|
511
|
+
useTransDuck();
|
|
512
|
+
captured = t('Hello');
|
|
513
|
+
return <span>{captured}</span>;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
render(
|
|
517
|
+
<TransDuckProvider language="DE">
|
|
518
|
+
<TestComp />
|
|
519
|
+
</TransDuckProvider>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
523
|
+
expect(captured!.pending).toBe(true);
|
|
524
|
+
expect(captured!.sourceLang).toBe('EN');
|
|
525
|
+
expect(captured!.lang).toBe('DE');
|
|
526
|
+
expect(captured!.toString()).toBe('Hello');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('t() returns a TranslationResult with pending=false on cache hit', async () => {
|
|
530
|
+
mockFetch.mockResolvedValue({
|
|
531
|
+
ok: true,
|
|
532
|
+
json: async () => ({ translations: { 'Hello||': 'Hallo' }, plurals: {} }),
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
let captured: ReturnType<typeof t> | null = null;
|
|
536
|
+
function TestComp() {
|
|
537
|
+
useTransDuck();
|
|
538
|
+
captured = t('Hello');
|
|
539
|
+
return <span data-testid="text">{captured}</span>;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
render(
|
|
543
|
+
<TransDuckProvider language="DE">
|
|
544
|
+
<TestComp />
|
|
545
|
+
</TransDuckProvider>
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
await waitFor(() => {
|
|
549
|
+
expect(screen.getByTestId('text').textContent).toBe('Hallo');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
553
|
+
expect(captured!.pending).toBe(false);
|
|
554
|
+
expect(captured!.source).toBe('cache');
|
|
555
|
+
expect(captured!.toString()).toBe('Hallo');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('t() returns a TranslationResult with pending=false when language matches source', () => {
|
|
559
|
+
let captured: ReturnType<typeof t> | null = null;
|
|
560
|
+
function TestComp() {
|
|
561
|
+
useTransDuck();
|
|
562
|
+
captured = t('Hello');
|
|
563
|
+
return <span>{captured}</span>;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
render(
|
|
567
|
+
<TransDuckProvider language="EN" sourceLang="EN">
|
|
568
|
+
<TestComp />
|
|
569
|
+
</TransDuckProvider>
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
573
|
+
expect(captured!.pending).toBe(false);
|
|
574
|
+
expect(captured!.toString()).toBe('Hello');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('tPlural() returns a TranslationResult with pending=true on cache miss', () => {
|
|
578
|
+
mockFetch.mockResolvedValue({
|
|
579
|
+
ok: true,
|
|
580
|
+
json: async () => ({ translations: {}, plurals: {} }),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
let captured: ReturnType<typeof tPlural> | null = null;
|
|
584
|
+
function TestComp() {
|
|
585
|
+
useTransDuck();
|
|
586
|
+
captured = tPlural('{count} message', '{count} messages', 2);
|
|
587
|
+
return <span>{captured}</span>;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
render(
|
|
591
|
+
<TransDuckProvider language="DE">
|
|
592
|
+
<TestComp />
|
|
593
|
+
</TransDuckProvider>
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
597
|
+
expect(captured!.pending).toBe(true);
|
|
598
|
+
expect(captured!.toString()).toBe('2 messages');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('tPlural() returns a TranslationResult with pending=false on cache hit', async () => {
|
|
602
|
+
const pluralKey = '{count} message\x00{count} messages||';
|
|
603
|
+
mockFetch.mockResolvedValue({
|
|
604
|
+
ok: true,
|
|
605
|
+
json: async () => ({
|
|
606
|
+
translations: {},
|
|
607
|
+
plurals: { [pluralKey]: { one: '{count} Nachricht', other: '{count} Nachrichten' } },
|
|
608
|
+
}),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
let captured: ReturnType<typeof tPlural> | null = null;
|
|
612
|
+
function TestComp() {
|
|
613
|
+
useTransDuck();
|
|
614
|
+
captured = tPlural('{count} message', '{count} messages', 3);
|
|
615
|
+
return <span data-testid="text">{captured}</span>;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
render(
|
|
619
|
+
<TransDuckProvider language="DE">
|
|
620
|
+
<TestComp />
|
|
621
|
+
</TransDuckProvider>
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
await waitFor(() => {
|
|
625
|
+
expect(screen.getByTestId('text').textContent).toBe('3 Nachrichten');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
629
|
+
expect(captured!.pending).toBe(false);
|
|
630
|
+
expect(captured!.source).toBe('cache');
|
|
631
|
+
});
|
|
632
|
+
|
|
502
633
|
it('useTransDuck() still works without destructuring (backward compat)', async () => {
|
|
503
634
|
mockFetch.mockResolvedValue({
|
|
504
635
|
ok: true,
|