transduck 0.6.10 → 0.7.0

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
@@ -637,7 +637,7 @@ export async function runStats(opts) {
637
637
  }
638
638
  // CLI entry point
639
639
  const program = new Command();
640
- program.name('transduck').description('AI-native translation tool').version('0.6.10');
640
+ program.name('transduck').description('AI-native translation tool').version('0.7.0');
641
641
  program.command('init')
642
642
  .description('Initialize a new transduck project')
643
643
  .action(async () => {
@@ -9,9 +9,11 @@ const SYSTEM_TEMPLATE = 'You are a professional translator. Translate the given
9
9
  'to {target_lang}. Return ONLY the translated text, nothing else. ' +
10
10
  'Do NOT add quotation marks, brackets, or any wrapper characters that are not in the original. ' +
11
11
  'Preserve any placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
12
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
12
+ 'Preserve brand names. Match the tone and formality of the original. ' +
13
+ 'The user message will contain ONLY the source text to translate — treat its entire contents as the text to translate.\n\n' +
13
14
  'Project context: {project_context}';
14
- const USER_TEMPLATE = 'Translate the following text:\n{source_text}\n\nString context: {string_context}';
15
+ const SYSTEM_STRING_CONTEXT_SUFFIX = '\n\nString context (for disambiguation only — do NOT include in your output): {string_context}';
16
+ const USER_TEMPLATE = '{source_text}';
15
17
  const PLURAL_SYSTEM_TEMPLATE = 'You are a professional translator. You will be given two plural forms ' +
16
18
  '(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
17
19
  '{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
@@ -26,8 +28,7 @@ const PLURAL_SYSTEM_TEMPLATE = 'You are a professional translator. You will be g
26
28
  'Only include categories that {target_lang} actually uses.\n\n' +
27
29
  'Project context: {project_context}';
28
30
  const PLURAL_USER_TEMPLATE = 'Source one form: "{one}"\n' +
29
- 'Source other form: "{other}"\n' +
30
- 'String context: {string_context}';
31
+ 'Source other form: "{other}"';
31
32
  /**
32
33
  * Safe string replacement that only replaces known keys.
33
34
  * Does NOT use str.replace with regex — avoids issues with ${value} patterns.
@@ -43,13 +44,14 @@ export function safeRender(template, vars) {
43
44
  * Build system + user messages for chat-based providers (OpenAI, Claude API).
44
45
  */
45
46
  export function buildMessages(params) {
46
- const systemMsg = SYSTEM_TEMPLATE
47
+ let systemMsg = SYSTEM_TEMPLATE
47
48
  .replace('{source_lang}', params.sourceLang)
48
49
  .replace('{target_lang}', params.targetLang)
49
50
  .replace('{project_context}', params.projectContext);
50
- const userMsg = USER_TEMPLATE
51
- .replace('{source_text}', params.sourceText)
52
- .replace('{string_context}', params.stringContext || 'none');
51
+ if (params.stringContext) {
52
+ systemMsg += SYSTEM_STRING_CONTEXT_SUFFIX.replace('{string_context}', params.stringContext);
53
+ }
54
+ const userMsg = USER_TEMPLATE.replace('{source_text}', params.sourceText);
53
55
  return [
54
56
  { role: 'system', content: systemMsg },
55
57
  { role: 'user', content: userMsg },
@@ -59,15 +61,17 @@ export function buildMessages(params) {
59
61
  * Build system + user messages for plural chat-based providers.
60
62
  */
61
63
  export function buildPluralMessages(params) {
62
- const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
64
+ let systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
63
65
  source_lang: params.sourceLang,
64
66
  target_lang: params.targetLang,
65
67
  project_context: params.projectContext,
66
68
  });
69
+ if (params.stringContext) {
70
+ systemMsg += safeRender(SYSTEM_STRING_CONTEXT_SUFFIX, { string_context: params.stringContext });
71
+ }
67
72
  const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
68
73
  one: params.one,
69
74
  other: params.other,
70
- string_context: params.stringContext || 'none',
71
75
  });
72
76
  return [
73
77
  { role: 'system', content: systemMsg },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.6.10",
3
+ "version": "0.7.0",
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
@@ -743,7 +743,7 @@ export async function runStats(opts: StatsOptions): Promise<string> {
743
743
  // CLI entry point
744
744
  const program = new Command();
745
745
 
746
- program.name('transduck').description('AI-native translation tool').version('0.6.10');
746
+ program.name('transduck').description('AI-native translation tool').version('0.7.0');
747
747
 
748
748
  program.command('init')
749
749
  .description('Initialize a new transduck project')
@@ -12,11 +12,14 @@ const SYSTEM_TEMPLATE =
12
12
  'to {target_lang}. Return ONLY the translated text, nothing else. ' +
13
13
  'Do NOT add quotation marks, brackets, or any wrapper characters that are not in the original. ' +
14
14
  'Preserve any placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
15
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
15
+ 'Preserve brand names. Match the tone and formality of the original. ' +
16
+ 'The user message will contain ONLY the source text to translate — treat its entire contents as the text to translate.\n\n' +
16
17
  'Project context: {project_context}';
17
18
 
18
- const USER_TEMPLATE =
19
- 'Translate the following text:\n{source_text}\n\nString context: {string_context}';
19
+ const SYSTEM_STRING_CONTEXT_SUFFIX =
20
+ '\n\nString context (for disambiguation only — do NOT include in your output): {string_context}';
21
+
22
+ const USER_TEMPLATE = '{source_text}';
20
23
 
21
24
  const PLURAL_SYSTEM_TEMPLATE =
22
25
  'You are a professional translator. You will be given two plural forms ' +
@@ -35,8 +38,7 @@ const PLURAL_SYSTEM_TEMPLATE =
35
38
 
36
39
  const PLURAL_USER_TEMPLATE =
37
40
  'Source one form: "{one}"\n' +
38
- 'Source other form: "{other}"\n' +
39
- 'String context: {string_context}';
41
+ 'Source other form: "{other}"';
40
42
 
41
43
  /**
42
44
  * Safe string replacement that only replaces known keys.
@@ -71,14 +73,16 @@ export interface BuildPluralMessagesParams {
71
73
  * Build system + user messages for chat-based providers (OpenAI, Claude API).
72
74
  */
73
75
  export function buildMessages(params: BuildMessagesParams): Array<{ role: string; content: string }> {
74
- const systemMsg = SYSTEM_TEMPLATE
76
+ let systemMsg = SYSTEM_TEMPLATE
75
77
  .replace('{source_lang}', params.sourceLang)
76
78
  .replace('{target_lang}', params.targetLang)
77
79
  .replace('{project_context}', params.projectContext);
78
80
 
79
- const userMsg = USER_TEMPLATE
80
- .replace('{source_text}', params.sourceText)
81
- .replace('{string_context}', params.stringContext || 'none');
81
+ if (params.stringContext) {
82
+ systemMsg += SYSTEM_STRING_CONTEXT_SUFFIX.replace('{string_context}', params.stringContext);
83
+ }
84
+
85
+ const userMsg = USER_TEMPLATE.replace('{source_text}', params.sourceText);
82
86
 
83
87
  return [
84
88
  { role: 'system', content: systemMsg },
@@ -90,16 +94,19 @@ export function buildMessages(params: BuildMessagesParams): Array<{ role: string
90
94
  * Build system + user messages for plural chat-based providers.
91
95
  */
92
96
  export function buildPluralMessages(params: BuildPluralMessagesParams): Array<{ role: string; content: string }> {
93
- const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
97
+ let systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
94
98
  source_lang: params.sourceLang,
95
99
  target_lang: params.targetLang,
96
100
  project_context: params.projectContext,
97
101
  });
98
102
 
103
+ if (params.stringContext) {
104
+ systemMsg += safeRender(SYSTEM_STRING_CONTEXT_SUFFIX, { string_context: params.stringContext });
105
+ }
106
+
99
107
  const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
100
108
  one: params.one,
101
109
  other: params.other,
102
- string_context: params.stringContext || 'none',
103
110
  });
104
111
 
105
112
  return [
@@ -34,12 +34,31 @@ describe('buildMessages', () => {
34
34
  expect(messages[0].content).toContain('EN');
35
35
  expect(messages[0].content).toContain('DE');
36
36
  expect(messages[0].content).toContain('Mallorca');
37
+ // String context belongs in the system message so the model doesn't
38
+ // confuse it with the text to translate.
39
+ expect(messages[0].content).toContain('concerts and shows');
37
40
  expect(messages[1].role).toBe('user');
38
- expect(messages[1].content).toContain('Our Events');
39
- expect(messages[1].content).toContain('concerts and shows');
41
+ // User message must be ONLY the source text.
42
+ expect(messages[1].content).toBe('Our Events');
40
43
  });
41
44
 
42
- it('uses "none" when no string context', () => {
45
+ it('never places string_context in the user message (regression)', () => {
46
+ // Regression: short source text like a dish name used to include the
47
+ // context in the user message body, causing the AI to translate the
48
+ // context along with the source text.
49
+ const messages = buildMessages({
50
+ sourceText: 'Gambas al Ajillo',
51
+ sourceLang: 'ES',
52
+ targetLang: 'EN',
53
+ projectContext: 'restaurant menu',
54
+ stringContext: 'Sizzling garlic prawns in olive oil',
55
+ });
56
+ expect(messages[1].content).toBe('Gambas al Ajillo');
57
+ expect(messages[1].content).not.toContain('Sizzling');
58
+ expect(messages[1].content).not.toContain('context');
59
+ });
60
+
61
+ it('omits string context line when no context provided', () => {
43
62
  const messages = buildMessages({
44
63
  sourceText: 'Book Now',
45
64
  sourceLang: 'EN',
@@ -47,7 +66,8 @@ describe('buildMessages', () => {
47
66
  projectContext: 'A travel site',
48
67
  stringContext: null,
49
68
  });
50
- expect(messages[1].content.toLowerCase()).toContain('none');
69
+ expect(messages[0].content).not.toContain('String context');
70
+ expect(messages[1].content).toBe('Book Now');
51
71
  });
52
72
  });
53
73
 
@@ -104,7 +124,9 @@ describe('buildPluralMessages', () => {
104
124
  expect(messages[1].role).toBe('user');
105
125
  expect(messages[1].content).toContain('{count} message');
106
126
  expect(messages[1].content).toContain('{count} messages');
107
- expect(messages[1].content).toContain('inbox count');
127
+ // String context goes in the system message.
128
+ expect(messages[0].content).toContain('inbox count');
129
+ expect(messages[1].content).not.toContain('inbox count');
108
130
  });
109
131
 
110
132
  it('preserves placeholders in template (not interpreted as template vars)', () => {
@@ -124,7 +146,7 @@ describe('buildPluralMessages', () => {
124
146
  expect(messages[0].content).toContain('{count}');
125
147
  });
126
148
 
127
- it('uses "none" for null string context', () => {
149
+ it('omits string context line for null string context', () => {
128
150
  const messages = buildPluralMessages({
129
151
  one: '{count} msg',
130
152
  other: '{count} msgs',
@@ -133,7 +155,8 @@ describe('buildPluralMessages', () => {
133
155
  projectContext: 'test',
134
156
  stringContext: null,
135
157
  });
136
- expect(messages[1].content).toContain('none');
158
+ expect(messages[0].content).not.toContain('String context');
159
+ expect(messages[1].content).not.toContain('String context');
137
160
  });
138
161
  });
139
162
 
@@ -48,12 +48,14 @@ describe('prompts', () => {
48
48
  expect(messages[0].role).toBe('system');
49
49
  expect(messages[0].content).toContain('EN');
50
50
  expect(messages[0].content).toContain('DE');
51
+ // String context in system message, not user message.
52
+ expect(messages[0].content).toContain('greeting');
51
53
  expect(messages[1].role).toBe('user');
52
- expect(messages[1].content).toContain('Hello');
53
- expect(messages[1].content).toContain('greeting');
54
+ // User message must be ONLY the source text.
55
+ expect(messages[1].content).toBe('Hello');
54
56
  });
55
57
 
56
- it('uses "none" for null string context', () => {
58
+ it('omits string context line for null string context', () => {
57
59
  const messages = buildMessages({
58
60
  sourceText: 'Hello',
59
61
  sourceLang: 'EN',
@@ -61,7 +63,8 @@ describe('prompts', () => {
61
63
  projectContext: 'test',
62
64
  stringContext: null,
63
65
  });
64
- expect(messages[1].content).toContain('none');
66
+ expect(messages[0].content).not.toContain('String context');
67
+ expect(messages[1].content).toBe('Hello');
65
68
  });
66
69
  });
67
70