@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +164 -0
  3. package/dist/ai/anthropic.d.ts +17 -0
  4. package/dist/ai/anthropic.d.ts.map +1 -0
  5. package/dist/ai/anthropic.js +58 -0
  6. package/dist/ai/anthropic.js.map +1 -0
  7. package/dist/ai/dedup.d.ts +19 -0
  8. package/dist/ai/dedup.d.ts.map +1 -0
  9. package/dist/ai/dedup.js +119 -0
  10. package/dist/ai/dedup.js.map +1 -0
  11. package/dist/ai/index.d.ts +65 -0
  12. package/dist/ai/index.d.ts.map +1 -0
  13. package/dist/ai/index.js +464 -0
  14. package/dist/ai/index.js.map +1 -0
  15. package/dist/ai/openai.d.ts +11 -0
  16. package/dist/ai/openai.d.ts.map +1 -0
  17. package/dist/ai/openai.js +62 -0
  18. package/dist/ai/openai.js.map +1 -0
  19. package/dist/ai/prompts.d.ts +20 -0
  20. package/dist/ai/prompts.d.ts.map +1 -0
  21. package/dist/ai/prompts.js +151 -0
  22. package/dist/ai/prompts.js.map +1 -0
  23. package/dist/cache/index.d.ts +69 -0
  24. package/dist/cache/index.d.ts.map +1 -0
  25. package/dist/cache/index.js +129 -0
  26. package/dist/cache/index.js.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/rewriter/index.d.ts +31 -0
  32. package/dist/rewriter/index.d.ts.map +1 -0
  33. package/dist/rewriter/index.js +128 -0
  34. package/dist/rewriter/index.js.map +1 -0
  35. package/dist/rewriter/transforms.d.ts +38 -0
  36. package/dist/rewriter/transforms.d.ts.map +1 -0
  37. package/dist/rewriter/transforms.js +189 -0
  38. package/dist/rewriter/transforms.js.map +1 -0
  39. package/dist/rewriter/ts-morph.d.ts +19 -0
  40. package/dist/rewriter/ts-morph.d.ts.map +1 -0
  41. package/dist/rewriter/ts-morph.js +121 -0
  42. package/dist/rewriter/ts-morph.js.map +1 -0
  43. package/dist/scanner/babel.d.ts +3 -0
  44. package/dist/scanner/babel.d.ts.map +1 -0
  45. package/dist/scanner/babel.js +504 -0
  46. package/dist/scanner/babel.js.map +1 -0
  47. package/dist/scanner/filters.d.ts +38 -0
  48. package/dist/scanner/filters.d.ts.map +1 -0
  49. package/dist/scanner/filters.js +133 -0
  50. package/dist/scanner/filters.js.map +1 -0
  51. package/dist/scanner/index.d.ts +22 -0
  52. package/dist/scanner/index.d.ts.map +1 -0
  53. package/dist/scanner/index.js +82 -0
  54. package/dist/scanner/index.js.map +1 -0
  55. package/dist/scanner/typescript.d.ts +3 -0
  56. package/dist/scanner/typescript.d.ts.map +1 -0
  57. package/dist/scanner/typescript.js +542 -0
  58. package/dist/scanner/typescript.js.map +1 -0
  59. package/dist/types.d.ts +205 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +3 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/validator/index.d.ts +65 -0
  64. package/dist/validator/index.d.ts.map +1 -0
  65. package/dist/validator/index.js +237 -0
  66. package/dist/validator/index.js.map +1 -0
  67. 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"}
@@ -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"}