ai-l10n-core 1.4.0 → 1.5.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 CHANGED
@@ -5,6 +5,68 @@ All notable changes to the core package will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.0] - 2026-04-16
9
+
10
+ ### Added
11
+ - **`ApiResponse<T>` type** — Generic discriminated union used as the return type of all API methods:
12
+ ```typescript
13
+ type ApiResponse<T> =
14
+ | { success: true; data: T }
15
+ | { success: false; reason: string; message: string };
16
+ ```
17
+ - **Balance API** — New `getBalance(apiKey)` method on `L10nTranslationService` retrieves the current character balance from the `GET /v2/balance` endpoint. Returns `ApiResponse<BalanceResponse>` (always resolves, never throws).
18
+ - **`BalanceResponse` type** — `{ currentBalance: number }`
19
+ - **Structured responses for all methods** — `getBalance()`, `getLanguages()`, and `predictLanguages()` now return `ApiResponse<T>` and never throw
20
+
21
+ ### Changed
22
+ - **`translate()` return type changed from `TranslationResult | null` to `TranslationResponse`** (breaking change)
23
+ - `TranslationResponse` is now `ApiResponse<TranslationResult> & { currentBalance?: number }`
24
+ - The method always resolves — it never throws
25
+ - On success: `{ success: true, data: TranslationResult, currentBalance?: number }`
26
+ - On error: `{ success: false, reason: string, message: string, currentBalance?: number }`
27
+ - `reason` values: `"noApiKey"`, `"unauthorized"`, `"paymentRequired"`, `"badRequest"`, `"requestTooLarge"`, `"serverError"`, `"networkError"`, `"translationError"`
28
+ - **`getLanguages()` now requires `apiKey` as its first parameter** (bug fix — previously no API key was sent)
29
+ - New signature: `getLanguages(apiKey: string, options?: ...): Promise<ApiResponse<SupportedLanguagesResponse>>`
30
+ - Returns structured `ApiResponse` instead of throwing; error reasons: `noApiKey`, `unauthorized`, `badRequest`, `networkError`
31
+ - **`getBalance()` no longer throws** — returns `ApiResponse<BalanceResponse>`; error reasons: `noApiKey`, `unauthorized`, `networkError`
32
+ - **`predictLanguages()` no longer throws** — returns `ApiResponse<Language[]>`; error reason: `networkError`
33
+
34
+ ### Migration
35
+ ```typescript
36
+ // translate() — Before (1.4.x)
37
+ try {
38
+ const result = await service.translate(request, apiKey);
39
+ if (!result) return; // null for 401/402
40
+ console.log(result.translations);
41
+ } catch (e) {
42
+ console.error(e.message); // thrown for 400/413/500
43
+ }
44
+
45
+ // translate() — After (1.5.0)
46
+ const response = await service.translate(request, apiKey);
47
+ if (!response.success) {
48
+ console.error(response.message, '| reason:', response.reason);
49
+ return;
50
+ }
51
+ console.log(response.data.translations);
52
+
53
+ // getBalance() — Before (1.4.x)
54
+ const { currentBalance } = await service.getBalance(apiKey); // threw on error
55
+
56
+ // getBalance() — After (1.5.0)
57
+ const result = await service.getBalance(apiKey);
58
+ if (!result.success) console.error(result.reason);
59
+ else console.log(result.data.currentBalance);
60
+
61
+ // getLanguages() — Before (1.4.x)
62
+ const { languages } = await service.getLanguages(options); // no apiKey, threw on error
63
+
64
+ // getLanguages() — After (1.5.0)
65
+ const result = await service.getLanguages(apiKey, options);
66
+ if (!result.success) console.error(result.reason);
67
+ else console.log(result.data.languages);
68
+ ```
69
+
8
70
  ## [1.4.0] - 2026-04-02
9
71
 
10
72
  ### Added
package/README.md CHANGED
@@ -7,7 +7,7 @@ Platform-independent core for AI-powered localization — translation API client
7
7
 
8
8
  This is the foundational library for the [ai-l10n](https://www.npmjs.com/package/ai-l10n) ecosystem. It provides the low-level translation API client, logger interface, and language utilities used by the SDK and CLI.
9
9
 
10
- Powered by [l10n.dev](https://l10n.dev).
10
+ Powered by [l10n](https://l10n.dev).dev
11
11
 
12
12
  ## Installation
13
13
 
@@ -117,7 +117,7 @@ const service = new L10nTranslationService(customLogger);
117
117
 
118
118
  #### Methods
119
119
 
120
- ##### `translate(request: TranslationRequest, apiKey: string): Promise<TranslationResult | null>`
120
+ ##### `translate(request: TranslationRequest, apiKey: string): Promise<TranslationResponse>`
121
121
 
122
122
  Translates localization content using the l10n.dev API.
123
123
 
@@ -125,11 +125,44 @@ Translates localization content using the l10n.dev API.
125
125
  - `request: TranslationRequest` — Translation request configuration
126
126
  - `apiKey: string` — API key for authentication
127
127
 
128
- **Returns:** `Promise<TranslationResult | null>` — Translation result, or `null` for 401/402 responses
128
+ **Returns:** `Promise<TranslationResponse>` — Always resolves (never throws). Check `success` field to determine success or failure.
129
129
 
130
- **Throws:** Error for 400, 413, 500, and other failure conditions
130
+ **Example:**
131
+ ```typescript
132
+ const response = await service.translate(request, apiKey);
133
+ if (!response.success) {
134
+ console.error(response.message, 'reason:', response.reason);
135
+ if (response.reason === 'paymentRequired') {
136
+ console.log('Current balance:', response.currentBalance);
137
+ }
138
+ } else {
139
+ console.log('Translated:', response.data.translations);
140
+ console.log('Balance remaining:', response.currentBalance);
141
+ }
142
+ ```
143
+
144
+ ##### `getBalance(apiKey: string): Promise<ApiResponse<BalanceResponse>>`
145
+
146
+ Retrieves the current character balance available for translation.
147
+
148
+ **Parameters:**
149
+ - `apiKey: string` — API key for authentication
150
+
151
+ **Returns:** `Promise<ApiResponse<BalanceResponse>>` — Always resolves (never throws). Check `success` to determine success or failure. On success, `data.currentBalance` holds the number of characters available.
131
152
 
132
- ##### `predictLanguages(input: string, limit?: number): Promise<Language[]>`
153
+ **Error reasons:** `noApiKey`, `unauthorized`, `networkError`
154
+
155
+ **Example:**
156
+ ```typescript
157
+ const response = await service.getBalance(apiKey);
158
+ if (!response.success) {
159
+ console.error(response.message, 'reason:', response.reason);
160
+ } else {
161
+ console.log(`Available: ${response.data.currentBalance.toLocaleString()} characters`);
162
+ }
163
+ ```
164
+
165
+ ##### `predictLanguages(input: string, limit?: number): Promise<ApiResponse<Language[]>>`
133
166
 
134
167
  Predicts possible language codes from a text input (language name in English or native language, region, or script).
135
168
 
@@ -137,39 +170,76 @@ Predicts possible language codes from a text input (language name in English or
137
170
  - `input: string` — Text to analyze
138
171
  - `limit?: number` — Maximum number of predictions (default: 10)
139
172
 
140
- **Returns:** `Promise<Language[]>`Array of predicted languages with codes and names
173
+ **Returns:** `Promise<ApiResponse<Language[]>>`Always resolves (never throws). On success, `data` is an array of predicted languages with codes and names.
174
+
175
+ **Error reasons:** `networkError`
141
176
 
142
- ##### `getLanguages(options?: { codes?: string[]; proficiencyLevels?: LanguageProficiencyLevel[] }): Promise<SupportedLanguagesResponse>`
177
+ ##### `getLanguages(apiKey: string, options?: { codes?: string[]; proficiencyLevels?: LanguageProficiencyLevel[] }): Promise<ApiResponse<SupportedLanguagesResponse>>`
143
178
 
144
179
  Retrieves a list of supported languages, optionally filtered by language codes or proficiency levels.
145
180
 
146
181
  **Parameters:**
182
+ - `apiKey: string` — API key for authentication
147
183
  - `options?` — Optional filter object
148
184
  - `codes?: string[]` — Filter by specific language codes (e.g., `["en", "es", "fr"]`)
149
185
  - `proficiencyLevels?: LanguageProficiencyLevel[]` — Filter by proficiency level: `"strong"`, `"high"`, `"moderate"`, or `"limited"`
150
186
 
151
- **Returns:** `Promise<SupportedLanguagesResponse>`Object containing a `languages` array of `SupportedLanguage` entries
187
+ **Returns:** `Promise<ApiResponse<SupportedLanguagesResponse>>`Always resolves (never throws). On success, `data.languages` is an array of `SupportedLanguage` entries.
152
188
 
153
- **Throws:** Error if the API request fails (non-2xx response)
189
+ **Error reasons:** `noApiKey`, `unauthorized`, `badRequest`, `networkError`
154
190
 
155
191
  **Example:**
156
192
  ```typescript
157
193
  // Get all supported languages
158
- const { languages } = await service.getLanguages();
194
+ const response = await service.getLanguages(apiKey);
195
+ if (!response.success) {
196
+ console.error(response.message, 'reason:', response.reason);
197
+ } else {
198
+ const { languages } = response.data;
199
+ }
159
200
 
160
201
  // Filter by proficiency level
161
- const { languages } = await service.getLanguages({
202
+ const response = await service.getLanguages(apiKey, {
162
203
  proficiencyLevels: ['strong', 'high'],
163
204
  });
164
205
 
165
206
  // Filter by specific codes
166
- const { languages } = await service.getLanguages({
207
+ const response = await service.getLanguages(apiKey, {
167
208
  codes: ['en', 'es', 'fr'],
168
209
  });
169
210
  ```
170
211
 
171
212
  #### Types
172
213
 
214
+ ##### ApiResponse\<T\>
215
+
216
+ ```typescript
217
+ type ApiResponse<T> =
218
+ | { success: true; data: T }
219
+ | { success: false; reason: string; message: string };
220
+ ```
221
+
222
+ All methods that contact the API return an `ApiResponse<T>`. Check `success` before accessing `data`.
223
+
224
+ ##### TranslationResponse
225
+
226
+ ```typescript
227
+ type TranslationResponse = ApiResponse<TranslationResult> & { currentBalance?: number };
228
+ ```
229
+
230
+ The union distributes over the intersection, so `currentBalance?` is available on both branches:
231
+ - On `success: true`: `data` holds the `TranslationResult`, `currentBalance` is the remaining balance.
232
+ - On `success: false`: `reason` and `message` describe the error, `currentBalance` is set when `reason` is `"paymentRequired"`.
233
+
234
+ ##### BalanceResponse
235
+
236
+ ```typescript
237
+ interface BalanceResponse {
238
+ /** Current balance of characters available for translation. */
239
+ currentBalance: number;
240
+ }
241
+ ```
242
+
173
243
  ##### TranslationRequest
174
244
 
175
245
  ```typescript
@@ -192,9 +262,6 @@ interface TranslationRequest {
192
262
  /** Translate metadata along with UI strings (e.g., ARB `@key` descriptions) */
193
263
  translateMetadata?: boolean;
194
264
 
195
- /** Return translations as JSON string */
196
- returnTranslationsAsString: boolean;
197
-
198
265
  /** Client identifier */
199
266
  client: string;
200
267
 
@@ -284,9 +351,6 @@ enum FinishReason {
284
351
  /** Some content was filtered due to content policy */
285
352
  contentFilter = "contentFilter",
286
353
 
287
- /** Insufficient character balance */
288
- insufficientBalance = "insufficientBalance",
289
-
290
354
  /** Translation failed with error */
291
355
  error = "error"
292
356
  }
@@ -319,17 +383,43 @@ interface SupportedLanguage {
319
383
 
320
384
  #### Error Handling
321
385
 
322
- The service throws errors for:
386
+ All methods always resolve — they never throw. Check `response.success`:
387
+
388
+ ##### `translate()` error reasons
389
+
390
+ | `reason` | Description |
391
+ |----------|-------------|
392
+ | `"noApiKey"` | API key was not provided |
393
+ | `"unauthorized"` | API key is invalid (401) |
394
+ | `"paymentRequired"` | Insufficient balance (402); `currentBalance` is set |
395
+ | `"badRequest"` | Validation error (400); `message` contains details |
396
+ | `"requestTooLarge"` | Request exceeds 5 MB (413) |
397
+ | `"serverError"` | Internal server error (500) |
398
+ | `"networkError"` | Connection or other failure |
399
+ | `"translationError"` | API returned `finishReason: "error"` |
400
+
401
+ ##### `getBalance()` error reasons
402
+
403
+ | `reason` | Description |
404
+ |----------|-------------|
405
+ | `"noApiKey"` | API key was not provided |
406
+ | `"unauthorized"` | API key is invalid (401) |
407
+ | `"networkError"` | Connection or other failure |
408
+
409
+ ##### `getLanguages()` error reasons
323
410
 
324
- - **400 Bad Request** — Validation error with details
325
- - **413 Request Too Large** — `"Request too large. Maximum request size is 5 MB."`
326
- - **500 Server Error** — `"An internal server error occurred (Error code: ...)"`
411
+ | `reason` | Description |
412
+ |----------|-------------|
413
+ | `"noApiKey"` | API key was not provided |
414
+ | `"unauthorized"` | API key is invalid (401) |
415
+ | `"badRequest"` | Validation error (400); `message` contains details |
416
+ | `"networkError"` | Connection or other failure |
327
417
 
328
- Returns `null` with error logged for:
418
+ ##### `predictLanguages()` error reasons
329
419
 
330
- - **Missing API Key** — `"API Key not set. Please configure your API Key first."`
331
- - **401 Unauthorized** — `"Unauthorized. Please check your API Key."`
332
- - **402 Payment Required** — `"Not enough characters remaining for this translation..."`
420
+ | `reason` | Description |
421
+ |----------|-------------|
422
+ | `"networkError"` | Connection or other failure |
333
423
 
334
424
  ---
335
425
 
@@ -484,4 +574,4 @@ MIT
484
574
 
485
575
  ## Credits
486
576
 
487
- Powered by [l10n.dev](https://l10n.dev) — AI-powered localization service
577
+ Powered by [l10n](https://l10n.dev).dev — AI-powered localization service
@@ -19,8 +19,4 @@ export declare class ConsoleLogger implements ILogger {
19
19
  * Log error message
20
20
  */
21
21
  logError(message: string, error?: unknown): void;
22
- /**
23
- * Log error details to console
24
- */
25
- private logErrorDetails;
26
22
  }
@@ -9,12 +9,11 @@ class ConsoleLogger {
9
9
  * Show and log error message
10
10
  */
11
11
  showAndLogError(message, error, context, linkBtnText, url) {
12
- console.error(`❌ ${message}`);
13
- if (context) {
14
- console.error(`Context: ${context}`);
15
- }
16
12
  if (error) {
17
- this.logErrorDetails(error, console.error);
13
+ console.error(`❌ ${message}`, context ?? "", error);
14
+ }
15
+ else {
16
+ console.error(`❌ ${message}`, context ?? "");
18
17
  }
19
18
  if (linkBtnText && url) {
20
19
  console.error(`${linkBtnText}: ${url}`);
@@ -30,32 +29,22 @@ class ConsoleLogger {
30
29
  * Log warning message
31
30
  */
32
31
  logWarning(message, error) {
33
- console.warn(`⚠️ ${message}`);
34
32
  if (error) {
35
- this.logErrorDetails(error, console.warn);
33
+ console.warn(`⚠️ ${message}`, error);
34
+ }
35
+ else {
36
+ console.warn(`⚠️ ${message}`);
36
37
  }
37
38
  }
38
39
  /**
39
40
  * Log error message
40
41
  */
41
42
  logError(message, error) {
42
- console.error(`❌ ${message}`);
43
43
  if (error) {
44
- this.logErrorDetails(error, console.error);
45
- }
46
- }
47
- /**
48
- * Log error details to console
49
- */
50
- logErrorDetails(error, logFn) {
51
- if (error instanceof Error) {
52
- logFn(`Error: ${error.message}`);
53
- if (error.stack) {
54
- logFn(error.stack);
55
- }
44
+ console.error(`❌ ${message}`, error);
56
45
  }
57
46
  else {
58
- logFn(`Error: ${String(error)}`);
47
+ console.error(`❌ ${message}`);
59
48
  }
60
49
  }
61
50
  }
@@ -16,7 +16,6 @@ export interface TranslationRequest {
16
16
  useShortening?: boolean;
17
17
  generatePluralForms?: boolean;
18
18
  translateMetadata?: boolean;
19
- returnTranslationsAsString: boolean;
20
19
  client: string;
21
20
  translateOnlyNewStrings?: boolean;
22
21
  targetStrings?: string;
@@ -50,7 +49,6 @@ export declare enum FinishReason {
50
49
  stop = "stop",
51
50
  length = "length",
52
51
  contentFilter = "contentFilter",
53
- insufficientBalance = "insufficientBalance",
54
52
  error = "error"
55
53
  }
56
54
  export interface Language {
@@ -82,13 +80,51 @@ export interface SupportedLanguage {
82
80
  export interface SupportedLanguagesResponse {
83
81
  languages: SupportedLanguage[];
84
82
  }
83
+ export interface BalanceResponse {
84
+ /** Current balance of characters available for translation. */
85
+ currentBalance: number;
86
+ }
87
+ /**
88
+ * Discriminated union returned by all public service methods.
89
+ * Check `success` to narrow to the data or error branch.
90
+ */
91
+ export type ApiResponse<T> = {
92
+ success: true;
93
+ data: T;
94
+ } | {
95
+ success: false;
96
+ reason: string;
97
+ message: string;
98
+ };
99
+ /**
100
+ * Response from translate(). Extends ApiResponse<TranslationResult> with an
101
+ * optional currentBalance field present on both the success and error branches.
102
+ *
103
+ * On success: data contains the TranslationResult; currentBalance is the remaining balance.
104
+ * On error: reason and message describe the failure; currentBalance is set on 402 errors.
105
+ */
106
+ export type TranslationResponse = ApiResponse<TranslationResult> & {
107
+ currentBalance?: number;
108
+ };
85
109
  export declare class L10nTranslationService {
86
110
  private readonly logger;
87
111
  constructor(logger?: ILogger);
88
- getLanguages(options?: {
112
+ getBalance(apiKey: string): Promise<ApiResponse<BalanceResponse>>;
113
+ getLanguages(apiKey: string, options?: {
89
114
  codes?: string[];
90
115
  proficiencyLevels?: LanguageProficiencyLevel[];
91
- }): Promise<SupportedLanguagesResponse>;
92
- predictLanguages(input: string, limit?: number): Promise<Language[]>;
93
- translate(request: TranslationRequest, apiKey: string): Promise<TranslationResult | null>;
116
+ }): Promise<ApiResponse<SupportedLanguagesResponse>>;
117
+ predictLanguages(input: string, limit?: number): Promise<ApiResponse<Language[]>>;
118
+ translate(request: TranslationRequest, apiKey: string): Promise<TranslationResponse>;
119
+ private checkApiKey;
120
+ /**
121
+ * Maps an HTTP error to a structured ApiResponse error.
122
+ * Handles 400 (with validation error extraction), 401, 403, 429, 502, 503, and 500.
123
+ */
124
+ private handleErrorResponse;
125
+ /**
126
+ * Attempts to parse the response body as JSON. If parsing fails, returns the raw text.
127
+ * Logs a warning if JSON parsing fails.
128
+ */
129
+ private parseErrorBody;
94
130
  }