@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.
Files changed (50) hide show
  1. package/README.md +273 -2
  2. package/build/metadata/manager.mjs +12 -12
  3. package/build/metadata/manager.mjs.map +1 -1
  4. package/build/plugin/build-translator.cjs +3 -3
  5. package/build/plugin/build-translator.mjs +6 -6
  6. package/build/plugin/build-translator.mjs.map +1 -1
  7. package/build/plugin/next.cjs +3 -3
  8. package/build/plugin/next.d.cts.map +1 -1
  9. package/build/plugin/next.d.mts.map +1 -1
  10. package/build/plugin/next.mjs +3 -3
  11. package/build/plugin/next.mjs.map +1 -1
  12. package/build/plugin/unplugin.cjs +3 -2
  13. package/build/plugin/unplugin.d.cts.map +1 -1
  14. package/build/plugin/unplugin.d.mts.map +1 -1
  15. package/build/plugin/unplugin.mjs +3 -2
  16. package/build/plugin/unplugin.mjs.map +1 -1
  17. package/build/react/server/ServerLingoProvider.d.cts +2 -2
  18. package/build/react/shared/LocaleSwitcher.d.cts +2 -2
  19. package/build/translation-server/translation-server.cjs +7 -17
  20. package/build/translation-server/translation-server.mjs +7 -17
  21. package/build/translation-server/translation-server.mjs.map +1 -1
  22. package/build/translators/cache-factory.mjs.map +1 -1
  23. package/build/translators/lingo/model-factory.cjs +5 -10
  24. package/build/translators/lingo/model-factory.mjs +5 -10
  25. package/build/translators/lingo/model-factory.mjs.map +1 -1
  26. package/build/translators/lingo/provider-details.cjs +69 -0
  27. package/build/translators/lingo/provider-details.mjs +69 -0
  28. package/build/translators/lingo/provider-details.mjs.map +1 -0
  29. package/build/translators/lingo/{service.cjs → translator.cjs} +11 -13
  30. package/build/translators/lingo/{service.mjs → translator.mjs} +12 -14
  31. package/build/translators/lingo/translator.mjs.map +1 -0
  32. package/build/translators/local-cache.mjs +8 -8
  33. package/build/translators/local-cache.mjs.map +1 -1
  34. package/build/translators/memory-cache.cjs +47 -0
  35. package/build/translators/memory-cache.mjs +47 -0
  36. package/build/translators/memory-cache.mjs.map +1 -0
  37. package/build/translators/pluralization/service.cjs +19 -44
  38. package/build/translators/pluralization/service.mjs +19 -44
  39. package/build/translators/pluralization/service.mjs.map +1 -1
  40. package/build/translators/pseudotranslator/index.cjs +2 -10
  41. package/build/translators/pseudotranslator/index.mjs +2 -10
  42. package/build/translators/pseudotranslator/index.mjs.map +1 -1
  43. package/build/translators/translation-service.cjs +55 -57
  44. package/build/translators/translation-service.mjs +55 -57
  45. package/build/translators/translation-service.mjs.map +1 -1
  46. package/package.json +7 -7
  47. package/build/translators/lingo/service.mjs.map +0 -1
  48. package/build/translators/translator-factory.cjs +0 -49
  49. package/build/translators/translator-factory.mjs +0 -50
  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
- const modelsConfig = { "*:*": config.model };
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.info(`Initialized pluralization service with ${this.modelName}`);
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 = /* @__PURE__ */ new Map();
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
- results.set(c.hash, cached);
48
- return false;
49
- }
50
- return true;
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
- this.logger.debug(`All ${candidates.length} candidates found in cache, skipping LLM call`);
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.info(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`);
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.modelName}`)).text.trim();
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.info(`Starting pluralization processing for ${totalEntries} entries`);
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.info(`Found ${candidates.length} plural candidates (${(candidates.length / totalEntries * 100).toFixed(1)}%)`);
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.info(`Pluralizing: "${entry.sourceText}" -> "${result.icuText}"`);
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.info(`Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`);
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
- const modelsConfig = { "*:*": config.model };
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.info(`Initialized pluralization service with ${this.modelName}`);
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 = /* @__PURE__ */ new Map();
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
- results.set(c.hash, cached);
47
- return false;
48
- }
49
- return true;
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
- this.logger.debug(`All ${candidates.length} candidates found in cache, skipping LLM call`);
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.info(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`);
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.modelName}`)).text.trim();
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.info(`Starting pluralization processing for ${totalEntries} entries`);
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.info(`Found ${candidates.length} plural candidates (${(candidates.length / totalEntries * 100).toFixed(1)}%)`);
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.info(`Pluralizing: "${entry.sourceText}" -> "${result.icuText}"`);
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.info(`Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`);
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
- this.logger.debug(`[TRACE-PSEUDO] setTimeout callback fired for ${locale}, processing entries`);
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
- this.logger.debug(`[TRACE-PSEUDO] setTimeout callback fired for ${locale}, processing entries`);
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 this.logger.debug(\n `[TRACE-PSEUDO] translate() ENTERED for ${locale} with ${Object.keys(entries).length} entries`,\n );\n const delay = this.config?.delayMedian ?? 0;\n const actualDelay = this.getRandomDelay(delay);\n\n this.logger.debug(\n `[TRACE-PSEUDO] Config delay: ${delay}ms, actual delay: ${actualDelay}ms`,\n );\n\n return new Promise<Record<string, string>>((resolve) => {\n this.logger.debug(\n `[TRACE-PSEUDO] Promise created, scheduling setTimeout for ${actualDelay}ms`,\n );\n\n setTimeout(() => {\n this.logger.debug(\n `[TRACE-PSEUDO] setTimeout callback fired for ${locale}, processing entries`,\n );\n\n const result = Object.fromEntries(\n Object.entries(entries).map(([hash, entry]) => {\n return [hash, `${locale}/${pseudolocalize(entry.text)}`];\n }),\n );\n\n this.logger.debug(\n `[TRACE-PSEUDO] Pseudolocalization complete, resolving with ${Object.keys(result).length} translations`,\n );\n resolve(result);\n this.logger.debug(`[TRACE-PSEUDO] Promise resolved for ${locale}`);\n }, actualDelay);\n\n this.logger.debug(\n `[TRACE-PSEUDO] setTimeout scheduled, returning promise`,\n );\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;AACxE,OAAK,OAAO,MACV,0CAA0C,OAAO,QAAQ,OAAO,KAAK,QAAQ,CAAC,OAAO,UACtF;EACD,MAAM,QAAQ,KAAK,QAAQ,eAAe;EAC1C,MAAM,cAAc,KAAK,eAAe,MAAM;AAE9C,OAAK,OAAO,MACV,gCAAgC,MAAM,oBAAoB,YAAY,IACvE;AAED,SAAO,IAAI,SAAiC,YAAY;AACtD,QAAK,OAAO,MACV,6DAA6D,YAAY,IAC1E;AAED,oBAAiB;AACf,SAAK,OAAO,MACV,gDAAgD,OAAO,sBACxD;IAED,MAAM,SAAS,OAAO,YACpB,OAAO,QAAQ,QAAQ,CAAC,KAAK,CAAC,MAAM,WAAW;AAC7C,YAAO,CAAC,MAAM,GAAG,OAAO,GAAG,eAAe,MAAM,KAAK,GAAG;MACxD,CACH;AAED,SAAK,OAAO,MACV,8DAA8D,OAAO,KAAK,OAAO,CAAC,OAAO,eAC1F;AACD,YAAQ,OAAO;AACf,SAAK,OAAO,MAAM,uCAAuC,SAAS;MACjE,YAAY;AAEf,QAAK,OAAO,MACV,yDACD;IACD;;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"}
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"}