transduck 0.0.1
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/backend.d.ts +40 -0
- package/dist/backend.js +85 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +383 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +43 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +212 -0
- package/dist/plural.d.ts +19 -0
- package/dist/plural.js +42 -0
- package/dist/storage.d.ts +40 -0
- package/dist/storage.js +178 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +30 -0
- package/package.json +28 -0
- package/src/backend.ts +138 -0
- package/src/cli.ts +450 -0
- package/src/config.ts +62 -0
- package/src/index.ts +251 -0
- package/src/plural.ts +47 -0
- package/src/storage.ts +229 -0
- package/src/validation.ts +30 -0
- package/tests/ait.test.ts +213 -0
- package/tests/backend.test.ts +184 -0
- package/tests/cli.test.ts +162 -0
- package/tests/config.test.ts +79 -0
- package/tests/plural.test.ts +114 -0
- package/tests/storage.test.ts +262 -0
- package/tests/validation.test.ts +47 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getPluralCategory, getPluralCategories, interpolateVars } from '../src/plural.js';
|
|
3
|
+
|
|
4
|
+
describe('getPluralCategory', () => {
|
|
5
|
+
it('returns "one" for count 1 in English', () => {
|
|
6
|
+
expect(getPluralCategory('EN', 1)).toBe('one');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns "other" for count 0 in English', () => {
|
|
10
|
+
expect(getPluralCategory('EN', 0)).toBe('other');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns "other" for count 5 in English', () => {
|
|
14
|
+
expect(getPluralCategory('EN', 5)).toBe('other');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns "one" for count 1 in German', () => {
|
|
18
|
+
expect(getPluralCategory('DE', 1)).toBe('one');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns "other" for count 2 in German', () => {
|
|
22
|
+
expect(getPluralCategory('DE', 2)).toBe('other');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns "few" for count 3 in Russian', () => {
|
|
26
|
+
expect(getPluralCategory('RU', 3)).toBe('few');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns "many" for count 5 in Russian', () => {
|
|
30
|
+
expect(getPluralCategory('RU', 5)).toBe('many');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns "one" for count 1 in Russian', () => {
|
|
34
|
+
expect(getPluralCategory('RU', 1)).toBe('one');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns "one" for count 21 in Russian', () => {
|
|
38
|
+
expect(getPluralCategory('RU', 21)).toBe('one');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns "other" for count 5 in Japanese', () => {
|
|
42
|
+
expect(getPluralCategory('JA', 5)).toBe('other');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns "other" for count 1 in Japanese', () => {
|
|
46
|
+
expect(getPluralCategory('JA', 1)).toBe('other');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getPluralCategories', () => {
|
|
51
|
+
it('returns one and other for English', () => {
|
|
52
|
+
const cats = getPluralCategories('EN');
|
|
53
|
+
expect(cats).toContain('one');
|
|
54
|
+
expect(cats).toContain('other');
|
|
55
|
+
expect(cats.size).toBe(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns one and other for German', () => {
|
|
59
|
+
const cats = getPluralCategories('DE');
|
|
60
|
+
expect(cats).toContain('one');
|
|
61
|
+
expect(cats).toContain('other');
|
|
62
|
+
expect(cats.size).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns one, few, many, other for Russian', () => {
|
|
66
|
+
const cats = getPluralCategories('RU');
|
|
67
|
+
expect(cats).toContain('one');
|
|
68
|
+
expect(cats).toContain('few');
|
|
69
|
+
expect(cats).toContain('many');
|
|
70
|
+
expect(cats).toContain('other');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns only other for Japanese', () => {
|
|
74
|
+
const cats = getPluralCategories('JA');
|
|
75
|
+
expect(cats).toEqual(new Set(['other']));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('interpolateVars', () => {
|
|
80
|
+
it('replaces placeholders with values', () => {
|
|
81
|
+
expect(interpolateVars('Hello {name}', { name: 'Tim' })).toBe('Hello Tim');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('replaces multiple placeholders', () => {
|
|
85
|
+
expect(interpolateVars('{count} items in {name}\'s cart', { count: 3, name: 'Tim' }))
|
|
86
|
+
.toBe("3 items in Tim's cart");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles numeric values', () => {
|
|
90
|
+
expect(interpolateVars('{count} messages', { count: 42 })).toBe('42 messages');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns text unchanged when no vars', () => {
|
|
94
|
+
expect(interpolateVars('Hello {name}')).toBe('Hello {name}');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns text unchanged when vars is null', () => {
|
|
98
|
+
expect(interpolateVars('Hello {name}', null)).toBe('Hello {name}');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('ignores extra keys in vars', () => {
|
|
102
|
+
expect(interpolateVars('Hello {name}', { name: 'Tim', extra: 'ignored' }))
|
|
103
|
+
.toBe('Hello Tim');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('replaces all occurrences of the same placeholder', () => {
|
|
107
|
+
expect(interpolateVars('{name} and {name}', { name: 'Tim' }))
|
|
108
|
+
.toBe('Tim and Tim');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles empty vars object', () => {
|
|
112
|
+
expect(interpolateVars('Hello', {})).toBe('Hello');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { mkdtempSync } from 'fs';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { TranslationStore } from '../src/storage.js';
|
|
7
|
+
|
|
8
|
+
function hash(text: string): string {
|
|
9
|
+
return createHash('sha256').update(text).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('TranslationStore', () => {
|
|
13
|
+
let store: TranslationStore;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-test-'));
|
|
17
|
+
store = new TranslationStore(join(tmpDir, 'test.duckdb'));
|
|
18
|
+
await store.initialize();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('creates table on initialize', async () => {
|
|
22
|
+
const result = await store.query('SELECT count(*) as c FROM translations');
|
|
23
|
+
expect(result[0].c).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('inserts and looks up translation', async () => {
|
|
27
|
+
await store.insert({
|
|
28
|
+
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'DE',
|
|
29
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
30
|
+
translatedText: 'Hallo', model: 'gpt-4.1-mini', status: 'translated',
|
|
31
|
+
});
|
|
32
|
+
const result = await store.lookup({
|
|
33
|
+
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'DE',
|
|
34
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
35
|
+
});
|
|
36
|
+
expect(result).toBe('Hallo');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns null on miss', async () => {
|
|
40
|
+
const result = await store.lookup({
|
|
41
|
+
sourceText: 'Missing', sourceLang: 'EN', targetLang: 'DE',
|
|
42
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
43
|
+
});
|
|
44
|
+
expect(result).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('skips failed translations in lookup', async () => {
|
|
48
|
+
await store.insert({
|
|
49
|
+
sourceText: 'Bad', sourceLang: 'EN', targetLang: 'DE',
|
|
50
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
51
|
+
translatedText: 'bad translation', model: 'gpt-4.1-mini', status: 'failed',
|
|
52
|
+
});
|
|
53
|
+
const result = await store.lookup({
|
|
54
|
+
sourceText: 'Bad', sourceLang: 'EN', targetLang: 'DE',
|
|
55
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
56
|
+
});
|
|
57
|
+
expect(result).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles duplicate insert without error', async () => {
|
|
61
|
+
const entry = {
|
|
62
|
+
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'DE',
|
|
63
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
64
|
+
translatedText: 'Hallo', model: 'gpt-4.1-mini', status: 'translated',
|
|
65
|
+
};
|
|
66
|
+
await store.insert(entry);
|
|
67
|
+
await store.insert(entry); // should not throw
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns stats', async () => {
|
|
71
|
+
await store.insert({
|
|
72
|
+
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'DE',
|
|
73
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
74
|
+
translatedText: 'Hallo', model: 'gpt-4.1-mini', status: 'translated',
|
|
75
|
+
});
|
|
76
|
+
await store.insert({
|
|
77
|
+
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'ES',
|
|
78
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
79
|
+
translatedText: 'Hola', model: 'gpt-4.1-mini', status: 'translated',
|
|
80
|
+
});
|
|
81
|
+
const stats = await store.stats();
|
|
82
|
+
expect(stats.totalTranslations).toBe(2);
|
|
83
|
+
expect(stats.byLanguage['DE']).toBe(1);
|
|
84
|
+
expect(stats.byLanguage['ES']).toBe(1);
|
|
85
|
+
expect(stats.totalFailed).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// --- Plural methods ---
|
|
89
|
+
|
|
90
|
+
it('inserts and looks up plural translations', async () => {
|
|
91
|
+
const lookupParams = {
|
|
92
|
+
sourceText: '{count} message\x00{count} messages',
|
|
93
|
+
sourceLang: 'EN', targetLang: 'DE',
|
|
94
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Insert plural forms
|
|
98
|
+
await store.insertPlural({
|
|
99
|
+
...lookupParams,
|
|
100
|
+
pluralCategory: 'one',
|
|
101
|
+
translatedText: '{count} Nachricht',
|
|
102
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
103
|
+
});
|
|
104
|
+
await store.insertPlural({
|
|
105
|
+
...lookupParams,
|
|
106
|
+
pluralCategory: 'other',
|
|
107
|
+
translatedText: '{count} Nachrichten',
|
|
108
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Lookup should return all forms
|
|
112
|
+
const result = await store.lookupPlural(lookupParams);
|
|
113
|
+
expect(result).toEqual({
|
|
114
|
+
one: '{count} Nachricht',
|
|
115
|
+
other: '{count} Nachrichten',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('lookupPlural returns empty object on miss', async () => {
|
|
120
|
+
const result = await store.lookupPlural({
|
|
121
|
+
sourceText: 'missing\x00missing',
|
|
122
|
+
sourceLang: 'EN', targetLang: 'DE',
|
|
123
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
124
|
+
});
|
|
125
|
+
expect(result).toEqual({});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('lookupPlural skips failed plural entries', async () => {
|
|
129
|
+
const lookupParams = {
|
|
130
|
+
sourceText: '{count} item\x00{count} items',
|
|
131
|
+
sourceLang: 'EN', targetLang: 'RU',
|
|
132
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
await store.insertPlural({
|
|
136
|
+
...lookupParams,
|
|
137
|
+
pluralCategory: 'one',
|
|
138
|
+
translatedText: '{count} элемент',
|
|
139
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
140
|
+
});
|
|
141
|
+
await store.insertPlural({
|
|
142
|
+
...lookupParams,
|
|
143
|
+
pluralCategory: 'few',
|
|
144
|
+
translatedText: 'bad translation',
|
|
145
|
+
model: 'gpt-4.1-mini', status: 'failed',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await store.lookupPlural(lookupParams);
|
|
149
|
+
expect(result).toEqual({ one: '{count} элемент' });
|
|
150
|
+
expect(result['few']).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('plural and regular entries do not interfere', async () => {
|
|
154
|
+
const baseParams = {
|
|
155
|
+
sourceLang: 'EN', targetLang: 'DE',
|
|
156
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Regular insert
|
|
160
|
+
await store.insert({
|
|
161
|
+
...baseParams,
|
|
162
|
+
sourceText: 'Hello',
|
|
163
|
+
translatedText: 'Hallo',
|
|
164
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Plural insert with same-ish source text
|
|
168
|
+
await store.insertPlural({
|
|
169
|
+
...baseParams,
|
|
170
|
+
sourceText: 'Hello',
|
|
171
|
+
pluralCategory: 'one',
|
|
172
|
+
translatedText: 'Hallo singular',
|
|
173
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Regular lookup should return regular entry
|
|
177
|
+
const regular = await store.lookup({ ...baseParams, sourceText: 'Hello' });
|
|
178
|
+
expect(regular).toBe('Hallo');
|
|
179
|
+
|
|
180
|
+
// Plural lookup should return plural entry
|
|
181
|
+
const plural = await store.lookupPlural({ ...baseParams, sourceText: 'Hello' });
|
|
182
|
+
expect(plural).toEqual({ one: 'Hallo singular' });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('handles duplicate plural insert without error', async () => {
|
|
186
|
+
const entry = {
|
|
187
|
+
sourceText: '{count} msg\x00{count} msgs',
|
|
188
|
+
sourceLang: 'EN', targetLang: 'DE',
|
|
189
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
190
|
+
pluralCategory: 'other',
|
|
191
|
+
translatedText: '{count} Nachrichten',
|
|
192
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
193
|
+
};
|
|
194
|
+
await store.insertPlural(entry);
|
|
195
|
+
await store.insertPlural(entry); // should not throw
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// --- Migration ---
|
|
199
|
+
|
|
200
|
+
it('migrates v1 schema to v2', async () => {
|
|
201
|
+
// Create a fresh store with v1 schema
|
|
202
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-migration-'));
|
|
203
|
+
const dbPath = join(tmpDir, 'migrate.duckdb');
|
|
204
|
+
|
|
205
|
+
// Manually create v1 schema (no plural_category column)
|
|
206
|
+
const { DuckDBInstance } = await import('@duckdb/node-api');
|
|
207
|
+
const instance = await DuckDBInstance.create(dbPath);
|
|
208
|
+
const conn = await instance.connect();
|
|
209
|
+
await conn.run(`
|
|
210
|
+
CREATE TABLE translations (
|
|
211
|
+
source_text TEXT NOT NULL,
|
|
212
|
+
source_lang TEXT NOT NULL,
|
|
213
|
+
target_lang TEXT NOT NULL,
|
|
214
|
+
project_context_hash TEXT NOT NULL,
|
|
215
|
+
string_context_hash TEXT NOT NULL,
|
|
216
|
+
translated_text TEXT NOT NULL,
|
|
217
|
+
model TEXT NOT NULL,
|
|
218
|
+
status TEXT NOT NULL DEFAULT 'translated',
|
|
219
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
220
|
+
PRIMARY KEY(source_text, source_lang, target_lang, project_context_hash, string_context_hash)
|
|
221
|
+
)
|
|
222
|
+
`);
|
|
223
|
+
// Insert a v1 row
|
|
224
|
+
await conn.run(`
|
|
225
|
+
INSERT INTO translations (source_text, source_lang, target_lang, project_context_hash, string_context_hash, translated_text, model, status)
|
|
226
|
+
VALUES ('Hello', 'EN', 'DE', '${hash('ctx')}', '${hash('')}', 'Hallo', 'gpt-4.1-mini', 'translated')
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
// Close the raw connection
|
|
230
|
+
// (DuckDB node-api handles cleanup via GC, just null out)
|
|
231
|
+
|
|
232
|
+
// Now open with TranslationStore — should migrate
|
|
233
|
+
const migratedStore = new TranslationStore(dbPath);
|
|
234
|
+
await migratedStore.initialize();
|
|
235
|
+
|
|
236
|
+
// Old data should still be accessible
|
|
237
|
+
const result = await migratedStore.lookup({
|
|
238
|
+
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'DE',
|
|
239
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
240
|
+
});
|
|
241
|
+
expect(result).toBe('Hallo');
|
|
242
|
+
|
|
243
|
+
// Plural methods should work
|
|
244
|
+
await migratedStore.insertPlural({
|
|
245
|
+
sourceText: 'test\x00tests',
|
|
246
|
+
sourceLang: 'EN', targetLang: 'DE',
|
|
247
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
248
|
+
pluralCategory: 'other',
|
|
249
|
+
translatedText: 'Tests',
|
|
250
|
+
model: 'gpt-4.1-mini', status: 'translated',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const pluralResult = await migratedStore.lookupPlural({
|
|
254
|
+
sourceText: 'test\x00tests',
|
|
255
|
+
sourceLang: 'EN', targetLang: 'DE',
|
|
256
|
+
projectContextHash: hash('ctx'), stringContextHash: hash(''),
|
|
257
|
+
});
|
|
258
|
+
expect(pluralResult).toEqual({ other: 'Tests' });
|
|
259
|
+
|
|
260
|
+
migratedStore.close();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractPlaceholders, validateTranslation } from '../src/validation.js';
|
|
3
|
+
|
|
4
|
+
describe('extractPlaceholders', () => {
|
|
5
|
+
it('extracts curly placeholders', () => {
|
|
6
|
+
expect(extractPlaceholders('Hello {name}')).toEqual(new Set(['{name}']));
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('extracts double curly placeholders', () => {
|
|
10
|
+
expect(extractPlaceholders('You have {{ count }} items')).toEqual(new Set(['{{ count }}']));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('extracts percent placeholders', () => {
|
|
14
|
+
expect(extractPlaceholders('Hello %s, you have %d items')).toEqual(new Set(['%s', '%d']));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('extracts dollar placeholders', () => {
|
|
18
|
+
expect(extractPlaceholders('Hello ${name}')).toEqual(new Set(['${name}']));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('extracts mixed placeholders', () => {
|
|
22
|
+
const result = extractPlaceholders('{greeting} %s ${name}');
|
|
23
|
+
expect(result).toEqual(new Set(['{greeting}', '%s', '${name}']));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns empty set for no placeholders', () => {
|
|
27
|
+
expect(extractPlaceholders('Hello world')).toEqual(new Set());
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('validateTranslation', () => {
|
|
32
|
+
it('passes valid translation', () => {
|
|
33
|
+
expect(validateTranslation('Hello {name}', 'Hallo {name}')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('fails empty translation', () => {
|
|
37
|
+
expect(validateTranslation('Hello', '')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('fails missing placeholder', () => {
|
|
41
|
+
expect(validateTranslation('Hello {name}', 'Hallo')).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('passes when no placeholders', () => {
|
|
45
|
+
expect(validateTranslation('Hello', 'Hallo')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|