ai-l10n-core 1.4.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/NOTICE +27 -0
- package/README.md +487 -0
- package/dist/consoleLogger.d.ts +26 -0
- package/dist/consoleLogger.js +62 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/languageUtils.d.ts +27 -0
- package/dist/languageUtils.js +79 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +2 -0
- package/dist/test/consoleLogger.test.d.ts +1 -0
- package/dist/test/consoleLogger.test.js +242 -0
- package/dist/test/translationService.test.d.ts +1 -0
- package/dist/test/translationService.test.js +454 -0
- package/dist/translationService.d.ts +94 -0
- package/dist/translationService.js +155 -0
- package/package.json +51 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.L10nTranslationService = exports.FinishReason = exports.FileSchema = void 0;
|
|
4
|
+
const consoleLogger_1 = require("./consoleLogger");
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
/**
|
|
7
|
+
* Supported file schema formats for translation requests.
|
|
8
|
+
* Used to enable schema-specific translation handling.
|
|
9
|
+
*/
|
|
10
|
+
var FileSchema;
|
|
11
|
+
(function (FileSchema) {
|
|
12
|
+
/** OpenAPI specification format */
|
|
13
|
+
FileSchema["OpenAPI"] = "openApi";
|
|
14
|
+
/** Flutter ARB (Application Resource Bundle) format */
|
|
15
|
+
FileSchema["ARBFlutter"] = "arbFlutter";
|
|
16
|
+
})(FileSchema || (exports.FileSchema = FileSchema = {}));
|
|
17
|
+
var FinishReason;
|
|
18
|
+
(function (FinishReason) {
|
|
19
|
+
FinishReason["stop"] = "stop";
|
|
20
|
+
FinishReason["length"] = "length";
|
|
21
|
+
FinishReason["contentFilter"] = "contentFilter";
|
|
22
|
+
FinishReason["insufficientBalance"] = "insufficientBalance";
|
|
23
|
+
FinishReason["error"] = "error";
|
|
24
|
+
})(FinishReason || (exports.FinishReason = FinishReason = {}));
|
|
25
|
+
// ── Translation Service ─────────────────────────────────────────────────────
|
|
26
|
+
class L10nTranslationService {
|
|
27
|
+
constructor(logger = new consoleLogger_1.ConsoleLogger()) {
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
}
|
|
30
|
+
async getLanguages(options) {
|
|
31
|
+
const url = new URL(`${constants_1.URLS.API_BASE}/v2/languages`);
|
|
32
|
+
if (options?.codes) {
|
|
33
|
+
for (const c of options.codes) {
|
|
34
|
+
url.searchParams.append("c", c);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (options?.proficiencyLevels) {
|
|
38
|
+
for (const p of options.proficiencyLevels) {
|
|
39
|
+
url.searchParams.append("p", p);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(url.toString());
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Failed to get languages: ${response.status} ${response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
return (await response.json());
|
|
47
|
+
}
|
|
48
|
+
async predictLanguages(input, limit = 10) {
|
|
49
|
+
const url = new URL(`${constants_1.URLS.API_BASE}/v2/languages/predict`);
|
|
50
|
+
url.searchParams.append("input", input);
|
|
51
|
+
url.searchParams.append("limit", limit.toString());
|
|
52
|
+
this.logger.logInfo(`Predicting languages for input (${input.length} characters)`);
|
|
53
|
+
const response = await fetch(url.toString());
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
this.logger.logWarning(`Language prediction failed - ${response.status} ${response.statusText}`);
|
|
56
|
+
const error = new Error(`Failed to predict languages: ${response.statusText}`);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const result = (await response.json());
|
|
60
|
+
this.logger.logInfo(`Successfully predicted ${result.languages.length} languages`);
|
|
61
|
+
return result.languages;
|
|
62
|
+
}
|
|
63
|
+
async translate(request, apiKey) {
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
this.logger.showAndLogError("API Key not set. Please configure your API Key first.", null, "", "Get API Key", constants_1.URLS.API_KEYS);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
this.logger.logInfo(`Starting translation to ${request.targetLanguageCode}`);
|
|
69
|
+
const response = await fetch(`${constants_1.URLS.API_BASE}/v2/translate`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"X-API-Key": apiKey,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(request),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
let errorMessage;
|
|
79
|
+
let errorData = null;
|
|
80
|
+
// Try to parse error response body
|
|
81
|
+
try {
|
|
82
|
+
errorData = await response.json();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Ignore JSON parsing errors
|
|
86
|
+
}
|
|
87
|
+
this.logger.logWarning(`Translation API error - ${response.status} ${response.statusText}`);
|
|
88
|
+
if (errorData) {
|
|
89
|
+
this.logger.logWarning(`API error details - ${JSON.stringify(errorData)}`);
|
|
90
|
+
}
|
|
91
|
+
switch (response.status) {
|
|
92
|
+
case 400: {
|
|
93
|
+
// Try to extract validation errors from the error response
|
|
94
|
+
let validationMessage = "Invalid request. Please check your input and try again.";
|
|
95
|
+
if (errorData && errorData.errors) {
|
|
96
|
+
const errorDetails = errorData.errors;
|
|
97
|
+
if (Array.isArray(errorDetails)) {
|
|
98
|
+
validationMessage = errorDetails.join(" ");
|
|
99
|
+
}
|
|
100
|
+
else if (typeof errorDetails === "object") {
|
|
101
|
+
validationMessage = Object.values(errorDetails)
|
|
102
|
+
.map((v) => (Array.isArray(v) ? v.join(" ") : v))
|
|
103
|
+
.join(" ");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
errorMessage = validationMessage;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 401:
|
|
110
|
+
errorMessage = "Unauthorized. Please check your API Key.";
|
|
111
|
+
this.logger.showAndLogError(errorMessage, errorData, "", "Get API Key", constants_1.URLS.API_KEYS);
|
|
112
|
+
return null;
|
|
113
|
+
case 402: {
|
|
114
|
+
// Try to extract required characters from the error response
|
|
115
|
+
let message = "Not enough characters remaining for this translation. You can try translating a smaller portion of your file or purchase more characters.";
|
|
116
|
+
const requiredBalance = errorData?.data?.requiredBalance;
|
|
117
|
+
if (requiredBalance) {
|
|
118
|
+
const currentBalance = errorData?.data?.currentBalance ?? 0;
|
|
119
|
+
message = `This translation requires ${requiredBalance.toLocaleString()} characters, but you only have ${currentBalance.toLocaleString()} characters available. You can try translating a smaller portion of your file or purchase more characters.`;
|
|
120
|
+
}
|
|
121
|
+
this.logger.showAndLogError(message, errorData, "", "Visit l10n.dev", constants_1.URLS.PRICING);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
case 413:
|
|
125
|
+
errorMessage = "Request too large. Maximum request size is 5 MB.";
|
|
126
|
+
break;
|
|
127
|
+
case 500:
|
|
128
|
+
errorMessage = `An internal server error occurred (Error code: ${errorData?.errorCode || "unknown"}). Please try again later.`;
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
errorMessage =
|
|
132
|
+
"Failed to translate. Please check your connection and try again.";
|
|
133
|
+
}
|
|
134
|
+
throw new Error(errorMessage);
|
|
135
|
+
}
|
|
136
|
+
const result = (await response.json());
|
|
137
|
+
// Handle finish reasons
|
|
138
|
+
if (result.finishReason) {
|
|
139
|
+
if (result.finishReason !== FinishReason.stop) {
|
|
140
|
+
this.logger.logWarning(`Translation finished with reason: ${result.finishReason}`);
|
|
141
|
+
}
|
|
142
|
+
switch (result.finishReason) {
|
|
143
|
+
case FinishReason.insufficientBalance:
|
|
144
|
+
const message = `Not enough characters remaining for this translation. You can try translating a smaller portion of your file or purchase more characters.`;
|
|
145
|
+
this.logger.showAndLogError(message, undefined, "", "Visit l10n.dev", constants_1.URLS.PRICING);
|
|
146
|
+
return result;
|
|
147
|
+
case FinishReason.error:
|
|
148
|
+
throw new Error("Translation failed due to an error.");
|
|
149
|
+
// Note: FinishReason.contentFilter and FinishReason.length return partial results with filteredStrings
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.L10nTranslationService = L10nTranslationService;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-l10n-core",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Platform-independent core for AI-powered localization — translation API client, language utilities, and shared types",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepare": "npm run build",
|
|
10
|
+
"test": "mocha",
|
|
11
|
+
"verify": "node scripts/verify.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"localization",
|
|
15
|
+
"l10n",
|
|
16
|
+
"i18n",
|
|
17
|
+
"translation",
|
|
18
|
+
"ai",
|
|
19
|
+
"sdk",
|
|
20
|
+
"api",
|
|
21
|
+
"typescript",
|
|
22
|
+
"core"
|
|
23
|
+
],
|
|
24
|
+
"author": "l10n.dev",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/l10n-dev/ai-l10n.git",
|
|
29
|
+
"directory": "core"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/l10n-dev/ai-l10n#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/l10n-dev/ai-l10n/issues"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"NOTICE",
|
|
40
|
+
"CHANGELOG.md"
|
|
41
|
+
],
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/mocha": "^10.0.10",
|
|
44
|
+
"@types/node": "^20.17.10",
|
|
45
|
+
"@types/sinon": "^17.0.3",
|
|
46
|
+
"mocha": "^11.0.1",
|
|
47
|
+
"sinon": "^19.0.2",
|
|
48
|
+
"tsx": "^4.21.0",
|
|
49
|
+
"typescript": "^5.3.0"
|
|
50
|
+
}
|
|
51
|
+
}
|