transduck 0.0.4 → 0.1.1
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.d.ts +8 -0
- package/dist/cli.js +167 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/handler.d.ts +23 -0
- package/dist/handler.js +153 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/react/provider.d.ts +23 -0
- package/dist/react/provider.js +218 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.js +180 -0
- package/package.json +25 -1
- package/src/cli.ts +195 -2
- package/src/config.ts +2 -0
- package/src/handler.ts +193 -0
- package/src/index.ts +14 -0
- package/src/react/index.ts +1 -0
- package/src/react/provider.tsx +287 -0
- package/src/scanner.ts +215 -0
- package/tests/cli.test.ts +63 -2
- package/tests/handler.test.ts +136 -0
- package/tests/react-provider.test.tsx +162 -0
- package/tests/scanner.test.ts +191 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +9 -0
package/dist/cli.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface WarmOptions {
|
|
|
31
31
|
configPath?: string;
|
|
32
32
|
}
|
|
33
33
|
export declare function runWarm(opts: WarmOptions): Promise<string>;
|
|
34
|
+
export interface ScanOptions {
|
|
35
|
+
dirs: string[];
|
|
36
|
+
warm?: boolean;
|
|
37
|
+
langs?: string[];
|
|
38
|
+
outputPath?: string;
|
|
39
|
+
configPath?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function runScan(opts: ScanOptions): Promise<string>;
|
|
34
42
|
export interface StatsOptions {
|
|
35
43
|
configPath?: string;
|
|
36
44
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { stringify as yamlStringify } from 'yaml';
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
@@ -9,6 +9,7 @@ import { TranslationStore } from './storage.js';
|
|
|
9
9
|
import { translate as backendTranslate, translatePlural as backendTranslatePlural } from './backend.js';
|
|
10
10
|
import { validateTranslation, extractPlaceholders } from './validation.js';
|
|
11
11
|
import { getPluralCategory, interpolateVars } from './plural.js';
|
|
12
|
+
import { scanDirectory } from './scanner.js';
|
|
12
13
|
function hash(text) {
|
|
13
14
|
return createHash('sha256').update(text).digest('hex');
|
|
14
15
|
}
|
|
@@ -285,6 +286,138 @@ export async function runWarm(opts) {
|
|
|
285
286
|
store.close();
|
|
286
287
|
return `Translated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`;
|
|
287
288
|
}
|
|
289
|
+
export async function runScan(opts) {
|
|
290
|
+
const cfg = loadConfig(opts.configPath);
|
|
291
|
+
const scanDirs = opts.dirs.length > 0 ? opts.dirs : [process.cwd()];
|
|
292
|
+
const entries = scanDirectory(scanDirs);
|
|
293
|
+
const regular = entries.filter(e => !e.plural);
|
|
294
|
+
const plurals = entries.filter(e => e.plural);
|
|
295
|
+
// Count scanned files
|
|
296
|
+
const allFiles = new Set();
|
|
297
|
+
for (const e of entries) {
|
|
298
|
+
for (const f of (e.files ?? [])) {
|
|
299
|
+
allFiles.add(f);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const lines = [];
|
|
303
|
+
lines.push(`Scanned files with matches: ${allFiles.size}`);
|
|
304
|
+
lines.push(`Found ${entries.length} strings (${regular.length} regular, ${plurals.length} plural)`);
|
|
305
|
+
lines.push('');
|
|
306
|
+
for (const e of entries) {
|
|
307
|
+
const locations = (e.files ?? []).join(', ');
|
|
308
|
+
if (e.plural) {
|
|
309
|
+
lines.push(` ait_plural("${e.one}", "${e.other}") ${locations}`);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const ctx = e.context ? `, context="${e.context}"` : '';
|
|
313
|
+
lines.push(` ait("${e.text}"${ctx}) ${locations}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Output to JSON
|
|
317
|
+
if (opts.outputPath) {
|
|
318
|
+
const outputEntries = entries.map(e => {
|
|
319
|
+
const out = {};
|
|
320
|
+
for (const [k, v] of Object.entries(e)) {
|
|
321
|
+
if (k !== 'files')
|
|
322
|
+
out[k] = v;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
});
|
|
326
|
+
writeFileSync(opts.outputPath, JSON.stringify(outputEntries, null, 2));
|
|
327
|
+
lines.push(`\nWrote ${entries.length} entries to ${opts.outputPath}`);
|
|
328
|
+
}
|
|
329
|
+
// Warm
|
|
330
|
+
if (opts.warm) {
|
|
331
|
+
const targetLangs = opts.langs && opts.langs.length > 0
|
|
332
|
+
? opts.langs.map(l => l.toUpperCase())
|
|
333
|
+
: cfg.targetLangs;
|
|
334
|
+
const store = new TranslationStore(cfg.storagePath);
|
|
335
|
+
await store.initialize();
|
|
336
|
+
const apiKey = process.env[cfg.apiKeyEnv];
|
|
337
|
+
const projectContextHash = hash(cfg.projectContext);
|
|
338
|
+
let translated = 0;
|
|
339
|
+
let skipped = 0;
|
|
340
|
+
let failed = 0;
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
if (entry.plural) {
|
|
343
|
+
const sourceKey = entry.one + '\x00' + entry.other;
|
|
344
|
+
const stringContextHash = hash(entry.context ?? '');
|
|
345
|
+
for (const lang of targetLangs) {
|
|
346
|
+
const cachedForms = await store.lookupPlural({
|
|
347
|
+
sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
348
|
+
projectContextHash, stringContextHash,
|
|
349
|
+
});
|
|
350
|
+
if (Object.keys(cachedForms).length > 0) {
|
|
351
|
+
skipped++;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const forms = await backendTranslatePlural({
|
|
356
|
+
one: entry.one, other: entry.other,
|
|
357
|
+
sourceLang: cfg.sourceLang, targetLang: lang,
|
|
358
|
+
projectContext: cfg.projectContext, stringContext: entry.context ?? null,
|
|
359
|
+
apiKey: apiKey, model: cfg.backendModel,
|
|
360
|
+
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
361
|
+
});
|
|
362
|
+
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
363
|
+
await store.insertPlural({
|
|
364
|
+
sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
365
|
+
projectContextHash, stringContextHash,
|
|
366
|
+
pluralCategory: cat, translatedText: translatedText,
|
|
367
|
+
model: cfg.backendModel, status: 'translated',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
translated++;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
failed++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
const stringContextHash = hash(entry.context ?? '');
|
|
379
|
+
for (const lang of targetLangs) {
|
|
380
|
+
const cached = await store.lookup({
|
|
381
|
+
sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
382
|
+
projectContextHash, stringContextHash,
|
|
383
|
+
});
|
|
384
|
+
if (cached !== null) {
|
|
385
|
+
skipped++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const result = await backendTranslate({
|
|
390
|
+
sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
391
|
+
projectContext: cfg.projectContext, stringContext: entry.context ?? null,
|
|
392
|
+
apiKey: apiKey, model: cfg.backendModel,
|
|
393
|
+
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
394
|
+
});
|
|
395
|
+
if (validateTranslation(entry.text, result)) {
|
|
396
|
+
await store.insert({
|
|
397
|
+
sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
398
|
+
projectContextHash, stringContextHash,
|
|
399
|
+
translatedText: result, model: cfg.backendModel, status: 'translated',
|
|
400
|
+
});
|
|
401
|
+
translated++;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
failed++;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
failed++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
store.close();
|
|
414
|
+
lines.push(`\nTranslated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`);
|
|
415
|
+
}
|
|
416
|
+
if (!opts.warm && !opts.outputPath) {
|
|
417
|
+
lines.push(`\nRun 'transduck scan --warm --langs DE,ES' to translate all strings.`);
|
|
418
|
+
}
|
|
419
|
+
return lines.join('\n');
|
|
420
|
+
}
|
|
288
421
|
export async function runStats(opts) {
|
|
289
422
|
const cfg = loadConfig(opts.configPath);
|
|
290
423
|
const store = new TranslationStore(cfg.storagePath);
|
|
@@ -323,6 +456,21 @@ program.command('init')
|
|
|
323
456
|
targetLangs: targetsRaw.split(',').map(s => s.trim()),
|
|
324
457
|
});
|
|
325
458
|
console.log(output);
|
|
459
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
460
|
+
const ask2 = (q) => new Promise(r => rl2.question(q, r));
|
|
461
|
+
const useReact = await ask2('Are you using React/Next.js? (y/n): ');
|
|
462
|
+
if (useReact.toLowerCase() === 'y') {
|
|
463
|
+
const routePath = await ask2('API route path (default: app/api/translations/route.ts): ');
|
|
464
|
+
const finalPath = join(dir, routePath.trim() || 'app/api/translations/route.ts');
|
|
465
|
+
// Create directory
|
|
466
|
+
const routeDir = dirname(finalPath);
|
|
467
|
+
mkdirSync(routeDir, { recursive: true });
|
|
468
|
+
// Write route file
|
|
469
|
+
writeFileSync(finalPath, `import { createTransDuckHandler } from 'transduck';\n\nexport const POST = createTransDuckHandler();\n`);
|
|
470
|
+
console.log(`Created ${finalPath}`);
|
|
471
|
+
console.log('Add TransDuckProvider to your layout — see docs: transduck/react');
|
|
472
|
+
}
|
|
473
|
+
rl2.close();
|
|
326
474
|
});
|
|
327
475
|
program.command('translate')
|
|
328
476
|
.description('Translate a single string')
|
|
@@ -375,6 +523,23 @@ program.command('stats')
|
|
|
375
523
|
const output = await runStats({ configPath: opts.config ?? '' });
|
|
376
524
|
console.log(output);
|
|
377
525
|
});
|
|
526
|
+
program.command('scan')
|
|
527
|
+
.description('Scan source code for translatable strings')
|
|
528
|
+
.option('--dir <path...>', 'Directories to scan (repeatable)')
|
|
529
|
+
.option('--warm', 'Translate all found strings')
|
|
530
|
+
.option('--langs <langs>', 'Comma-separated target languages (for --warm)')
|
|
531
|
+
.option('--output <path>', 'Write found strings to JSON')
|
|
532
|
+
.option('--config <path>', 'Path to transduck.yaml')
|
|
533
|
+
.action(async (opts) => {
|
|
534
|
+
const output = await runScan({
|
|
535
|
+
dirs: opts.dir ?? [],
|
|
536
|
+
warm: opts.warm ?? false,
|
|
537
|
+
langs: opts.langs ? opts.langs.split(',').map(s => s.trim()) : undefined,
|
|
538
|
+
outputPath: opts.output,
|
|
539
|
+
configPath: opts.config,
|
|
540
|
+
});
|
|
541
|
+
console.log(output);
|
|
542
|
+
});
|
|
378
543
|
export { program };
|
|
379
544
|
// Run CLI when executed directly (not when imported by tests or other modules)
|
|
380
545
|
if (typeof process !== 'undefined' && process.argv[1]) {
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface TranslationRequestString {
|
|
2
|
+
text: string;
|
|
3
|
+
context?: string;
|
|
4
|
+
}
|
|
5
|
+
interface TranslationRequestPlural {
|
|
6
|
+
one: string;
|
|
7
|
+
other: string;
|
|
8
|
+
context?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface TranslationRequest {
|
|
11
|
+
language: string;
|
|
12
|
+
strings: TranslationRequestString[];
|
|
13
|
+
plurals: TranslationRequestPlural[];
|
|
14
|
+
}
|
|
15
|
+
export interface TranslationResponse {
|
|
16
|
+
translations: Record<string, string>;
|
|
17
|
+
plurals: Record<string, Record<string, string>>;
|
|
18
|
+
}
|
|
19
|
+
/** @internal Reset the singleton store — for testing only. */
|
|
20
|
+
export declare function _resetHandlerStore(): void;
|
|
21
|
+
export declare function handleTranslationRequest(body: TranslationRequest, configPath?: string): Promise<TranslationResponse>;
|
|
22
|
+
export declare function createTransDuckHandler(configPath?: string): (request: Request) => Promise<Response>;
|
|
23
|
+
export {};
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { TranslationStore } from './storage.js';
|
|
4
|
+
import { translate as backendTranslate, translatePlural as backendTranslatePlural } from './backend.js';
|
|
5
|
+
import { validateTranslation } from './validation.js';
|
|
6
|
+
function hash(text) {
|
|
7
|
+
return createHash('sha256').update(text).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
let _store = null;
|
|
10
|
+
async function getStore(configPath) {
|
|
11
|
+
if (!_store) {
|
|
12
|
+
const cfg = loadConfig(configPath);
|
|
13
|
+
_store = new TranslationStore(cfg.storagePath);
|
|
14
|
+
await _store.initialize();
|
|
15
|
+
}
|
|
16
|
+
return _store;
|
|
17
|
+
}
|
|
18
|
+
/** @internal Reset the singleton store — for testing only. */
|
|
19
|
+
export function _resetHandlerStore() {
|
|
20
|
+
if (_store) {
|
|
21
|
+
_store.close();
|
|
22
|
+
}
|
|
23
|
+
_store = null;
|
|
24
|
+
}
|
|
25
|
+
export async function handleTranslationRequest(body, configPath) {
|
|
26
|
+
const cfg = loadConfig(configPath);
|
|
27
|
+
const store = await getStore(configPath);
|
|
28
|
+
const targetLang = body.language.toUpperCase();
|
|
29
|
+
const projectContextHash = hash(cfg.projectContext);
|
|
30
|
+
const apiKey = process.env[cfg.apiKeyEnv];
|
|
31
|
+
const translations = {};
|
|
32
|
+
const plurals = {};
|
|
33
|
+
// Translate regular strings
|
|
34
|
+
for (const item of body.strings) {
|
|
35
|
+
const stringContextHash = hash(item.context ?? '');
|
|
36
|
+
const key = `${item.text}||${item.context ?? ''}`;
|
|
37
|
+
// Cache lookup
|
|
38
|
+
const cached = await store.lookup({
|
|
39
|
+
sourceText: item.text,
|
|
40
|
+
sourceLang: cfg.sourceLang,
|
|
41
|
+
targetLang,
|
|
42
|
+
projectContextHash,
|
|
43
|
+
stringContextHash,
|
|
44
|
+
});
|
|
45
|
+
if (cached !== null) {
|
|
46
|
+
translations[key] = cached;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Backend call
|
|
50
|
+
try {
|
|
51
|
+
const translated = await backendTranslate({
|
|
52
|
+
sourceText: item.text,
|
|
53
|
+
sourceLang: cfg.sourceLang,
|
|
54
|
+
targetLang,
|
|
55
|
+
projectContext: cfg.projectContext,
|
|
56
|
+
stringContext: item.context ?? null,
|
|
57
|
+
apiKey: apiKey,
|
|
58
|
+
model: cfg.backendModel,
|
|
59
|
+
timeout: cfg.backendTimeout,
|
|
60
|
+
maxRetries: cfg.backendMaxRetries,
|
|
61
|
+
});
|
|
62
|
+
if (validateTranslation(item.text, translated)) {
|
|
63
|
+
await store.insert({
|
|
64
|
+
sourceText: item.text,
|
|
65
|
+
sourceLang: cfg.sourceLang,
|
|
66
|
+
targetLang,
|
|
67
|
+
projectContextHash,
|
|
68
|
+
stringContextHash,
|
|
69
|
+
translatedText: translated,
|
|
70
|
+
model: cfg.backendModel,
|
|
71
|
+
status: 'translated',
|
|
72
|
+
});
|
|
73
|
+
translations[key] = translated;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
translations[key] = item.text;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
translations[key] = item.text;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Translate plurals
|
|
84
|
+
for (const item of body.plurals) {
|
|
85
|
+
const stringContextHash = hash(item.context ?? '');
|
|
86
|
+
const sourceKey = item.one + '\x00' + item.other;
|
|
87
|
+
const responseKey = `${sourceKey}||${item.context ?? ''}`;
|
|
88
|
+
// Cache lookup
|
|
89
|
+
const cachedForms = await store.lookupPlural({
|
|
90
|
+
sourceText: sourceKey,
|
|
91
|
+
sourceLang: cfg.sourceLang,
|
|
92
|
+
targetLang,
|
|
93
|
+
projectContextHash,
|
|
94
|
+
stringContextHash,
|
|
95
|
+
});
|
|
96
|
+
if (Object.keys(cachedForms).length > 0) {
|
|
97
|
+
plurals[responseKey] = cachedForms;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Backend call
|
|
101
|
+
try {
|
|
102
|
+
const forms = await backendTranslatePlural({
|
|
103
|
+
one: item.one,
|
|
104
|
+
other: item.other,
|
|
105
|
+
sourceLang: cfg.sourceLang,
|
|
106
|
+
targetLang,
|
|
107
|
+
projectContext: cfg.projectContext,
|
|
108
|
+
stringContext: item.context ?? null,
|
|
109
|
+
apiKey: apiKey,
|
|
110
|
+
model: cfg.backendModel,
|
|
111
|
+
timeout: cfg.backendTimeout,
|
|
112
|
+
maxRetries: cfg.backendMaxRetries,
|
|
113
|
+
});
|
|
114
|
+
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
115
|
+
await store.insertPlural({
|
|
116
|
+
sourceText: sourceKey,
|
|
117
|
+
sourceLang: cfg.sourceLang,
|
|
118
|
+
targetLang,
|
|
119
|
+
projectContextHash,
|
|
120
|
+
stringContextHash,
|
|
121
|
+
pluralCategory: cat,
|
|
122
|
+
translatedText: translatedText,
|
|
123
|
+
model: cfg.backendModel,
|
|
124
|
+
status: 'translated',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
plurals[responseKey] = forms;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
plurals[responseKey] = { one: item.one, other: item.other };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { translations, plurals };
|
|
134
|
+
}
|
|
135
|
+
export function createTransDuckHandler(configPath) {
|
|
136
|
+
return async function POST(request) {
|
|
137
|
+
try {
|
|
138
|
+
const body = await request.json();
|
|
139
|
+
const result = await handleTranslationRequest(body, configPath);
|
|
140
|
+
return new Response(JSON.stringify(result), {
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.error('[transduck] Handler error:', err);
|
|
147
|
+
return new Response(JSON.stringify({ error: 'Translation failed' }), {
|
|
148
|
+
status: 500,
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare function setLanguage(lang: string): void;
|
|
|
5
5
|
export declare function _resetState(): void;
|
|
6
6
|
export declare function _getStore(): TranslationStore | null;
|
|
7
7
|
export declare function ait(sourceText: string, context?: string, vars?: Record<string, string | number>): Promise<string>;
|
|
8
|
+
export { createTransDuckHandler } from './handler.js';
|
|
8
9
|
export declare function aitPlural(one: string, other: string, count: number, opts?: {
|
|
9
10
|
context?: string;
|
|
10
11
|
vars?: Record<string, string | number>;
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,10 @@ export async function ait(sourceText, context, vars) {
|
|
|
49
49
|
});
|
|
50
50
|
if (cached !== null)
|
|
51
51
|
return interpolateVars(cached, vars);
|
|
52
|
+
// Read-only mode: skip backend, return source text
|
|
53
|
+
if (cfg.readOnly) {
|
|
54
|
+
return interpolateVars(sourceText, vars);
|
|
55
|
+
}
|
|
52
56
|
// In-process dedup
|
|
53
57
|
const lockKey = `${sourceText}|${cfg.sourceLang}|${state.targetLang}|${projectContextHash}|${stringContextHash}`;
|
|
54
58
|
if (state.pendingTranslations.has(lockKey)) {
|
|
@@ -99,6 +103,7 @@ export async function ait(sourceText, context, vars) {
|
|
|
99
103
|
const result = await translationPromise;
|
|
100
104
|
return interpolateVars(result, vars);
|
|
101
105
|
}
|
|
106
|
+
export { createTransDuckHandler } from './handler.js';
|
|
102
107
|
export async function aitPlural(one, other, count, opts) {
|
|
103
108
|
if (!state.config || !state.store) {
|
|
104
109
|
throw new Error('transduck not initialized. Call initialize() first.');
|
|
@@ -141,6 +146,12 @@ export async function aitPlural(one, other, count, opts) {
|
|
|
141
146
|
if (category in cached) {
|
|
142
147
|
return interpolateVars(cached[category], vars);
|
|
143
148
|
}
|
|
149
|
+
// Read-only mode: skip backend, fall back to source forms
|
|
150
|
+
if (cfg.readOnly) {
|
|
151
|
+
const fallbackCategory = getPluralCategory(cfg.sourceLang, count);
|
|
152
|
+
const fallback = fallbackCategory === 'one' ? one : other;
|
|
153
|
+
return interpolateVars(fallback, vars);
|
|
154
|
+
}
|
|
144
155
|
// Cache miss — call backend
|
|
145
156
|
const apiKey = process.env[cfg.apiKeyEnv];
|
|
146
157
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState } from './provider.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState } from './provider.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Hook that subscribes to translation updates. Call this in any component
|
|
4
|
+
* that uses t()/ait() to ensure it re-renders when translations are loaded.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useTransDuck(): void;
|
|
7
|
+
export declare function _resetReactState(): void;
|
|
8
|
+
export declare function t(sourceText: string, context?: string, vars?: Record<string, string | number>): string;
|
|
9
|
+
export declare function tPlural(one: string, other: string, count: number, opts?: {
|
|
10
|
+
context?: string;
|
|
11
|
+
vars?: Record<string, string | number>;
|
|
12
|
+
}): string;
|
|
13
|
+
export declare const ait: typeof t;
|
|
14
|
+
export declare const aitPlural: typeof tPlural;
|
|
15
|
+
interface TransDuckProviderProps {
|
|
16
|
+
language: string;
|
|
17
|
+
sourceLang?: string;
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
projectName?: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
export declare function TransDuckProvider({ language, sourceLang, endpoint, projectName, children, }: TransDuckProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export {};
|