transduck 0.6.8 → 0.6.10
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/react/provider.d.ts +3 -2
- package/dist/react/provider.js +36 -8
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/handler.ts +4 -2
- package/src/react/provider.tsx +38 -10
- package/tests/handler.test.ts +50 -0
- package/tests/react-provider.test.tsx +157 -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.6.
|
|
640
|
+
program.name('transduck').description('AI-native translation tool').version('0.6.10');
|
|
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 ?? ''}`;
|
package/dist/react/provider.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import { TranslationResult } from '../result.js';
|
|
2
3
|
interface ReactState {
|
|
3
4
|
language: string;
|
|
4
5
|
sourceLang: string;
|
|
@@ -29,11 +30,11 @@ export interface UseTransDuckReturn {
|
|
|
29
30
|
export declare function useTransDuck(): UseTransDuckReturn;
|
|
30
31
|
export declare function _resetReactState(): void;
|
|
31
32
|
export declare function _getReactState(): ReactState;
|
|
32
|
-
export declare function t(sourceText: string, context?: string, vars?: Record<string, string | number>):
|
|
33
|
+
export declare function t(sourceText: string, context?: string, vars?: Record<string, string | number>): TranslationResult;
|
|
33
34
|
export declare function tPlural(one: string, other: string, count: number, opts?: {
|
|
34
35
|
context?: string;
|
|
35
36
|
vars?: Record<string, string | number>;
|
|
36
|
-
}):
|
|
37
|
+
}): TranslationResult;
|
|
37
38
|
export declare const ait: typeof t;
|
|
38
39
|
export declare const aitPlural: typeof tPlural;
|
|
39
40
|
interface TransDuckProviderProps {
|
package/dist/react/provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { TranslationResult } from '../result.js';
|
|
4
5
|
let _state = {
|
|
5
6
|
language: '',
|
|
6
7
|
sourceLang: 'EN',
|
|
@@ -82,18 +83,31 @@ export function t(sourceText, context, vars) {
|
|
|
82
83
|
_state.knownKeys.add(key);
|
|
83
84
|
// Same language — return source
|
|
84
85
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
85
|
-
return interpolateVars(sourceText, vars)
|
|
86
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
87
|
+
pending: false,
|
|
88
|
+
sourceLang: _state.sourceLang,
|
|
89
|
+
lang: _state.language,
|
|
90
|
+
});
|
|
86
91
|
}
|
|
87
92
|
// Cache hit
|
|
88
93
|
const cached = _state.cache.get(key);
|
|
89
94
|
if (cached !== undefined) {
|
|
90
|
-
return interpolateVars(cached, vars)
|
|
95
|
+
return new TranslationResult(interpolateVars(cached, vars), {
|
|
96
|
+
pending: false,
|
|
97
|
+
sourceLang: _state.sourceLang,
|
|
98
|
+
lang: _state.language,
|
|
99
|
+
source: 'cache',
|
|
100
|
+
});
|
|
91
101
|
}
|
|
92
102
|
// Queue for fetch
|
|
93
103
|
_state.pendingStrings.add(key);
|
|
94
104
|
schedulePendingFlush();
|
|
95
|
-
// Return source text as fallback
|
|
96
|
-
return interpolateVars(sourceText, vars)
|
|
105
|
+
// Return source text as fallback, marked pending
|
|
106
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
107
|
+
pending: true,
|
|
108
|
+
sourceLang: _state.sourceLang,
|
|
109
|
+
lang: _state.language,
|
|
110
|
+
});
|
|
97
111
|
}
|
|
98
112
|
export function tPlural(one, other, count, opts) {
|
|
99
113
|
const context = opts?.context;
|
|
@@ -114,7 +128,11 @@ export function tPlural(one, other, count, opts) {
|
|
|
114
128
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
115
129
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
116
130
|
const form = rules.select(count) === 'one' ? one : other;
|
|
117
|
-
return interpolateVars(form, vars)
|
|
131
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
132
|
+
pending: false,
|
|
133
|
+
sourceLang: _state.sourceLang,
|
|
134
|
+
lang: _state.language,
|
|
135
|
+
});
|
|
118
136
|
}
|
|
119
137
|
// Cache hit
|
|
120
138
|
const cachedForms = _state.pluralCache.get(cacheKey);
|
|
@@ -122,15 +140,24 @@ export function tPlural(one, other, count, opts) {
|
|
|
122
140
|
const rules = new Intl.PluralRules(_state.language.toLowerCase());
|
|
123
141
|
const category = rules.select(count);
|
|
124
142
|
const form = cachedForms[category] ?? cachedForms['other'] ?? other;
|
|
125
|
-
return interpolateVars(form, vars)
|
|
143
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
144
|
+
pending: false,
|
|
145
|
+
sourceLang: _state.sourceLang,
|
|
146
|
+
lang: _state.language,
|
|
147
|
+
source: 'cache',
|
|
148
|
+
});
|
|
126
149
|
}
|
|
127
150
|
// Queue for fetch
|
|
128
151
|
_state.pendingPlurals.add(cacheKey);
|
|
129
152
|
schedulePendingFlush();
|
|
130
|
-
// Fallback to source form
|
|
153
|
+
// Fallback to source form, marked pending
|
|
131
154
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
132
155
|
const form = rules.select(count) === 'one' ? one : other;
|
|
133
|
-
return interpolateVars(form, vars)
|
|
156
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
157
|
+
pending: true,
|
|
158
|
+
sourceLang: _state.sourceLang,
|
|
159
|
+
lang: _state.language,
|
|
160
|
+
});
|
|
134
161
|
}
|
|
135
162
|
// Aliases
|
|
136
163
|
export const ait = t;
|
|
@@ -255,6 +282,7 @@ export function TransDuckProvider({ language, sourceLang = 'EN', endpoint = '/ap
|
|
|
255
282
|
headers: { 'Content-Type': 'application/json' },
|
|
256
283
|
body: JSON.stringify({
|
|
257
284
|
language: _state.language,
|
|
285
|
+
sourceLang: _state.sourceLang,
|
|
258
286
|
strings,
|
|
259
287
|
plurals,
|
|
260
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.6.
|
|
746
|
+
program.name('transduck').description('AI-native translation tool').version('0.6.10');
|
|
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/react/provider.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react';
|
|
4
|
+
import { TranslationResult } from '../result.js';
|
|
4
5
|
|
|
5
6
|
// --- Module-level state (accessed by stable t()/ait() functions) ---
|
|
6
7
|
|
|
@@ -133,27 +134,40 @@ export function t(
|
|
|
133
134
|
sourceText: string,
|
|
134
135
|
context?: string,
|
|
135
136
|
vars?: Record<string, string | number>,
|
|
136
|
-
):
|
|
137
|
+
): TranslationResult {
|
|
137
138
|
const key = `${sourceText}||${context ?? ''}`;
|
|
138
139
|
_state.knownKeys.add(key);
|
|
139
140
|
|
|
140
141
|
// Same language — return source
|
|
141
142
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
142
|
-
return interpolateVars(sourceText, vars)
|
|
143
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
144
|
+
pending: false,
|
|
145
|
+
sourceLang: _state.sourceLang,
|
|
146
|
+
lang: _state.language,
|
|
147
|
+
});
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
// Cache hit
|
|
146
151
|
const cached = _state.cache.get(key);
|
|
147
152
|
if (cached !== undefined) {
|
|
148
|
-
return interpolateVars(cached, vars)
|
|
153
|
+
return new TranslationResult(interpolateVars(cached, vars), {
|
|
154
|
+
pending: false,
|
|
155
|
+
sourceLang: _state.sourceLang,
|
|
156
|
+
lang: _state.language,
|
|
157
|
+
source: 'cache',
|
|
158
|
+
});
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
// Queue for fetch
|
|
152
162
|
_state.pendingStrings.add(key);
|
|
153
163
|
schedulePendingFlush();
|
|
154
164
|
|
|
155
|
-
// Return source text as fallback
|
|
156
|
-
return interpolateVars(sourceText, vars)
|
|
165
|
+
// Return source text as fallback, marked pending
|
|
166
|
+
return new TranslationResult(interpolateVars(sourceText, vars), {
|
|
167
|
+
pending: true,
|
|
168
|
+
sourceLang: _state.sourceLang,
|
|
169
|
+
lang: _state.language,
|
|
170
|
+
});
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
export function tPlural(
|
|
@@ -161,7 +175,7 @@ export function tPlural(
|
|
|
161
175
|
other: string,
|
|
162
176
|
count: number,
|
|
163
177
|
opts?: { context?: string; vars?: Record<string, string | number> },
|
|
164
|
-
):
|
|
178
|
+
): TranslationResult {
|
|
165
179
|
const context = opts?.context;
|
|
166
180
|
let vars: Record<string, string | number>;
|
|
167
181
|
if (!opts?.vars) {
|
|
@@ -180,7 +194,11 @@ export function tPlural(
|
|
|
180
194
|
if (_state.language.toUpperCase() === _state.sourceLang.toUpperCase()) {
|
|
181
195
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
182
196
|
const form = rules.select(count) === 'one' ? one : other;
|
|
183
|
-
return interpolateVars(form, vars)
|
|
197
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
198
|
+
pending: false,
|
|
199
|
+
sourceLang: _state.sourceLang,
|
|
200
|
+
lang: _state.language,
|
|
201
|
+
});
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
// Cache hit
|
|
@@ -189,17 +207,26 @@ export function tPlural(
|
|
|
189
207
|
const rules = new Intl.PluralRules(_state.language.toLowerCase());
|
|
190
208
|
const category = rules.select(count);
|
|
191
209
|
const form = cachedForms[category] ?? cachedForms['other'] ?? other;
|
|
192
|
-
return interpolateVars(form, vars)
|
|
210
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
211
|
+
pending: false,
|
|
212
|
+
sourceLang: _state.sourceLang,
|
|
213
|
+
lang: _state.language,
|
|
214
|
+
source: 'cache',
|
|
215
|
+
});
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
// Queue for fetch
|
|
196
219
|
_state.pendingPlurals.add(cacheKey);
|
|
197
220
|
schedulePendingFlush();
|
|
198
221
|
|
|
199
|
-
// Fallback to source form
|
|
222
|
+
// Fallback to source form, marked pending
|
|
200
223
|
const rules = new Intl.PluralRules(_state.sourceLang.toLowerCase());
|
|
201
224
|
const form = rules.select(count) === 'one' ? one : other;
|
|
202
|
-
return interpolateVars(form, vars)
|
|
225
|
+
return new TranslationResult(interpolateVars(form, vars), {
|
|
226
|
+
pending: true,
|
|
227
|
+
sourceLang: _state.sourceLang,
|
|
228
|
+
lang: _state.language,
|
|
229
|
+
});
|
|
203
230
|
}
|
|
204
231
|
|
|
205
232
|
// Aliases
|
|
@@ -355,6 +382,7 @@ export function TransDuckProvider({
|
|
|
355
382
|
headers: { 'Content-Type': 'application/json' },
|
|
356
383
|
body: JSON.stringify({
|
|
357
384
|
language: _state.language,
|
|
385
|
+
sourceLang: _state.sourceLang,
|
|
358
386
|
strings,
|
|
359
387
|
plurals,
|
|
360
388
|
}),
|
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
|
});
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { render, screen, cleanup, waitFor, act } from '@testing-library/react';
|
|
4
4
|
import { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState, _getReactState } from '../src/react/index.js';
|
|
5
5
|
import type { UseTransDuckReturn } from '../src/react/index.js';
|
|
6
|
+
import { TranslationResult } from '../src/result.js';
|
|
6
7
|
|
|
7
8
|
// Mock fetch globally
|
|
8
9
|
const mockFetch = vi.fn();
|
|
@@ -499,6 +500,162 @@ describe('TransDuckProvider + t()', () => {
|
|
|
499
500
|
});
|
|
500
501
|
});
|
|
501
502
|
|
|
503
|
+
it('t() returns a TranslationResult with pending=true on cache miss', () => {
|
|
504
|
+
mockFetch.mockResolvedValue({
|
|
505
|
+
ok: true,
|
|
506
|
+
json: async () => ({ translations: { 'Hello||': 'Hallo' }, plurals: {} }),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
let captured: ReturnType<typeof t> | null = null;
|
|
510
|
+
function TestComp() {
|
|
511
|
+
useTransDuck();
|
|
512
|
+
captured = t('Hello');
|
|
513
|
+
return <span>{captured}</span>;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
render(
|
|
517
|
+
<TransDuckProvider language="DE">
|
|
518
|
+
<TestComp />
|
|
519
|
+
</TransDuckProvider>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
523
|
+
expect(captured!.pending).toBe(true);
|
|
524
|
+
expect(captured!.sourceLang).toBe('EN');
|
|
525
|
+
expect(captured!.lang).toBe('DE');
|
|
526
|
+
expect(captured!.toString()).toBe('Hello');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('t() returns a TranslationResult with pending=false on cache hit', async () => {
|
|
530
|
+
mockFetch.mockResolvedValue({
|
|
531
|
+
ok: true,
|
|
532
|
+
json: async () => ({ translations: { 'Hello||': 'Hallo' }, plurals: {} }),
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
let captured: ReturnType<typeof t> | null = null;
|
|
536
|
+
function TestComp() {
|
|
537
|
+
useTransDuck();
|
|
538
|
+
captured = t('Hello');
|
|
539
|
+
return <span data-testid="text">{captured}</span>;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
render(
|
|
543
|
+
<TransDuckProvider language="DE">
|
|
544
|
+
<TestComp />
|
|
545
|
+
</TransDuckProvider>
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
await waitFor(() => {
|
|
549
|
+
expect(screen.getByTestId('text').textContent).toBe('Hallo');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
553
|
+
expect(captured!.pending).toBe(false);
|
|
554
|
+
expect(captured!.source).toBe('cache');
|
|
555
|
+
expect(captured!.toString()).toBe('Hallo');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('t() returns a TranslationResult with pending=false when language matches source', () => {
|
|
559
|
+
let captured: ReturnType<typeof t> | null = null;
|
|
560
|
+
function TestComp() {
|
|
561
|
+
useTransDuck();
|
|
562
|
+
captured = t('Hello');
|
|
563
|
+
return <span>{captured}</span>;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
render(
|
|
567
|
+
<TransDuckProvider language="EN" sourceLang="EN">
|
|
568
|
+
<TestComp />
|
|
569
|
+
</TransDuckProvider>
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
573
|
+
expect(captured!.pending).toBe(false);
|
|
574
|
+
expect(captured!.toString()).toBe('Hello');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('tPlural() returns a TranslationResult with pending=true on cache miss', () => {
|
|
578
|
+
mockFetch.mockResolvedValue({
|
|
579
|
+
ok: true,
|
|
580
|
+
json: async () => ({ translations: {}, plurals: {} }),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
let captured: ReturnType<typeof tPlural> | null = null;
|
|
584
|
+
function TestComp() {
|
|
585
|
+
useTransDuck();
|
|
586
|
+
captured = tPlural('{count} message', '{count} messages', 2);
|
|
587
|
+
return <span>{captured}</span>;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
render(
|
|
591
|
+
<TransDuckProvider language="DE">
|
|
592
|
+
<TestComp />
|
|
593
|
+
</TransDuckProvider>
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
597
|
+
expect(captured!.pending).toBe(true);
|
|
598
|
+
expect(captured!.toString()).toBe('2 messages');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('tPlural() returns a TranslationResult with pending=false on cache hit', async () => {
|
|
602
|
+
const pluralKey = '{count} message\x00{count} messages||';
|
|
603
|
+
mockFetch.mockResolvedValue({
|
|
604
|
+
ok: true,
|
|
605
|
+
json: async () => ({
|
|
606
|
+
translations: {},
|
|
607
|
+
plurals: { [pluralKey]: { one: '{count} Nachricht', other: '{count} Nachrichten' } },
|
|
608
|
+
}),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
let captured: ReturnType<typeof tPlural> | null = null;
|
|
612
|
+
function TestComp() {
|
|
613
|
+
useTransDuck();
|
|
614
|
+
captured = tPlural('{count} message', '{count} messages', 3);
|
|
615
|
+
return <span data-testid="text">{captured}</span>;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
render(
|
|
619
|
+
<TransDuckProvider language="DE">
|
|
620
|
+
<TestComp />
|
|
621
|
+
</TransDuckProvider>
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
await waitFor(() => {
|
|
625
|
+
expect(screen.getByTestId('text').textContent).toBe('3 Nachrichten');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
expect(captured).toBeInstanceOf(TranslationResult);
|
|
629
|
+
expect(captured!.pending).toBe(false);
|
|
630
|
+
expect(captured!.source).toBe('cache');
|
|
631
|
+
});
|
|
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
|
+
|
|
502
659
|
it('useTransDuck() still works without destructuring (backward compat)', async () => {
|
|
503
660
|
mockFetch.mockResolvedValue({
|
|
504
661
|
ok: true,
|