transduck 0.6.3 → 0.6.5
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 +1 -1
- package/dist/scanner.js +14 -2
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +7 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/scanner.ts +16 -2
- package/src/storage.ts +8 -0
- package/tests/scanner.test.ts +53 -0
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.
|
|
547
|
+
program.name('transduck').description('AI-native translation tool').version('0.6.5');
|
|
548
548
|
program.command('init')
|
|
549
549
|
.description('Initialize a new transduck project')
|
|
550
550
|
.action(async () => {
|
package/dist/scanner.js
CHANGED
|
@@ -12,11 +12,14 @@ const AIT_POSITIONAL_CTX = /ait\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
|
12
12
|
const AIT_PLURAL = /ait(?:_p|P)lural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
|
|
13
13
|
// {% ait "text" %} or {% ait "text" context="ctx" %}
|
|
14
14
|
const DJANGO_TAG = /\{%\s*ait\s+(['"])(.*?)\1(?:\s+context=(['"])(.*?)\3)?\s*%\}/g;
|
|
15
|
+
// {% ait_plural "one" "other" %} or {% ait_plural "one" "other" context="ctx" %}
|
|
16
|
+
const DJANGO_PLURAL_TAG = /\{%\s*ait_plural\s+(['"])(.*?)\1\s+(['"])(.*?)\3(?:\s+context=(['"])(.*?)\5)?\s*%\}/g;
|
|
15
17
|
// t("text") or t("text", "ctx") — only matched in files with transduck/react import
|
|
16
18
|
const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
17
19
|
// tPlural("one", "other") — only matched in files with transduck/react import
|
|
18
20
|
const T_PLURAL = /(?<![a-zA-Z_.$])tPlural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
|
|
19
21
|
const HAS_TRANSDUCK_REACT = /from\s+['"]transduck\/react['"]/;
|
|
22
|
+
const HAS_AIT_IDENTIFIER = /\bait\b/;
|
|
20
23
|
// File extensions that use JS-style positional context
|
|
21
24
|
const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx']);
|
|
22
25
|
// File extensions that may contain Django template tags
|
|
@@ -81,6 +84,15 @@ export function extractStrings(content, filename) {
|
|
|
81
84
|
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
82
85
|
results.push({ text, context, line: lineNum });
|
|
83
86
|
}
|
|
87
|
+
const djangoPluralRegex = new RegExp(DJANGO_PLURAL_TAG.source, 'g');
|
|
88
|
+
while ((match = djangoPluralRegex.exec(content)) !== null) {
|
|
89
|
+
const one = match[2];
|
|
90
|
+
const other = match[4];
|
|
91
|
+
const context = match[6] || null;
|
|
92
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
93
|
+
results.push({ plural: true, one, other, context, line: lineNum });
|
|
94
|
+
pluralSpans.push([match.index, match.index + match[0].length]);
|
|
95
|
+
}
|
|
84
96
|
}
|
|
85
97
|
// 3. Find ait() calls
|
|
86
98
|
const pattern = isJs ? AIT_POSITIONAL_CTX : AIT_KEYWORD_CTX;
|
|
@@ -102,8 +114,8 @@ export function extractStrings(content, filename) {
|
|
|
102
114
|
const lineNum = content.slice(0, pos).split('\n').length;
|
|
103
115
|
results.push({ text, context, line: lineNum });
|
|
104
116
|
}
|
|
105
|
-
// 4. t() and tPlural() —
|
|
106
|
-
if (HAS_TRANSDUCK_REACT.test(content)) {
|
|
117
|
+
// 4. t() and tPlural() — in files that import from transduck/react or use ait
|
|
118
|
+
if (HAS_TRANSDUCK_REACT.test(content) || (isJs && HAS_AIT_IDENTIFIER.test(content))) {
|
|
107
119
|
// t() calls
|
|
108
120
|
for (const m of content.matchAll(new RegExp(T_POSITIONAL.source, 'g'))) {
|
|
109
121
|
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
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.
|
|
664
|
+
program.name('transduck').description('AI-native translation tool').version('0.6.5');
|
|
665
665
|
|
|
666
666
|
program.command('init')
|
|
667
667
|
.description('Initialize a new transduck project')
|
package/src/scanner.ts
CHANGED
|
@@ -31,6 +31,9 @@ const AIT_PLURAL = /ait(?:_p|P)lural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
|
|
|
31
31
|
// {% ait "text" %} or {% ait "text" context="ctx" %}
|
|
32
32
|
const DJANGO_TAG = /\{%\s*ait\s+(['"])(.*?)\1(?:\s+context=(['"])(.*?)\3)?\s*%\}/g;
|
|
33
33
|
|
|
34
|
+
// {% ait_plural "one" "other" %} or {% ait_plural "one" "other" context="ctx" %}
|
|
35
|
+
const DJANGO_PLURAL_TAG = /\{%\s*ait_plural\s+(['"])(.*?)\1\s+(['"])(.*?)\3(?:\s+context=(['"])(.*?)\5)?\s*%\}/g;
|
|
36
|
+
|
|
34
37
|
// t("text") or t("text", "ctx") — only matched in files with transduck/react import
|
|
35
38
|
const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?)\3)?/g;
|
|
36
39
|
|
|
@@ -38,6 +41,7 @@ const T_POSITIONAL = /(?<![a-zA-Z_.$])t\s*\(\s*(['"])(.*?)\1(?:\s*,\s*(['"])(.*?
|
|
|
38
41
|
const T_PLURAL = /(?<![a-zA-Z_.$])tPlural\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3/g;
|
|
39
42
|
|
|
40
43
|
const HAS_TRANSDUCK_REACT = /from\s+['"]transduck\/react['"]/;
|
|
44
|
+
const HAS_AIT_IDENTIFIER = /\bait\b/;
|
|
41
45
|
|
|
42
46
|
// File extensions that use JS-style positional context
|
|
43
47
|
const JS_EXTENSIONS = new Set(['.js', '.ts', '.tsx', '.jsx']);
|
|
@@ -113,6 +117,16 @@ export function extractStrings(content: string, filename: string): ScanEntry[] {
|
|
|
113
117
|
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
114
118
|
results.push({ text, context, line: lineNum });
|
|
115
119
|
}
|
|
120
|
+
|
|
121
|
+
const djangoPluralRegex = new RegExp(DJANGO_PLURAL_TAG.source, 'g');
|
|
122
|
+
while ((match = djangoPluralRegex.exec(content)) !== null) {
|
|
123
|
+
const one = match[2];
|
|
124
|
+
const other = match[4];
|
|
125
|
+
const context = match[6] || null;
|
|
126
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
127
|
+
results.push({ plural: true, one, other, context, line: lineNum });
|
|
128
|
+
pluralSpans.push([match.index, match.index + match[0].length]);
|
|
129
|
+
}
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
// 3. Find ait() calls
|
|
@@ -138,8 +152,8 @@ export function extractStrings(content: string, filename: string): ScanEntry[] {
|
|
|
138
152
|
results.push({ text, context, line: lineNum });
|
|
139
153
|
}
|
|
140
154
|
|
|
141
|
-
// 4. t() and tPlural() —
|
|
142
|
-
if (HAS_TRANSDUCK_REACT.test(content)) {
|
|
155
|
+
// 4. t() and tPlural() — in files that import from transduck/react or use ait
|
|
156
|
+
if (HAS_TRANSDUCK_REACT.test(content) || (isJs && HAS_AIT_IDENTIFIER.test(content))) {
|
|
143
157
|
// t() calls
|
|
144
158
|
for (const m of content.matchAll(new RegExp(T_POSITIONAL.source, 'g'))) {
|
|
145
159
|
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
|
}
|
package/tests/scanner.test.ts
CHANGED
|
@@ -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');
|
|
@@ -156,6 +178,37 @@ ait_plural("{count} item", "{count} items", count=n)
|
|
|
156
178
|
expect(result[0].other).toBe('{count} long items description here');
|
|
157
179
|
});
|
|
158
180
|
|
|
181
|
+
it('extracts Django plural template tag', () => {
|
|
182
|
+
const result = extractStrings('{% ait_plural "{count} night" "{count} nights" %}', 'test.html');
|
|
183
|
+
expect(result).toHaveLength(1);
|
|
184
|
+
expect(result[0].plural).toBe(true);
|
|
185
|
+
expect(result[0].one).toBe('{count} night');
|
|
186
|
+
expect(result[0].other).toBe('{count} nights');
|
|
187
|
+
expect(result[0].context).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('extracts Django plural template tag with context', () => {
|
|
191
|
+
const result = extractStrings('{% ait_plural "{count} night" "{count} nights" context="Hotel stay duration" %}', 'test.html');
|
|
192
|
+
expect(result).toHaveLength(1);
|
|
193
|
+
expect(result[0].plural).toBe(true);
|
|
194
|
+
expect(result[0].one).toBe('{count} night');
|
|
195
|
+
expect(result[0].other).toBe('{count} nights');
|
|
196
|
+
expect(result[0].context).toBe('Hotel stay duration');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('extracts Django plural tag with single quotes', () => {
|
|
200
|
+
const result = extractStrings("{% ait_plural '{count} item' '{count} items' %}", 'test.jinja2');
|
|
201
|
+
expect(result).toHaveLength(1);
|
|
202
|
+
expect(result[0].plural).toBe(true);
|
|
203
|
+
expect(result[0].one).toBe('{count} item');
|
|
204
|
+
expect(result[0].other).toBe('{count} items');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('does not extract Django plural tag in .py files', () => {
|
|
208
|
+
const result = extractStrings('{% ait_plural "{count} night" "{count} nights" %}', 'test.py');
|
|
209
|
+
expect(result).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
159
212
|
it('extracts multi-line single-quoted strings', () => {
|
|
160
213
|
const code = `ait(\n 'This is a long '\n 'paragraph'\n)`;
|
|
161
214
|
const result = extractStrings(code, 'test.py');
|