transduck 0.6.8 → 0.6.9

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.8');
640
+ program.name('transduck').description('AI-native translation tool').version('0.6.9');
641
641
  program.command('init')
642
642
  .description('Initialize a new transduck project')
643
643
  .action(async () => {
@@ -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>): string;
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
- }): string;
37
+ }): TranslationResult;
37
38
  export declare const ait: typeof t;
38
39
  export declare const aitPlural: typeof tPlural;
39
40
  interface TransDuckProviderProps {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
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.8');
746
+ program.name('transduck').description('AI-native translation tool').version('0.6.9');
747
747
 
748
748
  program.command('init')
749
749
  .description('Initialize a new transduck project')
@@ -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
- ): string {
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
- ): string {
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
@@ -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,136 @@ 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
+
502
633
  it('useTransDuck() still works without destructuring (backward compat)', async () => {
503
634
  mockFetch.mockResolvedValue({
504
635
  ok: true,