transduck 0.6.9 → 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 +1 -1
- package/dist/handler.d.ts +1 -0
- package/dist/handler.js +3 -2
- package/dist/providers/prompts.js +14 -10
- package/dist/react/provider.js +1 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/handler.ts +4 -2
- package/src/providers/prompts.ts +18 -11
- package/src/react/provider.tsx +1 -0
- package/tests/backend.test.ts +30 -7
- package/tests/handler.test.ts +50 -0
- package/tests/providers.test.ts +7 -4
- package/tests/react-provider.test.tsx +26 -0
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.
|
|
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 () => {
|
package/dist/handler.d.ts
CHANGED
package/dist/handler.js
CHANGED
|
@@ -46,12 +46,13 @@ export async function handleTranslationRequest(body, configPath, opts) {
|
|
|
46
46
|
const store = await getStore(configPath);
|
|
47
47
|
const shared = await getSharedStore(configPath);
|
|
48
48
|
const targetLang = body.language.toUpperCase();
|
|
49
|
+
const bodySourceLang = body.sourceLang?.toUpperCase();
|
|
49
50
|
const projectContextHash = hash(cfg.projectContext);
|
|
50
51
|
const translations = {};
|
|
51
52
|
const plurals = {};
|
|
52
53
|
// Translate regular strings
|
|
53
54
|
for (const item of body.strings ?? []) {
|
|
54
|
-
const sourceLang = item.sourceLang?.toUpperCase() ?? cfg.sourceLang;
|
|
55
|
+
const sourceLang = item.sourceLang?.toUpperCase() ?? bodySourceLang ?? cfg.sourceLang;
|
|
55
56
|
const stringContextHash = hash(item.context ?? '');
|
|
56
57
|
const key = `${item.text}||${item.context ?? ''}`;
|
|
57
58
|
const lookupParams = {
|
|
@@ -109,7 +110,7 @@ export async function handleTranslationRequest(body, configPath, opts) {
|
|
|
109
110
|
}
|
|
110
111
|
// Translate plurals
|
|
111
112
|
for (const item of body.plurals ?? []) {
|
|
112
|
-
const sourceLang = item.sourceLang?.toUpperCase() ?? cfg.sourceLang;
|
|
113
|
+
const sourceLang = item.sourceLang?.toUpperCase() ?? bodySourceLang ?? cfg.sourceLang;
|
|
113
114
|
const stringContextHash = hash(item.context ?? '');
|
|
114
115
|
const sourceKey = item.one + '\x00' + item.other;
|
|
115
116
|
const responseKey = `${sourceKey}||${item.context ?? ''}`;
|
|
@@ -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
|
|
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
|
|
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}"
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
.replace('{
|
|
52
|
-
|
|
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
|
-
|
|
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/dist/react/provider.js
CHANGED
|
@@ -282,6 +282,7 @@ export function TransDuckProvider({ language, sourceLang = 'EN', endpoint = '/ap
|
|
|
282
282
|
headers: { 'Content-Type': 'application/json' },
|
|
283
283
|
body: JSON.stringify({
|
|
284
284
|
language: _state.language,
|
|
285
|
+
sourceLang: _state.sourceLang,
|
|
285
286
|
strings,
|
|
286
287
|
plurals,
|
|
287
288
|
}),
|
package/package.json
CHANGED
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.
|
|
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')
|
package/src/handler.ts
CHANGED
|
@@ -25,6 +25,7 @@ interface TranslationRequestPlural {
|
|
|
25
25
|
|
|
26
26
|
export interface TranslationRequest {
|
|
27
27
|
language: string;
|
|
28
|
+
sourceLang?: string;
|
|
28
29
|
strings: TranslationRequestString[];
|
|
29
30
|
plurals: TranslationRequestPlural[];
|
|
30
31
|
}
|
|
@@ -77,6 +78,7 @@ export async function handleTranslationRequest(
|
|
|
77
78
|
const store = await getStore(configPath);
|
|
78
79
|
const shared = await getSharedStore(configPath);
|
|
79
80
|
const targetLang = body.language.toUpperCase();
|
|
81
|
+
const bodySourceLang = body.sourceLang?.toUpperCase();
|
|
80
82
|
|
|
81
83
|
const projectContextHash = hash(cfg.projectContext);
|
|
82
84
|
|
|
@@ -85,7 +87,7 @@ export async function handleTranslationRequest(
|
|
|
85
87
|
|
|
86
88
|
// Translate regular strings
|
|
87
89
|
for (const item of body.strings ?? []) {
|
|
88
|
-
const sourceLang = item.sourceLang?.toUpperCase() ?? cfg.sourceLang;
|
|
90
|
+
const sourceLang = item.sourceLang?.toUpperCase() ?? bodySourceLang ?? cfg.sourceLang;
|
|
89
91
|
const stringContextHash = hash(item.context ?? '');
|
|
90
92
|
const key = `${item.text}||${item.context ?? ''}`;
|
|
91
93
|
const lookupParams = {
|
|
@@ -148,7 +150,7 @@ export async function handleTranslationRequest(
|
|
|
148
150
|
|
|
149
151
|
// Translate plurals
|
|
150
152
|
for (const item of body.plurals ?? []) {
|
|
151
|
-
const sourceLang = item.sourceLang?.toUpperCase() ?? cfg.sourceLang;
|
|
153
|
+
const sourceLang = item.sourceLang?.toUpperCase() ?? bodySourceLang ?? cfg.sourceLang;
|
|
152
154
|
const stringContextHash = hash(item.context ?? '');
|
|
153
155
|
const sourceKey = item.one + '\x00' + item.other;
|
|
154
156
|
const responseKey = `${sourceKey}||${item.context ?? ''}`;
|
package/src/providers/prompts.ts
CHANGED
|
@@ -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
|
|
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
|
|
19
|
-
'
|
|
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}"
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
.replace('{
|
|
81
|
-
|
|
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
|
-
|
|
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 [
|
package/src/react/provider.tsx
CHANGED
package/tests/backend.test.ts
CHANGED
|
@@ -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
|
-
|
|
39
|
-
expect(messages[1].content).
|
|
41
|
+
// User message must be ONLY the source text.
|
|
42
|
+
expect(messages[1].content).toBe('Our Events');
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
it('
|
|
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[
|
|
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
|
-
|
|
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('
|
|
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[
|
|
158
|
+
expect(messages[0].content).not.toContain('String context');
|
|
159
|
+
expect(messages[1].content).not.toContain('String context');
|
|
137
160
|
});
|
|
138
161
|
});
|
|
139
162
|
|
package/tests/handler.test.ts
CHANGED
|
@@ -133,4 +133,54 @@ describe('handleTranslationRequest', () => {
|
|
|
133
133
|
expect(result.plurals[pluralKey]).toBeDefined();
|
|
134
134
|
expect(result.plurals[pluralKey].one).toBe('{count} Artikel');
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
it('body-level sourceLang overrides config sourceLang', async () => {
|
|
138
|
+
const { TranslationStore } = await import('../src/storage.js');
|
|
139
|
+
const { createHash } = await import('crypto');
|
|
140
|
+
const hash = (t: string) => createHash('sha256').update(t).digest('hex');
|
|
141
|
+
|
|
142
|
+
// Cache entry stored under source FR (not config's EN)
|
|
143
|
+
const store = new TranslationStore(join(tmpDir, 'translations.db'));
|
|
144
|
+
await store.initialize();
|
|
145
|
+
await store.insert({
|
|
146
|
+
sourceText: 'Bonjour', sourceLang: 'FR', targetLang: 'DE',
|
|
147
|
+
projectContextHash: hash('A test site'), stringContextHash: hash(''),
|
|
148
|
+
translatedText: 'Hallo', model: 'gpt-4.1-mini', status: 'translated', stringContext: '',
|
|
149
|
+
});
|
|
150
|
+
store.close();
|
|
151
|
+
|
|
152
|
+
// With body-level sourceLang: FR, the handler should find the cache entry
|
|
153
|
+
const result = await handleTranslationRequest({
|
|
154
|
+
language: 'DE',
|
|
155
|
+
sourceLang: 'FR',
|
|
156
|
+
strings: [{ text: 'Bonjour' }],
|
|
157
|
+
plurals: [],
|
|
158
|
+
}, configPath);
|
|
159
|
+
|
|
160
|
+
expect(result.translations['Bonjour||']).toBe('Hallo');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('per-item sourceLang overrides body-level sourceLang', async () => {
|
|
164
|
+
const { TranslationStore } = await import('../src/storage.js');
|
|
165
|
+
const { createHash } = await import('crypto');
|
|
166
|
+
const hash = (t: string) => createHash('sha256').update(t).digest('hex');
|
|
167
|
+
|
|
168
|
+
const store = new TranslationStore(join(tmpDir, 'translations.db'));
|
|
169
|
+
await store.initialize();
|
|
170
|
+
await store.insert({
|
|
171
|
+
sourceText: 'Hola', sourceLang: 'ES', targetLang: 'DE',
|
|
172
|
+
projectContextHash: hash('A test site'), stringContextHash: hash(''),
|
|
173
|
+
translatedText: 'Hallo', model: 'gpt-4.1-mini', status: 'translated', stringContext: '',
|
|
174
|
+
});
|
|
175
|
+
store.close();
|
|
176
|
+
|
|
177
|
+
const result = await handleTranslationRequest({
|
|
178
|
+
language: 'DE',
|
|
179
|
+
sourceLang: 'FR',
|
|
180
|
+
strings: [{ text: 'Hola', sourceLang: 'ES' }],
|
|
181
|
+
plurals: [],
|
|
182
|
+
}, configPath);
|
|
183
|
+
|
|
184
|
+
expect(result.translations['Hola||']).toBe('Hallo');
|
|
185
|
+
});
|
|
136
186
|
});
|
package/tests/providers.test.ts
CHANGED
|
@@ -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
|
-
|
|
53
|
-
expect(messages[1].content).
|
|
54
|
+
// User message must be ONLY the source text.
|
|
55
|
+
expect(messages[1].content).toBe('Hello');
|
|
54
56
|
});
|
|
55
57
|
|
|
56
|
-
it('
|
|
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[
|
|
66
|
+
expect(messages[0].content).not.toContain('String context');
|
|
67
|
+
expect(messages[1].content).toBe('Hello');
|
|
65
68
|
});
|
|
66
69
|
});
|
|
67
70
|
|
|
@@ -630,6 +630,32 @@ describe('TransDuckProvider + t()', () => {
|
|
|
630
630
|
expect(captured!.source).toBe('cache');
|
|
631
631
|
});
|
|
632
632
|
|
|
633
|
+
it('fetch body includes sourceLang from provider prop', async () => {
|
|
634
|
+
mockFetch.mockResolvedValue({
|
|
635
|
+
ok: true,
|
|
636
|
+
json: async () => ({ translations: { 'Hello||': 'Hallo' }, plurals: {} }),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
function TestComp() {
|
|
640
|
+
useTransDuck();
|
|
641
|
+
return <span data-testid="text">{t('Hello')}</span>;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
render(
|
|
645
|
+
<TransDuckProvider language="DE" sourceLang="FR">
|
|
646
|
+
<TestComp />
|
|
647
|
+
</TransDuckProvider>
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
await waitFor(() => {
|
|
651
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
655
|
+
expect(body.sourceLang).toBe('FR');
|
|
656
|
+
expect(body.language).toBe('DE');
|
|
657
|
+
});
|
|
658
|
+
|
|
633
659
|
it('useTransDuck() still works without destructuring (backward compat)', async () => {
|
|
634
660
|
mockFetch.mockResolvedValue({
|
|
635
661
|
ok: true,
|