@djangocfg/llm 2.1.164
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/README.md +181 -0
- package/dist/index.cjs +1164 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.mjs +1128 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers/index.cjs +317 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +30 -0
- package/dist/providers/index.d.ts +30 -0
- package/dist/providers/index.mjs +304 -0
- package/dist/providers/index.mjs.map +1 -0
- package/dist/sdkrouter-D8GMBmTi.d.ts +171 -0
- package/dist/sdkrouter-hlQlVd0v.d.cts +171 -0
- package/dist/text-utils-DoYqMIr6.d.ts +289 -0
- package/dist/text-utils-VXWN-8Oq.d.cts +289 -0
- package/dist/translator/index.cjs +794 -0
- package/dist/translator/index.cjs.map +1 -0
- package/dist/translator/index.d.cts +24 -0
- package/dist/translator/index.d.ts +24 -0
- package/dist/translator/index.mjs +769 -0
- package/dist/translator/index.mjs.map +1 -0
- package/dist/types-D6lazgm1.d.cts +59 -0
- package/dist/types-D6lazgm1.d.ts +59 -0
- package/package.json +82 -0
- package/src/client.ts +119 -0
- package/src/index.ts +70 -0
- package/src/providers/anthropic.ts +98 -0
- package/src/providers/base.ts +90 -0
- package/src/providers/index.ts +15 -0
- package/src/providers/openai.ts +73 -0
- package/src/providers/sdkrouter.ts +279 -0
- package/src/translator/cache.ts +237 -0
- package/src/translator/index.ts +55 -0
- package/src/translator/json-translator.ts +408 -0
- package/src/translator/prompts.ts +90 -0
- package/src/translator/text-utils.ts +148 -0
- package/src/translator/types.ts +112 -0
- package/src/translator/validator.ts +181 -0
- package/src/types.ts +85 -0
- package/src/utils/env.ts +67 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/json.ts +44 -0
- package/src/utils/schema.ts +153 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Translation module
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Smart text-level caching (memory + persistent)
|
|
6
|
+
* - Technical content detection (URLs, emails, paths)
|
|
7
|
+
* - Parallel multi-language translation
|
|
8
|
+
* - Partial JSON - only sends uncached texts to LLM
|
|
9
|
+
* - Language auto-detection
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Main translator
|
|
13
|
+
export { JsonTranslator, createTranslator } from './json-translator';
|
|
14
|
+
|
|
15
|
+
// Types
|
|
16
|
+
export type {
|
|
17
|
+
LanguageCode,
|
|
18
|
+
TranslateOptions,
|
|
19
|
+
TranslateResult,
|
|
20
|
+
BatchTranslateItem,
|
|
21
|
+
CacheStats,
|
|
22
|
+
} from './types';
|
|
23
|
+
|
|
24
|
+
export { LANGUAGE_NAMES } from './types';
|
|
25
|
+
|
|
26
|
+
// Cache
|
|
27
|
+
export { TranslationCache, createCache } from './cache';
|
|
28
|
+
|
|
29
|
+
// Validation
|
|
30
|
+
export {
|
|
31
|
+
validateTranslation,
|
|
32
|
+
validateJsonKeys,
|
|
33
|
+
validatePlaceholders,
|
|
34
|
+
type ValidationResult,
|
|
35
|
+
} from './validator';
|
|
36
|
+
|
|
37
|
+
// Text utilities
|
|
38
|
+
export {
|
|
39
|
+
isTechnicalContent,
|
|
40
|
+
extractPlaceholders,
|
|
41
|
+
extractTranslatableValues,
|
|
42
|
+
detectScript,
|
|
43
|
+
containsCJK,
|
|
44
|
+
containsCyrillic,
|
|
45
|
+
containsArabic,
|
|
46
|
+
validatePlaceholders as validateTextPlaceholders,
|
|
47
|
+
} from './text-utils';
|
|
48
|
+
|
|
49
|
+
// Prompts (for advanced usage)
|
|
50
|
+
export {
|
|
51
|
+
buildJsonTranslationPrompt,
|
|
52
|
+
buildTextTranslationPrompt,
|
|
53
|
+
buildRetryPrompt,
|
|
54
|
+
getLanguageName,
|
|
55
|
+
} from './prompts';
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Translator using LLM
|
|
3
|
+
*
|
|
4
|
+
* Smart text-level caching for efficiency:
|
|
5
|
+
* 1. Extract all translatable strings from JSON
|
|
6
|
+
* 2. Check cache for each string
|
|
7
|
+
* 3. Send only uncached strings to LLM
|
|
8
|
+
* 4. Cache new translations
|
|
9
|
+
* 5. Apply all translations to original structure
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { LLMClient } from '../types';
|
|
13
|
+
import { extractJson } from '../utils/json';
|
|
14
|
+
import type { TranslateOptions, TranslateResult, LanguageCode } from './types';
|
|
15
|
+
import { buildJsonTranslationPrompt } from './prompts';
|
|
16
|
+
import { isTechnicalContent, detectScript } from './text-utils';
|
|
17
|
+
import { TranslationCache, createCache } from './cache';
|
|
18
|
+
|
|
19
|
+
/** Default model for translation - gpt-4o-mini is reliable and cost-effective */
|
|
20
|
+
const DEFAULT_TRANSLATION_MODEL = 'openai/gpt-4o-mini';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* JSON Translator class with smart caching
|
|
24
|
+
*/
|
|
25
|
+
export class JsonTranslator {
|
|
26
|
+
private cache: TranslationCache;
|
|
27
|
+
private defaultModel: string;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private client: LLMClient,
|
|
31
|
+
cache?: TranslationCache,
|
|
32
|
+
defaultModel?: string
|
|
33
|
+
) {
|
|
34
|
+
this.cache = cache ?? createCache();
|
|
35
|
+
this.defaultModel = defaultModel ?? DEFAULT_TRANSLATION_MODEL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect language from text
|
|
40
|
+
*/
|
|
41
|
+
detectLanguage(text: string): LanguageCode {
|
|
42
|
+
const script = detectScript(text);
|
|
43
|
+
switch (script) {
|
|
44
|
+
case 'cjk':
|
|
45
|
+
// Could be zh, ja, ko - default to zh
|
|
46
|
+
return 'zh';
|
|
47
|
+
case 'cyrillic':
|
|
48
|
+
return 'ru';
|
|
49
|
+
case 'arabic':
|
|
50
|
+
return 'ar';
|
|
51
|
+
default:
|
|
52
|
+
return 'en';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if text needs translation
|
|
58
|
+
*/
|
|
59
|
+
needsTranslation(
|
|
60
|
+
text: string,
|
|
61
|
+
sourceLang: string,
|
|
62
|
+
targetLang: string
|
|
63
|
+
): boolean {
|
|
64
|
+
if (!text || !text.trim()) return false;
|
|
65
|
+
if (isTechnicalContent(text)) return false;
|
|
66
|
+
if (sourceLang === targetLang) return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Translate single text
|
|
72
|
+
*/
|
|
73
|
+
async translateText(
|
|
74
|
+
text: string,
|
|
75
|
+
targetLanguage: LanguageCode,
|
|
76
|
+
options?: TranslateOptions
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
if (!text || !text.trim()) return text;
|
|
79
|
+
|
|
80
|
+
const sourceLang = options?.sourceLanguage ?? 'auto';
|
|
81
|
+
const actualSource = sourceLang === 'auto' ? this.detectLanguage(text) : sourceLang;
|
|
82
|
+
|
|
83
|
+
if (!this.needsTranslation(text, actualSource, targetLanguage)) {
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check cache
|
|
88
|
+
const cached = this.cache.get(text, actualSource, targetLanguage);
|
|
89
|
+
if (cached) return cached;
|
|
90
|
+
|
|
91
|
+
// Call LLM
|
|
92
|
+
const response = await this.client.chat(
|
|
93
|
+
`Translate this text to ${targetLanguage}. Return ONLY the translation:\n\n${text}`,
|
|
94
|
+
{
|
|
95
|
+
...options,
|
|
96
|
+
model: options?.model ?? this.defaultModel,
|
|
97
|
+
temperature: 0,
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const translated = response.content.trim();
|
|
102
|
+
this.cache.set(text, actualSource, targetLanguage, translated);
|
|
103
|
+
|
|
104
|
+
return translated;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Translate JSON object with smart text-level caching
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const translator = new JsonTranslator(llm)
|
|
113
|
+
* const result = await translator.translate(
|
|
114
|
+
* { title: 'Hello', items: ['World', 'Earth'] },
|
|
115
|
+
* 'ru'
|
|
116
|
+
* )
|
|
117
|
+
* // { data: { title: 'Привет', items: ['Мир', 'Земля'] }, valid: true, ... }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
async translate<T extends Record<string, unknown>>(
|
|
121
|
+
data: T,
|
|
122
|
+
targetLanguage: LanguageCode,
|
|
123
|
+
options?: TranslateOptions
|
|
124
|
+
): Promise<TranslateResult<T>> {
|
|
125
|
+
const sourceLang = options?.sourceLanguage ?? 'auto';
|
|
126
|
+
|
|
127
|
+
// Extract all translatable texts
|
|
128
|
+
const translatableTexts = this.extractTranslatableTexts(data, sourceLang, targetLanguage);
|
|
129
|
+
|
|
130
|
+
if (translatableTexts.size === 0) {
|
|
131
|
+
return {
|
|
132
|
+
data,
|
|
133
|
+
valid: true,
|
|
134
|
+
errors: [],
|
|
135
|
+
retries: 0,
|
|
136
|
+
sourceLanguage: sourceLang,
|
|
137
|
+
targetLanguage,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Detect source language from first text if auto
|
|
142
|
+
let actualSource = sourceLang;
|
|
143
|
+
if (sourceLang === 'auto') {
|
|
144
|
+
const firstText = [...translatableTexts][0];
|
|
145
|
+
actualSource = this.detectLanguage(firstText);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check cache for each text
|
|
149
|
+
const { cached, uncached } = this.cache.getMany(
|
|
150
|
+
[...translatableTexts],
|
|
151
|
+
actualSource,
|
|
152
|
+
targetLanguage
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// If everything cached, apply and return
|
|
156
|
+
if (uncached.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
data: this.applyTranslations(data, cached),
|
|
159
|
+
valid: true,
|
|
160
|
+
errors: [],
|
|
161
|
+
retries: 0,
|
|
162
|
+
sourceLanguage: actualSource,
|
|
163
|
+
targetLanguage,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create partial JSON with only uncached texts
|
|
168
|
+
const partialJson = this.createPartialJson(data, new Set(uncached));
|
|
169
|
+
const jsonStr = JSON.stringify(partialJson, null, 2);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Build prompt and call LLM
|
|
173
|
+
const prompt = buildJsonTranslationPrompt(jsonStr, actualSource, targetLanguage);
|
|
174
|
+
|
|
175
|
+
const response = await this.client.chat(prompt, {
|
|
176
|
+
...options,
|
|
177
|
+
model: options?.model ?? this.defaultModel,
|
|
178
|
+
temperature: 0,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Parse response
|
|
182
|
+
const translatedPartial = extractJson<Record<string, unknown>>(response.content);
|
|
183
|
+
|
|
184
|
+
// Extract translations by comparison
|
|
185
|
+
const newTranslations = this.extractTranslationsByComparison(
|
|
186
|
+
partialJson,
|
|
187
|
+
translatedPartial,
|
|
188
|
+
uncached
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Cache new translations
|
|
192
|
+
this.cache.setMany(newTranslations, actualSource, targetLanguage);
|
|
193
|
+
|
|
194
|
+
// Combine all translations
|
|
195
|
+
const allTranslations = new Map([...cached, ...newTranslations]);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
data: this.applyTranslations(data, allTranslations),
|
|
199
|
+
valid: true,
|
|
200
|
+
errors: [],
|
|
201
|
+
retries: 0,
|
|
202
|
+
sourceLanguage: actualSource,
|
|
203
|
+
targetLanguage,
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// On error, return what we have from cache
|
|
207
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
208
|
+
console.error('Translation failed:', errorMsg);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
data: cached.size > 0 ? this.applyTranslations(data, cached) : data,
|
|
212
|
+
valid: false,
|
|
213
|
+
errors: [errorMsg],
|
|
214
|
+
retries: 0,
|
|
215
|
+
sourceLanguage: actualSource,
|
|
216
|
+
targetLanguage,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Translate to multiple languages in parallel
|
|
223
|
+
*/
|
|
224
|
+
async translateToMany<T extends Record<string, unknown>>(
|
|
225
|
+
data: T,
|
|
226
|
+
targetLanguages: LanguageCode[],
|
|
227
|
+
options?: TranslateOptions
|
|
228
|
+
): Promise<Map<LanguageCode, TranslateResult<T>>> {
|
|
229
|
+
const results = new Map<LanguageCode, TranslateResult<T>>();
|
|
230
|
+
|
|
231
|
+
// Run translations in parallel
|
|
232
|
+
const promises = targetLanguages.map(async (lang) => {
|
|
233
|
+
const result = await this.translate(data, lang, options);
|
|
234
|
+
return { lang, result };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const settled = await Promise.all(promises);
|
|
238
|
+
for (const { lang, result } of settled) {
|
|
239
|
+
results.set(lang, result);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Extract all translatable texts from object
|
|
247
|
+
*/
|
|
248
|
+
private extractTranslatableTexts(
|
|
249
|
+
obj: unknown,
|
|
250
|
+
sourceLang: string,
|
|
251
|
+
targetLang: string
|
|
252
|
+
): Set<string> {
|
|
253
|
+
const texts = new Set<string>();
|
|
254
|
+
|
|
255
|
+
const extract = (item: unknown): void => {
|
|
256
|
+
if (typeof item === 'string') {
|
|
257
|
+
if (this.needsTranslation(item, sourceLang, targetLang)) {
|
|
258
|
+
texts.add(item);
|
|
259
|
+
}
|
|
260
|
+
} else if (Array.isArray(item)) {
|
|
261
|
+
for (const i of item) extract(i);
|
|
262
|
+
} else if (item !== null && typeof item === 'object') {
|
|
263
|
+
for (const v of Object.values(item)) extract(v);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
extract(obj);
|
|
268
|
+
return texts;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Apply translations to object
|
|
273
|
+
*/
|
|
274
|
+
private applyTranslations<T>(obj: T, translations: Map<string, string>): T {
|
|
275
|
+
const apply = (item: unknown): unknown => {
|
|
276
|
+
if (typeof item === 'string') {
|
|
277
|
+
return translations.get(item) ?? item;
|
|
278
|
+
} else if (Array.isArray(item)) {
|
|
279
|
+
return item.map(apply);
|
|
280
|
+
} else if (item !== null && typeof item === 'object') {
|
|
281
|
+
const result: Record<string, unknown> = {};
|
|
282
|
+
for (const [k, v] of Object.entries(item)) {
|
|
283
|
+
result[k] = apply(v);
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
return item;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return apply(obj) as T;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Create partial JSON with only texts that need translation
|
|
295
|
+
*/
|
|
296
|
+
private createPartialJson(data: unknown, textsToInclude: Set<string>): unknown {
|
|
297
|
+
const filter = (obj: unknown): unknown => {
|
|
298
|
+
if (typeof obj === 'string') {
|
|
299
|
+
return textsToInclude.has(obj) ? obj : 'SKIP';
|
|
300
|
+
} else if (Array.isArray(obj)) {
|
|
301
|
+
return obj.map(filter).filter((i) => this.hasTranslatable(i, textsToInclude));
|
|
302
|
+
} else if (obj !== null && typeof obj === 'object') {
|
|
303
|
+
const result: Record<string, unknown> = {};
|
|
304
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
305
|
+
const filtered = filter(v);
|
|
306
|
+
if (this.hasTranslatable(filtered, textsToInclude)) {
|
|
307
|
+
result[k] = filtered;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
return obj;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return filter(data);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if object contains translatable text
|
|
320
|
+
*/
|
|
321
|
+
private hasTranslatable(obj: unknown, textsSet: Set<string>): boolean {
|
|
322
|
+
if (typeof obj === 'string') return textsSet.has(obj);
|
|
323
|
+
if (Array.isArray(obj)) return obj.some((i) => this.hasTranslatable(i, textsSet));
|
|
324
|
+
if (obj !== null && typeof obj === 'object') {
|
|
325
|
+
return Object.values(obj).some((v) => this.hasTranslatable(v, textsSet));
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extract translations by comparing original and translated JSON
|
|
332
|
+
*/
|
|
333
|
+
private extractTranslationsByComparison(
|
|
334
|
+
original: unknown,
|
|
335
|
+
translated: unknown,
|
|
336
|
+
uncachedTexts: string[]
|
|
337
|
+
): Map<string, string> {
|
|
338
|
+
const translations = new Map<string, string>();
|
|
339
|
+
const uncachedSet = new Set(uncachedTexts);
|
|
340
|
+
|
|
341
|
+
const compare = (orig: unknown, trans: unknown): void => {
|
|
342
|
+
if (typeof orig === 'string' && typeof trans === 'string') {
|
|
343
|
+
// Save translation if original was in uncached list and translation is valid
|
|
344
|
+
if (uncachedSet.has(orig) && trans !== 'SKIP' && trans.trim()) {
|
|
345
|
+
translations.set(orig, trans);
|
|
346
|
+
}
|
|
347
|
+
} else if (Array.isArray(orig) && Array.isArray(trans)) {
|
|
348
|
+
for (let i = 0; i < Math.min(orig.length, trans.length); i++) {
|
|
349
|
+
compare(orig[i], trans[i]);
|
|
350
|
+
}
|
|
351
|
+
} else if (
|
|
352
|
+
orig !== null &&
|
|
353
|
+
typeof orig === 'object' &&
|
|
354
|
+
trans !== null &&
|
|
355
|
+
typeof trans === 'object'
|
|
356
|
+
) {
|
|
357
|
+
for (const key of Object.keys(orig as Record<string, unknown>)) {
|
|
358
|
+
if (key in (trans as Record<string, unknown>)) {
|
|
359
|
+
compare(
|
|
360
|
+
(orig as Record<string, unknown>)[key],
|
|
361
|
+
(trans as Record<string, unknown>)[key]
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
compare(original, translated);
|
|
369
|
+
return translations;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get translation statistics
|
|
374
|
+
*/
|
|
375
|
+
getStats() {
|
|
376
|
+
return this.cache.getStats();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Clear translation cache
|
|
381
|
+
*/
|
|
382
|
+
clearCache(sourceLang?: string, targetLang?: string) {
|
|
383
|
+
this.cache.clear(sourceLang, targetLang);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Translator options
|
|
389
|
+
*/
|
|
390
|
+
export interface TranslatorConfig {
|
|
391
|
+
cache?: TranslationCache;
|
|
392
|
+
/** Model for translation (default: openai/gpt-4o-mini) */
|
|
393
|
+
model?: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create JSON translator
|
|
398
|
+
*/
|
|
399
|
+
export function createTranslator(
|
|
400
|
+
client: LLMClient,
|
|
401
|
+
configOrCache?: TranslationCache | TranslatorConfig
|
|
402
|
+
): JsonTranslator {
|
|
403
|
+
// Support both old and new API
|
|
404
|
+
if (configOrCache instanceof TranslationCache) {
|
|
405
|
+
return new JsonTranslator(client, configOrCache);
|
|
406
|
+
}
|
|
407
|
+
return new JsonTranslator(client, configOrCache?.cache, configOrCache?.model);
|
|
408
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation prompts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LANGUAGE_NAMES } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get language name from code
|
|
9
|
+
*/
|
|
10
|
+
export function getLanguageName(code: string): string {
|
|
11
|
+
return LANGUAGE_NAMES[code] ?? code;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build JSON translation prompt
|
|
16
|
+
*/
|
|
17
|
+
export function buildJsonTranslationPrompt(
|
|
18
|
+
json: string,
|
|
19
|
+
sourceLanguage: string,
|
|
20
|
+
targetLanguage: string
|
|
21
|
+
): string {
|
|
22
|
+
const sourceName = getLanguageName(sourceLanguage);
|
|
23
|
+
const targetName = getLanguageName(targetLanguage);
|
|
24
|
+
|
|
25
|
+
return `Translate all string VALUES in this JSON from ${sourceName} to ${targetName}.
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
- Translate ALL string values to ${targetName}
|
|
29
|
+
- Keep JSON keys unchanged (English)
|
|
30
|
+
- Skip: URLs, emails, numbers, "SKIP"
|
|
31
|
+
- Keep placeholders: {name}, {{var}}, %s
|
|
32
|
+
|
|
33
|
+
${json}
|
|
34
|
+
|
|
35
|
+
Return ONLY the JSON with all values translated to ${targetName}:`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build text translation prompt
|
|
40
|
+
*/
|
|
41
|
+
export function buildTextTranslationPrompt(
|
|
42
|
+
text: string,
|
|
43
|
+
sourceLanguage: string,
|
|
44
|
+
targetLanguage: string
|
|
45
|
+
): string {
|
|
46
|
+
const sourceName = getLanguageName(sourceLanguage);
|
|
47
|
+
const targetName = getLanguageName(targetLanguage);
|
|
48
|
+
|
|
49
|
+
return `Translate from ${sourceName} to ${targetName}.
|
|
50
|
+
|
|
51
|
+
RULES:
|
|
52
|
+
1. Translate ONLY the text provided
|
|
53
|
+
2. Preserve formatting, numbers, URLs
|
|
54
|
+
3. Keep placeholders like {name}, {{var}} unchanged
|
|
55
|
+
4. Return ONLY the translation, no explanations
|
|
56
|
+
|
|
57
|
+
Text: ${text}
|
|
58
|
+
|
|
59
|
+
Translation:`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build retry prompt with errors
|
|
64
|
+
*/
|
|
65
|
+
export function buildRetryPrompt(
|
|
66
|
+
originalJson: string,
|
|
67
|
+
wrongJson: string,
|
|
68
|
+
errors: string[],
|
|
69
|
+
targetLanguage: string
|
|
70
|
+
): string {
|
|
71
|
+
const targetName = getLanguageName(targetLanguage);
|
|
72
|
+
|
|
73
|
+
return `Your previous translation had errors. Fix them.
|
|
74
|
+
|
|
75
|
+
ERRORS FOUND:
|
|
76
|
+
${errors.map((e) => `- ${e}`).join('\n')}
|
|
77
|
+
|
|
78
|
+
ORIGINAL JSON:
|
|
79
|
+
${originalJson}
|
|
80
|
+
|
|
81
|
+
YOUR WRONG TRANSLATION:
|
|
82
|
+
${wrongJson}
|
|
83
|
+
|
|
84
|
+
FIX THE ERRORS. Remember:
|
|
85
|
+
- JSON keys must stay in English
|
|
86
|
+
- Only translate string values
|
|
87
|
+
- Preserve all placeholders
|
|
88
|
+
|
|
89
|
+
Return ONLY the corrected JSON translated to ${targetName}:`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text utilities for translation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if text is technical content that shouldn't be translated
|
|
7
|
+
*/
|
|
8
|
+
export function isTechnicalContent(text: string): boolean {
|
|
9
|
+
const trimmed = text.trim();
|
|
10
|
+
|
|
11
|
+
// Empty or whitespace only
|
|
12
|
+
if (!trimmed) return true;
|
|
13
|
+
|
|
14
|
+
// URLs
|
|
15
|
+
if (/^(https?:\/\/|\/\/|www\.)/i.test(trimmed)) return true;
|
|
16
|
+
|
|
17
|
+
// Email addresses
|
|
18
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return true;
|
|
19
|
+
|
|
20
|
+
// File paths (starts with / or contains common extensions)
|
|
21
|
+
if (/^\/[a-zA-Z]/.test(trimmed)) return true;
|
|
22
|
+
if (/\.(js|ts|tsx|jsx|json|css|scss|html|md|py|go|rs)$/i.test(trimmed)) return true;
|
|
23
|
+
|
|
24
|
+
// Numbers only (including decimals and percentages)
|
|
25
|
+
if (/^[\d.,]+%?$/.test(trimmed)) return true;
|
|
26
|
+
|
|
27
|
+
// Technical identifiers (SCREAMING_SNAKE_CASE)
|
|
28
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(trimmed)) return true;
|
|
29
|
+
|
|
30
|
+
// Placeholders like {name}, {{var}}, %s, $1
|
|
31
|
+
if (/^(\{[^}]+\}|\{\{[^}]+\}\}|%[sd]|\$\d+)$/.test(trimmed)) return true;
|
|
32
|
+
|
|
33
|
+
// Single special characters
|
|
34
|
+
if (/^[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+$/.test(trimmed)) return true;
|
|
35
|
+
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if text contains CJK characters (Chinese, Japanese, Korean)
|
|
41
|
+
*/
|
|
42
|
+
export function containsCJK(text: string): boolean {
|
|
43
|
+
// CJK Unified Ideographs, Hiragana, Katakana, Hangul
|
|
44
|
+
return /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if text contains Cyrillic characters
|
|
49
|
+
*/
|
|
50
|
+
export function containsCyrillic(text: string): boolean {
|
|
51
|
+
return /[\u0400-\u04ff]/.test(text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if text contains Arabic characters
|
|
56
|
+
*/
|
|
57
|
+
export function containsArabic(text: string): boolean {
|
|
58
|
+
return /[\u0600-\u06ff]/.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detect script/language from text
|
|
63
|
+
*/
|
|
64
|
+
export function detectScript(
|
|
65
|
+
text: string
|
|
66
|
+
): 'cjk' | 'cyrillic' | 'arabic' | 'latin' | 'unknown' {
|
|
67
|
+
if (containsCJK(text)) return 'cjk';
|
|
68
|
+
if (containsCyrillic(text)) return 'cyrillic';
|
|
69
|
+
if (containsArabic(text)) return 'arabic';
|
|
70
|
+
if (/[a-zA-Z]/.test(text)) return 'latin';
|
|
71
|
+
return 'unknown';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract placeholders from text
|
|
76
|
+
*/
|
|
77
|
+
export function extractPlaceholders(text: string): string[] {
|
|
78
|
+
const patterns = [
|
|
79
|
+
/\{[^}]+\}/g, // {name}
|
|
80
|
+
/\{\{[^}]+\}\}/g, // {{name}}
|
|
81
|
+
/%[sd]/g, // %s, %d
|
|
82
|
+
/\$\d+/g, // $1, $2
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const placeholders: string[] = [];
|
|
86
|
+
for (const pattern of patterns) {
|
|
87
|
+
const matches = text.match(pattern);
|
|
88
|
+
if (matches) {
|
|
89
|
+
placeholders.push(...matches);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [...new Set(placeholders)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if all placeholders are preserved in translation
|
|
98
|
+
*/
|
|
99
|
+
export function validatePlaceholders(
|
|
100
|
+
original: string,
|
|
101
|
+
translated: string
|
|
102
|
+
): { valid: boolean; missing: string[]; extra: string[] } {
|
|
103
|
+
const originalPlaceholders = extractPlaceholders(original);
|
|
104
|
+
const translatedPlaceholders = extractPlaceholders(translated);
|
|
105
|
+
|
|
106
|
+
const missing = originalPlaceholders.filter(
|
|
107
|
+
(p) => !translatedPlaceholders.includes(p)
|
|
108
|
+
);
|
|
109
|
+
const extra = translatedPlaceholders.filter(
|
|
110
|
+
(p) => !originalPlaceholders.includes(p)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
valid: missing.length === 0 && extra.length === 0,
|
|
115
|
+
missing,
|
|
116
|
+
extra,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get values that need translation from JSON
|
|
122
|
+
*/
|
|
123
|
+
export function extractTranslatableValues(
|
|
124
|
+
obj: unknown,
|
|
125
|
+
path: string = ''
|
|
126
|
+
): Array<{ path: string; value: string }> {
|
|
127
|
+
const values: Array<{ path: string; value: string }> = [];
|
|
128
|
+
|
|
129
|
+
if (typeof obj === 'string') {
|
|
130
|
+
if (!isTechnicalContent(obj)) {
|
|
131
|
+
values.push({ path, value: obj });
|
|
132
|
+
}
|
|
133
|
+
} else if (Array.isArray(obj)) {
|
|
134
|
+
obj.forEach((item, index) => {
|
|
135
|
+
values.push(
|
|
136
|
+
...extractTranslatableValues(item, path ? `${path}[${index}]` : `[${index}]`)
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
} else if (obj !== null && typeof obj === 'object') {
|
|
140
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
141
|
+
values.push(
|
|
142
|
+
...extractTranslatableValues(value, path ? `${path}.${key}` : key)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return values;
|
|
148
|
+
}
|