@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 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)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from '../dist/cli.mjs';
4
+
5
+ const exitCode = await runCli(process.argv.slice(2));
6
+ process.exit(exitCode);
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