@concircle/i18n-ai-translator 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 +475 -0
- package/bin/i18n-ai-translator.mjs +6 -0
- package/dist/cli.cjs +6 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +21 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.mjs +6 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +6 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +149 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.mjs +6 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-DF4QMkU1.d.cts +151 -0
- package/dist/types-DF4QMkU1.d.ts +151 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Herbert Kaintz - Concircle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# @concircle/i18n-ai-translator
|
|
2
|
+
|
|
3
|
+
AI-powered internationalization (i18n) translator for UI5 applications. Automatically translates `i18n.properties` files to multiple languages using OpenAI, with support for glossaries, placeholder preservation, and intelligent caching.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- π€ **AI-Powered Translation**: Uses OpenAI GPT-4 for accurate, context-aware translations
|
|
8
|
+
- π **Placeholder Preservation**: Protects UI5 placeholders like `{0}`, `{name}`, `%s`, `${variable}` from being translated
|
|
9
|
+
- π **Glossary Support**: Define domain-specific vocabulary (e.g., SAP terminology) for consistent translations
|
|
10
|
+
- π **Batch Processing**: Parallel translation requests for efficiency
|
|
11
|
+
- πΎ **Smart Caching**: Local file-based cache with TTL and glossary hash invalidation
|
|
12
|
+
- π **Extensible Architecture**: Plugin-ready for future AI providers (Claude, Gemini, etc.)
|
|
13
|
+
- π¦ **Dual Format**: CommonJS and ES Modules support
|
|
14
|
+
- β
**Fully Tested**: Comprehensive test suite with focus on placeholder preservation
|
|
15
|
+
- π **TypeScript**: Full type safety and IntelliSense support
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @concircle/i18n-ai-translator
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Install From Git In Another App
|
|
24
|
+
|
|
25
|
+
If you want to use the CLI from another application's `npm run` scripts without publishing to npm yet, install this package directly from your Git remote:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -D git+ssh://git@github.com/DEIN-ORG/DEIN-REPO.git#main
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This package runs `prepare` on Git-based installs, so the CLI is built automatically during installation.
|
|
32
|
+
|
|
33
|
+
Then add a script in the consuming app:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"scripts": {
|
|
38
|
+
"i18n:translate": "i18n-ai-translator --input ./webapp/i18n/i18n.properties --languages de,fr --mode missing"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If your target system expects Java-style Unicode escapes in `.properties` files, enable them with:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"scripts": {
|
|
48
|
+
"i18n:translate": "i18n-ai-translator --input ./webapp/i18n/i18n.properties --languages de,fr --mode missing --encode-unicode"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or set it in your config file:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"encodeUnicode": true
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you only want escapes for specific target languages, use per-language overrides:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"encodeUnicode": false,
|
|
66
|
+
"languageOptions": {
|
|
67
|
+
"de": { "encodeUnicode": true },
|
|
68
|
+
"uk": { "encodeUnicode": false }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This keeps languages like Ukrainian readable in UTF-8:
|
|
74
|
+
|
|
75
|
+
```properties
|
|
76
|
+
ai.button_tooltip = Π¨Π ΠΏΠ»Π°Π½ΡΠ²Π°Π»ΡΠ½ΠΈΠΊ
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run it with:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run i18n:translate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
### Basic Usage (ES Modules)
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import { Translator } from '@concircle/i18n-ai-translator';
|
|
91
|
+
|
|
92
|
+
const translator = new Translator({
|
|
93
|
+
provider: 'openai',
|
|
94
|
+
openai: {
|
|
95
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
96
|
+
},
|
|
97
|
+
targetLanguages: ['de', 'fr'],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = await translator.translate({
|
|
101
|
+
inputPath: './src/i18n/i18n.properties',
|
|
102
|
+
outputFormat: 'new-files',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log('Translation complete!', result);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### With Glossary (Domain Vocabulary)
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const translator = new Translator({
|
|
112
|
+
provider: 'openai',
|
|
113
|
+
openai: { apiKey: process.env.OPENAI_API_KEY },
|
|
114
|
+
targetLanguages: ['de'],
|
|
115
|
+
glossary: {
|
|
116
|
+
'SAP': { doNotTranslate: true },
|
|
117
|
+
'HANA': { translation: 'SAP HANA', context: 'database' },
|
|
118
|
+
'Fiori': { doNotTranslate: true },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Ensures SAP terms are correctly handled during translation
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Load Config from File
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
import { loadConfig, Translator } from '@concircle/i18n-ai-translator';
|
|
129
|
+
|
|
130
|
+
const config = loadConfig('./config.json');
|
|
131
|
+
const translator = new Translator(config);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
### TranslatorConfig Interface
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
interface TranslatorConfig {
|
|
140
|
+
provider: 'openai'; // Future: 'claude', 'gemini'
|
|
141
|
+
openai: {
|
|
142
|
+
apiKey: string; // Required: OpenAI API key
|
|
143
|
+
model?: string; // Default: 'gpt-4'
|
|
144
|
+
temperature?: number; // Default: 0.3
|
|
145
|
+
maxTokens?: number; // Default: 2000
|
|
146
|
+
};
|
|
147
|
+
targetLanguages: string[]; // Required: e.g., ['de', 'fr', 'es']
|
|
148
|
+
encodeUnicode?: boolean; // Default: false, escape non-ASCII as \uXXXX
|
|
149
|
+
languageOptions?: Record<string, {
|
|
150
|
+
encodeUnicode?: boolean; // Per-language override
|
|
151
|
+
}>;
|
|
152
|
+
glossary?: Glossary; // Optional: Domain-specific vocabulary
|
|
153
|
+
cache?: {
|
|
154
|
+
enabled?: boolean; // Default: true
|
|
155
|
+
ttl?: number; // Default: 7 days (ms)
|
|
156
|
+
dir?: string; // Default: ~/.i18n-ai-translator-cache
|
|
157
|
+
};
|
|
158
|
+
batchSize?: number; // Default: 10 (parallel requests)
|
|
159
|
+
debug?: boolean; // Default: false
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Example Configuration File (config.json)
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"provider": "openai",
|
|
168
|
+
"openai": {
|
|
169
|
+
"apiKey": "sk-...",
|
|
170
|
+
"model": "gpt-4",
|
|
171
|
+
"temperature": 0.3,
|
|
172
|
+
"maxTokens": 2000
|
|
173
|
+
},
|
|
174
|
+
"targetLanguages": ["de", "fr", "es"],
|
|
175
|
+
"encodeUnicode": false,
|
|
176
|
+
"languageOptions": {
|
|
177
|
+
"de": { "encodeUnicode": true },
|
|
178
|
+
"uk": { "encodeUnicode": false }
|
|
179
|
+
},
|
|
180
|
+
"glossary": "./glossary.json",
|
|
181
|
+
"cache": {
|
|
182
|
+
"enabled": true,
|
|
183
|
+
"ttl": 604800000
|
|
184
|
+
},
|
|
185
|
+
"batchSize": 10,
|
|
186
|
+
"debug": false
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Example Glossary File (glossary.json)
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"SAP": {
|
|
195
|
+
"term": "SAP",
|
|
196
|
+
"translation": "SAP",
|
|
197
|
+
"doNotTranslate": true,
|
|
198
|
+
"context": "Enterprise Resource Planning System"
|
|
199
|
+
},
|
|
200
|
+
"HANA": {
|
|
201
|
+
"term": "HANA",
|
|
202
|
+
"translation": "SAP HANA",
|
|
203
|
+
"doNotTranslate": true,
|
|
204
|
+
"context": "In-memory database"
|
|
205
|
+
},
|
|
206
|
+
"module": {
|
|
207
|
+
"term": "module",
|
|
208
|
+
"translation": "Modul",
|
|
209
|
+
"context": "German translation"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## API Reference
|
|
215
|
+
|
|
216
|
+
### Translator Class
|
|
217
|
+
|
|
218
|
+
#### Constructor
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
new Translator(config: TranslatorConfig): Translator
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Methods
|
|
225
|
+
|
|
226
|
+
##### `translate(job: TranslationJob): Promise<TranslationResult>`
|
|
227
|
+
|
|
228
|
+
Translate a properties file to configured target languages.
|
|
229
|
+
|
|
230
|
+
**Parameters:**
|
|
231
|
+
- `job.inputPath`: Path to source `i18n.properties` file
|
|
232
|
+
- `job.outputFormat`: `'new-files'` | `'update-existing'` (default: `'new-files'`)
|
|
233
|
+
- `job.outputDir`: Output directory for new files
|
|
234
|
+
- `job.languages`: Override config languages for this job
|
|
235
|
+
|
|
236
|
+
**Returns:** Object with translation results per language
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
const result = await translator.translate({
|
|
240
|
+
inputPath: './src/i18n/i18n.properties',
|
|
241
|
+
outputFormat: 'new-files',
|
|
242
|
+
outputDir: './src/i18n',
|
|
243
|
+
languages: ['de'], // Optional: override
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Result example:
|
|
247
|
+
// {
|
|
248
|
+
// sourceFile: './src/i18n/i18n.properties',
|
|
249
|
+
// translations: {
|
|
250
|
+
// de: {
|
|
251
|
+
// outputFile: './src/i18n/i18n_de.properties',
|
|
252
|
+
// success: true,
|
|
253
|
+
// translatedKeysCount: 42
|
|
254
|
+
// },
|
|
255
|
+
// fr: {
|
|
256
|
+
// outputFile: './src/i18n/i18n_fr.properties',
|
|
257
|
+
// success: true,
|
|
258
|
+
// translatedKeysCount: 42
|
|
259
|
+
// }
|
|
260
|
+
// }
|
|
261
|
+
// }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
##### `clearCache(): void`
|
|
265
|
+
|
|
266
|
+
Clear the translation cache (useful for testing or after glossary changes)
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
translator.clearCache();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
##### `getCacheStats(): object`
|
|
273
|
+
|
|
274
|
+
Get cache statistics
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
const stats = translator.getCacheStats();
|
|
278
|
+
// { enabled: true, cacheDir: '...', ttl: ..., entriesCount: 5 }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
##### `getGlossary(): Glossary`
|
|
282
|
+
|
|
283
|
+
Get current glossary
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
const glossary = translator.getGlossary();
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
##### `getGlossaryTerms(): string[]`
|
|
290
|
+
|
|
291
|
+
Get all glossary terms
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
const terms = translator.getGlossaryTerms();
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## UI5 Properties File Format
|
|
298
|
+
|
|
299
|
+
### Input Example (i18n.properties)
|
|
300
|
+
|
|
301
|
+
```properties
|
|
302
|
+
# Application messages
|
|
303
|
+
app.title=My Application
|
|
304
|
+
app.version=1.0.0
|
|
305
|
+
|
|
306
|
+
# Messages with placeholders (will be preserved)
|
|
307
|
+
msg.save=Save {0} items
|
|
308
|
+
msg.delete=Delete {0} from {1}
|
|
309
|
+
|
|
310
|
+
# Named placeholders
|
|
311
|
+
form.email=Please enter {email}
|
|
312
|
+
|
|
313
|
+
# Format specifiers (preserved)
|
|
314
|
+
msg.count=Found %d results
|
|
315
|
+
msg.user=User %s not found
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Output Example (i18n_de.properties)
|
|
319
|
+
|
|
320
|
+
```properties
|
|
321
|
+
# Application messages
|
|
322
|
+
app.title=Meine Anwendung
|
|
323
|
+
app.version=1.0.0
|
|
324
|
+
|
|
325
|
+
# Messages with placeholders (PRESERVED unchanged)
|
|
326
|
+
msg.save=Speichern Sie {0} Elemente
|
|
327
|
+
msg.delete=LΓΆschen Sie {0} von {1}
|
|
328
|
+
|
|
329
|
+
# Named placeholders
|
|
330
|
+
form.email=Bitte geben Sie {email} ein
|
|
331
|
+
|
|
332
|
+
# Format specifiers (PRESERVED)
|
|
333
|
+
msg.count=Es wurden %d Ergebnisse gefunden
|
|
334
|
+
msg.user=Benutzer %s nicht gefunden
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Placeholder Protection
|
|
338
|
+
|
|
339
|
+
All placeholder types are automatically detected and preserved during translation:
|
|
340
|
+
|
|
341
|
+
- `{0}`, `{1}`, etc. - Numeric placeholders
|
|
342
|
+
- `{name}`, `{email}`, etc. - Named placeholders
|
|
343
|
+
- `%s`, `%d`, etc. - Printf-style format specifiers
|
|
344
|
+
- `${variable}` - Template variable syntax
|
|
345
|
+
- `%1$s`, `%2$d`, etc. - Positional format specifiers
|
|
346
|
+
|
|
347
|
+
## Environment Variables
|
|
348
|
+
|
|
349
|
+
Configuration can also be provided via environment variables:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
export OPENAI_API_KEY=sk-...
|
|
353
|
+
export OPENAI_MODEL=gpt-4
|
|
354
|
+
export TARGET_LANGUAGES=de,fr,es
|
|
355
|
+
export GLOSSARY_FILE=./glossary.json
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Examples
|
|
359
|
+
|
|
360
|
+
See `examples/` directory for complete working examples:
|
|
361
|
+
|
|
362
|
+
- `basic.mjs` - Minimal setup with environment variables
|
|
363
|
+
- `with-glossary.mjs` - Using domain-specific vocabulary
|
|
364
|
+
- `advanced.mjs` - Full configuration with caching and debugging
|
|
365
|
+
|
|
366
|
+
Run examples:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
export OPENAI_API_KEY=sk-...
|
|
370
|
+
node examples/basic.mjs
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Caching
|
|
374
|
+
|
|
375
|
+
The translator uses smart, local file-based caching to:
|
|
376
|
+
|
|
377
|
+
- **Reduce API costs**: Skip repeated translations
|
|
378
|
+
- **Improve performance**: Instant retrieval from cache
|
|
379
|
+
- **Invalidate on glossary changes**: Cache is invalidated when glossary is updated
|
|
380
|
+
- **Support TTL**: Default 7-day cache expiration
|
|
381
|
+
|
|
382
|
+
Cache location: `~/.i18n-ai-translator-cache/`
|
|
383
|
+
|
|
384
|
+
### Cache Configuration
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
cache: {
|
|
388
|
+
enabled: true, // Enable/disable caching
|
|
389
|
+
ttl: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
|
|
390
|
+
dir: '/custom/cache/dir' // Custom cache directory
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Performance & Batching
|
|
395
|
+
|
|
396
|
+
Translations are processed in parallel batches for efficiency:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
batchSize: 10 // Process up to 10 translations in parallel
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
This balances between:
|
|
403
|
+
- **Throughput**: Parallel processing speeds up translation
|
|
404
|
+
- **API Limits**: Stays within OpenAI rate limits (default 10 concurrent)
|
|
405
|
+
- **Cost**: Fewer API calls due to combining texts
|
|
406
|
+
|
|
407
|
+
## Future Provider Support
|
|
408
|
+
|
|
409
|
+
The architecture supports adding new AI providers. Implement the `AIProvider` interface:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
interface AIProvider {
|
|
413
|
+
translateTexts(texts: string[], targetLanguage: string, context?: string): Promise<string[]>;
|
|
414
|
+
getName(): string;
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Future providers planned:
|
|
419
|
+
- Claude (Anthropic)
|
|
420
|
+
- Gemini (Google)
|
|
421
|
+
- Offline/Local models
|
|
422
|
+
|
|
423
|
+
## Error Handling
|
|
424
|
+
|
|
425
|
+
The translator provides detailed error information:
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
try {
|
|
429
|
+
const result = await translator.translate({...});
|
|
430
|
+
|
|
431
|
+
for (const [lang, langResult] of Object.entries(result.translations)) {
|
|
432
|
+
if (!langResult.success) {
|
|
433
|
+
console.error(`Translation failed for ${lang}: ${langResult.error}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error('Translation job failed:', error.message);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Testing
|
|
442
|
+
|
|
443
|
+
Run the comprehensive test suite:
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
npm test # Run tests
|
|
447
|
+
npm run test:watch # Watch mode
|
|
448
|
+
npm run test:coverage # Coverage report
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Tests include:
|
|
452
|
+
- β
Placeholder extraction and restoration (critical)
|
|
453
|
+
- β
Properties file parsing and writing
|
|
454
|
+
- β
Glossary management and injection
|
|
455
|
+
- β
Cache behavior and TTL
|
|
456
|
+
- β
Configuration validation
|
|
457
|
+
|
|
458
|
+
## License
|
|
459
|
+
|
|
460
|
+
MIT Β© 2026 Herbert Kaintz - Concircle
|
|
461
|
+
|
|
462
|
+
## Contributing
|
|
463
|
+
|
|
464
|
+
Contributions welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
465
|
+
|
|
466
|
+
## Security
|
|
467
|
+
|
|
468
|
+
Please report security vulnerabilities to security contacts via email rather than public issues. See [SECURITY.md](./SECURITY.md) for details.
|
|
469
|
+
|
|
470
|
+
## Support
|
|
471
|
+
|
|
472
|
+
- π [API Documentation](./docs/)
|
|
473
|
+
- π [Examples](./examples/)
|
|
474
|
+
- π€ [Contributing Guide](./CONTRIBUTING.md)
|
|
475
|
+
- π [Security Policy](./SECURITY.md)
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";var ge=Object.create;var L=Object.defineProperty;var de=Object.getOwnPropertyDescriptor;var pe=Object.getOwnPropertyNames;var fe=Object.getPrototypeOf,me=Object.prototype.hasOwnProperty;var he=(r,e)=>{for(var t in e)L(r,t,{get:e[t],enumerable:!0})},z=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of pe(e))!me.call(r,o)&&o!==t&&L(r,o,{get:()=>e[o],enumerable:!(n=de(e,o))||n.enumerable});return r};var g=(r,e,t)=>(t=r!=null?ge(fe(r)):{},z(e||!r||!r.__esModule?L(t,"default",{value:r,enumerable:!0}):t,r)),ye=r=>z(L({},"__esModule",{value:!0}),r);var Be={};he(Be,{parseArgs:()=>ue,runCli:()=>ze});module.exports=ye(Be);var se=g(require("path"),1);var B=g(require("crypto"),1),u=g(require("fs"),1),x=g(require("path"),1),b=class{constructor(e){this.enabled=e?.enabled??!0,this.ttlMs=e?.ttlMs??10080*60*1e3;let t=process.env.HOME||process.env.USERPROFILE||"/tmp";this.cacheDir=e?.dir??x.default.join(t,".i18n-ai-translator-cache"),this.enabled&&!u.default.existsSync(this.cacheDir)&&u.default.mkdirSync(this.cacheDir,{recursive:!0})}get(e){if(!this.enabled)return null;let t=x.default.join(this.cacheDir,`${e}.json`);if(!u.default.existsSync(t))return null;try{let n=JSON.parse(u.default.readFileSync(t,"utf-8"));return Date.now()-n.createdAt>this.ttlMs?(u.default.unlinkSync(t),null):n.translation}catch{return null}}set(e,t){if(!this.enabled)return;let n=x.default.join(this.cacheDir,`${e}.json`),o={translation:t,createdAt:Date.now()};try{u.default.writeFileSync(n,JSON.stringify(o),"utf-8")}catch{}}clear(){if(!(!this.enabled||!u.default.existsSync(this.cacheDir)))for(let e of u.default.readdirSync(this.cacheDir))e.endsWith(".json")&&u.default.unlinkSync(x.default.join(this.cacheDir,e))}getStats(){let e=0;return this.enabled&&u.default.existsSync(this.cacheDir)&&(e=u.default.readdirSync(this.cacheDir).filter(t=>t.endsWith(".json")).length),{enabled:this.enabled,ttlMs:this.ttlMs,cacheDir:this.cacheDir,entriesCount:e}}static createKey(e){let t=JSON.stringify(e);return B.default.createHash("sha256").update(t).digest("hex")}};var N=g(require("fs"),1),P=class r{constructor(e){this.glossary=e??{}}static loadGlossary(e){return e?typeof e=="string"?r.loadFromFile(e):(r.validateGlossary(e),e):{}}static loadFromFile(e){if(!N.default.existsSync(e))throw new Error(`Glossary file not found: ${e}`);let t=N.default.readFileSync(e,"utf-8"),n=JSON.parse(t);return r.validateGlossary(n),n}static validateGlossary(e){if(!e||typeof e!="object"||Array.isArray(e))throw new Error("Glossary must be an object");let t=e;if(r.validateTerms(t.shared,"shared"),t.languages)for(let[n,o]of Object.entries(t.languages))r.validateTerms(o,`languages.${n}`)}static validateTerms(e,t){if(e){if(!Array.isArray(e))throw new Error(`${t} glossary terms must be an array`);for(let n of e){if(!n||typeof n!="object")throw new Error(`${t} glossary term must be an object`);if(!n.source||typeof n.source!="string")throw new Error(`${t} glossary term requires a string source value`);if(n.target!==void 0&&typeof n.target!="string")throw new Error(`${t} glossary term target must be a string`);if(n.context!==void 0&&typeof n.context!="string")throw new Error(`${t} glossary term context must be a string`);if(n.doNotTranslate!==void 0&&typeof n.doNotTranslate!="boolean")throw new Error(`${t} glossary term doNotTranslate must be a boolean`)}}}getGlossary(){return this.glossary}getTermsForLanguage(e){return[...this.glossary.shared??[],...this.glossary.languages?.[e]??[]]}hasTerms(e){return this.getTermsForLanguage(e).length>0}toJSON(){return JSON.stringify(this.glossary,null,2)}};var ve=/\{\d+\}|\{[a-zA-Z_][\w.-]*\}|\$\{[a-zA-Z_][\w.-]*\}|%\d+\$[sdif]|%[sdif]/g,we=/\\[nrtf]/g,J=new RegExp(`${ve.source}|${we.source}`,"g");function W(r){return Array.from(r.matchAll(J),e=>e[0])}function q(r){let e=[],t=0,n=r.replace(J,o=>{let s=`__I18N_PH_${t}__`;return t+=1,e.push({token:s,placeholder:o}),s});return{original:r,masked:n,tokens:e}}function Y(r,e){return e.reduce((t,n)=>t.split(n.token).join(n.placeholder),r)}function H(r,e){let t=W(r).sort(),n=W(e).sort();return{valid:t.length===n.length&&t.every((o,s)=>o===n[s]),expected:t,actual:n}}var O=g(require("fs"),1),w=g(require("path"),1);function be(r){let t=r.match(/\r\n|\n/)?.[0]??`
|
|
2
|
+
`,n=r.length>0&&r.endsWith(t),o=r.length===0?[]:r.split(/\r?\n/);return n&&o[o.length-1]===""&&o.pop(),{lines:o.map(Oe),eol:t,hasTrailingNewline:n}}function M(r){return O.default.existsSync(r)?be(O.default.readFileSync(r,"utf-8")):Z()}function Z(){return{lines:[],eol:`
|
|
3
|
+
`,hasTrailingNewline:!0}}function U(r){let e={};for(let t of r.lines)t.type==="entry"&&(e[t.key]=t.value);return e}function F(r){return{eol:r.eol,hasTrailingNewline:r.hasTrailingNewline,lines:r.lines.map(e=>({...e}))}}function Q(r,e,t){let n=Ee(r,e);if(n){n.value=t,n.modified=!0;return}r.lines.length>0&&r.lines[r.lines.length-1].type!=="blank"&&r.lines.push({type:"blank",raw:""}),r.lines.push({type:"entry",raw:"",key:e,value:t,separator:"=",leadingWhitespace:"",modified:!0})}function Pe(r,e={}){let n=r.lines.map(o=>Te(o,e)).join(r.eol);return n.length===0?"":r.hasTrailingNewline?`${n}${r.eol}`:n}function ee(r,e,t={}){let n=w.default.dirname(r);O.default.existsSync(n)||O.default.mkdirSync(n,{recursive:!0}),O.default.writeFileSync(r,Pe(e,t),"utf-8")}function re(r,e,t,n){let o=n??w.default.dirname(r),s=w.default.extname(r)||".properties",d=w.default.basename(r,s);if(t){let i=t.replace("{baseName}",d).replace("{language}",e).replace("{ext}",s);return w.default.join(o,i)}let p=d==="i18n"?`i18n_${e}${s}`:`${d}_${e}${s}`;return w.default.join(o,p)}function Oe(r){if(r.trim()==="")return{type:"blank",raw:r};if(/^\s*[#!]/.test(r))return{type:"comment",raw:r};let e=r.match(/^\s*/)[0],t=r.slice(e.length),n=-1,o="=",s=-1;for(let i=0;i<t.length;i+=1){let l=t[i];if((i>0?t[i-1]:"")!=="\\"){if(l==="="||l===":"){n=i,o=l;break}if(l===" "||l===" "||l==="\f"){n=i,o=l;let a=i;for(;a<t.length&&(t[a]===" "||t[a]===" "||t[a]==="\f");)a+=1;a<t.length&&(t[a]==="="||t[a]===":")&&(o=t[a],s=a+1);break}}}let d=n>=0?t.slice(0,n):t,p=n>=0?t.slice(s>=0?s:n+1).replace(/^[ \t\f]*/,""):"";return{type:"entry",raw:r,key:X(d.trimEnd()),value:X(p),separator:o,leadingWhitespace:e,modified:!1}}function Te(r,e){return r.type!=="entry"||!r.modified&&r.raw?r.raw:`${r.leadingWhitespace}${ke(r.key)}${r.separator}${xe(r.value,e)}`}function Ee(r,e){return r.lines.find(t=>t.type==="entry"&&t.key===e)}function X(r){return r.replace(/\\u([0-9a-fA-F]{4})/g,(e,t)=>String.fromCharCode(parseInt(t,16))).replace(/\\t/g," ").replace(/\\r/g,"\r").replace(/\\n/g,`
|
|
4
|
+
`).replace(/\\f/g,"\f").replace(/\\:/g,":").replace(/\\=/g,"=").replace(/\\\\/g,"\\")}function ke(r){return r.replace(/\\/g,"\\\\").replace(/:/g,"\\:").replace(/=/g,"\\=")}function xe(r,e){let t=r.replace(/\\/g,"\\\\").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t");return e.encodeUnicode?Array.from(t,n=>n.charCodeAt(0)>127?`\\u${n.charCodeAt(0).toString(16).padStart(4,"0")}`:n).join(""):t}var te=g(require("openai"),1);var A=class{};var Se={type:"object",additionalProperties:!1,required:["items"],properties:{items:{type:"array",items:{type:"object",additionalProperties:!1,required:["key","translatedValue"],properties:{key:{type:"string"},translatedValue:{type:"string"}}}}}},D=class extends A{constructor(e){if(super(),!e.apiKey)throw new Error("OpenAI API key is required");this.client=new te.default({apiKey:e.apiKey,baseURL:e.baseURL,organization:e.organization}),this.options={...e,model:e.model??"gpt-4.1-mini",temperature:e.temperature??0,maxOutputTokens:e.maxOutputTokens??4e3}}getName(){return"openai"}async translateBatch(e){let n=(await this.client.responses.create({model:e.model??this.options.model,temperature:this.options.temperature,max_output_tokens:this.options.maxOutputTokens,text:{format:{type:"json_schema",name:"translation_batch",schema:Se,strict:!0}},input:[{role:"system",content:Ce()},{role:"user",content:JSON.stringify(Re(e))}]})).output_text?.trim();if(!n)throw new Error("OpenAI Responses API returned an empty response");let o=JSON.parse(n);if(!Array.isArray(o.items))throw new Error("OpenAI response is missing items array");return{provider:"openai",rawResponse:n,items:o.items.map(s=>{if(!s.key||typeof s.translatedValue!="string")throw new Error("OpenAI response contains an invalid translation item");return{key:s.key,translatedValue:s.translatedValue}})}}};function Ce(){return["You translate SAP UI5 i18n property values.","Return JSON only.","Translate values, never keys.","Do not add explanations.","Keep placeholder tokens like __I18N_PH_0__ unchanged.","Preserve surrounding punctuation and semantic meaning.","Apply glossary terms strictly when provided."].join(" ")}function Re(r){return{sourceLanguage:r.sourceLanguage,targetLanguage:r.targetLanguage,glossaryTerms:r.glossaryTerms,rules:r.rules,items:r.items.map(e=>({key:e.key,value:e.maskedValue,placeholders:e.placeholders.map(t=>t.placeholder)}))}}var E=g(require("fs"),1),C=g(require("path"),1),ne=require("url"),G=g(require("dotenv"),1);var Le=["i18n-ai.config.json","i18n-ai.config.mjs","i18n-ai.config.js","ui5-ai-i18n.config.json","ui5-ai-i18n.config.mjs","ui5-ai-i18n.config.js"],Ae=["sourceLanguage","targetLanguages","translationMode","encodeUnicode","languageOptions","provider","providerOptions","files","glossary","cache","rules","batchSize","verbose"],De=["apiKey","model","baseURL","organization","temperature","maxOutputTokens"],Ie=["input","outputDir","languageFilePattern"],$e=["enabled","ttlMs","dir"],_e=["encodeUnicode"];async function I(r){let e=r?.cwd??process.cwd();Ge(e);let t=r?.configPath?C.default.resolve(e,r.configPath):Me(e),n=t?await Ue(t):{};t&&Fe(n,t);let o=Ne(je({sourceLanguage:"en",targetLanguages:[],translationMode:"missing",encodeUnicode:!1,provider:"openai",batchSize:20,verbose:!1,files:{input:"i18n/i18n.properties"},cache:{enabled:!0},rules:[],...n}),r?.overrides);return o.glossary&&(o.glossary=P.loadGlossary(o.glossary)),K(o),o}function K(r){if(!r.targetLanguages||r.targetLanguages.length===0)throw new Error("At least one target language is required");if(r.provider!=="openai")throw new Error(`Unsupported provider: ${r.provider}`);if(!r.files?.input)throw new Error("files.input is required");let e=r.translationMode??"missing";if(e!=="missing"&&e!=="overwrite")throw new Error(`Unsupported translationMode: ${e}`);if(r.batchSize!==void 0&&r.batchSize<1)throw new Error("batchSize must be at least 1");if(!r.providerOptions?.apiKey)throw new Error("OpenAI API key is required via config.providerOptions.apiKey or OPENAI_API_KEY")}function je(r){let e={...r.providerOptions};return!e.apiKey&&process.env.OPENAI_API_KEY&&(e.apiKey=process.env.OPENAI_API_KEY),!e.model&&process.env.OPENAI_MODEL&&(e.model=process.env.OPENAI_MODEL),{...r,providerOptions:e}}function Ne(r,e){return e?{...r,provider:e.provider??r.provider,translationMode:e.translationMode??r.translationMode,encodeUnicode:e.encodeUnicode??r.encodeUnicode,languageOptions:r.languageOptions,targetLanguages:e.targetLanguages??r.targetLanguages,verbose:e.verbose??r.verbose,files:{...r.files,input:e.input??r.files?.input},providerOptions:{...r.providerOptions,model:e.model??r.providerOptions?.model}}:r}function Me(r){for(let e of Le){let t=C.default.join(r,e);if(E.default.existsSync(t))return t}}async function Ue(r){if(!E.default.existsSync(r))throw new Error(`Config file not found: ${r}`);if(r.endsWith(".json"))return JSON.parse(E.default.readFileSync(r,"utf-8"));if(r.endsWith(".js")||r.endsWith(".mjs")){let e=await import((0,ne.pathToFileURL)(r).href);return e.default??e}throw new Error(`Unsupported config format: ${r}`)}function Fe(r,e){if(T(r,"Config file",e),S(r,Ae,"Config file",e),r.providerOptions!==void 0&&(T(r.providerOptions,"config.providerOptions",e),S(r.providerOptions,De,"config.providerOptions",e)),r.files!==void 0&&(T(r.files,"config.files",e),S(r.files,Ie,"config.files",e)),r.cache!==void 0&&(T(r.cache,"config.cache",e),S(r.cache,$e,"config.cache",e)),r.languageOptions!==void 0){T(r.languageOptions,"config.languageOptions",e);for(let[t,n]of Object.entries(r.languageOptions))T(n,`config.languageOptions.${t}`,e),S(n,_e,`config.languageOptions.${t}`,e)}}function S(r,e,t,n){for(let o of Object.keys(r))if(!e.includes(o))throw new Error(`Unknown option "${o}" in ${t} of ${n}.`)}function T(r,e,t){if(!r||typeof r!="object"||Array.isArray(r))throw new Error(`${e} in ${t} must be an object.`)}function oe(r,e="missing"){return r??e}function Ge(r){let e=C.default.join(r,".env"),t=C.default.join(r,".env.local");E.default.existsSync(e)&&G.default.config({path:e}),E.default.existsSync(t)&&G.default.config({path:t,override:!0})}var $=class{constructor(e="@concircle/i18n-ai-translator",t){this.prefix=e,this.debug_enabled=t??(typeof process<"u"&&process.env.DEBUG?.includes("i18n-ai-translator"))??!1}debug(e,t){this.debug_enabled&&console.debug(`[${this.prefix}:DEBUG] ${e}`,t||"")}info(e,t){console.info(`[${this.prefix}:INFO] ${e}`,t||"")}warn(e,t){console.warn(`[${this.prefix}:WARN] ${e}`,t||"")}error(e,t){console.error(`[${this.prefix}:ERROR] ${e}`,t?.message||""),t?.stack&&this.debug_enabled&&console.error(t.stack)}setDebug(e){this.debug_enabled=e}isDebugEnabled(){return this.debug_enabled}},_=class{debug(e,t){}info(e,t){}warn(e,t){}error(e,t){}};var V=class r{constructor(e){K(e),this.config=e,this.provider=Ke(e),this.glossaryManager=new P(e.glossary),this.cache=new b(e.cache),this.logger=e.verbose?new $("@concircle/i18n-ai-translator",!0):new _}static async fromConfig(e){let t=await I(e);return new r(t)}async translateProject(e){let t=e?.configPath||e?.cwd?await I({configPath:e.configPath,cwd:e.cwd,overrides:{input:e.inputPath,targetLanguages:e.languages,translationMode:e.mode,encodeUnicode:e.encodeUnicode,provider:e.provider,model:e.model,verbose:e.verbose}}):this.config;return(t===this.config?this:new r(t)).translateFile({inputPath:e?.inputPath,languages:e?.languages,mode:e?.mode,dryRun:e?.dryRun})}async translateFile(e){let t=se.default.resolve(process.cwd(),e?.inputPath??this.config.files?.input??"i18n/i18n.properties"),n=M(t),o=U(n),s=Object.keys(o),d=e?.languages??this.config.targetLanguages,p=oe(e?.mode,this.config.translationMode),i={sourceFile:t,translationMode:p,translations:{}};for(let l of d){this.logger.info("Translating language",{language:l});let f=re(t,l,this.config.files?.languageFilePattern,this.config.files?.outputDir),a=M(f),c=U(a),h=s.filter(j=>{if(p==="overwrite")return!0;let R=c[j];return R===void 0||R===""}),m=await this.translateEntries(o,h,l),y=a.lines.length>0?F(a):F(n);for(let[j,R]of Object.entries(m))Q(y,j,R);let v=[];e?.dryRun||ee(f,y,{encodeUnicode:this.resolveEncodeUnicode(l)}),i.translations[l]={outputFile:f,success:v.length===0,translatedKeysCount:Object.keys(m).length,skippedKeysCount:s.length-h.length,errors:v,dryRun:e?.dryRun??!1}}return i}clearCache(){this.cache.clear()}getCacheStats(){return this.cache.getStats()}getGlossaryTerms(e){return this.glossaryManager.getTermsForLanguage(e)}async translateEntries(e,t,n){let o=this.glossaryManager.getTermsForLanguage(n),s=this.config.rules??[],d=this.config.batchSize??20,p={};for(let i=0;i<t.length;i+=d){let l=t.slice(i,i+d),f=[];for(let c of l){let h=e[c],m=q(h),y=b.createKey({provider:this.provider.getName(),model:this.config.providerOptions?.model??"default",sourceLanguage:this.config.sourceLanguage??"en",targetLanguage:n,sourceValue:h,glossaryTerms:o,rules:s}),v=this.cache.get(y);if(v){p[c]=v;continue}f.push({key:c,sourceValue:h,maskedValue:m.masked,placeholders:m.tokens})}if(f.length===0)continue;let a=await this.provider.translateBatch({sourceLanguage:this.config.sourceLanguage??"en",targetLanguage:n,items:f,glossaryTerms:o,rules:s,model:this.config.providerOptions?.model});for(let c of f){let h=Ve(a.items,c.key),m=Y(h.translatedValue,c.placeholders),y=H(c.sourceValue,m);if(!y.valid)throw new Error(`Placeholder validation failed for key "${c.key}" in ${n}: expected ${y.expected.join(", ")}, got ${y.actual.join(", ")}`);p[c.key]=m;let v=b.createKey({provider:this.provider.getName(),model:this.config.providerOptions?.model??"default",sourceLanguage:this.config.sourceLanguage??"en",targetLanguage:n,sourceValue:c.sourceValue,glossaryTerms:o,rules:s});this.cache.set(v,m)}}return p}resolveEncodeUnicode(e){return this.config.languageOptions?.[e]?.encodeUnicode??this.config.encodeUnicode??!1}};async function ie(r){let e=await I({configPath:r?.configPath,cwd:r?.cwd,overrides:{input:r?.inputPath,targetLanguages:r?.languages,translationMode:r?.mode,encodeUnicode:r?.encodeUnicode,provider:r?.provider,model:r?.model,verbose:r?.verbose}});return new V(e).translateProject(r)}function Ke(r){if(r.provider==="openai")return new D(r.providerOptions??{});throw new Error(`Unsupported provider: ${r.provider}`)}function Ve(r,e){let t=r.find(n=>n.key===e);if(!t)throw new Error(`Provider response is missing translation for key "${e}"`);return t}var ae=["missing","overwrite"],le=["openai"];async function ze(r,e={log:console.log,error:console.error}){let t;try{t=ue(r)}catch(n){return e.error(n instanceof Error?n.message:String(n)),e.log(ce()),1}if(t.help||r.length===0)return e.log(ce()),0;try{let n=await ie({configPath:t.config,inputPath:t.input,languages:t.languages,mode:t.mode,provider:t.provider,model:t.model,dryRun:t.dryRun,encodeUnicode:t.encodeUnicode,verbose:t.verbose});return e.log(JSON.stringify(n,null,2)),0}catch(n){return e.error(n instanceof Error?n.message:String(n)),1}}function ue(r){let e={dryRun:!1,verbose:!1,help:!1},t=[...r];for(t[0]==="help"&&(t.shift(),e.help=!0);t.length>0;){let n=t.shift();switch(n){case"--config":e.config=k(t,"--config");break;case"--input":e.input=k(t,"--input");break;case"--languages":e.languages=k(t,"--languages").split(",").map(o=>o.trim()).filter(Boolean);break;case"--mode":{let o=k(t,"--mode");if(!ae.includes(o))throw new Error(`Unknown value "${o}" for option "--mode". Expected one of: ${ae.join(", ")}.`);e.mode=o;break}case"--provider":{let o=k(t,"--provider");if(!le.includes(o))throw new Error(`Unknown value "${o}" for option "--provider". Expected one of: ${le.join(", ")}.`);e.provider=o;break}case"--model":e.model=k(t,"--model");break;case"--dry-run":e.dryRun=!0;break;case"--encode-unicode":e.encodeUnicode=!0;break;case"--verbose":e.verbose=!0;break;case"--help":case"-h":e.help=!0;break;default:throw n.startsWith("-")?new Error(`Unknown option "${n}". Use --help to see supported options.`):new Error(`Unknown argument: ${n}`)}}return e}function k(r,e){let t=r.shift();if(!t||t.startsWith("-"))throw new Error(`Missing value for option "${e}".`);return t}function ce(){return["i18n-ai-translator [options]","","Options:"," --config <path> Path to i18n-ai config file"," --input <path> Source i18n.properties file"," --languages <list> Comma-separated target languages"," --mode <mode> missing | overwrite"," --provider <name> Reserved for future providers"," --model <name> Override provider model"," --dry-run Do not write files"," --encode-unicode Write non-ASCII characters as \\uXXXX escapes"," --verbose Enable verbose logs"," --help Show this help text"].join(`
|
|
5
|
+
`)}0&&(module.exports={parseArgs,runCli});
|
|
6
|
+
//# sourceMappingURL=cli.cjs.map
|