transduck 0.5.2 → 0.5.3

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
@@ -493,7 +493,7 @@ export async function runStats(opts) {
493
493
  }
494
494
  // CLI entry point
495
495
  const program = new Command();
496
- program.name('transduck').description('AI-native translation tool').version('0.5.2');
496
+ program.name('transduck').description('AI-native translation tool').version('0.5.3');
497
497
  program.command('init')
498
498
  .description('Initialize a new transduck project')
499
499
  .action(async () => {
package/dist/scanner.js CHANGED
@@ -32,10 +32,24 @@ function shouldSkipDir(dirname) {
32
32
  return true;
33
33
  return false;
34
34
  }
35
+ /**
36
+ * Join adjacent string literals so the regex can match multi-line strings.
37
+ *
38
+ * Handles:
39
+ * - Python implicit concatenation: "foo" \n "bar" -> "foobar"
40
+ * - JS + concatenation: "foo" + \n "bar" -> "foobar"
41
+ * - Both single and double quotes
42
+ */
43
+ function normalizeMultilineStrings(content) {
44
+ // Join adjacent string literals: "..." \s+ "..." or '...' \s+ '...'
45
+ // Also handles "..." + \s+ "..." for JS
46
+ return content.replace(/(['"])\s*\+?\s*\n\s*\1/g, '');
47
+ }
35
48
  /**
36
49
  * Extract translatable strings from file content.
37
50
  */
38
51
  export function extractStrings(content, filename) {
52
+ content = normalizeMultilineStrings(content);
39
53
  const results = [];
40
54
  const ext = extname(filename).toLowerCase();
41
55
  const isJs = JS_EXTENSIONS.has(ext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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
@@ -611,7 +611,7 @@ export async function runStats(opts: StatsOptions): Promise<string> {
611
611
  // CLI entry point
612
612
  const program = new Command();
613
613
 
614
- program.name('transduck').description('AI-native translation tool').version('0.5.2');
614
+ program.name('transduck').description('AI-native translation tool').version('0.5.3');
615
615
 
616
616
  program.command('init')
617
617
  .description('Initialize a new transduck project')
package/src/scanner.ts CHANGED
@@ -57,10 +57,28 @@ function shouldSkipDir(dirname: string): boolean {
57
57
  return false;
58
58
  }
59
59
 
60
+ /**
61
+ * Join adjacent string literals so the regex can match multi-line strings.
62
+ *
63
+ * Handles:
64
+ * - Python implicit concatenation: "foo" \n "bar" -> "foobar"
65
+ * - JS + concatenation: "foo" + \n "bar" -> "foobar"
66
+ * - Both single and double quotes
67
+ */
68
+ function normalizeMultilineStrings(content: string): string {
69
+ // Join adjacent string literals: "..." \s+ "..." or '...' \s+ '...'
70
+ // Also handles "..." + \s+ "..." for JS
71
+ return content.replace(
72
+ /(['"])\s*\+?\s*\n\s*\1/g,
73
+ '',
74
+ );
75
+ }
76
+
60
77
  /**
61
78
  * Extract translatable strings from file content.
62
79
  */
63
80
  export function extractStrings(content: string, filename: string): ScanEntry[] {
81
+ content = normalizeMultilineStrings(content);
64
82
  const results: ScanEntry[] = [];
65
83
  const ext = extname(filename).toLowerCase();
66
84
  const isJs = JS_EXTENSIONS.has(ext);
@@ -124,6 +124,44 @@ ait_plural("{count} item", "{count} items", count=n)
124
124
  expect(result[0].one).toBe('{count} item');
125
125
  expect(result[0].other).toBe('{count} items');
126
126
  });
127
+
128
+ it('extracts multi-line Python implicit concatenation', () => {
129
+ const code = `ait(\n "This is a long "\n "paragraph that spans "\n "multiple lines"\n)`;
130
+ const result = extractStrings(code, 'test.py');
131
+ expect(result).toHaveLength(1);
132
+ expect(result[0].text).toBe('This is a long paragraph that spans multiple lines');
133
+ });
134
+
135
+ it('extracts multi-line with context', () => {
136
+ const code = `ait(\n "This is a long "\n "paragraph",\n context="Some context"\n)`;
137
+ const result = extractStrings(code, 'test.py');
138
+ expect(result).toHaveLength(1);
139
+ expect(result[0].text).toBe('This is a long paragraph');
140
+ expect(result[0].context).toBe('Some context');
141
+ });
142
+
143
+ it('extracts multi-line JS + concatenation', () => {
144
+ const code = `ait(\n "This is a long " +\n "paragraph that spans " +\n "multiple lines"\n)`;
145
+ const result = extractStrings(code, 'test.js');
146
+ expect(result).toHaveLength(1);
147
+ expect(result[0].text).toBe('This is a long paragraph that spans multiple lines');
148
+ });
149
+
150
+ it('extracts multi-line plural strings', () => {
151
+ const code = `ait_plural(\n "{count} long item "\n "description here",\n "{count} long items "\n "description here",\n count=n\n)`;
152
+ const result = extractStrings(code, 'test.py');
153
+ expect(result).toHaveLength(1);
154
+ expect(result[0].plural).toBe(true);
155
+ expect(result[0].one).toBe('{count} long item description here');
156
+ expect(result[0].other).toBe('{count} long items description here');
157
+ });
158
+
159
+ it('extracts multi-line single-quoted strings', () => {
160
+ const code = `ait(\n 'This is a long '\n 'paragraph'\n)`;
161
+ const result = extractStrings(code, 'test.py');
162
+ expect(result).toHaveLength(1);
163
+ expect(result[0].text).toBe('This is a long paragraph');
164
+ });
127
165
  });
128
166
 
129
167
  describe('scanDirectory', () => {