locai 2.1.0

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 (72) hide show
  1. package/README.md +928 -0
  2. package/dist/cli.js +718 -0
  3. package/dist/commands/review.js +357 -0
  4. package/dist/commands/translator.js +29 -0
  5. package/dist/config/index.js +52 -0
  6. package/dist/core/adapters/base-adapter.js +1 -0
  7. package/dist/core/adapters/factory.js +44 -0
  8. package/dist/core/adapters/json-adapter.js +20 -0
  9. package/dist/core/adapters/po-adapter.js +50 -0
  10. package/dist/core/adapters/properties-adapter.js +31 -0
  11. package/dist/core/adapters/yaml-adapter.js +24 -0
  12. package/dist/core/context-processor.js +247 -0
  13. package/dist/core/detection/framework-detector.js +56 -0
  14. package/dist/core/detection/structure-detector.js +30 -0
  15. package/dist/core/fallback-provider.js +442 -0
  16. package/dist/core/orchestrator.js +438 -0
  17. package/dist/core/pipeline/Pipeline.js +48 -0
  18. package/dist/core/pipeline/PipelineStep.js +13 -0
  19. package/dist/core/pipeline/context.js +21 -0
  20. package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
  21. package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
  22. package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
  23. package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
  24. package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
  25. package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
  26. package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
  27. package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
  28. package/dist/core/pipeline/steps/TranslationStep.js +65 -0
  29. package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
  30. package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
  31. package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
  32. package/dist/core/provider-factory.js +139 -0
  33. package/dist/providers/base-provider.js +230 -0
  34. package/dist/providers/dashscope.js +128 -0
  35. package/dist/providers/deepseek.js +111 -0
  36. package/dist/providers/gemini.js +135 -0
  37. package/dist/providers/openai.js +203 -0
  38. package/dist/providers/xai.js +111 -0
  39. package/dist/services/embedding-provider.js +50 -0
  40. package/dist/services/translation-service.js +609 -0
  41. package/dist/services/vector-store.js +79 -0
  42. package/dist/utils/ai-context-analyzer.js +460 -0
  43. package/dist/utils/confidence-scorer.js +277 -0
  44. package/dist/utils/error-helper.js +386 -0
  45. package/dist/utils/file-manager.js +357 -0
  46. package/dist/utils/glossary-manager.js +194 -0
  47. package/dist/utils/graceful-shutdown.js +118 -0
  48. package/dist/utils/input-validator.js +663 -0
  49. package/dist/utils/logger.js +374 -0
  50. package/dist/utils/object-transformer.js +177 -0
  51. package/dist/utils/progress-tracker.js +277 -0
  52. package/dist/utils/prompt-templates.js +415 -0
  53. package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
  54. package/dist/utils/quality/base-checker.js +73 -0
  55. package/dist/utils/quality/code-block-checker.js +214 -0
  56. package/dist/utils/quality/html-tag-checker.js +64 -0
  57. package/dist/utils/quality/index.js +220 -0
  58. package/dist/utils/quality/length-checker.js +140 -0
  59. package/dist/utils/quality/markdown-checker.js +220 -0
  60. package/dist/utils/quality/placeholder-checker.js +142 -0
  61. package/dist/utils/quality/punctuation-checker.js +50 -0
  62. package/dist/utils/quality/quote-balance-checker.js +143 -0
  63. package/dist/utils/quality/special-characters-checker.js +229 -0
  64. package/dist/utils/quality/style-guide-checker.js +192 -0
  65. package/dist/utils/quality/text-sanitizer.js +120 -0
  66. package/dist/utils/quality/tone-verifier.js +91 -0
  67. package/dist/utils/rate-limiter.js +398 -0
  68. package/dist/utils/retry-helper.js +166 -0
  69. package/dist/utils/state-manager.js +162 -0
  70. package/dist/utils/statistics-manager.js +51 -0
  71. package/dist/utils/ui-manager.js +98 -0
  72. package/package.json +80 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Base class for translation providers
3
+ */
4
+ import ErrorHelper from "../utils/error-helper.js";
5
+ import ConfidenceScorer from "../utils/confidence-scorer.js";
6
+ class BaseProvider {
7
+ name;
8
+ config;
9
+ defaultModel;
10
+ defaultTemperature;
11
+ defaultMaxTokens;
12
+ commonHeaders;
13
+ constructor(name, config = {}) {
14
+ this.name = name;
15
+ this.config = config;
16
+ this.defaultModel = config.model;
17
+ this.defaultTemperature = config.temperature || 0.3;
18
+ this.defaultMaxTokens = config.maxTokens || 2000;
19
+ this.commonHeaders = {
20
+ "Content-Type": "application/json",
21
+ "User-Agent": "locai/1.0",
22
+ };
23
+ }
24
+ async translate(text, sourceLang, targetLang, _options = {}) {
25
+ throw new Error(`translate method must be implemented by ${this.name} provider`);
26
+ }
27
+ async analyze(prompt, options = {}) {
28
+ throw new Error(`analyze method must be implemented by ${this.name} provider if supported`);
29
+ }
30
+ async chat(messages, options = {}) {
31
+ throw new Error(`chat method must be implemented by ${this.name} provider if supported`);
32
+ }
33
+ getApiKey() {
34
+ throw new Error(`getApiKey method must be implemented by ${this.name} provider`);
35
+ }
36
+ getEndpoint() {
37
+ throw new Error(`getEndpoint method must be implemented by ${this.name} provider`);
38
+ }
39
+ validateRequest(text, sourceLang, targetLang) {
40
+ if (!text || typeof text !== "string") {
41
+ throw new Error("Text must be a non-empty string");
42
+ }
43
+ if (!sourceLang || !targetLang) {
44
+ throw new Error("Source and target languages are required");
45
+ }
46
+ if (text.length > 10000) {
47
+ throw new Error("Text too long for translation (max 10000 characters)");
48
+ }
49
+ }
50
+ validateResponse(response, providerName) {
51
+ if (!response) {
52
+ throw new Error(`No response from ${providerName} API`);
53
+ }
54
+ if (!response.data) {
55
+ throw new Error(`Invalid response format from ${providerName}`);
56
+ }
57
+ return true;
58
+ }
59
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
60
+ handleApiError(error, providerName) {
61
+ if (error.response) {
62
+ const status = error.response.status;
63
+ const apiMessage = error.response.data?.error?.message || error.response.data?.message || null;
64
+ // Rate limit error
65
+ if (status === 429) {
66
+ const retryAfter = error.response.headers?.["retry-after"] || 60;
67
+ const limit = error.response.headers?.["x-ratelimit-limit"];
68
+ const remaining = error.response.headers?.["x-ratelimit-remaining"];
69
+ throw ErrorHelper.rateLimitError(providerName, {
70
+ limit,
71
+ current: limit ? limit - remaining : null,
72
+ retryAfter,
73
+ statusCode: status,
74
+ });
75
+ }
76
+ // Server error (5xx)
77
+ if (status >= 500) {
78
+ throw ErrorHelper.serverError(providerName, status, apiMessage);
79
+ }
80
+ // Authentication error (401, 403)
81
+ if (status === 401 || status === 403) {
82
+ throw ErrorHelper.authError(providerName, status, apiMessage);
83
+ }
84
+ // Other API errors
85
+ const apiError = ErrorHelper.createError("API_RESPONSE_ERROR", {
86
+ provider: providerName,
87
+ statusCode: status,
88
+ apiMessage,
89
+ });
90
+ apiError.message = `${providerName} API error (${status})${apiMessage ? `: ${apiMessage}` : ""}`;
91
+ throw apiError;
92
+ }
93
+ // Network errors
94
+ if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
95
+ throw ErrorHelper.networkError(providerName);
96
+ }
97
+ // Timeout errors
98
+ if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
99
+ throw ErrorHelper.timeoutError(providerName, error.config?.timeout);
100
+ }
101
+ // Unknown errors - preserve original for debugging
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ const unknownError = new Error(`${providerName}: ${error.message}`);
104
+ unknownError.code = "ERR_UNKNOWN";
105
+ unknownError.originalError = error;
106
+ throw unknownError;
107
+ }
108
+ generatePrompt(text, sourceLang, targetLang, options = {}) {
109
+ const context = options.detectedContext;
110
+ let prompt = `Translate the following text from ${sourceLang} to ${targetLang}`;
111
+ if (context?.category && context.category !== "general") {
112
+ const categoryPrompts = {
113
+ technical: ". Preserve technical terms and variable names exactly as they appear.",
114
+ defi: ". Keep DeFi and cryptocurrency terms in English.",
115
+ marketing: ". Use persuasive and engaging language appropriate for marketing.",
116
+ legal: ". Maintain formal tone and precise legal terminology.",
117
+ ui: ". Keep UI terms consistent and clear for user interface elements.",
118
+ };
119
+ prompt += categoryPrompts[context.category] || "";
120
+ }
121
+ if (options.lengthControl?.mode === "strict") {
122
+ prompt += " Keep the translation length similar to the original.";
123
+ }
124
+ const placeholderRegex = /\{[^}]+\}/g;
125
+ const hasPlaceholders = placeholderRegex.test(text);
126
+ if (hasPlaceholders) {
127
+ prompt +=
128
+ "\n\nCRITICAL: This text contains placeholders like {variable}. You MUST preserve them EXACTLY as they appear. Do NOT translate the placeholder names, do NOT modify the curly braces, and do NOT add any text around them.";
129
+ }
130
+ prompt += `\n\nText to translate: "${text}"`;
131
+ prompt += "\n\nProvide only the translation without explanations or quotes.";
132
+ return prompt;
133
+ }
134
+ getConfig(options = {}) {
135
+ const apiKey = this.getApiKey();
136
+ if (!apiKey) {
137
+ throw new Error(`API key not configured for ${this.name} provider`);
138
+ }
139
+ return {
140
+ model: options.model || this.defaultModel,
141
+ temperature: options.temperature ?? this.defaultTemperature,
142
+ maxTokens: options.maxTokens || this.defaultMaxTokens,
143
+ max_tokens: options.maxTokens || this.defaultMaxTokens, // Backward compatibility
144
+ apiKey: apiKey,
145
+ endpoint: this.getEndpoint(),
146
+ };
147
+ }
148
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
149
+ extractTranslation(response, providerName) {
150
+ // Check for error response first
151
+ if (response.error) {
152
+ const errorMsg = response.error.message || response.error.error || JSON.stringify(response.error);
153
+ throw new Error(`api: ${providerName} - ${errorMsg}`);
154
+ }
155
+ let translation = null;
156
+ if (response.choices && response.choices[0]?.message?.content) {
157
+ translation = response.choices[0].message.content.trim();
158
+ }
159
+ else if (response.choices && response.choices[0]?.text) {
160
+ translation = response.choices[0].text.trim();
161
+ }
162
+ else if (response.choices && response.choices[0]?.delta?.content) {
163
+ translation = response.choices[0].delta.content.trim();
164
+ }
165
+ else if (response.candidates && response.candidates[0]?.content?.parts?.[0]?.text) {
166
+ translation = response.candidates[0].content.parts[0].text.trim();
167
+ }
168
+ else if (response.output && response.output.text) {
169
+ translation = response.output.text.trim();
170
+ }
171
+ else if (response.text) {
172
+ translation = response.text.trim();
173
+ }
174
+ else if (response.content) {
175
+ translation = response.content.trim();
176
+ }
177
+ else if (response.data &&
178
+ response.data.choices &&
179
+ response.data.choices[0]?.message?.content) {
180
+ translation = response.data.choices[0].message.content.trim();
181
+ }
182
+ if (!translation) {
183
+ console.error(`Unable to extract translation from ${providerName} response. Response structure:`, {
184
+ keys: Object.keys(response),
185
+ choices: response.choices ? `Array(${response.choices.length})` : "undefined",
186
+ choicesStructure: response.choices && response.choices[0]
187
+ ? Object.keys(response.choices[0])
188
+ : "undefined",
189
+ });
190
+ throw new Error(`Unable to extract translation from ${providerName} response`);
191
+ }
192
+ return translation;
193
+ }
194
+ /**
195
+ * Extract translation with confidence score
196
+ */
197
+ extractTranslationWithConfidence(
198
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
199
+ response, providerName, sourceText, sourceLang, targetLang, category = "general") {
200
+ const translation = this.extractTranslation(response, providerName);
201
+ // Extract AI confidence from response
202
+ const aiConfidence = ConfidenceScorer.extractAIConfidence(response, providerName);
203
+ // Calculate comprehensive confidence score
204
+ const confidenceResult = ConfidenceScorer.calculateConfidence({
205
+ aiConfidence,
206
+ sourceText,
207
+ translation,
208
+ sourceLang,
209
+ targetLang,
210
+ provider: providerName,
211
+ category,
212
+ });
213
+ return {
214
+ translation,
215
+ confidence: confidenceResult,
216
+ rawResponse: response,
217
+ };
218
+ }
219
+ sanitizeTranslation(translation) {
220
+ if (!translation || typeof translation !== "string") {
221
+ throw new Error("Invalid translation format");
222
+ }
223
+ return translation
224
+ .replace(/^["']|["']$/g, "")
225
+ .replace(/^\s*Translation:\s*/i, "")
226
+ .replace(/^\s*Result:\s*/i, "")
227
+ .trim();
228
+ }
229
+ }
230
+ export default BaseProvider;
@@ -0,0 +1,128 @@
1
+ import axios from "axios";
2
+ import BaseProvider from "./base-provider.js";
3
+ import { getPrompt, getAnalysisPrompt } from "../utils/prompt-templates.js";
4
+ import RetryHelper from "../utils/retry-helper.js";
5
+ /**
6
+ * Provider implementation for Alibaba DashScope (Qwen) models.
7
+ */
8
+ class DashScopeProvider extends BaseProvider {
9
+ client;
10
+ generationClient;
11
+ /**
12
+ * Create a new DashScopeProvider instance.
13
+ * @param {ProviderConfig} config - Provider configuration.
14
+ */
15
+ constructor(config = {}) {
16
+ super("dashscope", config);
17
+ this.client = axios.create({
18
+ baseURL: "https://dashscope-intl.aliyuncs.com",
19
+ headers: {
20
+ ...this.commonHeaders,
21
+ Authorization: `Bearer ${this.getApiKey()}`,
22
+ },
23
+ timeout: 30000,
24
+ maxRedirects: 0,
25
+ validateStatus: (status) => status < 500,
26
+ });
27
+ // Generation client for analysis
28
+ this.generationClient = axios.create({
29
+ baseURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc",
30
+ headers: {
31
+ ...this.commonHeaders,
32
+ Authorization: `Bearer ${this.getApiKey()}`,
33
+ },
34
+ timeout: 30000,
35
+ maxRedirects: 0,
36
+ validateStatus: (status) => status < 500,
37
+ });
38
+ }
39
+ getApiKey() {
40
+ return process.env.DASHSCOPE_API_KEY;
41
+ }
42
+ getEndpoint() {
43
+ return "/compatible-mode/v1/chat/completions";
44
+ }
45
+ getGenerationEndpoint() {
46
+ return "/text-generation/generation";
47
+ }
48
+ /**
49
+ * Translate text using DashScope.
50
+ */
51
+ async translate(text, sourceLang, targetLang, options = {}) {
52
+ this.validateRequest(text, sourceLang, targetLang);
53
+ const config = this.getConfig(options.apiConfig?.dashscope);
54
+ const promptData = getPrompt("dashscope", sourceLang, targetLang, text, options);
55
+ return RetryHelper.withRetry(async () => {
56
+ try {
57
+ const response = await this.client.post(this.getEndpoint(), {
58
+ model: config.model || "qwen-plus",
59
+ ...promptData,
60
+ temperature: config.temperature || 0.3,
61
+ max_tokens: config.maxTokens || 2000,
62
+ });
63
+ this.validateResponse(response, this.name);
64
+ const translation = this.extractTranslation(response.data, this.name);
65
+ return this.sanitizeTranslation(translation);
66
+ }
67
+ catch (error) {
68
+ this.handleApiError(error, this.name);
69
+ throw error;
70
+ }
71
+ }, {
72
+ maxRetries: options.retryOptions?.maxRetries || 2,
73
+ initialDelay: options.retryOptions?.initialDelay || 1000,
74
+ context: "DashScope Provider",
75
+ logContext: {
76
+ source: sourceLang,
77
+ target: targetLang,
78
+ },
79
+ });
80
+ }
81
+ async analyze(prompt, options = {}) {
82
+ const config = this.getConfig({
83
+ model: options.model || "qwen-plus",
84
+ temperature: options.temperature || 0.2,
85
+ maxTokens: options.maxTokens || 1000,
86
+ });
87
+ const promptData = getAnalysisPrompt("dashscope", prompt, {
88
+ ...options,
89
+ ...config,
90
+ });
91
+ return RetryHelper.withRetry(async () => {
92
+ try {
93
+ const response = await this.generationClient.post(this.getGenerationEndpoint(), {
94
+ ...promptData,
95
+ });
96
+ if (!response.data?.output?.text) {
97
+ throw new Error("Invalid response format from DashScope API");
98
+ }
99
+ const result = response.data.output.text.trim();
100
+ return this.sanitizeTranslation(result);
101
+ }
102
+ catch (error) {
103
+ this.handleApiError(error, this.name);
104
+ throw error;
105
+ }
106
+ }, {
107
+ maxRetries: options.maxRetries || 2,
108
+ initialDelay: options.initialDelay || 1000,
109
+ context: "DashScope Provider Analysis",
110
+ });
111
+ }
112
+ }
113
+ // Lazy singleton - created on first use
114
+ let dashscopeProvider = null;
115
+ function getProvider() {
116
+ if (!dashscopeProvider) {
117
+ dashscopeProvider = new DashScopeProvider();
118
+ }
119
+ return dashscopeProvider;
120
+ }
121
+ // Export both class and legacy functions
122
+ async function translate(text, sourceLang, targetLang, options = {}) {
123
+ return getProvider().translate(text, sourceLang, targetLang, options);
124
+ }
125
+ async function analyze(prompt, options = {}) {
126
+ return getProvider().analyze(prompt, options);
127
+ }
128
+ export { translate, analyze, DashScopeProvider };
@@ -0,0 +1,111 @@
1
+ import axios from "axios";
2
+ import BaseProvider from "./base-provider.js";
3
+ import { getPrompt, getAnalysisPrompt } from "../utils/prompt-templates.js";
4
+ import RetryHelper from "../utils/retry-helper.js";
5
+ /**
6
+ * Provider implementation for DeepSeek API.
7
+ */
8
+ class DeepSeekProvider extends BaseProvider {
9
+ client;
10
+ /**
11
+ * Create a new DeepSeekProvider instance.
12
+ * @param {ProviderConfig} config - Provider configuration.
13
+ */
14
+ constructor(config = {}) {
15
+ super("deepseek", config);
16
+ this.client = axios.create({
17
+ baseURL: "https://api.deepseek.com/v1",
18
+ headers: {
19
+ ...this.commonHeaders,
20
+ Authorization: `Bearer ${this.getApiKey()}`,
21
+ },
22
+ timeout: 45000,
23
+ maxRedirects: 0,
24
+ validateStatus: (status) => status < 500,
25
+ });
26
+ }
27
+ getApiKey() {
28
+ return process.env.DEEPSEEK_API_KEY;
29
+ }
30
+ getEndpoint() {
31
+ return "/chat/completions";
32
+ }
33
+ /**
34
+ * Translate text using DeepSeek.
35
+ */
36
+ async translate(text, sourceLang, targetLang, options = {}) {
37
+ this.validateRequest(text, sourceLang, targetLang);
38
+ const config = this.getConfig(options.apiConfig?.deepseek);
39
+ const promptData = getPrompt("deepseek", sourceLang, targetLang, text, options);
40
+ return RetryHelper.withRetry(async () => {
41
+ try {
42
+ const response = await this.client.post(this.getEndpoint(), {
43
+ model: config.model || "deepseek-chat",
44
+ ...promptData,
45
+ temperature: config.temperature || 0.3,
46
+ max_tokens: config.maxTokens || 2000,
47
+ });
48
+ this.validateResponse(response, this.name);
49
+ const translation = this.extractTranslation(response.data, this.name);
50
+ return this.sanitizeTranslation(translation);
51
+ }
52
+ catch (error) {
53
+ this.handleApiError(error, this.name);
54
+ throw error;
55
+ }
56
+ }, {
57
+ maxRetries: options.retryOptions?.maxRetries || 3,
58
+ initialDelay: options.retryOptions?.initialDelay || 2000,
59
+ context: "DeepSeek Provider",
60
+ logContext: {
61
+ source: sourceLang,
62
+ target: targetLang,
63
+ },
64
+ });
65
+ }
66
+ async analyze(prompt, options = {}) {
67
+ const config = this.getConfig({
68
+ model: options.model || "deepseek-chat",
69
+ temperature: options.temperature || 0.2,
70
+ maxTokens: options.maxTokens || 1000,
71
+ });
72
+ const promptData = getAnalysisPrompt("deepseek", prompt, options);
73
+ return RetryHelper.withRetry(async () => {
74
+ try {
75
+ const response = await this.client.post(this.getEndpoint(), {
76
+ model: config.model,
77
+ ...promptData,
78
+ temperature: config.temperature,
79
+ max_tokens: config.maxTokens,
80
+ });
81
+ this.validateResponse(response, this.name);
82
+ const result = this.extractTranslation(response.data, this.name);
83
+ return this.sanitizeTranslation(result);
84
+ }
85
+ catch (error) {
86
+ this.handleApiError(error, this.name);
87
+ throw error;
88
+ }
89
+ }, {
90
+ maxRetries: options.maxRetries || 2,
91
+ initialDelay: options.initialDelay || 1000,
92
+ context: "DeepSeek Provider Analysis",
93
+ });
94
+ }
95
+ }
96
+ // Lazy singleton - created on first use
97
+ let deepseekProvider = null;
98
+ function getProvider() {
99
+ if (!deepseekProvider) {
100
+ deepseekProvider = new DeepSeekProvider();
101
+ }
102
+ return deepseekProvider;
103
+ }
104
+ // Export both class and legacy functions
105
+ async function translate(text, sourceLang, targetLang, options = {}) {
106
+ return getProvider().translate(text, sourceLang, targetLang, options);
107
+ }
108
+ async function analyze(prompt, options = {}) {
109
+ return getProvider().analyze(prompt, options);
110
+ }
111
+ export { translate, analyze, DeepSeekProvider };
@@ -0,0 +1,135 @@
1
+ import axios from "axios";
2
+ import BaseProvider from "./base-provider.js";
3
+ import { getPrompt, getAnalysisPrompt } from "../utils/prompt-templates.js";
4
+ import RetryHelper from "../utils/retry-helper.js";
5
+ /**
6
+ * Provider implementation for Google Gemini models.
7
+ */
8
+ class GeminiProvider extends BaseProvider {
9
+ client;
10
+ /**
11
+ * Create a new GeminiProvider instance.
12
+ * @param {ProviderConfig} config - Provider configuration.
13
+ */
14
+ constructor(config = {}) {
15
+ super("gemini", config);
16
+ this.client = axios.create({
17
+ baseURL: "https://generativelanguage.googleapis.com/v1beta",
18
+ headers: {
19
+ ...this.commonHeaders,
20
+ },
21
+ timeout: 30000,
22
+ maxRedirects: 0,
23
+ validateStatus: (status) => status < 500,
24
+ });
25
+ }
26
+ getApiKey() {
27
+ return process.env.GEMINI_API_KEY;
28
+ }
29
+ getEndpoint(model) {
30
+ return `/models/${model || "gemini-2.0-flash-exp"}:generateContent`;
31
+ }
32
+ /**
33
+ * Translate text using Gemini.
34
+ */
35
+ async translate(text, sourceLang, targetLang, options = {}) {
36
+ this.validateRequest(text, sourceLang, targetLang);
37
+ const config = this.getConfig(options.apiConfig?.gemini);
38
+ const model = config.model || "gemini-2.0-flash-exp";
39
+ const apiKey = this.getApiKey();
40
+ if (!apiKey) {
41
+ throw new Error("GEMINI_API_KEY environment variable not found");
42
+ }
43
+ const promptData = getPrompt("gemini", sourceLang, targetLang, text, options);
44
+ return RetryHelper.withRetry(async () => {
45
+ try {
46
+ const response = await this.client.post(this.getEndpoint(model), {
47
+ ...promptData,
48
+ generationConfig: {
49
+ temperature: config.temperature || 0.3,
50
+ maxOutputTokens: config.maxTokens || 2048,
51
+ },
52
+ }, {
53
+ params: { key: apiKey },
54
+ });
55
+ if (!response.data?.candidates || response.data.candidates.length === 0) {
56
+ throw new Error("Failed to get translation candidate from Gemini API");
57
+ }
58
+ if (!response.data.candidates[0]?.content?.parts?.[0]?.text) {
59
+ throw new Error("Invalid response format from Gemini API");
60
+ }
61
+ const translation = response.data.candidates[0].content.parts[0].text.trim();
62
+ return this.sanitizeTranslation(translation);
63
+ }
64
+ catch (error) {
65
+ this.handleApiError(error, this.name);
66
+ throw error;
67
+ }
68
+ }, {
69
+ maxRetries: options.retryOptions?.maxRetries || 2,
70
+ initialDelay: options.retryOptions?.initialDelay || 1000,
71
+ context: "Gemini Provider",
72
+ logContext: {
73
+ source: sourceLang,
74
+ target: targetLang,
75
+ },
76
+ });
77
+ }
78
+ async analyze(prompt, options = {}) {
79
+ const config = this.getConfig({
80
+ model: options.model || "gemini-2.0-flash-exp",
81
+ temperature: options.temperature || 0.2,
82
+ maxTokens: options.maxTokens || 1000,
83
+ });
84
+ const apiKey = this.getApiKey();
85
+ if (!apiKey) {
86
+ throw new Error("GEMINI_API_KEY environment variable not found");
87
+ }
88
+ const promptData = getAnalysisPrompt("gemini", prompt, options);
89
+ return RetryHelper.withRetry(async () => {
90
+ try {
91
+ const response = await this.client.post(this.getEndpoint(config.model), {
92
+ ...promptData,
93
+ generationConfig: {
94
+ temperature: config.temperature,
95
+ maxOutputTokens: config.maxTokens,
96
+ },
97
+ }, {
98
+ params: { key: apiKey },
99
+ });
100
+ if (!response.data?.candidates || response.data.candidates.length === 0) {
101
+ throw new Error("Failed to get analysis result from Gemini API");
102
+ }
103
+ if (!response.data.candidates[0]?.content?.parts?.[0]?.text) {
104
+ throw new Error("Invalid response format from Gemini API");
105
+ }
106
+ const result = response.data.candidates[0].content.parts[0].text.trim();
107
+ return this.sanitizeTranslation(result);
108
+ }
109
+ catch (error) {
110
+ this.handleApiError(error, this.name);
111
+ throw error;
112
+ }
113
+ }, {
114
+ maxRetries: options.maxRetries || 2,
115
+ initialDelay: options.initialDelay || 1000,
116
+ context: "Gemini Provider Analysis",
117
+ });
118
+ }
119
+ }
120
+ // Lazy singleton - created on first use
121
+ let geminiProvider = null;
122
+ function getProvider() {
123
+ if (!geminiProvider) {
124
+ geminiProvider = new GeminiProvider();
125
+ }
126
+ return geminiProvider;
127
+ }
128
+ // Export both class and legacy functions
129
+ async function translate(text, sourceLang, targetLang, options = {}) {
130
+ return getProvider().translate(text, sourceLang, targetLang, options);
131
+ }
132
+ async function analyze(prompt, options = {}) {
133
+ return getProvider().analyze(prompt, options);
134
+ }
135
+ export { translate, analyze, GeminiProvider };