lokal-core 1.0.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/README.md ADDED
@@ -0,0 +1,636 @@
1
+ # LOKAL - AI-Powered Localization for React
2
+
3
+ [![CI](https://github.com/devcoda25/lokal/actions/workflows/ci.yml/badge.svg)](https://github.com/devcoda25/lokal/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/lokal-core.svg)](https://www.npmjs.com/package/lokal-core)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ LOKAL is an AI-powered localization ecosystem that helps you extract, translate, and manage translation strings in your React applications.
8
+
9
+ ## Features
10
+
11
+ - **CLI Commands** - Easy command-line workflow
12
+ - **AST-based Extraction** - Automatically finds translation strings in your code
13
+ - **AI Translation** - Powered by Google Gemini, OpenAI, or DeepSeek
14
+ - **Incremental Sync** - Only translates changed content using content hashing
15
+ - **File Storage** - JSON-based locale file management
16
+ - **Framework Ready** - Works with React and React Native
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ # Install CLI globally
22
+ npm install -g lokal
23
+
24
+ # Or use with npx
25
+ npx lokal --help
26
+ ```
27
+
28
+ ## CLI Commands
29
+
30
+ LOKAL provides three main CLI commands:
31
+
32
+ ### 1. `lokal init`
33
+
34
+ Initialize LOKAL in your project:
35
+
36
+ ```bash
37
+ npx lokal init
38
+ ```
39
+
40
+ **Options:**
41
+ - `-l, --locales <locales>` - Comma-separated locales (default: "en")
42
+ - `-d, --default-locale <locale>` - Default locale (default: "en")
43
+ - `-f, --force` - Force reinitialization
44
+
45
+ **What it does:**
46
+ - Creates `lokal.config.js`
47
+ - Creates `locales/` directory
48
+ - Creates default locale file (e.g., `locales/en.json`)
49
+
50
+ ### 2. `lokal scan`
51
+
52
+ Scan source files for translation strings:
53
+
54
+ ```bash
55
+ npx lokal scan
56
+ ```
57
+
58
+ **Options:**
59
+ - `-c, --config <path>` - Path to config file
60
+ - `-o, --output <path>` - Output directory
61
+ - `-v, --verbose` - Show detailed output
62
+
63
+ **What it does:**
64
+ - Scans `./src` for `t()` and `<T>` patterns
65
+ - Extracts all translation strings
66
+ - Updates the default locale file
67
+
68
+ ### 3. `lokal translate`
69
+
70
+ Translate missing strings using AI:
71
+
72
+ ```bash
73
+ # Translate to first non-default locale
74
+ npx lokal translate
75
+
76
+ # Translate to specific locale
77
+ npx lokal translate --locale es
78
+
79
+ # Translate all locales
80
+ npx lokal translate --all
81
+ ```
82
+
83
+ **Options:**
84
+ - `-c, --config <path>` - Path to config file
85
+ - `-l, --locale <locale>` - Specific locale to translate
86
+ - `-a, --all` - Translate all locales
87
+ - `-v, --verbose` - Show detailed output
88
+
89
+ ## Configuration
90
+
91
+ After running `lokal init`, a `lokal.config.js` file is created:
92
+
93
+ ```javascript
94
+ module.exports = {
95
+ // Supported locales
96
+ locales: ['en', 'es', 'fr'],
97
+
98
+ // Default locale
99
+ defaultLocale: 'en',
100
+
101
+ // Function name for translations (t("key"))
102
+ functionName: 't',
103
+
104
+ // Component name for translations (<T>key</T>)
105
+ componentName: 'T',
106
+
107
+ // Source directory to scan
108
+ sourceDir: './src',
109
+
110
+ // Output directory for locale files
111
+ outputDir: './locales',
112
+
113
+ // AI Translation settings
114
+ ai: {
115
+ provider: 'gemini', // 'openai', 'gemini', or 'deepseek'
116
+ apiKey: process.env.GEMINI_API_KEY,
117
+ model: 'gemini-2.5-flash'
118
+ }
119
+ };
120
+ ```
121
+
122
+ ## Complete Workflow
123
+
124
+ ```bash
125
+ # Step 1: Initialize your project
126
+ npx lokal init --locales en,es,fr --default-locale en
127
+
128
+ # Step 2: Add translation strings to your code
129
+ # t("Hello") or <T>Hello</T>
130
+
131
+ # Step 3: Scan for strings (run after adding new strings)
132
+ npx lokal scan
133
+
134
+ # Step 4: Translate with AI
135
+ npx lokal translate --all
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Library Usage (Advanced)
141
+
142
+ You can also use LOKAL as a library in your code:
143
+
144
+ ```typescript
145
+ import { ASTParser, GeminiProvider, AITranslator, FileStorage } from 'lokal-core';
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Architecture
151
+
152
+ LOKAL consists of three main components:
153
+
154
+ ```
155
+ ┌─────────────────────────────────────────────┐
156
+ │ LOKAL Core │
157
+ ├─────────────────────────────────────────────┤
158
+ │ │
159
+ │ ┌─────────────┐ ┌─────────────┐ │
160
+ │ │ AST Parser │ │ AI Translate│ │
161
+ │ └──────┬──────┘ └──────┬──────┘ │
162
+ │ │ │ │
163
+ │ ▼ ▼ │
164
+ │ ┌─────────────────────────────┐ │
165
+ │ │ Translation Workflow │ │
166
+ │ │ 1. Extract strings │ │
167
+ │ │ 2. Translate with AI │ │
168
+ │ │ 3. Save to files │ │
169
+ │ └──────────────┬──────────────┘ │
170
+ │ ▼ │
171
+ │ ┌─────────────────────────────┐ │
172
+ │ │ File Storage │ │
173
+ │ └─────────────────────────────┘ │
174
+ │ │
175
+ └─────────────────────────────────────────────┘
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 1. AST Parser
181
+
182
+ The AST Parser scans your source code and extracts translation strings using Abstract Syntax Tree parsing.
183
+
184
+ ### Supported Patterns
185
+
186
+ | Pattern | Code Example | Extracts |
187
+ |---------|--------------|----------|
188
+ | Function call | `t("Hello World")` | `"Hello World"` |
189
+ | Function with variable | `t(greeting)` | ❌ (not extracted) |
190
+ | JSX Component | `<T>Welcome</T>` | `"Welcome"` |
191
+ | JSX with expression | `<T>{name}</T>` | ❌ (not extracted) |
192
+
193
+ ### Basic Usage
194
+
195
+ ```typescript
196
+ import { ASTParser } from 'lokal-core';
197
+
198
+ // Create parser with default settings
199
+ const parser = new ASTParser({
200
+ filePath: './src' // Path to scan
201
+ });
202
+
203
+ // Parse a single file
204
+ const result = parser.parseFile('./src/components/Button.tsx');
205
+
206
+ console.log(result.strings);
207
+ // [
208
+ // { key: "Submit", value: "Submit", file: "...", line: 10, column: 5 },
209
+ // { key: "Cancel", value: "Cancel", file: "...", line: 11, column: 5 }
210
+ // ]
211
+ ```
212
+
213
+ ### Custom Function and Component Names
214
+
215
+ By default, LOKAL looks for `t()` function and `<T>` component. You can customize this:
216
+
217
+ ```typescript
218
+ const parser = new ASTParser({
219
+ filePath: './src',
220
+ functionName: 'translate', // Looks for translate("string")
221
+ componentName: 'Trans' // Looks for <Trans>string</Trans>
222
+ });
223
+ ```
224
+
225
+ ### Scan Directory
226
+
227
+ ```typescript
228
+ // Scan entire directory
229
+ const result = parser.scanDirectory('./src', ['.ts', '.tsx', '.js', '.jsx']);
230
+
231
+ // result.strings - Array of extracted strings
232
+ // result.errors - Any parsing errors
233
+ ```
234
+
235
+ ### Type Definitions
236
+
237
+ ```typescript
238
+ interface ExtractedString {
239
+ key: string; // Translation key (same as value for simple strings)
240
+ value: string; // The actual string content
241
+ file: string; // File path where found
242
+ line: number; // Line number
243
+ column: number; // Column number
244
+ }
245
+
246
+ interface ScanResult {
247
+ strings: ExtractedString[];
248
+ errors: string[];
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## 2. AI Translation
255
+
256
+ LOKAL supports multiple AI providers for translating your strings.
257
+
258
+ ### Supported Providers
259
+
260
+ | Provider | Model | API Key |
261
+ |----------|-------|---------|
262
+ | Google Gemini | gemini-2.5-flash | Google AI Studio |
263
+ | OpenAI | gpt-4o-mini | OpenAI Platform |
264
+ | DeepSeek | deepseek-chat | DeepSeek Platform |
265
+
266
+ ### Google Gemini (Recommended)
267
+
268
+ ```typescript
269
+ import { GeminiProvider, AITranslator } from 'lokal-core';
270
+
271
+ // Create Gemini provider
272
+ const gemini = new GeminiProvider(
273
+ 'YOUR-GEMINI-API-KEY', // Get from https://aistudio.google.com/app/apikey
274
+ 'gemini-2.5-flash' // Model name (optional)
275
+ );
276
+
277
+ // Create translator
278
+ const translator = new AITranslator(gemini);
279
+ ```
280
+
281
+ ### OpenAI
282
+
283
+ ```typescript
284
+ import { OpenAIProvider, AITranslator } from 'lokal-core';
285
+
286
+ const openai = new OpenAIProvider(
287
+ 'YOUR-OPENAI-API-KEY', // Get from https://platform.openai.com/api-keys
288
+ 'gpt-4o-mini' // Model (optional)
289
+ );
290
+
291
+ const translator = new AITranslator(openai);
292
+ ```
293
+
294
+ ### DeepSeek
295
+
296
+ ```typescript
297
+ import { DeepSeekProvider, AITranslator } from 'lokal-core';
298
+
299
+ const deepseek = new DeepSeekProvider(
300
+ 'YOUR-DEEPSEEK-API-KEY',
301
+ 'deepseek-chat'
302
+ );
303
+
304
+ const translator = new AITranslator(deepseek);
305
+ ```
306
+
307
+ ### Translate Strings
308
+
309
+ ```typescript
310
+ // Single translation
311
+ const result = await translator.translate({
312
+ sourceText: 'Hello World',
313
+ sourceLocale: 'en',
314
+ targetLocale: 'es'
315
+ });
316
+
317
+ console.log(result.translatedText); // "Hola Mundo"
318
+
319
+ // Batch translation
320
+ const results = await translator.translateBatch([
321
+ { sourceText: 'Hello', sourceLocale: 'en', targetLocale: 'es' },
322
+ { sourceText: 'Goodbye', sourceLocale: 'en', targetLocale: 'es' }
323
+ ]);
324
+ ```
325
+
326
+ ### Incremental Sync
327
+
328
+ LOKAL uses content hashing to skip translating unchanged strings:
329
+
330
+ ```typescript
331
+ // First translation
332
+ const sourceData = { greeting: 'Hello', welcome: 'Welcome' };
333
+ const targetData = {};
334
+
335
+ const translated1 = await translator.translateMissingKeys(
336
+ sourceData,
337
+ targetData,
338
+ 'en',
339
+ 'es'
340
+ );
341
+ // translated1 = { greeting: 'Hola', welcome: 'Bienvenido' }
342
+
343
+ // Second call with SAME source - skips already translated
344
+ const translated2 = await translator.translateMissingKeys(
345
+ sourceData, // Same as before
346
+ translated1, // Previous result
347
+ 'en',
348
+ 'es'
349
+ );
350
+ // Returns translated1 immediately (skips API calls)
351
+
352
+ // Third call with CHANGED source - only translates changed
353
+ sourceData.greeting = 'Hi'; // Changed!
354
+ const translated3 = await translator.translateMissingKeys(
355
+ sourceData,
356
+ translated2,
357
+ 'en',
358
+ 'es'
359
+ );
360
+ // Only 'greeting' is re-translated
361
+ ```
362
+
363
+ ---
364
+
365
+ ## 3. File Storage
366
+
367
+ LOKAL provides file-based storage for your locale files.
368
+
369
+ ### Basic Usage
370
+
371
+ ```typescript
372
+ import { FileStorage } from 'lokal-core';
373
+
374
+ const storage = new FileStorage('./locales');
375
+ ```
376
+
377
+ ### Save Locale
378
+
379
+ ```typescript
380
+ const localeData = {
381
+ common: {
382
+ greeting: 'Hello',
383
+ goodbye: 'Goodbye'
384
+ },
385
+ home: {
386
+ title: 'Welcome',
387
+ subtitle: 'Welcome to our app'
388
+ }
389
+ };
390
+
391
+ storage.saveLocale('en', localeData);
392
+ // Creates: locales/en.json
393
+ ```
394
+
395
+ ### Load Locale
396
+
397
+ ```typescript
398
+ const enData = storage.loadLocale('en');
399
+ // Returns: { locale: 'en', data: {...}, hash: '...', lastUpdated: '...' }
400
+
401
+ // Get just the data
402
+ const data = enData?.data;
403
+ ```
404
+
405
+ ### Merge Data
406
+
407
+ ```typescript
408
+ // Merge new keys into existing locale
409
+ const merged = storage.mergeLocaleData('en', newKeys, preserveExisting = true);
410
+
411
+ // Example:
412
+ const existing = { common: { greeting: 'Hello' } };
413
+ const newKeys = { common: { greeting: 'Hi', newKey: 'New' } };
414
+ const result = storage.mergeLocaleData('en', newKeys, true);
415
+ // result = { common: { greeting: 'Hello', newKey: 'New' } }
416
+ ```
417
+
418
+ ### Other Operations
419
+
420
+ ```typescript
421
+ // Check if locale exists
422
+ storage.localeExists('en'); // true/false
423
+
424
+ // Get all available locales
425
+ storage.getAvailableLocales(); // ['en', 'es', 'fr']
426
+
427
+ // Delete locale
428
+ storage.deleteLocale('fr');
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Complete Example: Translate a React App
434
+
435
+ ### Step 1: Create the translation script
436
+
437
+ ```typescript
438
+ // translate.ts
439
+ import { ASTParser, GeminiProvider, AITranslator, FileStorage } from 'lokal-core';
440
+
441
+ async function translateApp() {
442
+ const SOURCE_DIR = './src';
443
+ const TARGET_LOCALES = ['es', 'fr', 'de', 'ja'];
444
+ const SOURCE_LOCALE = 'en';
445
+
446
+ // 1. Extract strings from source
447
+ console.log('🔍 Scanning for translation strings...');
448
+ const parser = new ASTParser({ filePath: SOURCE_DIR });
449
+ const scanResult = parser.scanDirectory(SOURCE_DIR);
450
+
451
+ if (scanResult.errors.length > 0) {
452
+ console.warn('⚠️ Some files had errors:', scanResult.errors);
453
+ }
454
+
455
+ // 2. Build source locale data
456
+ const sourceData: Record<string, any> = {};
457
+ for (const str of scanResult.strings) {
458
+ sourceData[str.key] = str.value;
459
+ }
460
+
461
+ // 3. Save source locale
462
+ console.log('💾 Saving source locale...');
463
+ const storage = new FileStorage('./locales');
464
+ storage.saveLocale(SOURCE_LOCALE, sourceData);
465
+
466
+ // 4. Translate to each target locale
467
+ console.log('🤖 Starting AI translation...');
468
+ const provider = new GeminiProvider('YOUR-API-KEY');
469
+ const translator = new AITranslator(provider);
470
+
471
+ for (const targetLocale of TARGET_LOCALES) {
472
+ console.log(` Translating to ${targetLocale}...`);
473
+
474
+ const targetData = storage.loadLocale(targetLocale)?.data || {};
475
+ const translated = await translator.translateMissingKeys(
476
+ sourceData,
477
+ targetData,
478
+ SOURCE_LOCALE,
479
+ targetLocale
480
+ );
481
+
482
+ storage.saveLocale(targetLocale, translated);
483
+ console.log(` ✓ ${targetLocale} complete`);
484
+ }
485
+
486
+ console.log('✅ Translation complete!');
487
+ }
488
+
489
+ translateApp().catch(console.error);
490
+ ```
491
+
492
+ ### Step 2: Run the script
493
+
494
+ ```bash
495
+ npx tsx translate.ts
496
+ ```
497
+
498
+ ### Step 3: Use in your React app
499
+
500
+ ```tsx
501
+ // locales/en.json
502
+ {
503
+ "greeting": "Hello",
504
+ "welcome": "Welcome to our app"
505
+ }
506
+
507
+ // locales/es.json
508
+ {
509
+ "greeting": "Hola",
510
+ "welcome": "Bienvenido a nuestra aplicación"
511
+ }
512
+
513
+ // App.jsx
514
+ import en from './locales/en.json';
515
+ import es from './locales/es.json';
516
+
517
+ const locales = { en, es };
518
+ const [locale, setLocale] = useState('en');
519
+
520
+ function t(key) {
521
+ return locales[locale]?.[key] || key;
522
+ }
523
+
524
+ function App() {
525
+ return (
526
+ <div>
527
+ <h1>{t('greeting')}</h1>
528
+ <p>{t('welcome')}</p>
529
+ </div>
530
+ );
531
+ }
532
+ ```
533
+
534
+ ---
535
+
536
+ ## API Reference
537
+
538
+ ### ASTParser
539
+
540
+ ```typescript
541
+ new ASTParser(options: ScanOptions)
542
+ ```
543
+
544
+ **ScanOptions:**
545
+ - `filePath: string` - Path to file or directory
546
+ - `functionName?: string` - Translation function name (default: 't')
547
+ - `componentName?: string` - Translation component name (default: 'T')
548
+
549
+ **Methods:**
550
+ - `parseFile(filePath: string): ScanResult` - Parse single file
551
+ - `parseContent(content: string, filePath?: string): ScanResult` - Parse content string
552
+ - `scanDirectory(dirPath: string, extensions?: string[]): ScanResult` - Scan directory
553
+
554
+ ### TranslationProvider
555
+
556
+ ```typescript
557
+ interface TranslationProvider {
558
+ translate(request: TranslationRequest): Promise<TranslationResult>;
559
+ translateBatch(requests: TranslationRequest[]): Promise<TranslationResult[]>;
560
+ }
561
+ ```
562
+
563
+ **TranslationRequest:**
564
+ ```typescript
565
+ {
566
+ sourceText: string;
567
+ sourceLocale: string;
568
+ targetLocale: string;
569
+ context?: string;
570
+ }
571
+ ```
572
+
573
+ **TranslationResult:**
574
+ ```typescript
575
+ {
576
+ translatedText: string;
577
+ sourceText: string;
578
+ success: boolean;
579
+ error?: string;
580
+ }
581
+ ```
582
+
583
+ ### AITranslator
584
+
585
+ ```typescript
586
+ new AITranslator(provider: TranslationProvider)
587
+ ```
588
+
589
+ **Methods:**
590
+ - `translateMissingKeys(sourceData, targetData, sourceLocale, targetLocale): Promise<LocaleData>`
591
+
592
+ ### FileStorage
593
+
594
+ ```typescript
595
+ new FileStorage(basePath: string)
596
+ ```
597
+
598
+ **Methods:**
599
+ - `loadLocale(locale: string): LocaleFile | null`
600
+ - `saveLocale(locale: string, data: LocaleData): void`
601
+ - `loadAllLocales(): Map<string, LocaleFile>`
602
+ - `localeExists(locale: string): boolean`
603
+ - `deleteLocale(locale: string): boolean`
604
+ - `getAvailableLocales(): string[]`
605
+ - `mergeLocaleData(locale: string, newData: LocaleData, preserveExisting?: boolean): LocaleData`
606
+
607
+ ---
608
+
609
+ ## Error Handling
610
+
611
+ All AI providers return proper error information:
612
+
613
+ ```typescript
614
+ const result = await translator.translate({
615
+ sourceText: 'Hello',
616
+ sourceLocale: 'en',
617
+ targetLocale: 'es'
618
+ });
619
+
620
+ if (!result.success) {
621
+ console.error('Translation failed:', result.error);
622
+ // Handle errors appropriately
623
+ }
624
+ ```
625
+
626
+ Common errors:
627
+ - `429` - Rate limit exceeded
628
+ - `insufficient_quota` - No API credits left
629
+ - `invalid_api_key` - Invalid API key
630
+ - `model_not_found` - Model doesn't exist
631
+
632
+ ---
633
+
634
+ ## License
635
+
636
+ MIT
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "lokal-core",
3
+ "version": "1.0.0",
4
+ "description": "Core business logic for LOKAL - AST parsing, AI connectors, and storage",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "dev": "tsup --watch",
17
+ "clean": "rm -rf dist",
18
+ "lint": "echo 'Use root lint command'"
19
+ },
20
+ "dependencies": {
21
+ "@babel/parser": "^7.23.0",
22
+ "@babel/traverse": "^7.23.0",
23
+ "@babel/types": "^7.23.0",
24
+ "cosmiconfig": "^8.3.0",
25
+ "deepmerge": "^4.3.1"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.10.0",
29
+ "eslint": "^8.57.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.3.0"
32
+ },
33
+ "peerDependencies": {},
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }