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 +636 -0
- package/package.json +37 -0
- package/src/ai/translator.ts +366 -0
- package/src/ast/parser.ts +171 -0
- package/src/config/loader.ts +160 -0
- package/src/index.ts +14 -0
- package/src/lokal-core.d.ts +113 -0
- package/src/storage/file-storage.ts +200 -0
- package/src/types/@babel/traverse.d.ts +74 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Core exports
|
|
2
|
+
export { ASTParser, type ExtractedString, type ScanOptions, type ScanResult } from './ast/parser';
|
|
3
|
+
export { ConfigLoader, type LokalConfig, defaultConfig } from './config/loader';
|
|
4
|
+
export { FileStorage, ContentHasher, type LocaleData, type LocaleFile } from './storage/file-storage';
|
|
5
|
+
export {
|
|
6
|
+
AITranslator,
|
|
7
|
+
TranslationProviderFactory,
|
|
8
|
+
OpenAIProvider,
|
|
9
|
+
GeminiProvider,
|
|
10
|
+
type TranslationProvider,
|
|
11
|
+
type TranslationRequest,
|
|
12
|
+
type TranslationResult,
|
|
13
|
+
type TranslationBatch
|
|
14
|
+
} from './ai/translator';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
declare module 'lokal-core' {
|
|
2
|
+
export class ASTParser {
|
|
3
|
+
constructor(options: {
|
|
4
|
+
filePath: string;
|
|
5
|
+
functionName?: string;
|
|
6
|
+
componentName?: string;
|
|
7
|
+
});
|
|
8
|
+
scanDirectory(dirPath: string): ScanResult;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExtractedString {
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
location?: {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScanResult {
|
|
22
|
+
strings: ExtractedString[];
|
|
23
|
+
errors: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ScanOptions {
|
|
27
|
+
filePath: string;
|
|
28
|
+
functionName?: string;
|
|
29
|
+
componentName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ConfigLoader {
|
|
33
|
+
load(configPath?: string): Promise<LokalConfig>;
|
|
34
|
+
loadSync(configPath?: string): LokalConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LokalConfig {
|
|
38
|
+
sourceDir: string;
|
|
39
|
+
outputDir: string;
|
|
40
|
+
defaultLocale: string;
|
|
41
|
+
functionName: string;
|
|
42
|
+
componentName: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const defaultConfig: LokalConfig;
|
|
46
|
+
|
|
47
|
+
export class FileStorage {
|
|
48
|
+
constructor(outputDir: string);
|
|
49
|
+
loadLocale(locale: string): LocaleFile | null;
|
|
50
|
+
saveLocale(locale: string, data: LocaleData): void;
|
|
51
|
+
mergeLocaleData(locale: string, newData: Record<string, string>): LocaleData;
|
|
52
|
+
getAvailableLocales(): string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LocaleData {
|
|
56
|
+
[key: string]: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LocaleFile {
|
|
60
|
+
locale: string;
|
|
61
|
+
data: LocaleData;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ContentHasher {
|
|
65
|
+
static hash(content: string): string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class AITranslator {
|
|
69
|
+
constructor(provider: TranslationProvider, options?: {
|
|
70
|
+
batchSize?: number;
|
|
71
|
+
});
|
|
72
|
+
translateBatch(request: TranslationBatch): Promise<TranslationResult[]>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TranslationProvider {
|
|
76
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TranslationRequest {
|
|
80
|
+
text: string;
|
|
81
|
+
sourceLocale: string;
|
|
82
|
+
targetLocale: string;
|
|
83
|
+
context?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TranslationResult {
|
|
87
|
+
translatedText: string;
|
|
88
|
+
success: boolean;
|
|
89
|
+
error?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TranslationBatch {
|
|
93
|
+
items: TranslationRequest[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class OpenAIProvider implements TranslationProvider {
|
|
97
|
+
constructor(apiKey: string, options?: {
|
|
98
|
+
model?: string;
|
|
99
|
+
temperature?: number;
|
|
100
|
+
});
|
|
101
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class GeminiProvider implements TranslationProvider {
|
|
105
|
+
constructor(apiKey: string, options?: {
|
|
106
|
+
model?: string;
|
|
107
|
+
temperature?: number;
|
|
108
|
+
});
|
|
109
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function TranslationProviderFactory(provider: 'openai' | 'gemini', apiKey: string, options?: object): TranslationProvider;
|
|
113
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
export interface LocaleData {
|
|
6
|
+
[key: string]: string | LocaleData;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LocaleFile {
|
|
10
|
+
locale: string;
|
|
11
|
+
data: LocaleData;
|
|
12
|
+
hash: string;
|
|
13
|
+
lastUpdated: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Content hash for incremental sync
|
|
18
|
+
*/
|
|
19
|
+
export class ContentHasher {
|
|
20
|
+
/**
|
|
21
|
+
* Generate a hash for content to detect changes
|
|
22
|
+
*/
|
|
23
|
+
static hash(content: string): string {
|
|
24
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a hash for a locale data object
|
|
29
|
+
*/
|
|
30
|
+
static hashLocaleData(data: LocaleData): string {
|
|
31
|
+
const content = JSON.stringify(data, Object.keys(data).sort());
|
|
32
|
+
return this.hash(content);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* File system persistence for locale files
|
|
38
|
+
*/
|
|
39
|
+
export class FileStorage {
|
|
40
|
+
private basePath: string;
|
|
41
|
+
|
|
42
|
+
constructor(basePath: string) {
|
|
43
|
+
this.basePath = basePath;
|
|
44
|
+
this.ensureDirectoryExists(basePath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ensure directory exists
|
|
49
|
+
*/
|
|
50
|
+
private ensureDirectoryExists(dirPath: string): void {
|
|
51
|
+
if (!fs.existsSync(dirPath)) {
|
|
52
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load locale data from file
|
|
58
|
+
*/
|
|
59
|
+
loadLocale(locale: string): LocaleFile | null {
|
|
60
|
+
const filePath = this.getLocalePath(locale);
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(filePath)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
68
|
+
return JSON.parse(content) as LocaleFile;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn(`Failed to load locale ${locale}: ${error}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Save locale data to file
|
|
77
|
+
*/
|
|
78
|
+
saveLocale(locale: string, data: LocaleData): void {
|
|
79
|
+
const filePath = this.getLocalePath(locale);
|
|
80
|
+
const hash = ContentHasher.hashLocaleData(data);
|
|
81
|
+
|
|
82
|
+
const localeFile: LocaleFile = {
|
|
83
|
+
locale,
|
|
84
|
+
data,
|
|
85
|
+
hash,
|
|
86
|
+
lastUpdated: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const content = JSON.stringify(localeFile, null, 2);
|
|
90
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get all locale files
|
|
95
|
+
*/
|
|
96
|
+
loadAllLocales(): Map<string, LocaleFile> {
|
|
97
|
+
const locales = new Map<string, LocaleFile>();
|
|
98
|
+
|
|
99
|
+
if (!fs.existsSync(this.basePath)) {
|
|
100
|
+
return locales;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const files = fs.readdirSync(this.basePath);
|
|
104
|
+
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
if (file.endsWith('.json')) {
|
|
107
|
+
const locale = file.replace('.json', '');
|
|
108
|
+
const localeFile = this.loadLocale(locale);
|
|
109
|
+
if (localeFile) {
|
|
110
|
+
locales.set(locale, localeFile);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return locales;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the file path for a locale
|
|
120
|
+
*/
|
|
121
|
+
private getLocalePath(locale: string): string {
|
|
122
|
+
return path.join(this.basePath, `${locale}.json`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a locale file exists
|
|
127
|
+
*/
|
|
128
|
+
localeExists(locale: string): boolean {
|
|
129
|
+
return fs.existsSync(this.getLocalePath(locale));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete a locale file
|
|
134
|
+
*/
|
|
135
|
+
deleteLocale(locale: string): boolean {
|
|
136
|
+
const filePath = this.getLocalePath(locale);
|
|
137
|
+
|
|
138
|
+
if (fs.existsSync(filePath)) {
|
|
139
|
+
fs.unlinkSync(filePath);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all available locales
|
|
148
|
+
*/
|
|
149
|
+
getAvailableLocales(): string[] {
|
|
150
|
+
if (!fs.existsSync(this.basePath)) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return fs.readdirSync(this.basePath)
|
|
155
|
+
.filter(file => file.endsWith('.json'))
|
|
156
|
+
.map(file => file.replace('.json', ''));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Merge new data with existing locale data
|
|
161
|
+
*/
|
|
162
|
+
mergeLocaleData(locale: string, newData: LocaleData, preserveExisting: boolean = true): LocaleData {
|
|
163
|
+
const existing = this.loadLocale(locale);
|
|
164
|
+
|
|
165
|
+
if (!existing) {
|
|
166
|
+
return newData;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (preserveExisting) {
|
|
170
|
+
return this.deepMerge(existing.data, newData);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return newData;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Deep merge two objects
|
|
178
|
+
*/
|
|
179
|
+
private deepMerge(target: any, source: any): any {
|
|
180
|
+
if (typeof target !== 'object' || typeof source !== 'object') {
|
|
181
|
+
return source;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = { ...target };
|
|
185
|
+
|
|
186
|
+
for (const key in source) {
|
|
187
|
+
if (source.hasOwnProperty(key)) {
|
|
188
|
+
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
|
189
|
+
result[key] = this.deepMerge(target[key] || {}, source[key]);
|
|
190
|
+
} else {
|
|
191
|
+
result[key] = source[key];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default FileStorage;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for @babel/traverse
|
|
3
|
+
* This file provides type definitions when the package's built-in types are not detected
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module '@babel/traverse' {
|
|
7
|
+
import { Node as BabelNode } from '@babel/types';
|
|
8
|
+
|
|
9
|
+
export interface NodePath<T = BabelNode> {
|
|
10
|
+
node: T;
|
|
11
|
+
parent: BabelNode | null;
|
|
12
|
+
parentPath: NodePath | null;
|
|
13
|
+
scope: Scope;
|
|
14
|
+
hub: any;
|
|
15
|
+
context: any;
|
|
16
|
+
state: any;
|
|
17
|
+
opts: any;
|
|
18
|
+
|
|
19
|
+
get(key: string): any;
|
|
20
|
+
pushContext(context: any): void;
|
|
21
|
+
popContext(): void;
|
|
22
|
+
call(callback: (path: NodePath) => void, key: string): void;
|
|
23
|
+
isReferencedIdentifier(): boolean;
|
|
24
|
+
isBindingIdentifier(): boolean;
|
|
25
|
+
replaceWith(node: BabelNode | NodePath): void;
|
|
26
|
+
remove(): void;
|
|
27
|
+
find(callback: (path: NodePath) => boolean): NodePath | null;
|
|
28
|
+
findParent(callback: (path: NodePath) => boolean): NodePath | null;
|
|
29
|
+
traverse(visitors: Visitor<any>): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Scope {
|
|
33
|
+
parent: Scope | null;
|
|
34
|
+
parentBlock: BabelNode;
|
|
35
|
+
block: BabelNode;
|
|
36
|
+
path: NodePath;
|
|
37
|
+
references: Map<string, NodePath[]>;
|
|
38
|
+
bindings: Map<string, Binding>;
|
|
39
|
+
|
|
40
|
+
getBinding(name: string): Binding | undefined;
|
|
41
|
+
hasBinding(name: string): boolean;
|
|
42
|
+
getOwnBinding(name: string): Binding | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Binding {
|
|
46
|
+
kind: 'var' | 'let' | 'const' | 'module' | 'param' | 'local' | 'unknown';
|
|
47
|
+
path: NodePath;
|
|
48
|
+
scope: Scope;
|
|
49
|
+
constant: boolean;
|
|
50
|
+
constantViolations: NodePath[];
|
|
51
|
+
referencers: NodePath[];
|
|
52
|
+
references: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Visitor<T> {
|
|
56
|
+
[key: string]: (path: NodePath<T>, state: any) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TraverseOptions {
|
|
60
|
+
scope?: boolean;
|
|
61
|
+
pragma?: string;
|
|
62
|
+
createParentMap?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default function traverse<S extends BabelNode = BabelNode>(
|
|
66
|
+
parent: S,
|
|
67
|
+
visitors: Visitor,
|
|
68
|
+
scope?: Scope,
|
|
69
|
+
state?: any,
|
|
70
|
+
opts?: TraverseOptions
|
|
71
|
+
): void;
|
|
72
|
+
|
|
73
|
+
export { BabelNode as Node };
|
|
74
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"target": "ES2020"
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"src/**/*"
|
|
11
|
+
],
|
|
12
|
+
"exclude": [
|
|
13
|
+
"node_modules",
|
|
14
|
+
"dist",
|
|
15
|
+
"**/*.test.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|