transduck 0.0.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.
@@ -0,0 +1,40 @@
1
+ interface BuildMessagesParams {
2
+ sourceText: string;
3
+ sourceLang: string;
4
+ targetLang: string;
5
+ projectContext: string;
6
+ stringContext: string | null;
7
+ }
8
+ interface TranslateParams extends BuildMessagesParams {
9
+ apiKey: string;
10
+ model: string;
11
+ timeout: number;
12
+ maxRetries: number;
13
+ _clientOverride?: any;
14
+ }
15
+ interface BuildPluralMessagesParams {
16
+ one: string;
17
+ other: string;
18
+ sourceLang: string;
19
+ targetLang: string;
20
+ projectContext: string;
21
+ stringContext: string | null;
22
+ }
23
+ interface TranslatePluralParams extends BuildPluralMessagesParams {
24
+ apiKey: string;
25
+ model: string;
26
+ timeout: number;
27
+ maxRetries: number;
28
+ _clientOverride?: any;
29
+ }
30
+ export declare function buildMessages(params: BuildMessagesParams): Array<{
31
+ role: string;
32
+ content: string;
33
+ }>;
34
+ export declare function translate(params: TranslateParams): Promise<string>;
35
+ export declare function buildPluralMessages(params: BuildPluralMessagesParams): Array<{
36
+ role: string;
37
+ content: string;
38
+ }>;
39
+ export declare function translatePlural(params: TranslatePluralParams): Promise<Record<string, string>>;
40
+ export {};
@@ -0,0 +1,85 @@
1
+ import OpenAI from 'openai';
2
+ /* eslint-disable no-template-curly-in-string */
3
+ const SYSTEM_TEMPLATE = 'You are a professional translator. Translate the given text from {source_lang} ' +
4
+ 'to {target_lang}. Return ONLY the translated text, nothing else. Preserve any ' +
5
+ 'placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
6
+ 'Preserve brand names. Match the tone and formality of the original.\n\n' +
7
+ 'Project context: {project_context}';
8
+ const USER_TEMPLATE = `Translate: "{source_text}"\nString context: {string_context}`;
9
+ const PLURAL_SYSTEM_TEMPLATE = 'You are a professional translator. You will be given two plural forms ' +
10
+ '(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
11
+ '{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
12
+ 'mapping plural categories to translated strings. Do not include explanation.\n\n' +
13
+ 'Preserve any placeholders like {name}, {count}, %s, ${value} exactly as ' +
14
+ 'they appear. Preserve brand names. Match the tone and formality of the original.\n\n' +
15
+ 'CLDR plural categories are: zero, one, two, few, many, other.\n' +
16
+ 'Only include categories that {target_lang} actually uses.\n\n' +
17
+ 'Project context: {project_context}';
18
+ const PLURAL_USER_TEMPLATE = 'Source one form: "{one}"\n' +
19
+ 'Source other form: "{other}"\n' +
20
+ 'String context: {string_context}';
21
+ function safeRender(template, vars) {
22
+ let result = template;
23
+ for (const [key, value] of Object.entries(vars)) {
24
+ result = result.replaceAll(`{${key}}`, value);
25
+ }
26
+ return result;
27
+ }
28
+ export function buildMessages(params) {
29
+ const systemMsg = SYSTEM_TEMPLATE
30
+ .replace('{source_lang}', params.sourceLang)
31
+ .replace('{target_lang}', params.targetLang)
32
+ .replace('{project_context}', params.projectContext);
33
+ const userMsg = USER_TEMPLATE
34
+ .replace('{source_text}', params.sourceText)
35
+ .replace('{string_context}', params.stringContext || 'none');
36
+ return [
37
+ { role: 'system', content: systemMsg },
38
+ { role: 'user', content: userMsg },
39
+ ];
40
+ }
41
+ export async function translate(params) {
42
+ const client = params._clientOverride ?? new OpenAI({
43
+ apiKey: params.apiKey,
44
+ timeout: params.timeout * 1000,
45
+ maxRetries: params.maxRetries,
46
+ });
47
+ const messages = buildMessages(params);
48
+ const response = await client.chat.completions.create({
49
+ model: params.model,
50
+ messages,
51
+ temperature: 0.3,
52
+ });
53
+ return response.choices[0].message.content.trim();
54
+ }
55
+ export function buildPluralMessages(params) {
56
+ const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
57
+ source_lang: params.sourceLang,
58
+ target_lang: params.targetLang,
59
+ project_context: params.projectContext,
60
+ });
61
+ const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
62
+ one: params.one,
63
+ other: params.other,
64
+ string_context: params.stringContext || 'none',
65
+ });
66
+ return [
67
+ { role: 'system', content: systemMsg },
68
+ { role: 'user', content: userMsg },
69
+ ];
70
+ }
71
+ export async function translatePlural(params) {
72
+ const client = params._clientOverride ?? new OpenAI({
73
+ apiKey: params.apiKey,
74
+ timeout: params.timeout * 1000,
75
+ maxRetries: params.maxRetries,
76
+ });
77
+ const messages = buildPluralMessages(params);
78
+ const response = await client.chat.completions.create({
79
+ model: params.model,
80
+ messages,
81
+ temperature: 0.3,
82
+ });
83
+ const raw = response.choices[0].message.content.trim();
84
+ return JSON.parse(raw);
85
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ export interface InitOptions {
4
+ dir: string;
5
+ name: string;
6
+ context: string;
7
+ sourceLang: string;
8
+ targetLangs: string[];
9
+ }
10
+ export declare function runInit(opts: InitOptions): Promise<string>;
11
+ export interface TranslateOptions {
12
+ text: string;
13
+ targetLang: string;
14
+ stringContext?: string;
15
+ configPath?: string;
16
+ vars?: Record<string, string | number>;
17
+ }
18
+ export declare function runTranslate(opts: TranslateOptions): Promise<string>;
19
+ export interface TranslatePluralOptions {
20
+ one: string;
21
+ other: string;
22
+ count: number;
23
+ targetLang: string;
24
+ stringContext?: string;
25
+ configPath?: string;
26
+ }
27
+ export declare function runTranslatePlural(opts: TranslatePluralOptions): Promise<string>;
28
+ export interface WarmOptions {
29
+ filePath: string;
30
+ langs: string[];
31
+ configPath?: string;
32
+ }
33
+ export declare function runWarm(opts: WarmOptions): Promise<string>;
34
+ export interface StatsOptions {
35
+ configPath?: string;
36
+ }
37
+ export declare function runStats(opts: StatsOptions): Promise<string>;
38
+ declare const program: Command;
39
+ export { program };
package/dist/cli.js ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'crypto';
3
+ import { readFileSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { Command } from 'commander';
6
+ import { stringify as yamlStringify } from 'yaml';
7
+ import { loadConfig } from './config.js';
8
+ import { TranslationStore } from './storage.js';
9
+ import { translate as backendTranslate, translatePlural as backendTranslatePlural } from './backend.js';
10
+ import { validateTranslation, extractPlaceholders } from './validation.js';
11
+ import { getPluralCategory, interpolateVars } from './plural.js';
12
+ function hash(text) {
13
+ return createHash('sha256').update(text).digest('hex');
14
+ }
15
+ export async function runInit(opts) {
16
+ const config = {
17
+ project: { name: opts.name, context: opts.context },
18
+ languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
19
+ storage: { path: './translations.duckdb' },
20
+ backend: { api_key_env: 'OPENAI_API_KEY', model: 'gpt-4.1-mini', timeout_seconds: 10, max_retries: 2 },
21
+ };
22
+ const configPath = join(opts.dir, 'transduck.yaml');
23
+ writeFileSync(configPath, yamlStringify(config));
24
+ const dbPath = join(opts.dir, 'translations.duckdb');
25
+ const store = new TranslationStore(dbPath);
26
+ await store.initialize();
27
+ store.close();
28
+ return `Created ${configPath}\nCreated ${dbPath}`;
29
+ }
30
+ export async function runTranslate(opts) {
31
+ const cfg = loadConfig(opts.configPath);
32
+ const targetLang = opts.targetLang.toUpperCase();
33
+ const store = new TranslationStore(cfg.storagePath);
34
+ await store.initialize();
35
+ const projectContextHash = hash(cfg.projectContext);
36
+ const stringContextHash = hash(opts.stringContext ?? '');
37
+ const cached = await store.lookup({
38
+ sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
39
+ projectContextHash, stringContextHash,
40
+ });
41
+ if (cached !== null) {
42
+ store.close();
43
+ return `[cached] ${interpolateVars(cached, opts.vars)}`;
44
+ }
45
+ const apiKey = process.env[cfg.apiKeyEnv];
46
+ const translated = await backendTranslate({
47
+ sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
48
+ projectContext: cfg.projectContext, stringContext: opts.stringContext ?? null,
49
+ apiKey: apiKey, model: cfg.backendModel,
50
+ timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
51
+ });
52
+ if (!validateTranslation(opts.text, translated)) {
53
+ await store.insert({
54
+ sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
55
+ projectContextHash, stringContextHash,
56
+ translatedText: translated, model: cfg.backendModel, status: 'failed',
57
+ });
58
+ store.close();
59
+ return `[failed] Validation failed: ${translated}`;
60
+ }
61
+ await store.insert({
62
+ sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
63
+ projectContextHash, stringContextHash,
64
+ translatedText: translated, model: cfg.backendModel, status: 'translated',
65
+ });
66
+ store.close();
67
+ return `[new] ${interpolateVars(translated, opts.vars)}`;
68
+ }
69
+ export async function runTranslatePlural(opts) {
70
+ const cfg = loadConfig(opts.configPath);
71
+ const targetLang = opts.targetLang.toUpperCase();
72
+ const store = new TranslationStore(cfg.storagePath);
73
+ await store.initialize();
74
+ const projectContextHash = hash(cfg.projectContext);
75
+ const stringContextHash = hash(opts.stringContext ?? '');
76
+ // Auto-include {count} in vars
77
+ const countVal = Number.isInteger(opts.count) ? opts.count : opts.count;
78
+ const varsDict = { count: countVal };
79
+ // Build cache key
80
+ const sourceKey = opts.one + '\x00' + opts.other;
81
+ // Determine needed plural category for target language
82
+ const neededCategory = getPluralCategory(targetLang, opts.count);
83
+ // Check cache
84
+ const cachedForms = await store.lookupPlural({
85
+ sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang,
86
+ projectContextHash, stringContextHash,
87
+ });
88
+ if (neededCategory in cachedForms) {
89
+ const result = interpolateVars(cachedForms[neededCategory], varsDict);
90
+ store.close();
91
+ return `[cached] ${result}`;
92
+ }
93
+ // Cache miss — call backend
94
+ try {
95
+ const apiKey = process.env[cfg.apiKeyEnv];
96
+ const forms = await backendTranslatePlural({
97
+ one: opts.one, other: opts.other,
98
+ sourceLang: cfg.sourceLang, targetLang,
99
+ projectContext: cfg.projectContext, stringContext: opts.stringContext ?? null,
100
+ apiKey: apiKey, model: cfg.backendModel,
101
+ timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
102
+ });
103
+ // Validate each form
104
+ const sourcePlaceholders = new Set([
105
+ ...extractPlaceholders(opts.one),
106
+ ...extractPlaceholders(opts.other),
107
+ ]);
108
+ const validCategories = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
109
+ let allValid = true;
110
+ for (const [cat, translatedText] of Object.entries(forms)) {
111
+ if (!validCategories.has(cat)) {
112
+ allValid = false;
113
+ break;
114
+ }
115
+ if (!translatedText || !translatedText.trim()) {
116
+ allValid = false;
117
+ break;
118
+ }
119
+ if (sourcePlaceholders.size > 0) {
120
+ const translatedPhs = extractPlaceholders(translatedText);
121
+ for (const p of sourcePlaceholders) {
122
+ if (!translatedPhs.has(p)) {
123
+ allValid = false;
124
+ break;
125
+ }
126
+ }
127
+ if (!allValid)
128
+ break;
129
+ }
130
+ }
131
+ if (!allValid || !('other' in forms)) {
132
+ // Store as failed, fall back to source
133
+ for (const [cat, translatedText] of Object.entries(forms)) {
134
+ await store.insertPlural({
135
+ sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang,
136
+ projectContextHash, stringContextHash,
137
+ pluralCategory: cat, translatedText,
138
+ model: cfg.backendModel, status: 'failed',
139
+ });
140
+ }
141
+ const sourceCategory = getPluralCategory(cfg.sourceLang, opts.count);
142
+ const fallback = sourceCategory === 'other' ? opts.other : opts.one;
143
+ const result = interpolateVars(fallback, varsDict);
144
+ store.close();
145
+ return `[failed] ${result}`;
146
+ }
147
+ // Store all forms
148
+ for (const [cat, translatedText] of Object.entries(forms)) {
149
+ await store.insertPlural({
150
+ sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang,
151
+ projectContextHash, stringContextHash,
152
+ pluralCategory: cat, translatedText,
153
+ model: cfg.backendModel, status: 'translated',
154
+ });
155
+ }
156
+ // Select the right form
157
+ const selected = forms[neededCategory] ?? forms['other'];
158
+ const result = interpolateVars(selected, varsDict);
159
+ store.close();
160
+ return `[new] ${result}`;
161
+ }
162
+ catch {
163
+ const sourceCategory = getPluralCategory(cfg.sourceLang, opts.count);
164
+ const fallback = sourceCategory === 'other' ? opts.other : opts.one;
165
+ const result = interpolateVars(fallback, varsDict);
166
+ store.close();
167
+ return `[failed] ${result}`;
168
+ }
169
+ }
170
+ export async function runWarm(opts) {
171
+ const cfg = loadConfig(opts.configPath);
172
+ const targetLangs = opts.langs.map(l => l.toUpperCase());
173
+ const store = new TranslationStore(cfg.storagePath);
174
+ await store.initialize();
175
+ const content = readFileSync(opts.filePath, 'utf-8');
176
+ let entries;
177
+ if (opts.filePath.endsWith('.json')) {
178
+ entries = JSON.parse(content);
179
+ }
180
+ else {
181
+ entries = content.split('\n').filter(l => l.trim()).map(text => ({ text: text.trim() }));
182
+ }
183
+ const apiKey = process.env[cfg.apiKeyEnv];
184
+ const projectContextHash = hash(cfg.projectContext);
185
+ let translated = 0, skipped = 0, failed = 0;
186
+ for (const entry of entries) {
187
+ if (entry.plural) {
188
+ // Plural entry
189
+ const sourceKey = entry.one + '\x00' + entry.other;
190
+ const stringContextHash = hash(entry.context ?? '');
191
+ for (const lang of targetLangs) {
192
+ const cachedForms = await store.lookupPlural({
193
+ sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
194
+ projectContextHash, stringContextHash,
195
+ });
196
+ if (Object.keys(cachedForms).length > 0) {
197
+ skipped++;
198
+ continue;
199
+ }
200
+ try {
201
+ const forms = await backendTranslatePlural({
202
+ one: entry.one, other: entry.other,
203
+ sourceLang: cfg.sourceLang, targetLang: lang,
204
+ projectContext: cfg.projectContext, stringContext: entry.context ?? null,
205
+ apiKey: apiKey, model: cfg.backendModel,
206
+ timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
207
+ });
208
+ const sourcePlaceholders = new Set([
209
+ ...extractPlaceholders(entry.one),
210
+ ...extractPlaceholders(entry.other),
211
+ ]);
212
+ const validCategories = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
213
+ let anyStored = false;
214
+ for (const [cat, translatedText] of Object.entries(forms)) {
215
+ if (!validCategories.has(cat) || !translatedText)
216
+ continue;
217
+ const translatedPhs = extractPlaceholders(translatedText);
218
+ let allPresent = true;
219
+ for (const p of sourcePlaceholders) {
220
+ if (!translatedPhs.has(p)) {
221
+ allPresent = false;
222
+ break;
223
+ }
224
+ }
225
+ await store.insertPlural({
226
+ sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
227
+ projectContextHash, stringContextHash,
228
+ pluralCategory: cat, translatedText: translatedText,
229
+ model: cfg.backendModel, status: allPresent ? 'translated' : 'failed',
230
+ });
231
+ anyStored = true;
232
+ }
233
+ if (anyStored)
234
+ translated++;
235
+ else
236
+ failed++;
237
+ }
238
+ catch {
239
+ failed++;
240
+ }
241
+ }
242
+ }
243
+ else {
244
+ // Regular entry
245
+ const stringContextHash = hash(entry.context ?? '');
246
+ for (const lang of targetLangs) {
247
+ const cached = await store.lookup({
248
+ sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
249
+ projectContextHash, stringContextHash,
250
+ });
251
+ if (cached !== null) {
252
+ skipped++;
253
+ continue;
254
+ }
255
+ try {
256
+ const result = await backendTranslate({
257
+ sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
258
+ projectContext: cfg.projectContext, stringContext: entry.context ?? null,
259
+ apiKey: apiKey, model: cfg.backendModel,
260
+ timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
261
+ });
262
+ if (validateTranslation(entry.text, result)) {
263
+ await store.insert({
264
+ sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
265
+ projectContextHash, stringContextHash,
266
+ translatedText: result, model: cfg.backendModel, status: 'translated',
267
+ });
268
+ translated++;
269
+ }
270
+ else {
271
+ await store.insert({
272
+ sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
273
+ projectContextHash, stringContextHash,
274
+ translatedText: result, model: cfg.backendModel, status: 'failed',
275
+ });
276
+ failed++;
277
+ }
278
+ }
279
+ catch {
280
+ failed++;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ store.close();
286
+ return `Translated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`;
287
+ }
288
+ export async function runStats(opts) {
289
+ const cfg = loadConfig(opts.configPath);
290
+ const store = new TranslationStore(cfg.storagePath);
291
+ await store.initialize();
292
+ const st = await store.stats();
293
+ const lines = [
294
+ `Total translations: ${st.totalTranslations}`,
295
+ `Failed translations: ${st.totalFailed}`,
296
+ ];
297
+ if (Object.keys(st.byLanguage).length > 0) {
298
+ lines.push('By language:');
299
+ for (const [lang, count] of Object.entries(st.byLanguage).sort()) {
300
+ lines.push(` ${lang}: ${count}`);
301
+ }
302
+ }
303
+ store.close();
304
+ return lines.join('\n');
305
+ }
306
+ // CLI entry point
307
+ const program = new Command();
308
+ program.name('transduck').description('AI-native translation tool').version('0.1.0');
309
+ program.command('init')
310
+ .description('Initialize a new transduck project')
311
+ .action(async () => {
312
+ const readline = await import('readline');
313
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
314
+ const ask = (q) => new Promise(r => rl.question(q, r));
315
+ const dir = await ask('Project directory: ');
316
+ const name = await ask('Project name: ');
317
+ const context = await ask('Project context: ');
318
+ const sourceLang = await ask('Source language (e.g. EN): ');
319
+ const targetsRaw = await ask('Target languages (comma-separated): ');
320
+ rl.close();
321
+ const output = await runInit({
322
+ dir, name, context, sourceLang,
323
+ targetLangs: targetsRaw.split(',').map(s => s.trim()),
324
+ });
325
+ console.log(output);
326
+ });
327
+ program.command('translate')
328
+ .description('Translate a single string')
329
+ .argument('<text>', 'Source text to translate')
330
+ .requiredOption('--to <lang>', 'Target language code')
331
+ .option('--context <context>', 'String context')
332
+ .option('--config <path>', 'Path to transduck.yaml')
333
+ .option('--vars <json>', 'JSON vars for interpolation')
334
+ .action(async (text, opts) => {
335
+ const varsDict = opts.vars ? JSON.parse(opts.vars) : undefined;
336
+ const output = await runTranslate({
337
+ text, targetLang: opts.to, stringContext: opts.context,
338
+ configPath: opts.config, vars: varsDict,
339
+ });
340
+ console.log(output);
341
+ });
342
+ program.command('translate-plural')
343
+ .description('Translate a plural string')
344
+ .requiredOption('--one <text>', 'Singular form')
345
+ .requiredOption('--other <text>', 'Plural form')
346
+ .requiredOption('--count <number>', 'Count value', parseFloat)
347
+ .requiredOption('--to <lang>', 'Target language code')
348
+ .option('--context <context>', 'String context')
349
+ .option('--config <path>', 'Path to transduck.yaml')
350
+ .action(async (opts) => {
351
+ const output = await runTranslatePlural({
352
+ one: opts.one, other: opts.other, count: opts.count,
353
+ targetLang: opts.to, stringContext: opts.context,
354
+ configPath: opts.config,
355
+ });
356
+ console.log(output);
357
+ });
358
+ program.command('warm')
359
+ .description('Pre-generate translations from a file')
360
+ .requiredOption('--file <path>', 'JSON or text file with strings')
361
+ .requiredOption('--langs <langs>', 'Comma-separated target languages')
362
+ .option('--config <path>', 'Path to transduck.yaml')
363
+ .action(async (opts) => {
364
+ const output = await runWarm({
365
+ filePath: opts.file,
366
+ langs: opts.langs.split(',').map(s => s.trim()),
367
+ configPath: opts.config,
368
+ });
369
+ console.log(output);
370
+ });
371
+ program.command('stats')
372
+ .description('Show translation database statistics')
373
+ .option('--config <path>', 'Path to transduck.yaml')
374
+ .action(async (opts) => {
375
+ const output = await runStats({ configPath: opts.config ?? '' });
376
+ console.log(output);
377
+ });
378
+ export { program };
379
+ // Only run if this is the entry point
380
+ const isMain = process.argv[1]?.endsWith('cli.js') || process.argv[1]?.endsWith('cli.ts');
381
+ if (isMain) {
382
+ program.parse();
383
+ }
@@ -0,0 +1,12 @@
1
+ export interface TransduckConfig {
2
+ projectName: string;
3
+ projectContext: string;
4
+ sourceLang: string;
5
+ targetLangs: string[];
6
+ storagePath: string;
7
+ apiKeyEnv: string;
8
+ backendModel: string;
9
+ backendTimeout: number;
10
+ backendMaxRetries: number;
11
+ }
12
+ export declare function loadConfig(path?: string): TransduckConfig;
package/dist/config.js ADDED
@@ -0,0 +1,43 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { resolve, dirname, join } from 'path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ const CONFIG_FILENAME = 'transduck.yaml';
5
+ function findConfig() {
6
+ const envPath = process.env.TRANSDUCK_CONFIG;
7
+ if (envPath) {
8
+ if (existsSync(envPath))
9
+ return resolve(envPath);
10
+ throw new Error(`TRANSDUCK_CONFIG points to ${envPath} which does not exist`);
11
+ }
12
+ let current = process.cwd();
13
+ while (true) {
14
+ const candidate = join(current, CONFIG_FILENAME);
15
+ if (existsSync(candidate))
16
+ return candidate;
17
+ const parent = dirname(current);
18
+ if (parent === current)
19
+ break;
20
+ current = parent;
21
+ }
22
+ throw new Error(`Could not find ${CONFIG_FILENAME}. Run 'transduck init' or set TRANSDUCK_CONFIG.`);
23
+ }
24
+ export function loadConfig(path) {
25
+ const configPath = path ? resolve(path) : findConfig();
26
+ if (!existsSync(configPath)) {
27
+ throw new Error(`Config file not found: ${configPath}`);
28
+ }
29
+ const raw = parseYaml(readFileSync(configPath, 'utf-8'));
30
+ const configDir = dirname(configPath);
31
+ const storagePath = resolve(configDir, raw.storage.path);
32
+ return {
33
+ projectName: raw.project.name,
34
+ projectContext: raw.project.context,
35
+ sourceLang: String(raw.languages.source).toUpperCase(),
36
+ targetLangs: raw.languages.targets.map((l) => String(l).toUpperCase()),
37
+ storagePath,
38
+ apiKeyEnv: raw.backend.api_key_env,
39
+ backendModel: raw.backend.model,
40
+ backendTimeout: raw.backend.timeout_seconds,
41
+ backendMaxRetries: raw.backend.max_retries,
42
+ };
43
+ }
@@ -0,0 +1,11 @@
1
+ import { type TransduckConfig } from './config.js';
2
+ import { TranslationStore } from './storage.js';
3
+ export declare function initialize(config?: TransduckConfig): Promise<void>;
4
+ export declare function setLanguage(lang: string): void;
5
+ export declare function _resetState(): void;
6
+ export declare function _getStore(): TranslationStore | null;
7
+ export declare function ait(sourceText: string, context?: string, vars?: Record<string, string | number>): Promise<string>;
8
+ export declare function aitPlural(one: string, other: string, count: number, opts?: {
9
+ context?: string;
10
+ vars?: Record<string, string | number>;
11
+ }): Promise<string>;