anylang-dev 0.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.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/bin/anylang.js +8 -0
- package/package.json +41 -0
- package/src/cli.js +107 -0
- package/src/config.js +51 -0
- package/src/env.js +32 -0
- package/src/extract.js +348 -0
- package/src/pipeline.js +214 -0
- package/src/providers.js +320 -0
- package/src/runtime.d.ts +12 -0
- package/src/runtime.js +39 -0
package/src/providers.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { loadDotEnv } from "./env.js";
|
|
2
|
+
|
|
3
|
+
const OPENAI_COMPATIBLE_PRESETS = {
|
|
4
|
+
openai: {
|
|
5
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
6
|
+
baseUrl: "https://api.openai.com/v1",
|
|
7
|
+
model: "gpt-4.1-mini"
|
|
8
|
+
},
|
|
9
|
+
"openai-compatible": {
|
|
10
|
+
apiKeyEnv: "ANYLANG_API_KEY",
|
|
11
|
+
baseUrl: "https://api.openai.com/v1",
|
|
12
|
+
model: "gpt-4.1-mini"
|
|
13
|
+
},
|
|
14
|
+
mistral: {
|
|
15
|
+
apiKeyEnv: "MISTRAL_API_KEY",
|
|
16
|
+
baseUrl: "https://api.mistral.ai/v1",
|
|
17
|
+
model: "mistral-large-latest"
|
|
18
|
+
},
|
|
19
|
+
deepseek: {
|
|
20
|
+
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
21
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
22
|
+
model: "deepseek-chat"
|
|
23
|
+
},
|
|
24
|
+
groq: {
|
|
25
|
+
apiKeyEnv: "GROQ_API_KEY",
|
|
26
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
27
|
+
model: "llama-3.3-70b-versatile"
|
|
28
|
+
},
|
|
29
|
+
openrouter: {
|
|
30
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
31
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
32
|
+
model: "openai/gpt-4.1-mini"
|
|
33
|
+
},
|
|
34
|
+
perplexity: {
|
|
35
|
+
apiKeyEnv: "PERPLEXITY_API_KEY",
|
|
36
|
+
baseUrl: "https://api.perplexity.ai",
|
|
37
|
+
model: "sonar"
|
|
38
|
+
},
|
|
39
|
+
xai: {
|
|
40
|
+
apiKeyEnv: "XAI_API_KEY",
|
|
41
|
+
baseUrl: "https://api.x.ai/v1",
|
|
42
|
+
model: "grok-3-mini"
|
|
43
|
+
},
|
|
44
|
+
together: {
|
|
45
|
+
apiKeyEnv: "TOGETHER_API_KEY",
|
|
46
|
+
baseUrl: "https://api.together.xyz/v1",
|
|
47
|
+
model: "meta-llama/Llama-3.3-70B-Instruct-Turbo"
|
|
48
|
+
},
|
|
49
|
+
fireworks: {
|
|
50
|
+
apiKeyEnv: "FIREWORKS_API_KEY",
|
|
51
|
+
baseUrl: "https://api.fireworks.ai/inference/v1",
|
|
52
|
+
model: "accounts/fireworks/models/llama-v3p3-70b-instruct"
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const NATIVE_PRESETS = {
|
|
57
|
+
gemini: {
|
|
58
|
+
apiKeyEnv: "GEMINI_API_KEY",
|
|
59
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
60
|
+
model: "gemini-2.5-flash"
|
|
61
|
+
},
|
|
62
|
+
anthropic: {
|
|
63
|
+
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
64
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
65
|
+
model: "claude-3-5-haiku-latest",
|
|
66
|
+
version: "2023-06-01"
|
|
67
|
+
},
|
|
68
|
+
cohere: {
|
|
69
|
+
apiKeyEnv: "COHERE_API_KEY",
|
|
70
|
+
baseUrl: "https://api.cohere.com/v2",
|
|
71
|
+
model: "command-a-03-2025"
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const supportedProviders = [
|
|
76
|
+
...Object.keys(NATIVE_PRESETS),
|
|
77
|
+
...Object.keys(OPENAI_COMPATIBLE_PRESETS)
|
|
78
|
+
].sort();
|
|
79
|
+
|
|
80
|
+
export function createTranslator(providerConfig) {
|
|
81
|
+
loadDotEnv();
|
|
82
|
+
|
|
83
|
+
if (!providerConfig || providerConfig.name === "none") {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const name = providerConfig.name;
|
|
88
|
+
if (OPENAI_COMPATIBLE_PRESETS[name]) {
|
|
89
|
+
return new OpenAICompatibleTranslator(resolvePreset(OPENAI_COMPATIBLE_PRESETS[name], providerConfig));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (name === "gemini") {
|
|
93
|
+
return new GeminiTranslator(resolvePreset(NATIVE_PRESETS.gemini, providerConfig));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (name === "anthropic") {
|
|
97
|
+
return new AnthropicTranslator(resolvePreset(NATIVE_PRESETS.anthropic, providerConfig));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (name === "cohere") {
|
|
101
|
+
return new CohereTranslator(resolvePreset(NATIVE_PRESETS.cohere, providerConfig));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`Unsupported provider: ${providerConfig.name}. Supported providers: ${supportedProviders.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolvePreset(preset, config) {
|
|
108
|
+
return {
|
|
109
|
+
...preset,
|
|
110
|
+
...withoutName(config)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function withoutName(config) {
|
|
115
|
+
const { name, ...rest } = config || {};
|
|
116
|
+
return rest;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readApiKey(config) {
|
|
120
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
121
|
+
if (!apiKey) {
|
|
122
|
+
throw new Error(`Missing provider API key. Set ${config.apiKeyEnv} in .env or run with --dry-run.`);
|
|
123
|
+
}
|
|
124
|
+
return apiKey;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function translationSystemPrompt() {
|
|
128
|
+
return [
|
|
129
|
+
"You translate website UI copy.",
|
|
130
|
+
"Return only the translated text.",
|
|
131
|
+
"Preserve placeholders, variables, punctuation intent, and whitespace shape.",
|
|
132
|
+
"Do not add explanations."
|
|
133
|
+
].join(" ");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function translationUserPrompt({ text, sourceLocale, targetLocale }) {
|
|
137
|
+
return `Translate from ${sourceLocale} to ${targetLocale}:\n${text}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class OpenAICompatibleTranslator {
|
|
141
|
+
constructor(config) {
|
|
142
|
+
this.config = config;
|
|
143
|
+
this.apiKey = readApiKey(config);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async translate({ text, sourceLocale, targetLocale }) {
|
|
147
|
+
const response = await fetch(`${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"content-type": "application/json",
|
|
151
|
+
authorization: `Bearer ${this.apiKey}`
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
model: this.config.model,
|
|
155
|
+
temperature: 0,
|
|
156
|
+
messages: [
|
|
157
|
+
{
|
|
158
|
+
role: "system",
|
|
159
|
+
content: translationSystemPrompt()
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
role: "user",
|
|
163
|
+
content: translationUserPrompt({ text, sourceLocale, targetLocale })
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const body = await response.text();
|
|
171
|
+
throw new Error(`Translation provider failed (${response.status}): ${body}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const json = await response.json();
|
|
175
|
+
const translated = json.choices?.[0]?.message?.content?.trim();
|
|
176
|
+
if (!translated) throw new Error("Translation provider returned an empty response.");
|
|
177
|
+
return translated;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
class GeminiTranslator {
|
|
182
|
+
constructor(config) {
|
|
183
|
+
this.config = config;
|
|
184
|
+
this.apiKey = readApiKey(config);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async translate({ text, sourceLocale, targetLocale }) {
|
|
188
|
+
const baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
189
|
+
const model = encodeURIComponent(this.config.model);
|
|
190
|
+
const response = await fetch(`${baseUrl}/models/${model}:generateContent`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: {
|
|
193
|
+
"content-type": "application/json",
|
|
194
|
+
"x-goog-api-key": this.apiKey
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
generationConfig: {
|
|
198
|
+
temperature: 0
|
|
199
|
+
},
|
|
200
|
+
contents: [
|
|
201
|
+
{
|
|
202
|
+
role: "user",
|
|
203
|
+
parts: [
|
|
204
|
+
{
|
|
205
|
+
text: [
|
|
206
|
+
translationSystemPrompt(),
|
|
207
|
+
"",
|
|
208
|
+
translationUserPrompt({ text, sourceLocale, targetLocale })
|
|
209
|
+
].join("\n")
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const body = await response.text();
|
|
219
|
+
throw new Error(`Gemini provider failed (${response.status}): ${body}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const json = await response.json();
|
|
223
|
+
const translated = json.candidates?.[0]?.content?.parts
|
|
224
|
+
?.map((part) => part.text || "")
|
|
225
|
+
.join("")
|
|
226
|
+
.trim();
|
|
227
|
+
|
|
228
|
+
if (!translated) throw new Error("Gemini provider returned an empty response.");
|
|
229
|
+
return translated;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class AnthropicTranslator {
|
|
234
|
+
constructor(config) {
|
|
235
|
+
this.config = config;
|
|
236
|
+
this.apiKey = readApiKey(config);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async translate({ text, sourceLocale, targetLocale }) {
|
|
240
|
+
const response = await fetch(`${this.config.baseUrl.replace(/\/$/, "")}/messages`, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: {
|
|
243
|
+
"content-type": "application/json",
|
|
244
|
+
"x-api-key": this.apiKey,
|
|
245
|
+
"anthropic-version": this.config.version
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
model: this.config.model,
|
|
249
|
+
max_tokens: 2048,
|
|
250
|
+
temperature: 0,
|
|
251
|
+
system: translationSystemPrompt(),
|
|
252
|
+
messages: [
|
|
253
|
+
{
|
|
254
|
+
role: "user",
|
|
255
|
+
content: translationUserPrompt({ text, sourceLocale, targetLocale })
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
const body = await response.text();
|
|
263
|
+
throw new Error(`Anthropic provider failed (${response.status}): ${body}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const json = await response.json();
|
|
267
|
+
const translated = json.content
|
|
268
|
+
?.map((part) => part.type === "text" ? part.text : "")
|
|
269
|
+
.join("")
|
|
270
|
+
.trim();
|
|
271
|
+
|
|
272
|
+
if (!translated) throw new Error("Anthropic provider returned an empty response.");
|
|
273
|
+
return translated;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
class CohereTranslator {
|
|
278
|
+
constructor(config) {
|
|
279
|
+
this.config = config;
|
|
280
|
+
this.apiKey = readApiKey(config);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async translate({ text, sourceLocale, targetLocale }) {
|
|
284
|
+
const response = await fetch(`${this.config.baseUrl.replace(/\/$/, "")}/chat`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: {
|
|
287
|
+
"content-type": "application/json",
|
|
288
|
+
authorization: `Bearer ${this.apiKey}`
|
|
289
|
+
},
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
model: this.config.model,
|
|
292
|
+
temperature: 0,
|
|
293
|
+
messages: [
|
|
294
|
+
{
|
|
295
|
+
role: "system",
|
|
296
|
+
content: translationSystemPrompt()
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
role: "user",
|
|
300
|
+
content: translationUserPrompt({ text, sourceLocale, targetLocale })
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
})
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
const body = await response.text();
|
|
308
|
+
throw new Error(`Cohere provider failed (${response.status}): ${body}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const json = await response.json();
|
|
312
|
+
const translated = json.message?.content
|
|
313
|
+
?.map((part) => part.type === "text" ? part.text : "")
|
|
314
|
+
.join("")
|
|
315
|
+
.trim();
|
|
316
|
+
|
|
317
|
+
if (!translated) throw new Error("Cohere provider returned an empty response.");
|
|
318
|
+
return translated;
|
|
319
|
+
}
|
|
320
|
+
}
|
package/src/runtime.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function configureAnyLang(options?: {
|
|
2
|
+
locale?: string;
|
|
3
|
+
catalogs?: Record<string, Record<string, string | {
|
|
4
|
+
source?: string;
|
|
5
|
+
text: string;
|
|
6
|
+
variables?: string[];
|
|
7
|
+
}>>;
|
|
8
|
+
}): void;
|
|
9
|
+
|
|
10
|
+
export function setAnyLangLocale(locale: string): void;
|
|
11
|
+
|
|
12
|
+
export function $tr(key: string, sourceOrLocale?: string, locale?: string): string;
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
let activeLocale = "en";
|
|
2
|
+
let catalogs = {};
|
|
3
|
+
|
|
4
|
+
export function configureAnyLang(options = {}) {
|
|
5
|
+
activeLocale = options.locale || activeLocale;
|
|
6
|
+
catalogs = options.catalogs || catalogs;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setAnyLangLocale(locale) {
|
|
10
|
+
activeLocale = locale;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function $tr(key, sourceOrLocale, locale) {
|
|
14
|
+
const { source, selectedLocale } = resolveArgs(key, sourceOrLocale, locale);
|
|
15
|
+
const entry = catalogs[selectedLocale]?.[key];
|
|
16
|
+
|
|
17
|
+
if (typeof entry === "string") return entry || source;
|
|
18
|
+
if (entry && typeof entry.text === "string") return entry.text || source;
|
|
19
|
+
|
|
20
|
+
return source;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveArgs(key, sourceOrLocale, locale) {
|
|
24
|
+
if (locale !== undefined) {
|
|
25
|
+
return {
|
|
26
|
+
source: typeof sourceOrLocale === "string" ? sourceOrLocale : key,
|
|
27
|
+
selectedLocale: locale
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof sourceOrLocale === "string" && catalogs[sourceOrLocale]) {
|
|
32
|
+
return { source: key, selectedLocale: sourceOrLocale };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
source: typeof sourceOrLocale === "string" ? sourceOrLocale : key,
|
|
37
|
+
selectedLocale: activeLocale
|
|
38
|
+
};
|
|
39
|
+
}
|