@saidksi/localizer-core 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 +164 -0
- package/dist/ai/anthropic.d.ts +17 -0
- package/dist/ai/anthropic.d.ts.map +1 -0
- package/dist/ai/anthropic.js +58 -0
- package/dist/ai/anthropic.js.map +1 -0
- package/dist/ai/dedup.d.ts +19 -0
- package/dist/ai/dedup.d.ts.map +1 -0
- package/dist/ai/dedup.js +119 -0
- package/dist/ai/dedup.js.map +1 -0
- package/dist/ai/index.d.ts +65 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +464 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/openai.d.ts +11 -0
- package/dist/ai/openai.d.ts.map +1 -0
- package/dist/ai/openai.js +62 -0
- package/dist/ai/openai.js.map +1 -0
- package/dist/ai/prompts.d.ts +20 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +151 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/cache/index.d.ts +69 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +129 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/rewriter/index.d.ts +31 -0
- package/dist/rewriter/index.d.ts.map +1 -0
- package/dist/rewriter/index.js +128 -0
- package/dist/rewriter/index.js.map +1 -0
- package/dist/rewriter/transforms.d.ts +38 -0
- package/dist/rewriter/transforms.d.ts.map +1 -0
- package/dist/rewriter/transforms.js +189 -0
- package/dist/rewriter/transforms.js.map +1 -0
- package/dist/rewriter/ts-morph.d.ts +19 -0
- package/dist/rewriter/ts-morph.d.ts.map +1 -0
- package/dist/rewriter/ts-morph.js +121 -0
- package/dist/rewriter/ts-morph.js.map +1 -0
- package/dist/scanner/babel.d.ts +3 -0
- package/dist/scanner/babel.d.ts.map +1 -0
- package/dist/scanner/babel.js +504 -0
- package/dist/scanner/babel.js.map +1 -0
- package/dist/scanner/filters.d.ts +38 -0
- package/dist/scanner/filters.d.ts.map +1 -0
- package/dist/scanner/filters.js +133 -0
- package/dist/scanner/filters.js.map +1 -0
- package/dist/scanner/index.d.ts +22 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +82 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/typescript.d.ts +3 -0
- package/dist/scanner/typescript.d.ts.map +1 -0
- package/dist/scanner/typescript.js +542 -0
- package/dist/scanner/typescript.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validator/index.d.ts +65 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +237 -0
- package/dist/validator/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 SaidKSI
|
|
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", BASIS 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,164 @@
|
|
|
1
|
+
# @localize/core
|
|
2
|
+
|
|
3
|
+
Core library for the Localize i18n CLI tool. Provides AST-based string detection, AI-powered i18n key generation, code transformation, and validation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Scanner**: AST-based detection of hardcoded strings in TypeScript/JavaScript code
|
|
8
|
+
- **AI Integration**: Provider-agnostic support for Anthropic and OpenAI APIs
|
|
9
|
+
- **Rewriter**: Automatic code transformation to replace hardcoded strings with i18n function calls
|
|
10
|
+
- **Validator**: Key coverage validation across multiple language files
|
|
11
|
+
- **Cache**: Smart caching to avoid re-processing unchanged files
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @localize/core
|
|
17
|
+
# or
|
|
18
|
+
pnpm add @localize/core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Scanner, AIClient, Rewriter, Validator } from "@localize/core";
|
|
25
|
+
|
|
26
|
+
// 1. Scan for hardcoded strings
|
|
27
|
+
const scanner = new Scanner({ defaultLanguage: "en" });
|
|
28
|
+
const results = await scanner.scan("./src");
|
|
29
|
+
|
|
30
|
+
// 2. Generate keys and translations
|
|
31
|
+
const aiClient = new AIClient({
|
|
32
|
+
provider: "anthropic",
|
|
33
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
34
|
+
});
|
|
35
|
+
const translations = await aiClient.generateTranslations(results);
|
|
36
|
+
|
|
37
|
+
// 3. Rewrite source code
|
|
38
|
+
const rewriter = new Rewriter();
|
|
39
|
+
const modified = await rewriter.rewrite(filePath, results[0], translations);
|
|
40
|
+
|
|
41
|
+
// 4. Validate translations
|
|
42
|
+
const validator = new Validator();
|
|
43
|
+
const coverage = await validator.validate("./messages");
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### Scanner
|
|
49
|
+
|
|
50
|
+
Detects hardcoded strings in source code:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const scanner = new Scanner({
|
|
54
|
+
defaultLanguage: "en",
|
|
55
|
+
include: ["src/**/*.{ts,tsx,js,jsx}"],
|
|
56
|
+
exclude: ["**/*.test.ts", "node_modules"],
|
|
57
|
+
ignoreFiles: [],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const results = await scanner.scan(dirOrFilePath);
|
|
61
|
+
// Returns: ScanResult[] with { filePath, string, line, column, context }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### AIClient
|
|
65
|
+
|
|
66
|
+
Generates semantic i18n keys and translations:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const client = new AIClient({
|
|
70
|
+
provider: "anthropic" | "openai",
|
|
71
|
+
apiKey: "your-api-key",
|
|
72
|
+
model: "claude-3-sonnet" | "gpt-4",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const translations = await client.generateTranslations(scanResults, {
|
|
76
|
+
languages: ["en", "fr", "es"],
|
|
77
|
+
keyStyle: "dot.notation" | "snake_case",
|
|
78
|
+
glossary: { /* domain-specific terms */ },
|
|
79
|
+
});
|
|
80
|
+
// Returns: { key: string, translations: { [lang]: string } }[]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Rewriter
|
|
84
|
+
|
|
85
|
+
Transforms source code to use i18n function calls:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const rewriter = new Rewriter();
|
|
89
|
+
const modified = await rewriter.rewrite(
|
|
90
|
+
filePath,
|
|
91
|
+
scanResult,
|
|
92
|
+
translation,
|
|
93
|
+
{
|
|
94
|
+
i18nLibrary: "react-i18next",
|
|
95
|
+
dryRun: false,
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
// Returns: { modified: string, diff: string }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Validator
|
|
102
|
+
|
|
103
|
+
Validates translation coverage:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const validator = new Validator({
|
|
107
|
+
messagesDir: "./messages",
|
|
108
|
+
languages: ["en", "fr"],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const report = await validator.validate();
|
|
112
|
+
// Returns coverage % per language and missing keys
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
Create a `.localize.config.json` in your project root:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"defaultLanguage": "en",
|
|
122
|
+
"languages": ["en", "fr", "es"],
|
|
123
|
+
"messagesDir": "./messages",
|
|
124
|
+
"include": ["src/**/*.{ts,tsx,js,jsx}"],
|
|
125
|
+
"exclude": ["**/*.test.ts", "**/*.spec.ts"],
|
|
126
|
+
"aiProvider": "anthropic",
|
|
127
|
+
"aiModel": "claude-3-sonnet-20240229",
|
|
128
|
+
"keyStyle": "dot.notation",
|
|
129
|
+
"i18nLibrary": "react-i18next",
|
|
130
|
+
"fileOrganization": "per-page",
|
|
131
|
+
"strictMode": true,
|
|
132
|
+
"glossary": {}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Install dependencies
|
|
140
|
+
pnpm install
|
|
141
|
+
|
|
142
|
+
# Build
|
|
143
|
+
pnpm build
|
|
144
|
+
|
|
145
|
+
# Tests
|
|
146
|
+
pnpm test
|
|
147
|
+
pnpm test:watch
|
|
148
|
+
pnpm test:coverage
|
|
149
|
+
|
|
150
|
+
# Type check
|
|
151
|
+
pnpm lint
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
161
|
+
|
|
162
|
+
## Repository
|
|
163
|
+
|
|
164
|
+
https://github.com/SaidKSI/localize-core
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AIRequest, AIResponse } from "../types.js";
|
|
2
|
+
export declare const ANTHROPIC_RATES: Record<string, {
|
|
3
|
+
input: number;
|
|
4
|
+
output: number;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function estimateCost(model: string, inputTokens: number, outputTokens: number): number;
|
|
7
|
+
export interface AnthropicResult {
|
|
8
|
+
/** Maps string value → parsed AI response */
|
|
9
|
+
responses: Map<string, AIResponse>;
|
|
10
|
+
totalCostUsd: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Call the Anthropic API for a batch of AI requests.
|
|
14
|
+
* Runs up to MAX_CONCURRENCY requests in parallel.
|
|
15
|
+
*/
|
|
16
|
+
export declare function callAnthropic(requests: AIRequest[], model: string, apiKey: string): Promise<AnthropicResult>;
|
|
17
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/ai/anthropic.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAQzD,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAI7E,CAAC;AAEF,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,MAAM,CAGR;AAID,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,SAAS,EAAE,EACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAuD1B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import pLimit from "p-limit";
|
|
3
|
+
import { buildTranslationPrompt, parseAIResponse } from "./prompts.js";
|
|
4
|
+
const MAX_CONCURRENCY = 5;
|
|
5
|
+
// ─── Cost estimation ──────────────────────────────────────────────────────────
|
|
6
|
+
// Approximate rates in USD per token (updated 2025)
|
|
7
|
+
export const ANTHROPIC_RATES = {
|
|
8
|
+
"claude-opus-4-6": { input: 15 / 1_000_000, output: 75 / 1_000_000 },
|
|
9
|
+
"claude-sonnet-4-6": { input: 3 / 1_000_000, output: 15 / 1_000_000 },
|
|
10
|
+
"claude-haiku-4-5": { input: 0.8 / 1_000_000, output: 4 / 1_000_000 },
|
|
11
|
+
};
|
|
12
|
+
export function estimateCost(model, inputTokens, outputTokens) {
|
|
13
|
+
const rates = ANTHROPIC_RATES[model] ?? ANTHROPIC_RATES["claude-sonnet-4-6"];
|
|
14
|
+
return inputTokens * rates.input + outputTokens * rates.output;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Call the Anthropic API for a batch of AI requests.
|
|
18
|
+
* Runs up to MAX_CONCURRENCY requests in parallel.
|
|
19
|
+
*/
|
|
20
|
+
export async function callAnthropic(requests, model, apiKey) {
|
|
21
|
+
const client = new Anthropic({ apiKey });
|
|
22
|
+
const limit = pLimit(MAX_CONCURRENCY);
|
|
23
|
+
const responses = new Map();
|
|
24
|
+
let totalCostUsd = 0;
|
|
25
|
+
await Promise.all(requests.map((request) => limit(async () => {
|
|
26
|
+
const prompt = buildTranslationPrompt(request);
|
|
27
|
+
let message;
|
|
28
|
+
try {
|
|
29
|
+
message = await client.messages.create({
|
|
30
|
+
model,
|
|
31
|
+
max_tokens: 1024,
|
|
32
|
+
messages: [{ role: "user", content: prompt }],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// Log and skip — don't fail the entire batch
|
|
37
|
+
console.error(`[localize] Anthropic call failed for "${request.value}": ${String(err)}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const rawText = message.content[0]?.type === "text" ? message.content[0].text : "";
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = parseAIResponse(rawText);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error(`[localize] Failed to parse response for "${request.value}": ${String(err)}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Track cost
|
|
50
|
+
totalCostUsd += estimateCost(model, message.usage.input_tokens, message.usage.output_tokens);
|
|
51
|
+
responses.set(request.value, {
|
|
52
|
+
key: parsed.key,
|
|
53
|
+
translations: parsed.translations,
|
|
54
|
+
});
|
|
55
|
+
})));
|
|
56
|
+
return { responses, totalCostUsd };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/ai/anthropic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ScanResult, AIRequest, LocalizeConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Group scan results by string value.
|
|
4
|
+
* All results with the same value share one AI call.
|
|
5
|
+
* The first result in each group provides context for the AI prompt.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deduplicateResults(results: ScanResult[]): Map<string, ScanResult[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Build one AIRequest per deduplicated group.
|
|
10
|
+
* Uses the first (representative) ScanResult for context.
|
|
11
|
+
* Includes related strings from the same component to guide consistent key naming.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildAIRequests(groups: Map<string, ScanResult[]>, config: LocalizeConfig, allResults?: ScanResult[]): AIRequest[];
|
|
14
|
+
/**
|
|
15
|
+
* Apply resolved keys back to all ScanResults.
|
|
16
|
+
* All results with the same string value get the same key.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyResolvedKeys(results: ScanResult[], responses: Map<string, string>): ScanResult[];
|
|
19
|
+
//# sourceMappingURL=dedup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup.d.ts","sourceRoot":"","sources":["../../src/ai/dedup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA4BzE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,UAAU,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAW3B;AAmBD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,EACjC,MAAM,EAAE,cAAc,EACtB,UAAU,CAAC,EAAE,UAAU,EAAE,GACxB,SAAS,EAAE,CAmDb;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,UAAU,EAAE,EACrB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,UAAU,EAAE,CAKd"}
|
package/dist/ai/dedup.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { basename, extname } from "path";
|
|
2
|
+
// ─── Component context ────────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Derive a human-readable component description from a file path.
|
|
5
|
+
* "src/pages/LoginPage.tsx" → "LoginPage component"
|
|
6
|
+
*/
|
|
7
|
+
function getComponentContext(filePath) {
|
|
8
|
+
const name = basename(filePath, extname(filePath));
|
|
9
|
+
// PascalCase → spaced words: "LoginPage" → "Login Page"
|
|
10
|
+
const spaced = name.replace(/([A-Z])/g, " $1").trim();
|
|
11
|
+
return `${spaced} component`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Extract element description from a ScanResult context string.
|
|
15
|
+
* "JSXText inside <h1>" → "<h1>"
|
|
16
|
+
* "\"placeholder\" attribute on <input>" → "<input>"
|
|
17
|
+
* "Template literal" → "template literal"
|
|
18
|
+
*/
|
|
19
|
+
function getElementFromContext(context) {
|
|
20
|
+
const match = context.match(/<([^>]+)>/);
|
|
21
|
+
return match ? `<${match[1]}>` : context;
|
|
22
|
+
}
|
|
23
|
+
// ─── Deduplication ───────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Group scan results by string value.
|
|
26
|
+
* All results with the same value share one AI call.
|
|
27
|
+
* The first result in each group provides context for the AI prompt.
|
|
28
|
+
*/
|
|
29
|
+
export function deduplicateResults(results) {
|
|
30
|
+
const groups = new Map();
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
const existing = groups.get(result.value);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.push(result);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
groups.set(result.value, [result]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return groups;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derive a grouping key for finding sibling strings in the same component or object.
|
|
44
|
+
* "src/pages/Dashboard.tsx" + null → "src/pages/Dashboard.tsx:dashboard"
|
|
45
|
+
* "src/pages/Dashboard.tsx" + "statusConfig" → "src/pages/Dashboard.tsx:dashboard:statusConfig"
|
|
46
|
+
*
|
|
47
|
+
* For object properties, includes the object name so properties from the same
|
|
48
|
+
* object are grouped together (e.g., statusConfig.online, statusConfig.offline).
|
|
49
|
+
*/
|
|
50
|
+
function getContextKey(filePath, objectKey) {
|
|
51
|
+
const componentName = basename(filePath, extname(filePath)).toLowerCase();
|
|
52
|
+
let key = `${filePath}:${componentName}`;
|
|
53
|
+
if (objectKey) {
|
|
54
|
+
key += `:${objectKey}`;
|
|
55
|
+
}
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build one AIRequest per deduplicated group.
|
|
60
|
+
* Uses the first (representative) ScanResult for context.
|
|
61
|
+
* Includes related strings from the same component to guide consistent key naming.
|
|
62
|
+
*/
|
|
63
|
+
export function buildAIRequests(groups, config, allResults) {
|
|
64
|
+
const requests = [];
|
|
65
|
+
// Build a map of contextKey → all string values in that context
|
|
66
|
+
// Used to find sibling strings
|
|
67
|
+
const contextToValues = new Map();
|
|
68
|
+
if (allResults) {
|
|
69
|
+
for (const result of allResults) {
|
|
70
|
+
const contextKey = getContextKey(result.file, result.objectKey);
|
|
71
|
+
if (!contextToValues.has(contextKey)) {
|
|
72
|
+
contextToValues.set(contextKey, new Set());
|
|
73
|
+
}
|
|
74
|
+
contextToValues.get(contextKey).add(result.value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [value, results] of groups) {
|
|
78
|
+
const rep = results[0];
|
|
79
|
+
const contextKey = getContextKey(rep.file, rep.objectKey);
|
|
80
|
+
// Extract glossary entries relevant to this specific string
|
|
81
|
+
const glossary = {};
|
|
82
|
+
for (const [lang, terms] of Object.entries(config.glossary)) {
|
|
83
|
+
const term = terms[value];
|
|
84
|
+
if (term !== undefined)
|
|
85
|
+
glossary[lang] = term;
|
|
86
|
+
}
|
|
87
|
+
// Find related strings (other strings in the same component, excluding self)
|
|
88
|
+
const relatedStrings = allResults
|
|
89
|
+
? Array.from(contextToValues.get(contextKey) ?? [])
|
|
90
|
+
.filter((v) => v !== value)
|
|
91
|
+
.sort()
|
|
92
|
+
: [];
|
|
93
|
+
const request = {
|
|
94
|
+
file: rep.file,
|
|
95
|
+
componentContext: getComponentContext(rep.file),
|
|
96
|
+
element: getElementFromContext(rep.context),
|
|
97
|
+
surroundingCode: rep.surroundingCode,
|
|
98
|
+
value,
|
|
99
|
+
keyStyle: config.keyStyle,
|
|
100
|
+
glossary,
|
|
101
|
+
targetLanguages: config.languages,
|
|
102
|
+
contextKey,
|
|
103
|
+
...(relatedStrings.length > 0 && { relatedStrings }),
|
|
104
|
+
};
|
|
105
|
+
requests.push(request);
|
|
106
|
+
}
|
|
107
|
+
return requests;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Apply resolved keys back to all ScanResults.
|
|
111
|
+
* All results with the same string value get the same key.
|
|
112
|
+
*/
|
|
113
|
+
export function applyResolvedKeys(results, responses) {
|
|
114
|
+
return results.map((r) => ({
|
|
115
|
+
...r,
|
|
116
|
+
resolvedKey: responses.get(r.value) ?? null,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=dedup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedup.js","sourceRoot":"","sources":["../../src/ai/dedup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ScanResult, LocalizeConfig } from "../types.js";
|
|
2
|
+
export interface TranslateOptions {
|
|
3
|
+
dryRun?: boolean;
|
|
4
|
+
/** Override config.overwriteExisting for this run */
|
|
5
|
+
overwrite?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface TranslateResult {
|
|
8
|
+
/** Scan results updated with resolvedKey populated */
|
|
9
|
+
results: ScanResult[];
|
|
10
|
+
/** Estimated USD cost of all AI calls made */
|
|
11
|
+
aiCostUsd: number;
|
|
12
|
+
/** Absolute paths of messages JSON files written (empty if dryRun) */
|
|
13
|
+
messagesWritten: string[];
|
|
14
|
+
/** Number of unique strings sent to AI */
|
|
15
|
+
uniqueStrings: number;
|
|
16
|
+
/** Number of AI calls actually made (may be less if some were already cached externally) */
|
|
17
|
+
aiCalls: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Translate a list of scan results using the configured AI provider.
|
|
21
|
+
*
|
|
22
|
+
* Steps:
|
|
23
|
+
* 1. Deduplicate by string value
|
|
24
|
+
* 2. Build AI requests (one per unique string)
|
|
25
|
+
* 3. Call Anthropic or OpenAI
|
|
26
|
+
* 4. Assign resolvedKey back to all results
|
|
27
|
+
* 5. Write to messages/{lang}/{pageName}.json (unless dryRun)
|
|
28
|
+
*/
|
|
29
|
+
export declare function translateStrings(scanResults: ScanResult[], config: LocalizeConfig, apiKey: string, options?: TranslateOptions): Promise<TranslateResult>;
|
|
30
|
+
export interface ExistingKeyEntry {
|
|
31
|
+
/** The i18n key, e.g. "auth.sign_in_button" */
|
|
32
|
+
key: string;
|
|
33
|
+
/** The string value in the default language */
|
|
34
|
+
value: string;
|
|
35
|
+
/** Page name derived from the JSON filename, e.g. "login" */
|
|
36
|
+
pageName: string;
|
|
37
|
+
}
|
|
38
|
+
export interface TranslateExistingResult {
|
|
39
|
+
translated: number;
|
|
40
|
+
aiCostUsd: number;
|
|
41
|
+
messagesWritten: string[];
|
|
42
|
+
/** Number of AI API calls made (one per unique string value) */
|
|
43
|
+
aiCalls: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Translate a list of existing (key, value, pageName) entries into target languages.
|
|
47
|
+
* Used by `localize translate --from-existing` and `localize add-lang`.
|
|
48
|
+
*
|
|
49
|
+
* Skips entries that already have translations in target languages
|
|
50
|
+
* unless `overwrite` is true.
|
|
51
|
+
*/
|
|
52
|
+
export declare function translateExistingKeys(entries: ExistingKeyEntry[], config: LocalizeConfig, apiKey: string, options?: {
|
|
53
|
+
dryRun?: boolean;
|
|
54
|
+
overwrite?: boolean;
|
|
55
|
+
langs?: string[];
|
|
56
|
+
}): Promise<TranslateExistingResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Send a minimal test request to verify an API key is valid.
|
|
59
|
+
* Used by `localize init` before saving the key to ~/.localize.
|
|
60
|
+
* Returns true if the key works, false otherwise.
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateApiKey(provider: LocalizeConfig["aiProvider"], model: string, apiKey: string): Promise<boolean>;
|
|
63
|
+
export { deduplicateResults, buildAIRequests } from "./dedup.js";
|
|
64
|
+
export { buildTranslationPrompt, parseAIResponse } from "./prompts.js";
|
|
65
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAc,cAAc,EAAa,MAAM,aAAa,CAAC;AA+QrF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,0CAA0C;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,4FAA4F;IAC5F,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,UAAU,EAAE,EACzB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAoE1B;AAID,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GACxE,OAAO,CAAC,uBAAuB,CAAC,CAmKlC;AAID;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,cAAc,CAAC,YAAY,CAAC,EACtC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAuBlB;AAGD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|