transduck 0.6.4 → 0.6.6

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 CHANGED
@@ -544,7 +544,7 @@ export async function runStats(opts) {
544
544
  }
545
545
  // CLI entry point
546
546
  const program = new Command();
547
- program.name('transduck').description('AI-native translation tool').version('0.6.4');
547
+ program.name('transduck').description('AI-native translation tool').version('0.6.6');
548
548
  program.command('init')
549
549
  .description('Initialize a new transduck project')
550
550
  .action(async () => {
package/dist/scanner.js CHANGED
@@ -19,6 +19,7 @@ const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?
19
19
  // tPlural("one", "other") — only matched in files with transduck/react import
20
20
  const T_PLURAL = /(?<![a-zA-Z_.$])tPlural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
21
21
  const HAS_TRANSDUCK_REACT = /from\s+['"]transduck\/react['"]/;
22
+ const HAS_AIT_IDENTIFIER = /\bait\b/;
22
23
  // File extensions that use JS-style positional context
23
24
  const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx']);
24
25
  // File extensions that may contain Django template tags
@@ -26,12 +27,15 @@ const TEMPLATE_EXTENSIONS = new Set(['.html', '.jinja', '.jinja2']);
26
27
  // Supported file extensions for scanning
27
28
  const SCAN_EXTENSIONS = new Set(['.py', '.js', '.ts', '.tsx', '.jsx', '.html', '.jinja', '.jinja2']);
28
29
  // Directories to skip
29
- const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', '__pycache__', '.git', 'dist', 'build', '.next']);
30
+ const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', '__pycache__', '.git', 'dist', 'build', '.next', 'site-packages']);
30
31
  function shouldSkipDir(dirname) {
31
32
  if (SKIP_DIRS.has(dirname))
32
33
  return true;
33
34
  if (dirname.includes('egg-info'))
34
35
  return true;
36
+ // Skip venv variants: venv312, .venv3, env, .env, etc.
37
+ if (dirname.startsWith('venv') || dirname.startsWith('.venv') || dirname.startsWith('env') || dirname.startsWith('.env'))
38
+ return true;
35
39
  return false;
36
40
  }
37
41
  /**
@@ -113,8 +117,8 @@ export function extractStrings(content, filename) {
113
117
  const lineNum = content.slice(0, pos).split('\n').length;
114
118
  results.push({ text, context, line: lineNum });
115
119
  }
116
- // 4. t() and tPlural() — only in files that import from transduck/react
117
- if (HAS_TRANSDUCK_REACT.test(content)) {
120
+ // 4. t() and tPlural() — in files that import from transduck/react or use ait
121
+ if (HAS_TRANSDUCK_REACT.test(content) || (isJs && HAS_AIT_IDENTIFIER.test(content))) {
118
122
  // t() calls
119
123
  for (const m of content.matchAll(new RegExp(T_POSITIONAL.source, 'g'))) {
120
124
  const pos = m.index;
package/dist/storage.d.ts CHANGED
@@ -42,6 +42,8 @@ export declare class TranslationStore {
42
42
  * Delete entries matching optional filters. Returns count deleted.
43
43
  */
44
44
  clear(targetLang?: string, failedOnly?: boolean): Promise<number>;
45
+ /** Flush WAL to main database file so it's visible to git/Docker/deploys. */
46
+ checkpoint(): void;
45
47
  close(): void;
46
48
  }
47
49
  export {};
package/dist/storage.js CHANGED
@@ -119,8 +119,15 @@ export class TranslationStore {
119
119
  const result = db.prepare(sql).run(...params);
120
120
  return result.changes;
121
121
  }
122
+ /** Flush WAL to main database file so it's visible to git/Docker/deploys. */
123
+ checkpoint() {
124
+ if (this.db) {
125
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
126
+ }
127
+ }
122
128
  close() {
123
129
  if (this.db) {
130
+ this.checkpoint();
124
131
  this.db.close();
125
132
  this.db = null;
126
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "AI-native translation tool using source text as keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/cli.ts CHANGED
@@ -661,7 +661,7 @@ export async function runStats(opts: StatsOptions): Promise<string> {
661
661
  // CLI entry point
662
662
  const program = new Command();
663
663
 
664
- program.name('transduck').description('AI-native translation tool').version('0.6.4');
664
+ program.name('transduck').description('AI-native translation tool').version('0.6.6');
665
665
 
666
666
  program.command('init')
667
667
  .description('Initialize a new transduck project')
package/src/scanner.ts CHANGED
@@ -41,6 +41,7 @@ const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?
41
41
  const T_PLURAL = /(?<![a-zA-Z_.$])tPlural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
42
42
 
43
43
  const HAS_TRANSDUCK_REACT = /from\s+['"]transduck\/react['"]/;
44
+ const HAS_AIT_IDENTIFIER = /\bait\b/;
44
45
 
45
46
  // File extensions that use JS-style positional context
46
47
  const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx']);
@@ -52,11 +53,13 @@ const TEMPLATE_EXTENSIONS = new Set(['.html', '.jinja', '.jinja2']);
52
53
  const SCAN_EXTENSIONS = new Set(['.py', '.js', '.ts', '.tsx', '.jsx', '.html', '.jinja', '.jinja2']);
53
54
 
54
55
  // Directories to skip
55
- const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', '__pycache__', '.git', 'dist', 'build', '.next']);
56
+ const SKIP_DIRS = new Set(['node_modules', '.venv', 'venv', '__pycache__', '.git', 'dist', 'build', '.next', 'site-packages']);
56
57
 
57
58
  function shouldSkipDir(dirname: string): boolean {
58
59
  if (SKIP_DIRS.has(dirname)) return true;
59
60
  if (dirname.includes('egg-info')) return true;
61
+ // Skip venv variants: venv312, .venv3, env, .env, etc.
62
+ if (dirname.startsWith('venv') || dirname.startsWith('.venv') || dirname.startsWith('env') || dirname.startsWith('.env')) return true;
60
63
  return false;
61
64
  }
62
65
 
@@ -151,8 +154,8 @@ export function extractStrings(content: string, filename: string): ScanEntry[] {
151
154
  results.push({ text, context, line: lineNum });
152
155
  }
153
156
 
154
- // 4. t() and tPlural() — only in files that import from transduck/react
155
- if (HAS_TRANSDUCK_REACT.test(content)) {
157
+ // 4. t() and tPlural() — in files that import from transduck/react or use ait
158
+ if (HAS_TRANSDUCK_REACT.test(content) || (isJs && HAS_AIT_IDENTIFIER.test(content))) {
156
159
  // t() calls
157
160
  for (const m of content.matchAll(new RegExp(T_POSITIONAL.source, 'g'))) {
158
161
  const pos = m.index!;
package/src/storage.ts CHANGED
@@ -182,8 +182,16 @@ export class TranslationStore {
182
182
  return result.changes;
183
183
  }
184
184
 
185
+ /** Flush WAL to main database file so it's visible to git/Docker/deploys. */
186
+ checkpoint(): void {
187
+ if (this.db) {
188
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
189
+ }
190
+ }
191
+
185
192
  close(): void {
186
193
  if (this.db) {
194
+ this.checkpoint();
187
195
  this.db.close();
188
196
  this.db = null;
189
197
  }
@@ -125,6 +125,28 @@ ait_plural("{count} item", "{count} items", count=n)
125
125
  expect(result[0].other).toBe('{count} items');
126
126
  });
127
127
 
128
+ it('extracts t() from server component with ait (issue #2)', () => {
129
+ const code = `import { ait } from "@/lib/transduck";
130
+ const t = async (str, ctx) => String(await ait(str, ctx));
131
+ const headline = await t("Your property visits, organized.", "Landing page headline");`;
132
+ const result = extractStrings(code, 'test.tsx');
133
+ const texts = result.filter(e => !e.plural).map(e => e.text);
134
+ expect(texts).toContain('Your property visits, organized.');
135
+ });
136
+
137
+ it('extracts t() from direct transduck import', () => {
138
+ const code = `import { ait } from 'transduck';\nconst t = ait;\nt("Hello")`;
139
+ const result = extractStrings(code, 'test.ts');
140
+ const texts = result.filter(e => !e.plural).map(e => e.text);
141
+ expect(texts).toContain('Hello');
142
+ });
143
+
144
+ it('does not false-positive t() in .py without ait', () => {
145
+ const code = `from gettext import gettext as t\nt('Hello')`;
146
+ const result = extractStrings(code, 'test.py');
147
+ expect(result).toHaveLength(0);
148
+ });
149
+
128
150
  it('extracts multi-line Python implicit concatenation', () => {
129
151
  const code = `ait(\n "This is a long "\n "paragraph that spans "\n "multiple lines"\n)`;
130
152
  const result = extractStrings(code, 'test.py');
@@ -239,6 +261,29 @@ describe('scanDirectory', () => {
239
261
  expect(result).toHaveLength(1);
240
262
  });
241
263
 
264
+ it('skips venv with suffix (venv312)', () => {
265
+ const tmp = makeTmpDir();
266
+ const venv = join(tmp, 'venv312');
267
+ mkdirSync(venv);
268
+ writeFileSync(join(venv, 'lib.py'), 'ait("Hidden")');
269
+ writeFileSync(join(tmp, 'app.py'), 'ait("Visible")');
270
+ const result = scanDirectory([tmp]);
271
+ expect(result).toHaveLength(1);
272
+ expect(result[0].text).toBe('Visible');
273
+ });
274
+
275
+ it('skips env and .env directories', () => {
276
+ const tmp = makeTmpDir();
277
+ for (const name of ['env', '.env']) {
278
+ const d = join(tmp, name);
279
+ mkdirSync(d);
280
+ writeFileSync(join(d, 'lib.py'), 'ait("Hidden")');
281
+ }
282
+ writeFileSync(join(tmp, 'app.py'), 'ait("Visible")');
283
+ const result = scanDirectory([tmp]);
284
+ expect(result).toHaveLength(1);
285
+ });
286
+
242
287
  it('filters extensions', () => {
243
288
  const tmp = makeTmpDir();
244
289
  writeFileSync(join(tmp, 'readme.md'), 'ait("Not a code file")');