@lingo.dev/compiler 0.1.1 → 0.1.3
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 +273 -2
- package/build/metadata/manager.mjs +12 -12
- package/build/metadata/manager.mjs.map +1 -1
- package/build/plugin/build-translator.cjs +3 -3
- package/build/plugin/build-translator.mjs +6 -6
- package/build/plugin/build-translator.mjs.map +1 -1
- package/build/plugin/next.cjs +3 -3
- package/build/plugin/next.d.cts.map +1 -1
- package/build/plugin/next.d.mts.map +1 -1
- package/build/plugin/next.mjs +3 -3
- package/build/plugin/next.mjs.map +1 -1
- package/build/plugin/unplugin.cjs +3 -2
- package/build/plugin/unplugin.d.cts.map +1 -1
- package/build/plugin/unplugin.d.mts.map +1 -1
- package/build/plugin/unplugin.mjs +3 -2
- package/build/plugin/unplugin.mjs.map +1 -1
- package/build/react/server/ServerLingoProvider.d.cts +2 -2
- package/build/react/shared/LocaleSwitcher.d.cts +2 -2
- package/build/translation-server/translation-server.cjs +7 -17
- package/build/translation-server/translation-server.mjs +7 -17
- package/build/translation-server/translation-server.mjs.map +1 -1
- package/build/translators/cache-factory.mjs.map +1 -1
- package/build/translators/lingo/model-factory.cjs +5 -10
- package/build/translators/lingo/model-factory.mjs +5 -10
- package/build/translators/lingo/model-factory.mjs.map +1 -1
- package/build/translators/lingo/provider-details.cjs +69 -0
- package/build/translators/lingo/provider-details.mjs +69 -0
- package/build/translators/lingo/provider-details.mjs.map +1 -0
- package/build/translators/lingo/{service.cjs → translator.cjs} +11 -13
- package/build/translators/lingo/{service.mjs → translator.mjs} +12 -14
- package/build/translators/lingo/translator.mjs.map +1 -0
- package/build/translators/local-cache.mjs +8 -8
- package/build/translators/local-cache.mjs.map +1 -1
- package/build/translators/memory-cache.cjs +47 -0
- package/build/translators/memory-cache.mjs +47 -0
- package/build/translators/memory-cache.mjs.map +1 -0
- package/build/translators/pluralization/service.cjs +19 -44
- package/build/translators/pluralization/service.mjs +19 -44
- package/build/translators/pluralization/service.mjs.map +1 -1
- package/build/translators/pseudotranslator/index.cjs +2 -10
- package/build/translators/pseudotranslator/index.mjs +2 -10
- package/build/translators/pseudotranslator/index.mjs.map +1 -1
- package/build/translators/translation-service.cjs +55 -57
- package/build/translators/translation-service.mjs +55 -57
- package/build/translators/translation-service.mjs.map +1 -1
- package/package.json +7 -7
- package/build/translators/lingo/service.mjs.map +0 -1
- package/build/translators/translator-factory.cjs +0 -49
- package/build/translators/translator-factory.mjs +0 -50
- package/build/translators/translator-factory.mjs.map +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/translators/memory-cache.ts
|
|
2
|
+
/**
|
|
3
|
+
* In memory translation cache implementation
|
|
4
|
+
*/
|
|
5
|
+
var MemoryTranslationCache = class {
|
|
6
|
+
cache = /* @__PURE__ */ new Map();
|
|
7
|
+
constructor() {}
|
|
8
|
+
async get(locale, hashes) {
|
|
9
|
+
const localeCache = this.cache.get(locale);
|
|
10
|
+
if (!localeCache) return {};
|
|
11
|
+
if (hashes) return hashes.reduce((acc, hash) => ({
|
|
12
|
+
...acc,
|
|
13
|
+
[hash]: localeCache.get(hash)
|
|
14
|
+
}), {});
|
|
15
|
+
return Object.fromEntries(localeCache);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Update cache with new translations (merge)
|
|
19
|
+
*/
|
|
20
|
+
async update(locale, translations) {
|
|
21
|
+
let localeCache = this.cache.get(locale);
|
|
22
|
+
if (!localeCache) {
|
|
23
|
+
localeCache = /* @__PURE__ */ new Map();
|
|
24
|
+
this.cache.set(locale, localeCache);
|
|
25
|
+
}
|
|
26
|
+
for (const [key, value] of Object.entries(translations)) localeCache.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Replace entire cache for a locale
|
|
30
|
+
*/
|
|
31
|
+
async set(locale, translations) {
|
|
32
|
+
this.cache.set(locale, new Map(Object.entries(translations)));
|
|
33
|
+
}
|
|
34
|
+
async has(locale) {
|
|
35
|
+
return this.cache.has(locale);
|
|
36
|
+
}
|
|
37
|
+
async clear(locale) {
|
|
38
|
+
this.cache.delete(locale);
|
|
39
|
+
}
|
|
40
|
+
async clearAll() {
|
|
41
|
+
this.cache.clear();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
export { MemoryTranslationCache };
|
|
47
|
+
//# sourceMappingURL=memory-cache.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-cache.mjs","names":[],"sources":["../../src/translators/memory-cache.ts"],"sourcesContent":["import type { TranslationCache } from \"./cache\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\n/**\n * In memory translation cache implementation\n */\nexport class MemoryTranslationCache implements TranslationCache {\n private cache: Map<LocaleCode, Map<string, string>> = new Map();\n\n constructor() {}\n\n async get(\n locale: LocaleCode,\n hashes?: string[],\n ): Promise<Record<string, string>> {\n const localeCache = this.cache.get(locale);\n if (!localeCache) {\n return {};\n }\n if (hashes) {\n return hashes.reduce(\n (acc, hash) => ({ ...acc, [hash]: localeCache.get(hash) }),\n {},\n );\n }\n return Object.fromEntries(localeCache);\n }\n\n /**\n * Update cache with new translations (merge)\n */\n async update(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n let localeCache = this.cache.get(locale);\n if (!localeCache) {\n localeCache = new Map();\n this.cache.set(locale, localeCache);\n }\n for (const [key, value] of Object.entries(translations)) {\n localeCache.set(key, value);\n }\n }\n\n /**\n * Replace entire cache for a locale\n */\n async set(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n this.cache.set(locale, new Map(Object.entries(translations)));\n }\n\n async has(locale: LocaleCode): Promise<boolean> {\n return this.cache.has(locale);\n }\n\n async clear(locale: LocaleCode): Promise<void> {\n this.cache.delete(locale);\n }\n\n async clearAll(): Promise<void> {\n this.cache.clear();\n }\n}\n"],"mappings":";;;;AAMA,IAAa,yBAAb,MAAgE;CAC9D,AAAQ,wBAA8C,IAAI,KAAK;CAE/D,cAAc;CAEd,MAAM,IACJ,QACA,QACiC;EACjC,MAAM,cAAc,KAAK,MAAM,IAAI,OAAO;AAC1C,MAAI,CAAC,YACH,QAAO,EAAE;AAEX,MAAI,OACF,QAAO,OAAO,QACX,KAAK,UAAU;GAAE,GAAG;IAAM,OAAO,YAAY,IAAI,KAAK;GAAE,GACzD,EAAE,CACH;AAEH,SAAO,OAAO,YAAY,YAAY;;;;;CAMxC,MAAM,OACJ,QACA,cACe;EACf,IAAI,cAAc,KAAK,MAAM,IAAI,OAAO;AACxC,MAAI,CAAC,aAAa;AAChB,iCAAc,IAAI,KAAK;AACvB,QAAK,MAAM,IAAI,QAAQ,YAAY;;AAErC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,aAAa,CACrD,aAAY,IAAI,KAAK,MAAM;;;;;CAO/B,MAAM,IACJ,QACA,cACe;AACf,OAAK,MAAM,IAAI,QAAQ,IAAI,IAAI,OAAO,QAAQ,aAAa,CAAC,CAAC;;CAG/D,MAAM,IAAI,QAAsC;AAC9C,SAAO,KAAK,MAAM,IAAI,OAAO;;CAG/B,MAAM,MAAM,QAAmC;AAC7C,OAAK,MAAM,OAAO,OAAO;;CAG3B,MAAM,WAA0B;AAC9B,OAAK,MAAM,OAAO"}
|
|
@@ -14,23 +14,17 @@ let ai = require("ai");
|
|
|
14
14
|
*/
|
|
15
15
|
var PluralizationService = class {
|
|
16
16
|
languageModel;
|
|
17
|
-
modelName;
|
|
18
17
|
cache = /* @__PURE__ */ new Map();
|
|
19
18
|
prompt;
|
|
20
19
|
sourceLocale;
|
|
21
20
|
constructor(config, logger) {
|
|
22
21
|
this.logger = logger;
|
|
23
22
|
const localeModel = require_model_factory.parseModelString(config.model);
|
|
24
|
-
if (!localeModel) throw new Error(`Invalid model format: "${config.model}"`);
|
|
25
|
-
|
|
26
|
-
this.logger.info("Validating API keys for pluralization...");
|
|
27
|
-
const validatedKeys = require_model_factory.validateAndGetApiKeys(modelsConfig);
|
|
28
|
-
this.logger.info("✅ API keys validated for pluralization");
|
|
29
|
-
this.languageModel = require_model_factory.createAiModel(localeModel, validatedKeys);
|
|
30
|
-
this.modelName = `${localeModel.provider}:${localeModel.name}`;
|
|
23
|
+
if (!localeModel) throw new Error(`Invalid model format in pluralization service: "${config.model}"`);
|
|
24
|
+
this.languageModel = require_model_factory.createAiModel(localeModel, require_model_factory.validateAndGetApiKeys({ "*:*": config.model }));
|
|
31
25
|
this.sourceLocale = config.sourceLocale;
|
|
32
26
|
this.prompt = require_prompt.getSystemPrompt({ sourceLocale: config.sourceLocale });
|
|
33
|
-
this.logger.
|
|
27
|
+
this.logger.debug(`Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`);
|
|
34
28
|
}
|
|
35
29
|
/**
|
|
36
30
|
* Generate ICU formats for multiple candidates in a single batch
|
|
@@ -40,23 +34,20 @@ var PluralizationService = class {
|
|
|
40
34
|
* @returns Map of hash -> ICU generation result
|
|
41
35
|
*/
|
|
42
36
|
async generateBatch(candidates, batchSize = 10) {
|
|
43
|
-
const results =
|
|
44
|
-
const uncachedCandidates = candidates.filter((c) => {
|
|
37
|
+
const { uncachedCandidates, results } = candidates.reduce((acc, c) => {
|
|
45
38
|
const cached = this.cache.get(c.hash);
|
|
46
|
-
if (cached)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
if (cached) acc.results.set(c.hash, cached);
|
|
40
|
+
else acc.uncachedCandidates.push(c);
|
|
41
|
+
return acc;
|
|
42
|
+
}, {
|
|
43
|
+
uncachedCandidates: [],
|
|
44
|
+
results: /* @__PURE__ */ new Map()
|
|
51
45
|
});
|
|
52
|
-
if (uncachedCandidates.length === 0)
|
|
53
|
-
|
|
54
|
-
return results;
|
|
55
|
-
}
|
|
56
|
-
this.logger.info(`Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`);
|
|
46
|
+
if (uncachedCandidates.length === 0) return results;
|
|
47
|
+
this.logger.debug(`Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`);
|
|
57
48
|
for (let i = 0; i < uncachedCandidates.length; i += batchSize) {
|
|
58
49
|
const batch = uncachedCandidates.slice(i, i + batchSize);
|
|
59
|
-
this.logger.
|
|
50
|
+
this.logger.debug(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`);
|
|
60
51
|
const batchResults = await this.processBatch(batch);
|
|
61
52
|
for (const [hash, result] of batchResults) {
|
|
62
53
|
results.set(hash, result);
|
|
@@ -98,7 +89,7 @@ var PluralizationService = class {
|
|
|
98
89
|
content: require_parse_xml.obj2xml(batchRequest)
|
|
99
90
|
}
|
|
100
91
|
]
|
|
101
|
-
}), require_timeout.DEFAULT_TIMEOUTS.AI_API * 2, `Pluralization with ${this.
|
|
92
|
+
}), require_timeout.DEFAULT_TIMEOUTS.AI_API * 2, `Pluralization with ${this.languageModel}`)).text.trim();
|
|
102
93
|
this.logger.debug(`LLM XML response: ${responseText.substring(0, 200)}...`);
|
|
103
94
|
const parsed = require_parse_xml.parseXmlFromResponseText(responseText);
|
|
104
95
|
const resultArray = Array.isArray(parsed.results.result) ? parsed.results.result : [parsed.results.result];
|
|
@@ -124,7 +115,7 @@ var PluralizationService = class {
|
|
|
124
115
|
}
|
|
125
116
|
}
|
|
126
117
|
for (const candidate of candidates) if (!results.has(candidate.hash)) {
|
|
127
|
-
this.logger.warn(`No result returned for candidate: ${candidate.sourceText}`);
|
|
118
|
+
this.logger.warn(`No result returned for a candidate: ${candidate.sourceText}`);
|
|
128
119
|
results.set(candidate.hash, {
|
|
129
120
|
success: false,
|
|
130
121
|
error: "No result returned by LLM"
|
|
@@ -164,9 +155,9 @@ var PluralizationService = class {
|
|
|
164
155
|
failed: 0,
|
|
165
156
|
durationMs: 0
|
|
166
157
|
};
|
|
167
|
-
this.logger.
|
|
158
|
+
this.logger.debug(`Starting pluralization processing for ${totalEntries} entries`);
|
|
168
159
|
const candidates = require_pattern_detector.detectPluralCandidates(Object.fromEntries(Object.entries(metadata.entries).map(([hash, entry]) => [hash, entry.sourceText])), this.logger);
|
|
169
|
-
this.logger.
|
|
160
|
+
this.logger.debug(`Found ${candidates.length} plural candidates (${(candidates.length / totalEntries * 100).toFixed(1)}%)`);
|
|
170
161
|
if (candidates.length === 0) return {
|
|
171
162
|
total: totalEntries,
|
|
172
163
|
candidates: 0,
|
|
@@ -210,12 +201,12 @@ var PluralizationService = class {
|
|
|
210
201
|
failed++;
|
|
211
202
|
continue;
|
|
212
203
|
}
|
|
213
|
-
this.logger.
|
|
204
|
+
this.logger.debug(`Pluralizing: "${entry.sourceText}" -> "${result.icuText}"`);
|
|
214
205
|
entry.sourceText = result.icuText;
|
|
215
206
|
pluralized++;
|
|
216
207
|
}
|
|
217
208
|
const duration = performance.now() - startTime;
|
|
218
|
-
this.logger.
|
|
209
|
+
this.logger.debug(`Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`);
|
|
219
210
|
return {
|
|
220
211
|
total: totalEntries,
|
|
221
212
|
candidates: candidates.length,
|
|
@@ -225,22 +216,6 @@ var PluralizationService = class {
|
|
|
225
216
|
durationMs: duration
|
|
226
217
|
};
|
|
227
218
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Clear the cache
|
|
230
|
-
*/
|
|
231
|
-
clearCache() {
|
|
232
|
-
this.cache.clear();
|
|
233
|
-
this.logger.debug("Pluralization cache cleared");
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Get cache statistics
|
|
237
|
-
*/
|
|
238
|
-
getCacheStats() {
|
|
239
|
-
return {
|
|
240
|
-
size: this.cache.size,
|
|
241
|
-
hits: 0
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
219
|
};
|
|
245
220
|
|
|
246
221
|
//#endregion
|
|
@@ -13,23 +13,17 @@ import { generateText } from "ai";
|
|
|
13
13
|
*/
|
|
14
14
|
var PluralizationService = class {
|
|
15
15
|
languageModel;
|
|
16
|
-
modelName;
|
|
17
16
|
cache = /* @__PURE__ */ new Map();
|
|
18
17
|
prompt;
|
|
19
18
|
sourceLocale;
|
|
20
19
|
constructor(config, logger) {
|
|
21
20
|
this.logger = logger;
|
|
22
21
|
const localeModel = parseModelString(config.model);
|
|
23
|
-
if (!localeModel) throw new Error(`Invalid model format: "${config.model}"`);
|
|
24
|
-
|
|
25
|
-
this.logger.info("Validating API keys for pluralization...");
|
|
26
|
-
const validatedKeys = validateAndGetApiKeys(modelsConfig);
|
|
27
|
-
this.logger.info("✅ API keys validated for pluralization");
|
|
28
|
-
this.languageModel = createAiModel(localeModel, validatedKeys);
|
|
29
|
-
this.modelName = `${localeModel.provider}:${localeModel.name}`;
|
|
22
|
+
if (!localeModel) throw new Error(`Invalid model format in pluralization service: "${config.model}"`);
|
|
23
|
+
this.languageModel = createAiModel(localeModel, validateAndGetApiKeys({ "*:*": config.model }));
|
|
30
24
|
this.sourceLocale = config.sourceLocale;
|
|
31
25
|
this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale });
|
|
32
|
-
this.logger.
|
|
26
|
+
this.logger.debug(`Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`);
|
|
33
27
|
}
|
|
34
28
|
/**
|
|
35
29
|
* Generate ICU formats for multiple candidates in a single batch
|
|
@@ -39,23 +33,20 @@ var PluralizationService = class {
|
|
|
39
33
|
* @returns Map of hash -> ICU generation result
|
|
40
34
|
*/
|
|
41
35
|
async generateBatch(candidates, batchSize = 10) {
|
|
42
|
-
const results =
|
|
43
|
-
const uncachedCandidates = candidates.filter((c) => {
|
|
36
|
+
const { uncachedCandidates, results } = candidates.reduce((acc, c) => {
|
|
44
37
|
const cached = this.cache.get(c.hash);
|
|
45
|
-
if (cached)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
if (cached) acc.results.set(c.hash, cached);
|
|
39
|
+
else acc.uncachedCandidates.push(c);
|
|
40
|
+
return acc;
|
|
41
|
+
}, {
|
|
42
|
+
uncachedCandidates: [],
|
|
43
|
+
results: /* @__PURE__ */ new Map()
|
|
50
44
|
});
|
|
51
|
-
if (uncachedCandidates.length === 0)
|
|
52
|
-
|
|
53
|
-
return results;
|
|
54
|
-
}
|
|
55
|
-
this.logger.info(`Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`);
|
|
45
|
+
if (uncachedCandidates.length === 0) return results;
|
|
46
|
+
this.logger.debug(`Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`);
|
|
56
47
|
for (let i = 0; i < uncachedCandidates.length; i += batchSize) {
|
|
57
48
|
const batch = uncachedCandidates.slice(i, i + batchSize);
|
|
58
|
-
this.logger.
|
|
49
|
+
this.logger.debug(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`);
|
|
59
50
|
const batchResults = await this.processBatch(batch);
|
|
60
51
|
for (const [hash, result] of batchResults) {
|
|
61
52
|
results.set(hash, result);
|
|
@@ -97,7 +88,7 @@ var PluralizationService = class {
|
|
|
97
88
|
content: obj2xml(batchRequest)
|
|
98
89
|
}
|
|
99
90
|
]
|
|
100
|
-
}), DEFAULT_TIMEOUTS.AI_API * 2, `Pluralization with ${this.
|
|
91
|
+
}), DEFAULT_TIMEOUTS.AI_API * 2, `Pluralization with ${this.languageModel}`)).text.trim();
|
|
101
92
|
this.logger.debug(`LLM XML response: ${responseText.substring(0, 200)}...`);
|
|
102
93
|
const parsed = parseXmlFromResponseText(responseText);
|
|
103
94
|
const resultArray = Array.isArray(parsed.results.result) ? parsed.results.result : [parsed.results.result];
|
|
@@ -123,7 +114,7 @@ var PluralizationService = class {
|
|
|
123
114
|
}
|
|
124
115
|
}
|
|
125
116
|
for (const candidate of candidates) if (!results.has(candidate.hash)) {
|
|
126
|
-
this.logger.warn(`No result returned for candidate: ${candidate.sourceText}`);
|
|
117
|
+
this.logger.warn(`No result returned for a candidate: ${candidate.sourceText}`);
|
|
127
118
|
results.set(candidate.hash, {
|
|
128
119
|
success: false,
|
|
129
120
|
error: "No result returned by LLM"
|
|
@@ -163,9 +154,9 @@ var PluralizationService = class {
|
|
|
163
154
|
failed: 0,
|
|
164
155
|
durationMs: 0
|
|
165
156
|
};
|
|
166
|
-
this.logger.
|
|
157
|
+
this.logger.debug(`Starting pluralization processing for ${totalEntries} entries`);
|
|
167
158
|
const candidates = detectPluralCandidates(Object.fromEntries(Object.entries(metadata.entries).map(([hash, entry]) => [hash, entry.sourceText])), this.logger);
|
|
168
|
-
this.logger.
|
|
159
|
+
this.logger.debug(`Found ${candidates.length} plural candidates (${(candidates.length / totalEntries * 100).toFixed(1)}%)`);
|
|
169
160
|
if (candidates.length === 0) return {
|
|
170
161
|
total: totalEntries,
|
|
171
162
|
candidates: 0,
|
|
@@ -209,12 +200,12 @@ var PluralizationService = class {
|
|
|
209
200
|
failed++;
|
|
210
201
|
continue;
|
|
211
202
|
}
|
|
212
|
-
this.logger.
|
|
203
|
+
this.logger.debug(`Pluralizing: "${entry.sourceText}" -> "${result.icuText}"`);
|
|
213
204
|
entry.sourceText = result.icuText;
|
|
214
205
|
pluralized++;
|
|
215
206
|
}
|
|
216
207
|
const duration = performance.now() - startTime;
|
|
217
|
-
this.logger.
|
|
208
|
+
this.logger.debug(`Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`);
|
|
218
209
|
return {
|
|
219
210
|
total: totalEntries,
|
|
220
211
|
candidates: candidates.length,
|
|
@@ -224,22 +215,6 @@ var PluralizationService = class {
|
|
|
224
215
|
durationMs: duration
|
|
225
216
|
};
|
|
226
217
|
}
|
|
227
|
-
/**
|
|
228
|
-
* Clear the cache
|
|
229
|
-
*/
|
|
230
|
-
clearCache() {
|
|
231
|
-
this.cache.clear();
|
|
232
|
-
this.logger.debug("Pluralization cache cleared");
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Get cache statistics
|
|
236
|
-
*/
|
|
237
|
-
getCacheStats() {
|
|
238
|
-
return {
|
|
239
|
-
size: this.cache.size,
|
|
240
|
-
hits: 0
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
218
|
};
|
|
244
219
|
|
|
245
220
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["logger: Logger","modelsConfig: Record<string, string>","batchRequest: PluralizationBatch"],"sources":["../../../src/translators/pluralization/service.ts"],"sourcesContent":["/**\n * Pluralization service with batching and caching\n */\n\nimport type { LanguageModel } from \"ai\";\nimport { generateText } from \"ai\";\nimport type {\n ICUGenerationResult,\n PluralCandidate,\n PluralizationBatch,\n PluralizationConfig,\n PluralizationResponse,\n PluralizationStats,\n} from \"./types\";\nimport {\n createAiModel,\n parseModelString,\n validateAndGetApiKeys,\n} from \"../lingo/model-factory\";\nimport { Logger } from \"../../utils/logger\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../../utils/timeout\";\nimport { getSystemPrompt } from \"./prompt\";\nimport { obj2xml, parseXmlFromResponseText } from \"../parse-xml\";\nimport { shots } from \"./shots\";\nimport type { MetadataSchema } from \"../../types\";\nimport { detectPluralCandidates } from \"./pattern-detector\";\nimport { validateICU } from \"./icu-validator\";\n\n/**\n * Pluralization service with batching and model reuse\n */\nexport class PluralizationService {\n private readonly languageModel: LanguageModel;\n private readonly modelName: string;\n private cache = new Map<string, ICUGenerationResult>();\n private readonly prompt: string;\n private readonly sourceLocale: string;\n\n constructor(\n config: PluralizationConfig,\n private logger: Logger,\n ) {\n const localeModel = parseModelString(config.model);\n if (!localeModel) {\n throw new Error(`Invalid model format: \"${config.model}\"`);\n }\n\n // Validate and fetch API keys for the pluralization provider\n // We need to create a models config that validateAndFetchApiKeys can use\n const modelsConfig: Record<string, string> = {\n \"*:*\": config.model, // Single model for pluralization\n };\n\n this.logger.info(\"Validating API keys for pluralization...\");\n const validatedKeys = validateAndGetApiKeys(modelsConfig);\n this.logger.info(\"✅ API keys validated for pluralization\");\n\n this.languageModel = createAiModel(localeModel, validatedKeys);\n this.modelName = `${localeModel.provider}:${localeModel.name}`;\n this.sourceLocale = config.sourceLocale;\n this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale });\n\n this.logger.info(\n `Initialized pluralization service with ${this.modelName}`,\n );\n }\n\n /**\n * Generate ICU formats for multiple candidates in a single batch\n *\n * @param candidates Array of plural candidates\n * @param batchSize Maximum candidates per batch (default: 10)\n * @returns Map of hash -> ICU generation result\n */\n async generateBatch(\n candidates: PluralCandidate[],\n batchSize: number = 10,\n ): Promise<Map<string, ICUGenerationResult>> {\n const results = new Map<string, ICUGenerationResult>();\n\n // Check cache first\n const uncachedCandidates = candidates.filter((c) => {\n const cached = this.cache.get(c.hash);\n if (cached) {\n results.set(c.hash, cached);\n return false;\n }\n return true;\n });\n\n if (uncachedCandidates.length === 0) {\n this.logger.debug(\n `All ${candidates.length} candidates found in cache, skipping LLM call`,\n );\n return results;\n }\n\n this.logger.info(\n `Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`,\n );\n\n // Process in batches\n for (let i = 0; i < uncachedCandidates.length; i += batchSize) {\n const batch = uncachedCandidates.slice(i, i + batchSize);\n\n this.logger.info(\n `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`,\n );\n\n const batchResults = await this.processBatch(batch);\n\n // Store results and cache them\n for (const [hash, result] of batchResults) {\n results.set(hash, result);\n this.cache.set(hash, result);\n }\n }\n\n return results;\n }\n\n /**\n * Process a single batch of candidates\n */\n private async processBatch(\n candidates: PluralCandidate[],\n ): Promise<Map<string, ICUGenerationResult>> {\n const results = new Map<string, ICUGenerationResult>();\n\n try {\n // Prepare batch request in XML format\n const batchRequest: PluralizationBatch = {\n version: 0.1,\n sourceLocale: this.sourceLocale,\n candidates: {\n candidate: candidates.map((c) => ({\n hash: c.hash,\n text: c.sourceText,\n })),\n },\n };\n\n // Call LLM with XML format and few-shot examples\n const response = await withTimeout(\n generateText({\n model: this.languageModel,\n messages: [\n {\n role: \"system\",\n content: this.prompt,\n },\n // Add few-shot examples\n ...shots.flatMap((shotsTuple) => [\n {\n role: \"user\" as const,\n content: obj2xml(shotsTuple[0]),\n },\n {\n role: \"assistant\" as const,\n content: obj2xml(shotsTuple[1]),\n },\n ]),\n {\n role: \"user\",\n content: obj2xml(batchRequest),\n },\n ],\n }),\n DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch\n `Pluralization with ${this.modelName}`,\n );\n\n const responseText = response.text.trim();\n this.logger.debug(\n `LLM XML response: ${responseText.substring(0, 200)}...`,\n );\n // Parse XML response\n const parsed =\n parseXmlFromResponseText<PluralizationResponse>(responseText);\n\n // Process results\n const resultArray = Array.isArray(parsed.results.result)\n ? parsed.results.result\n : [parsed.results.result];\n\n for (const result of resultArray) {\n const candidate = candidates.find((c) => c.hash === result.hash);\n if (!candidate) {\n this.logger.warn(`No candidate found for hash: ${result.hash}`);\n continue;\n }\n\n if (result.shouldPluralize && result.icuText) {\n this.logger.debug(\n `✓ ICU format generated for \"${candidate.sourceText}\": \"${result.icuText}\"`,\n );\n results.set(result.hash, {\n success: true,\n icuText: result.icuText,\n reasoning: result.reasoning,\n });\n } else {\n this.logger.debug(\n `✗ Pluralization not appropriate for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n results.set(result.hash, {\n success: false,\n reasoning: result.reasoning,\n });\n }\n }\n\n // Handle missing results (LLM didn't return result for some candidates)\n for (const candidate of candidates) {\n if (!results.has(candidate.hash)) {\n this.logger.warn(\n `No result returned for candidate: ${candidate.sourceText}`,\n );\n results.set(candidate.hash, {\n success: false,\n error: \"No result returned by LLM\",\n });\n }\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"Unknown error\";\n this.logger.error(`Failed to process batch: ${errorMsg}`);\n\n // Mark all candidates as failed\n for (const candidate of candidates) {\n results.set(candidate.hash, {\n success: false,\n error: errorMsg,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Process metadata entries for pluralization\n *\n * This is the main entry point that:\n * 1. Detects plural candidates using pattern matching\n * 2. Generates ICU format using LLM (batched)\n * 3. Validates the ICU format\n * 4. Updates metadata entries in-place (modifies sourceText)\n * 5. Returns statistics\n * @param metadata Metadata schema with translation entries\n\n * @returns Statistics about the pluralization process\n */\n async process(metadata: MetadataSchema): Promise<PluralizationStats> {\n const startTime = performance.now();\n const totalEntries = Object.keys(metadata.entries).length;\n\n if (totalEntries === 0) {\n return {\n total: 0,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: 0,\n };\n }\n\n this.logger.info(\n `Starting pluralization processing for ${totalEntries} entries`,\n );\n\n // Step 1: Detect plural candidates using pattern matching\n const entriesMap: Record<string, string> = Object.fromEntries(\n Object.entries(metadata.entries).map(([hash, entry]) => [\n hash,\n entry.sourceText,\n ]),\n );\n\n const candidates = detectPluralCandidates(entriesMap, this.logger);\n\n this.logger.info(\n `Found ${candidates.length} plural candidates (${((candidates.length / totalEntries) * 100).toFixed(1)}%)`,\n );\n\n if (candidates.length === 0) {\n const endTime = performance.now();\n return {\n total: totalEntries,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: endTime - startTime,\n };\n }\n\n // Step 2: Generate ICU formats with batching\n this.logger.debug(\"Generating ICU formats with batching...\");\n const icuResults = await this.generateBatch(candidates, 10);\n\n // Step 3: Validate and update metadata entries\n this.logger.debug(\"Validating and updating entries...\");\n let pluralized = 0;\n let rejected = 0;\n let failed = 0;\n\n for (const candidate of candidates) {\n const result = icuResults.get(candidate.hash);\n const entry = metadata.entries[candidate.hash];\n this.logger.debug(`Processing candidate: ${candidate.sourceText}`);\n if (!entry) {\n this.logger.warn(`Entry not found for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (!result) {\n this.logger.warn(`No result for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (result.error) {\n this.logger.warn(\n `Error generating ICU for \"${candidate.sourceText}\": ${result.error}`,\n );\n failed++;\n continue;\n }\n\n if (!result.success || !result.icuText) {\n this.logger.debug(\n `Rejected pluralization for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n rejected++;\n continue;\n }\n\n const isValid = validateICU(\n result.icuText,\n candidate.sourceText,\n this.logger,\n );\n\n if (!isValid) {\n this.logger.warn(\n `Invalid ICU format generated for \"${candidate.sourceText}\", falling back to original`,\n );\n failed++;\n continue;\n }\n\n // Update metadata entry in-place\n this.logger.info(\n `Pluralizing: \"${entry.sourceText}\" -> \"${result.icuText}\"`,\n );\n entry.sourceText = result.icuText;\n pluralized++;\n }\n\n const endTime = performance.now();\n const duration = endTime - startTime;\n\n this.logger.info(\n `Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`,\n );\n\n return {\n total: totalEntries,\n candidates: candidates.length,\n pluralized,\n rejected,\n failed,\n durationMs: duration,\n };\n }\n\n /**\n * Clear the cache\n */\n clearCache(): void {\n this.cache.clear();\n this.logger.debug(\"Pluralization cache cleared\");\n }\n\n /**\n * Get cache statistics\n */\n getCacheStats(): { size: number; hits: number } {\n return {\n size: this.cache.size,\n hits: 0, // We don't track hits currently\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["logger: Logger","batchRequest: PluralizationBatch"],"sources":["../../../src/translators/pluralization/service.ts"],"sourcesContent":["/**\n * Pluralization service with batching and caching\n */\n\nimport type { LanguageModel } from \"ai\";\nimport { generateText } from \"ai\";\nimport type {\n ICUGenerationResult,\n PluralCandidate,\n PluralizationBatch,\n PluralizationConfig,\n PluralizationResponse,\n PluralizationStats,\n} from \"./types\";\nimport {\n createAiModel,\n parseModelString,\n validateAndGetApiKeys,\n} from \"../lingo/model-factory\";\nimport { Logger } from \"../../utils/logger\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../../utils/timeout\";\nimport { getSystemPrompt } from \"./prompt\";\nimport { obj2xml, parseXmlFromResponseText } from \"../parse-xml\";\nimport { shots } from \"./shots\";\nimport type { MetadataSchema } from \"../../types\";\nimport { detectPluralCandidates } from \"./pattern-detector\";\nimport { validateICU } from \"./icu-validator\";\n\n/**\n * Pluralization service with batching and model reuse\n */\nexport class PluralizationService {\n private readonly languageModel: LanguageModel;\n private cache = new Map<string, ICUGenerationResult>();\n private readonly prompt: string;\n private readonly sourceLocale: string;\n\n constructor(\n config: PluralizationConfig,\n private logger: Logger,\n ) {\n const localeModel = parseModelString(config.model);\n if (!localeModel) {\n throw new Error(`Invalid model format in pluralization service: \"${config.model}\"`);\n }\n\n // Validate and fetch API keys for the pluralization provider\n // We need to create a models config that validateAndFetchApiKeys can use\n const modelsConfig: Record<string, string> = {\n \"*:*\": config.model,\n };\n\n const validatedKeys = validateAndGetApiKeys(modelsConfig);\n\n this.languageModel = createAiModel(localeModel, validatedKeys);\n this.sourceLocale = config.sourceLocale;\n this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale });\n\n this.logger.debug(\n `Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`,\n );\n }\n\n /**\n * Generate ICU formats for multiple candidates in a single batch\n *\n * @param candidates Array of plural candidates\n * @param batchSize Maximum candidates per batch (default: 10)\n * @returns Map of hash -> ICU generation result\n */\n async generateBatch(\n candidates: PluralCandidate[],\n batchSize: number = 10,\n ): Promise<Map<string, ICUGenerationResult>> {\n const { uncachedCandidates, results } = candidates.reduce(\n (acc, c) => {\n const cached = this.cache.get(c.hash);\n if (cached) {\n acc.results.set(c.hash, cached);\n } else {\n acc.uncachedCandidates.push(c);\n }\n return acc;\n },\n {\n uncachedCandidates: [] as PluralCandidate[],\n results: new Map<string, ICUGenerationResult>(),\n },\n );\n\n if (uncachedCandidates.length === 0) {\n return results;\n }\n\n this.logger.debug(\n `Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`,\n );\n\n // Process in batches\n for (let i = 0; i < uncachedCandidates.length; i += batchSize) {\n const batch = uncachedCandidates.slice(i, i + batchSize);\n\n this.logger.debug(\n `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`,\n );\n\n const batchResults = await this.processBatch(batch);\n\n // Store results and cache them\n for (const [hash, result] of batchResults) {\n results.set(hash, result);\n this.cache.set(hash, result);\n }\n }\n\n return results;\n }\n\n /**\n * Process a single batch of candidates\n */\n private async processBatch(\n candidates: PluralCandidate[],\n ): Promise<Map<string, ICUGenerationResult>> {\n const results = new Map<string, ICUGenerationResult>();\n\n try {\n // Prepare batch request in XML format\n const batchRequest: PluralizationBatch = {\n version: 0.1,\n sourceLocale: this.sourceLocale,\n candidates: {\n candidate: candidates.map((c) => ({\n hash: c.hash,\n text: c.sourceText,\n })),\n },\n };\n\n // Call LLM with XML format and few-shot examples\n const response = await withTimeout(\n generateText({\n model: this.languageModel,\n messages: [\n {\n role: \"system\",\n content: this.prompt,\n },\n // Add few-shot examples\n ...shots.flatMap((shotsTuple) => [\n {\n role: \"user\" as const,\n content: obj2xml(shotsTuple[0]),\n },\n {\n role: \"assistant\" as const,\n content: obj2xml(shotsTuple[1]),\n },\n ]),\n {\n role: \"user\",\n content: obj2xml(batchRequest),\n },\n ],\n }),\n DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch\n `Pluralization with ${this.languageModel}`,\n );\n\n const responseText = response.text.trim();\n this.logger.debug(\n `LLM XML response: ${responseText.substring(0, 200)}...`,\n );\n // Parse XML response\n const parsed =\n parseXmlFromResponseText<PluralizationResponse>(responseText);\n\n // Process results\n const resultArray = Array.isArray(parsed.results.result)\n ? parsed.results.result\n : [parsed.results.result];\n\n for (const result of resultArray) {\n const candidate = candidates.find((c) => c.hash === result.hash);\n if (!candidate) {\n this.logger.warn(`No candidate found for hash: ${result.hash}`);\n continue;\n }\n\n if (result.shouldPluralize && result.icuText) {\n this.logger.debug(\n `✓ ICU format generated for \"${candidate.sourceText}\": \"${result.icuText}\"`,\n );\n results.set(result.hash, {\n success: true,\n icuText: result.icuText,\n reasoning: result.reasoning,\n });\n } else {\n this.logger.debug(\n `✗ Pluralization not appropriate for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n results.set(result.hash, {\n success: false,\n reasoning: result.reasoning,\n });\n }\n }\n\n // Handle missing results (LLM didn't return result for some candidates)\n for (const candidate of candidates) {\n if (!results.has(candidate.hash)) {\n this.logger.warn(\n `No result returned for a candidate: ${candidate.sourceText}`,\n );\n results.set(candidate.hash, {\n success: false,\n error: \"No result returned by LLM\",\n });\n }\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"Unknown error\";\n this.logger.error(`Failed to process batch: ${errorMsg}`);\n\n // Mark all candidates as failed\n for (const candidate of candidates) {\n results.set(candidate.hash, {\n success: false,\n error: errorMsg,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Process metadata entries for pluralization\n *\n * This is the main entry point that:\n * 1. Detects plural candidates using pattern matching\n * 2. Generates ICU format using LLM (batched)\n * 3. Validates the ICU format\n * 4. Updates metadata entries in-place (modifies sourceText)\n * 5. Returns statistics\n * @param metadata Metadata schema with translation entries\n\n * @returns Statistics about the pluralization process\n */\n async process(metadata: MetadataSchema): Promise<PluralizationStats> {\n const startTime = performance.now();\n const totalEntries = Object.keys(metadata.entries).length;\n\n if (totalEntries === 0) {\n return {\n total: 0,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: 0,\n };\n }\n\n this.logger.debug(\n `Starting pluralization processing for ${totalEntries} entries`,\n );\n\n // Step 1: Detect plural candidates using pattern matching\n const entriesMap: Record<string, string> = Object.fromEntries(\n Object.entries(metadata.entries).map(([hash, entry]) => [\n hash,\n entry.sourceText,\n ]),\n );\n\n const candidates = detectPluralCandidates(entriesMap, this.logger);\n\n this.logger.debug(\n `Found ${candidates.length} plural candidates (${((candidates.length / totalEntries) * 100).toFixed(1)}%)`,\n );\n\n if (candidates.length === 0) {\n const endTime = performance.now();\n return {\n total: totalEntries,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: endTime - startTime,\n };\n }\n\n // Step 2: Generate ICU formats with batching\n this.logger.debug(\"Generating ICU formats with batching...\");\n const icuResults = await this.generateBatch(candidates, 10);\n\n // Step 3: Validate and update metadata entries\n this.logger.debug(\"Validating and updating entries...\");\n let pluralized = 0;\n let rejected = 0;\n let failed = 0;\n\n for (const candidate of candidates) {\n const result = icuResults.get(candidate.hash);\n const entry = metadata.entries[candidate.hash];\n this.logger.debug(`Processing candidate: ${candidate.sourceText}`);\n if (!entry) {\n this.logger.warn(`Entry not found for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (!result) {\n this.logger.warn(`No result for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (result.error) {\n this.logger.warn(\n `Error generating ICU for \"${candidate.sourceText}\": ${result.error}`,\n );\n failed++;\n continue;\n }\n\n if (!result.success || !result.icuText) {\n this.logger.debug(\n `Rejected pluralization for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n rejected++;\n continue;\n }\n\n const isValid = validateICU(\n result.icuText,\n candidate.sourceText,\n this.logger,\n );\n\n if (!isValid) {\n this.logger.warn(\n `Invalid ICU format generated for \"${candidate.sourceText}\", falling back to original`,\n );\n failed++;\n continue;\n }\n\n this.logger.debug(\n `Pluralizing: \"${entry.sourceText}\" -> \"${result.icuText}\"`,\n );\n entry.sourceText = result.icuText;\n pluralized++;\n }\n\n const endTime = performance.now();\n const duration = endTime - startTime;\n\n this.logger.debug(\n `Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`,\n );\n\n return {\n total: totalEntries,\n candidates: candidates.length,\n pluralized,\n rejected,\n failed,\n durationMs: duration,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -10,22 +10,14 @@ var PseudoTranslator = class {
|
|
|
10
10
|
this.logger = logger;
|
|
11
11
|
}
|
|
12
12
|
translate(locale, entries) {
|
|
13
|
-
this.logger.debug(`[TRACE-PSEUDO] translate() ENTERED for ${locale} with ${Object.keys(entries).length} entries`);
|
|
14
13
|
const delay = this.config?.delayMedian ?? 0;
|
|
15
14
|
const actualDelay = this.getRandomDelay(delay);
|
|
16
|
-
this.logger.debug(`[TRACE-PSEUDO] Config delay: ${delay}ms, actual delay: ${actualDelay}ms`);
|
|
17
15
|
return new Promise((resolve) => {
|
|
18
|
-
this.logger.debug(`[TRACE-PSEUDO] Promise created, scheduling setTimeout for ${actualDelay}ms`);
|
|
19
16
|
setTimeout(() => {
|
|
20
|
-
|
|
21
|
-
const result = Object.fromEntries(Object.entries(entries).map(([hash, entry]) => {
|
|
17
|
+
resolve(Object.fromEntries(Object.entries(entries).map(([hash, entry]) => {
|
|
22
18
|
return [hash, `${locale}/${pseudolocalize(entry.text)}`];
|
|
23
|
-
}));
|
|
24
|
-
this.logger.debug(`[TRACE-PSEUDO] Pseudolocalization complete, resolving with ${Object.keys(result).length} translations`);
|
|
25
|
-
resolve(result);
|
|
26
|
-
this.logger.debug(`[TRACE-PSEUDO] Promise resolved for ${locale}`);
|
|
19
|
+
})));
|
|
27
20
|
}, actualDelay);
|
|
28
|
-
this.logger.debug(`[TRACE-PSEUDO] setTimeout scheduled, returning promise`);
|
|
29
21
|
});
|
|
30
22
|
}
|
|
31
23
|
getRandomDelay(median) {
|
|
@@ -9,22 +9,14 @@ var PseudoTranslator = class {
|
|
|
9
9
|
this.logger = logger;
|
|
10
10
|
}
|
|
11
11
|
translate(locale, entries) {
|
|
12
|
-
this.logger.debug(`[TRACE-PSEUDO] translate() ENTERED for ${locale} with ${Object.keys(entries).length} entries`);
|
|
13
12
|
const delay = this.config?.delayMedian ?? 0;
|
|
14
13
|
const actualDelay = this.getRandomDelay(delay);
|
|
15
|
-
this.logger.debug(`[TRACE-PSEUDO] Config delay: ${delay}ms, actual delay: ${actualDelay}ms`);
|
|
16
14
|
return new Promise((resolve) => {
|
|
17
|
-
this.logger.debug(`[TRACE-PSEUDO] Promise created, scheduling setTimeout for ${actualDelay}ms`);
|
|
18
15
|
setTimeout(() => {
|
|
19
|
-
|
|
20
|
-
const result = Object.fromEntries(Object.entries(entries).map(([hash, entry]) => {
|
|
16
|
+
resolve(Object.fromEntries(Object.entries(entries).map(([hash, entry]) => {
|
|
21
17
|
return [hash, `${locale}/${pseudolocalize(entry.text)}`];
|
|
22
|
-
}));
|
|
23
|
-
this.logger.debug(`[TRACE-PSEUDO] Pseudolocalization complete, resolving with ${Object.keys(result).length} translations`);
|
|
24
|
-
resolve(result);
|
|
25
|
-
this.logger.debug(`[TRACE-PSEUDO] Promise resolved for ${locale}`);
|
|
18
|
+
})));
|
|
26
19
|
}, actualDelay);
|
|
27
|
-
this.logger.debug(`[TRACE-PSEUDO] setTimeout scheduled, returning promise`);
|
|
28
20
|
});
|
|
29
21
|
}
|
|
30
22
|
getRandomDelay(median) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["config: PseudoTranslatorConfig","logger: Logger","PSEUDO_MAP: Record<string, string>","parts: Array<{ text: string; preserve: boolean }>","match: RegExpExecArray | null"],"sources":["../../../src/translators/pseudotranslator/index.ts"],"sourcesContent":["/**\n * Pseudotranslator for testing without actual translation APIs\n */\n\nimport type { TranslatableEntry, Translator } from \"../api\";\nimport { Logger } from \"../../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\nexport interface PseudoTranslatorConfig {\n delayMedian?: number;\n}\n\n/**\n * Pseudo-translator that uses pseudolocalization\n * Useful for testing i18n without actual translation APIs\n */\nexport class PseudoTranslator implements Translator<PseudoTranslatorConfig> {\n constructor(\n readonly config: PseudoTranslatorConfig,\n private readonly logger: Logger,\n ) {}\n\n translate(locale: LocaleCode, entries: Record<string, TranslatableEntry>) {\n
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["config: PseudoTranslatorConfig","logger: Logger","PSEUDO_MAP: Record<string, string>","parts: Array<{ text: string; preserve: boolean }>","match: RegExpExecArray | null"],"sources":["../../../src/translators/pseudotranslator/index.ts"],"sourcesContent":["/**\n * Pseudotranslator for testing without actual translation APIs\n */\n\nimport type { TranslatableEntry, Translator } from \"../api\";\nimport { Logger } from \"../../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\nexport interface PseudoTranslatorConfig {\n delayMedian?: number;\n}\n\n/**\n * Pseudo-translator that uses pseudolocalization\n * Useful for testing i18n without actual translation APIs\n */\nexport class PseudoTranslator implements Translator<PseudoTranslatorConfig> {\n constructor(\n readonly config: PseudoTranslatorConfig,\n private readonly logger: Logger,\n ) {}\n\n translate(locale: LocaleCode, entries: Record<string, TranslatableEntry>) {\n const delay = this.config?.delayMedian ?? 0;\n const actualDelay = this.getRandomDelay(delay);\n\n return new Promise<Record<string, string>>((resolve) => {\n setTimeout(() => {\n\n const result = Object.fromEntries(\n Object.entries(entries).map(([hash, entry]) => {\n return [hash, `${locale}/${pseudolocalize(entry.text)}`];\n }),\n );\n\n resolve(result);\n }, actualDelay);\n });\n }\n\n private getRandomDelay(median: number): number {\n if (median === 0) return 0;\n // Generate random delay with distribution around median\n // Use a simple approach: median ± 50%\n const min = median * 0.5;\n const max = median * 1.5;\n return Math.floor(Math.random() * (max - min + 1)) + min;\n }\n}\n\n/**\n * Character map for pseudolocalization\n */\nconst PSEUDO_MAP: Record<string, string> = {\n a: \"á\",\n b: \"ḅ\",\n c: \"ç\",\n d: \"ḍ\",\n e: \"é\",\n f: \"ƒ\",\n g: \"ĝ\",\n h: \"ĥ\",\n i: \"í\",\n j: \"ĵ\",\n k: \"ḳ\",\n l: \"ĺ\",\n m: \"ṁ\",\n n: \"ñ\",\n o: \"ó\",\n p: \"ṗ\",\n q: \"ɋ\",\n r: \"ŕ\",\n s: \"ś\",\n t: \"ţ\",\n u: \"ú\",\n v: \"ṿ\",\n w: \"ŵ\",\n x: \"ẋ\",\n y: \"ý\",\n z: \"ẑ\",\n A: \"Á\",\n B: \"Ḅ\",\n C: \"Ç\",\n D: \"Ḍ\",\n E: \"É\",\n F: \"Ƒ\",\n G: \"Ĝ\",\n H: \"Ĥ\",\n I: \"Í\",\n J: \"Ĵ\",\n K: \"Ḳ\",\n L: \"Ĺ\",\n M: \"Ṁ\",\n N: \"Ñ\",\n O: \"Ó\",\n P: \"Ṗ\",\n Q: \"Ɋ\",\n R: \"Ŕ\",\n S: \"Ś\",\n T: \"Ţ\",\n U: \"Ú\",\n V: \"Ṿ\",\n W: \"Ŵ\",\n X: \"Ẋ\",\n Y: \"Ý\",\n Z: \"Ẑ\",\n};\n\n/**\n * Pseudolocalize a string\n * Adds brackets, expands length by ~30%, and uses accented characters\n * Preserves variable placeholders {name} and component tags <a0>, </a0>\n */\nexport function pseudolocalize(text: string): string {\n // Don't pseudolocalize if it's just whitespace or a variable placeholder\n if (!text.trim() || text.match(/^{.*}$/)) {\n return text;\n }\n\n // Regular expression to match patterns we should NOT translate:\n // - Variable placeholders: {varName}\n // - Component tags: <tagName> or </tagName>\n const preserveRegex = /(\\{\\w+}|<\\/?\\w+\\/?>)/g;\n\n // Split text into parts that should be preserved and parts that should be translated\n const parts: Array<{ text: string; preserve: boolean }> = [];\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = preserveRegex.exec(text)) !== null) {\n // Add text before the match (to be translated)\n if (match.index > lastIndex) {\n parts.push({\n text: text.substring(lastIndex, match.index),\n preserve: false,\n });\n }\n\n // Add the matched pattern (to be preserved)\n parts.push({\n text: match[0],\n preserve: true,\n });\n\n lastIndex = preserveRegex.lastIndex;\n }\n\n // Add remaining text (to be translated)\n if (lastIndex < text.length) {\n parts.push({\n text: text.substring(lastIndex),\n preserve: false,\n });\n }\n\n // Convert characters in translatable parts only\n let result = \"\";\n for (const part of parts) {\n if (part.preserve) {\n // Keep placeholders and tags as-is\n result += part.text;\n } else {\n // Pseudolocalize the text\n for (const char of part.text) {\n result += PSEUDO_MAP[char] || char;\n }\n }\n }\n\n // Add padding to simulate longer translations (~30% longer)\n const padding = \" \".repeat(Math.ceil(text.length * 0.3));\n\n // Wrap in brackets to identify translated strings\n return `${result}${padding}`;\n}\n"],"mappings":";;;;;AAgBA,IAAa,mBAAb,MAA4E;CAC1E,YACE,AAASA,QACT,AAAiBC,QACjB;EAFS;EACQ;;CAGnB,UAAU,QAAoB,SAA4C;EACxE,MAAM,QAAQ,KAAK,QAAQ,eAAe;EAC1C,MAAM,cAAc,KAAK,eAAe,MAAM;AAE9C,SAAO,IAAI,SAAiC,YAAY;AACtD,oBAAiB;AAQf,YANe,OAAO,YACpB,OAAO,QAAQ,QAAQ,CAAC,KAAK,CAAC,MAAM,WAAW;AAC7C,YAAO,CAAC,MAAM,GAAG,OAAO,GAAG,eAAe,MAAM,KAAK,GAAG;MACxD,CACH,CAEc;MACd,YAAY;IACf;;CAGJ,AAAQ,eAAe,QAAwB;AAC7C,MAAI,WAAW,EAAG,QAAO;EAGzB,MAAM,MAAM,SAAS;EACrB,MAAM,MAAM,SAAS;AACrB,SAAO,KAAK,MAAM,KAAK,QAAQ,IAAI,MAAM,MAAM,GAAG,GAAG;;;;;;AAOzD,MAAMC,aAAqC;CACzC,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACJ;;;;;;AAOD,SAAgB,eAAe,MAAsB;AAEnD,KAAI,CAAC,KAAK,MAAM,IAAI,KAAK,MAAM,SAAS,CACtC,QAAO;CAMT,MAAM,gBAAgB;CAGtB,MAAMC,QAAoD,EAAE;CAC5D,IAAI,YAAY;CAChB,IAAIC;AAEJ,SAAQ,QAAQ,cAAc,KAAK,KAAK,MAAM,MAAM;AAElD,MAAI,MAAM,QAAQ,UAChB,OAAM,KAAK;GACT,MAAM,KAAK,UAAU,WAAW,MAAM,MAAM;GAC5C,UAAU;GACX,CAAC;AAIJ,QAAM,KAAK;GACT,MAAM,MAAM;GACZ,UAAU;GACX,CAAC;AAEF,cAAY,cAAc;;AAI5B,KAAI,YAAY,KAAK,OACnB,OAAM,KAAK;EACT,MAAM,KAAK,UAAU,UAAU;EAC/B,UAAU;EACX,CAAC;CAIJ,IAAI,SAAS;AACb,MAAK,MAAM,QAAQ,MACjB,KAAI,KAAK,SAEP,WAAU,KAAK;KAGf,MAAK,MAAM,QAAQ,KAAK,KACtB,WAAU,WAAW,SAAS;CAMpC,MAAM,UAAU,IAAI,OAAO,KAAK,KAAK,KAAK,SAAS,GAAI,CAAC;AAGxD,QAAO,GAAG,SAAS"}
|