@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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// ─── Language names ───────────────────────────────────────────────────────────
|
|
2
|
+
const LANGUAGE_NAMES = {
|
|
3
|
+
en: "English",
|
|
4
|
+
fr: "French",
|
|
5
|
+
ar: "Arabic",
|
|
6
|
+
es: "Spanish",
|
|
7
|
+
de: "German",
|
|
8
|
+
it: "Italian",
|
|
9
|
+
pt: "Portuguese",
|
|
10
|
+
ja: "Japanese",
|
|
11
|
+
zh: "Chinese (Simplified)",
|
|
12
|
+
ko: "Korean",
|
|
13
|
+
ru: "Russian",
|
|
14
|
+
nl: "Dutch",
|
|
15
|
+
pl: "Polish",
|
|
16
|
+
tr: "Turkish",
|
|
17
|
+
sv: "Swedish",
|
|
18
|
+
da: "Danish",
|
|
19
|
+
fi: "Finnish",
|
|
20
|
+
nb: "Norwegian",
|
|
21
|
+
cs: "Czech",
|
|
22
|
+
hu: "Hungarian",
|
|
23
|
+
ro: "Romanian",
|
|
24
|
+
uk: "Ukrainian",
|
|
25
|
+
he: "Hebrew",
|
|
26
|
+
hi: "Hindi",
|
|
27
|
+
th: "Thai",
|
|
28
|
+
vi: "Vietnamese",
|
|
29
|
+
id: "Indonesian",
|
|
30
|
+
};
|
|
31
|
+
export function getLanguageName(code) {
|
|
32
|
+
return LANGUAGE_NAMES[code] ?? code.toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
// ─── Prompt builder ───────────────────────────────────────────────────────────
|
|
35
|
+
export function buildTranslationPrompt(request) {
|
|
36
|
+
const { file, componentContext, element, surroundingCode, value, keyStyle, glossary, targetLanguages, } = request;
|
|
37
|
+
const indentedCode = surroundingCode
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map((l) => ` ${l}`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
const glossarySection = Object.keys(glossary).length > 0
|
|
42
|
+
? `Glossary (use these exact terms):\n${Object.entries(glossary)
|
|
43
|
+
.map(([lang, term]) => ` ${lang}: "${term}"`)
|
|
44
|
+
.join("\n")}\n\n`
|
|
45
|
+
: "";
|
|
46
|
+
const relatedStringsSection = request.relatedStrings && request.relatedStrings.length > 0
|
|
47
|
+
? `Related strings in same component (for consistent naming):\n${request.relatedStrings
|
|
48
|
+
.slice(0, 5)
|
|
49
|
+
.map((s) => ` "${s}"`)
|
|
50
|
+
.join("\n")}\n\n`
|
|
51
|
+
: "";
|
|
52
|
+
const languageList = targetLanguages
|
|
53
|
+
.map((code) => `${getLanguageName(code)} (${code})`)
|
|
54
|
+
.join(", ");
|
|
55
|
+
const exampleKey = keyStyle === "dot.notation"
|
|
56
|
+
? "section.descriptive_key"
|
|
57
|
+
: "section_descriptive_key";
|
|
58
|
+
const exampleTranslations = Object.fromEntries(targetLanguages.map((lang) => [lang, "..."]));
|
|
59
|
+
// Detect whether this string contains i18next interpolation placeholders
|
|
60
|
+
const hasInterpolation = /\{\{[^}]+\}\}/.test(value);
|
|
61
|
+
const interpolationRule = hasInterpolation
|
|
62
|
+
? `- This string contains i18next interpolation placeholders like {{count}}. Preserve ALL {{placeholder}} tokens exactly as-is in every translation — do NOT translate, rename, or remove them.\n`
|
|
63
|
+
: "";
|
|
64
|
+
return `You are an i18n key naming and translation assistant.
|
|
65
|
+
|
|
66
|
+
File: ${file}
|
|
67
|
+
Component: ${componentContext}
|
|
68
|
+
Element: ${element}
|
|
69
|
+
Surrounding code:
|
|
70
|
+
${indentedCode}
|
|
71
|
+
|
|
72
|
+
String: "${value}"
|
|
73
|
+
Key style: ${keyStyle}
|
|
74
|
+
${glossarySection}${relatedStringsSection}Tasks:
|
|
75
|
+
1. Generate a concise semantic i18n key in ${keyStyle} format
|
|
76
|
+
2. Translate the string into: ${languageList}
|
|
77
|
+
|
|
78
|
+
Rules:
|
|
79
|
+
- Key must be lowercase and context-aware (e.g. "auth.sign_in_button", not "button1")
|
|
80
|
+
- If related strings exist in the same component, use consistent namespace roots (e.g. all under "dashboard.statistics" not mixed namespaces)
|
|
81
|
+
- Use glossary terms exactly when provided
|
|
82
|
+
- Translations must be natural, not word-for-word literal
|
|
83
|
+
${interpolationRule}- Return ONLY valid JSON — no explanation, no markdown fences
|
|
84
|
+
|
|
85
|
+
Return exactly this shape:
|
|
86
|
+
{ "key": "${exampleKey}", "translations": ${JSON.stringify(exampleTranslations)} }`;
|
|
87
|
+
}
|
|
88
|
+
// ─── Translation-only prompt (--from-existing) ───────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Simpler prompt used when keys already exist and we only need translations.
|
|
91
|
+
* Returns a flat { lang: translation } object — no key generation.
|
|
92
|
+
*/
|
|
93
|
+
export function buildTranslationOnlyPrompt(value, targetLanguages, glossary = {}) {
|
|
94
|
+
const languageList = targetLanguages
|
|
95
|
+
.map((code) => `${getLanguageName(code)} (${code})`)
|
|
96
|
+
.join(", ");
|
|
97
|
+
const glossarySection = Object.keys(glossary).length > 0
|
|
98
|
+
? `Glossary (use these exact terms):\n${Object.entries(glossary)
|
|
99
|
+
.map(([lang, term]) => ` ${lang}: "${term}"`)
|
|
100
|
+
.join("\n")}\n\n`
|
|
101
|
+
: "";
|
|
102
|
+
const example = Object.fromEntries(targetLanguages.map((l) => [l, "..."]));
|
|
103
|
+
return `You are a professional translator.
|
|
104
|
+
|
|
105
|
+
String: "${value}"
|
|
106
|
+
Translate into: ${languageList}
|
|
107
|
+
${glossarySection}Rules:
|
|
108
|
+
- Translations must be natural, not word-for-word literal
|
|
109
|
+
- Use glossary terms exactly when provided
|
|
110
|
+
- Return ONLY valid JSON with no explanation or markdown
|
|
111
|
+
|
|
112
|
+
Return exactly:
|
|
113
|
+
${JSON.stringify(example)}`;
|
|
114
|
+
}
|
|
115
|
+
/** Parse a flat translation-only response: { fr: "...", ar: "..." } */
|
|
116
|
+
export function parseTranslationOnlyResponse(text) {
|
|
117
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
118
|
+
if (!match)
|
|
119
|
+
throw new Error(`No JSON found in AI response:\n${text}`);
|
|
120
|
+
const parsed = JSON.parse(match[0]);
|
|
121
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
122
|
+
throw new Error(`Invalid translation response: ${match[0]}`);
|
|
123
|
+
}
|
|
124
|
+
return parsed;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Extract and validate the JSON object from an AI response string.
|
|
128
|
+
* Tolerates leading/trailing explanation text around the JSON.
|
|
129
|
+
*/
|
|
130
|
+
export function parseAIResponse(text) {
|
|
131
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
132
|
+
if (!match) {
|
|
133
|
+
throw new Error(`No JSON object found in AI response:\n${text}`);
|
|
134
|
+
}
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(match[0]);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new Error(`Failed to parse AI response JSON: ${String(err)}\n${match[0]}`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof parsed !== "object" ||
|
|
143
|
+
parsed === null ||
|
|
144
|
+
typeof parsed["key"] !== "string" ||
|
|
145
|
+
typeof parsed["translations"] !== "object" ||
|
|
146
|
+
parsed["translations"] === null) {
|
|
147
|
+
throw new Error(`AI response missing required fields (key, translations):\n${match[0]}`);
|
|
148
|
+
}
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/ai/prompts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CacheStore } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute the SHA-256 hex digest of a string (file source contents).
|
|
4
|
+
* Exported for unit testing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hashSource(source: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Read the cache store from `{projectRoot}/.localize/cache.json`.
|
|
9
|
+
* Returns an empty store if the file doesn't exist or is unreadable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readCache(projectRoot: string): Promise<CacheStore>;
|
|
12
|
+
/**
|
|
13
|
+
* Persist the cache store to `{projectRoot}/.localize/cache.json`.
|
|
14
|
+
* Creates the `.localize/` directory if it doesn't exist.
|
|
15
|
+
*/
|
|
16
|
+
export declare function writeCache(projectRoot: string, store: CacheStore): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Delete the cache file entirely.
|
|
19
|
+
* Used by `localize run --force` to trigger a full re-process.
|
|
20
|
+
*/
|
|
21
|
+
export declare function clearCache(projectRoot: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Check whether a file (identified by its path and current source contents)
|
|
24
|
+
* has already been processed and hasn't changed since.
|
|
25
|
+
*
|
|
26
|
+
* @param store The loaded CacheStore
|
|
27
|
+
* @param relPath File path relative to project root (used as the cache key)
|
|
28
|
+
* @param source Current file contents
|
|
29
|
+
*/
|
|
30
|
+
export declare function isCached(store: CacheStore, relPath: string, source: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Record that a file has been successfully processed.
|
|
33
|
+
* Returns a new CacheStore with the entry upserted (immutable update).
|
|
34
|
+
*
|
|
35
|
+
* @param store The current CacheStore
|
|
36
|
+
* @param relPath File path relative to project root
|
|
37
|
+
* @param source File contents at time of processing
|
|
38
|
+
* @param stringCount Number of strings processed in this file
|
|
39
|
+
*/
|
|
40
|
+
export declare function markCached(store: CacheStore, relPath: string, source: string, stringCount: number): CacheStore;
|
|
41
|
+
/**
|
|
42
|
+
* Remove a single file's entry from the cache.
|
|
43
|
+
* Returns a new CacheStore (immutable update).
|
|
44
|
+
*/
|
|
45
|
+
export declare function evictEntry(store: CacheStore, relPath: string): CacheStore;
|
|
46
|
+
/**
|
|
47
|
+
* Filter a list of file paths down to those that are NOT in the cache
|
|
48
|
+
* (or whose contents have changed since last processing).
|
|
49
|
+
*
|
|
50
|
+
* @param store Loaded CacheStore
|
|
51
|
+
* @param files Array of { relPath, source } objects
|
|
52
|
+
*/
|
|
53
|
+
export declare function filterUncached(store: CacheStore, files: Array<{
|
|
54
|
+
relPath: string;
|
|
55
|
+
source: string;
|
|
56
|
+
}>): Array<{
|
|
57
|
+
relPath: string;
|
|
58
|
+
source: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Apply a batch of processed files to the cache store.
|
|
62
|
+
* Returns a new CacheStore with all entries upserted.
|
|
63
|
+
*/
|
|
64
|
+
export declare function markBatchCached(store: CacheStore, processed: Array<{
|
|
65
|
+
relPath: string;
|
|
66
|
+
source: string;
|
|
67
|
+
stringCount: number;
|
|
68
|
+
}>): CacheStore;
|
|
69
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,aAAa,CAAC;AAoB1D;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAID;;;GAGG;AACH,wBAAsB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAWxE;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE;AAID;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAIT;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,UAAU,CAUZ;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAGzE;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAChD,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAE5C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,GACzE,UAAU,CAMZ"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
3
|
+
import { resolve, join } from "path";
|
|
4
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
+
const CACHE_DIR = ".localize";
|
|
6
|
+
const CACHE_FILE = "cache.json";
|
|
7
|
+
const CURRENT_VERSION = 1;
|
|
8
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
9
|
+
function getCachePath(projectRoot) {
|
|
10
|
+
return join(resolve(projectRoot), CACHE_DIR, CACHE_FILE);
|
|
11
|
+
}
|
|
12
|
+
function emptyStore() {
|
|
13
|
+
return { version: CURRENT_VERSION, entries: {} };
|
|
14
|
+
}
|
|
15
|
+
// ─── Hash ─────────────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Compute the SHA-256 hex digest of a string (file source contents).
|
|
18
|
+
* Exported for unit testing.
|
|
19
|
+
*/
|
|
20
|
+
export function hashSource(source) {
|
|
21
|
+
return createHash("sha256").update(source, "utf-8").digest("hex");
|
|
22
|
+
}
|
|
23
|
+
// ─── Read / Write ─────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Read the cache store from `{projectRoot}/.localize/cache.json`.
|
|
26
|
+
* Returns an empty store if the file doesn't exist or is unreadable.
|
|
27
|
+
*/
|
|
28
|
+
export async function readCache(projectRoot) {
|
|
29
|
+
const cachePath = getCachePath(projectRoot);
|
|
30
|
+
try {
|
|
31
|
+
const content = await readFile(cachePath, "utf-8");
|
|
32
|
+
const parsed = JSON.parse(content);
|
|
33
|
+
// Invalidate on version mismatch
|
|
34
|
+
if (parsed.version !== CURRENT_VERSION)
|
|
35
|
+
return emptyStore();
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return emptyStore();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Persist the cache store to `{projectRoot}/.localize/cache.json`.
|
|
44
|
+
* Creates the `.localize/` directory if it doesn't exist.
|
|
45
|
+
*/
|
|
46
|
+
export async function writeCache(projectRoot, store) {
|
|
47
|
+
const cachePath = getCachePath(projectRoot);
|
|
48
|
+
await mkdir(join(resolve(projectRoot), CACHE_DIR), { recursive: true });
|
|
49
|
+
await writeFile(cachePath, JSON.stringify(store, null, 2) + "\n", "utf-8");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Delete the cache file entirely.
|
|
53
|
+
* Used by `localize run --force` to trigger a full re-process.
|
|
54
|
+
*/
|
|
55
|
+
export async function clearCache(projectRoot) {
|
|
56
|
+
const cachePath = getCachePath(projectRoot);
|
|
57
|
+
try {
|
|
58
|
+
await unlink(cachePath);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Already gone — that's fine
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ─── Cache checks ─────────────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Check whether a file (identified by its path and current source contents)
|
|
67
|
+
* has already been processed and hasn't changed since.
|
|
68
|
+
*
|
|
69
|
+
* @param store The loaded CacheStore
|
|
70
|
+
* @param relPath File path relative to project root (used as the cache key)
|
|
71
|
+
* @param source Current file contents
|
|
72
|
+
*/
|
|
73
|
+
export function isCached(store, relPath, source) {
|
|
74
|
+
const entry = store.entries[relPath];
|
|
75
|
+
if (!entry)
|
|
76
|
+
return false;
|
|
77
|
+
return entry.hash === hashSource(source);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Record that a file has been successfully processed.
|
|
81
|
+
* Returns a new CacheStore with the entry upserted (immutable update).
|
|
82
|
+
*
|
|
83
|
+
* @param store The current CacheStore
|
|
84
|
+
* @param relPath File path relative to project root
|
|
85
|
+
* @param source File contents at time of processing
|
|
86
|
+
* @param stringCount Number of strings processed in this file
|
|
87
|
+
*/
|
|
88
|
+
export function markCached(store, relPath, source, stringCount) {
|
|
89
|
+
const entry = {
|
|
90
|
+
hash: hashSource(source),
|
|
91
|
+
processedAt: new Date().toISOString(),
|
|
92
|
+
stringCount,
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
...store,
|
|
96
|
+
entries: { ...store.entries, [relPath]: entry },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Remove a single file's entry from the cache.
|
|
101
|
+
* Returns a new CacheStore (immutable update).
|
|
102
|
+
*/
|
|
103
|
+
export function evictEntry(store, relPath) {
|
|
104
|
+
const { [relPath]: _, ...rest } = store.entries;
|
|
105
|
+
return { ...store, entries: rest };
|
|
106
|
+
}
|
|
107
|
+
// ─── Batch helpers ────────────────────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Filter a list of file paths down to those that are NOT in the cache
|
|
110
|
+
* (or whose contents have changed since last processing).
|
|
111
|
+
*
|
|
112
|
+
* @param store Loaded CacheStore
|
|
113
|
+
* @param files Array of { relPath, source } objects
|
|
114
|
+
*/
|
|
115
|
+
export function filterUncached(store, files) {
|
|
116
|
+
return files.filter(({ relPath, source }) => !isCached(store, relPath, source));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply a batch of processed files to the cache store.
|
|
120
|
+
* Returns a new CacheStore with all entries upserted.
|
|
121
|
+
*/
|
|
122
|
+
export function markBatchCached(store, processed) {
|
|
123
|
+
let updated = store;
|
|
124
|
+
for (const { relPath, source, stringCount } of processed) {
|
|
125
|
+
updated = markCached(updated, relPath, source, stringCount);
|
|
126
|
+
}
|
|
127
|
+
return updated;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAGrC,iFAAiF;AAEjF,MAAM,SAAS,GAAG,WAAW,CAAC;AAC9B,MAAM,UAAU,GAAG,YAAY,CAAC;AAChC,MAAM,eAAe,GAAG,CAAU,CAAC;AAEnC,gFAAgF;AAEhF,SAAS,YAAY,CAAC,WAAmB;IACvC,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AACnD,CAAC;AAED,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AAED,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,WAAmB;IACjD,MAAM,SAAS,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAe,CAAC;QACjD,iCAAiC;QACjC,IAAI,MAAM,CAAC,OAAO,KAAK,eAAe;YAAE,OAAO,UAAU,EAAE,CAAC;QAC5D,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,UAAU,EAAE,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,WAAmB,EACnB,KAAiB;IAEjB,MAAM,SAAS,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAC5C,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,MAAM,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AAC7E,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,WAAmB;IAClD,MAAM,SAAS,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,6BAA6B;IAC/B,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAiB,EACjB,OAAe,EACf,MAAc;IAEd,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CACxB,KAAiB,EACjB,OAAe,EACf,MAAc,EACd,WAAmB;IAEnB,MAAM,KAAK,GAAe;QACxB,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC;QACxB,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACrC,WAAW;KACZ,CAAC;IACF,OAAO;QACL,GAAG,KAAK;QACR,OAAO,EAAE,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE;KAChD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAiB,EAAE,OAAe;IAC3D,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC;IAChD,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,iFAAiF;AAEjF;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAiB,EACjB,KAAiD;IAEjD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAiB,EACjB,SAA0E;IAE1E,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,SAAS,EAAE,CAAC;QACzD,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { scanFile, scanFiles, scanDirectory, buildScanReport } from "./scanner/index.js";
|
|
3
|
+
export { shouldFilter, NON_TRANSLATABLE_ATTRS, TRANSLATION_FNS } from "./scanner/filters.js";
|
|
4
|
+
export * from "./rewriter/index.js";
|
|
5
|
+
export * from "./ai/index.js";
|
|
6
|
+
export { flattenKeys, readLanguageKeys, validateCoverage, isFullyCovered, getMissingKeys, resolveKeysFromMessages, type ValidateOptions, } from "./validator/index.js";
|
|
7
|
+
export * from "./cache/index.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG7F,cAAc,qBAAqB,CAAC;AAGpC,cAAc,eAAe,CAAC;AAG9B,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAC;AAG9B,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Types — import these in all modules
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
// Scanner
|
|
4
|
+
export { scanFile, scanFiles, scanDirectory, buildScanReport } from "./scanner/index.js";
|
|
5
|
+
export { shouldFilter, NON_TRANSLATABLE_ATTRS, TRANSLATION_FNS } from "./scanner/filters.js";
|
|
6
|
+
// Rewriter — Step 6
|
|
7
|
+
export * from "./rewriter/index.js";
|
|
8
|
+
// AI client — Step 5
|
|
9
|
+
export * from "./ai/index.js";
|
|
10
|
+
// Validator — Step 7
|
|
11
|
+
export { flattenKeys, readLanguageKeys, validateCoverage, isFullyCovered, getMissingKeys, resolveKeysFromMessages, } from "./validator/index.js";
|
|
12
|
+
// Cache — Step 8
|
|
13
|
+
export * from "./cache/index.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,cAAc,YAAY,CAAC;AAE3B,UAAU;AACV,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE7F,oBAAoB;AACpB,cAAc,qBAAqB,CAAC;AAEpC,qBAAqB;AACrB,cAAc,eAAe,CAAC;AAE9B,qBAAqB;AACrB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,uBAAuB,GAExB,MAAM,sBAAsB,CAAC;AAE9B,iBAAiB;AACjB,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ScanResult, RewriteResult, LocalizeConfig } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a readable line-level diff between two source strings.
|
|
4
|
+
* Shows changed lines with - / + prefixes and up to 2 lines of context.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateDiff(original: string, modified: string, filePath: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Rewrite a source file: replace hardcoded strings with t() calls,
|
|
9
|
+
* add the i18n import and hook if missing.
|
|
10
|
+
*
|
|
11
|
+
* Does NOT write to disk — returns the modified source and diff.
|
|
12
|
+
* The CLI layer handles confirmation and calls `applyRewrite` to persist.
|
|
13
|
+
*/
|
|
14
|
+
export declare function rewriteFile(filePath: string, results: ScanResult[], config: LocalizeConfig): Promise<RewriteResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Write the modified source from a RewriteResult to disk.
|
|
17
|
+
* Call this after the user confirms the diff.
|
|
18
|
+
* Returns the result with `applied: true`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function applyRewrite(result: RewriteResult): Promise<RewriteResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Rewrite multiple files. Each file is processed independently.
|
|
23
|
+
* Results are returned in the same order as the input file list.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rewriteFiles(fileResultsMap: Map<string, ScanResult[]>, config: LocalizeConfig): Promise<RewriteResult[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Group a flat ScanResult[] by source file.
|
|
28
|
+
* Useful before calling rewriteFiles.
|
|
29
|
+
*/
|
|
30
|
+
export declare function groupResultsByFile(results: ScanResult[]): Map<string, ScanResult[]>;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rewriter/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAM7E;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM,CA6CR;AAID;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,EAAE,EACrB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,aAAa,CAAC,CA4CxB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAGhF;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,EACzC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,aAAa,EAAE,CAAC,CAO1B;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,UAAU,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAW3B"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { resolve, basename, extname } from "path";
|
|
3
|
+
import { applyStringReplacements, getAdapter } from "./transforms.js";
|
|
4
|
+
import { ensureTranslationBoilerplate } from "./ts-morph.js";
|
|
5
|
+
// ─── Diff generation ─────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Generate a readable line-level diff between two source strings.
|
|
8
|
+
* Shows changed lines with - / + prefixes and up to 2 lines of context.
|
|
9
|
+
*/
|
|
10
|
+
export function generateDiff(original, modified, filePath) {
|
|
11
|
+
if (original === modified)
|
|
12
|
+
return "";
|
|
13
|
+
const origLines = original.split("\n");
|
|
14
|
+
const modLines = modified.split("\n");
|
|
15
|
+
const output = [`--- ${filePath}`, `+++ ${filePath}`];
|
|
16
|
+
// Collect all changed line indices
|
|
17
|
+
const maxLen = Math.max(origLines.length, modLines.length);
|
|
18
|
+
const changedLines = new Set();
|
|
19
|
+
for (let i = 0; i < maxLen; i++) {
|
|
20
|
+
if (origLines[i] !== modLines[i])
|
|
21
|
+
changedLines.add(i);
|
|
22
|
+
}
|
|
23
|
+
// Build context windows (±2 lines around each change)
|
|
24
|
+
const CONTEXT = 2;
|
|
25
|
+
const toShow = new Set();
|
|
26
|
+
for (const idx of changedLines) {
|
|
27
|
+
for (let c = Math.max(0, idx - CONTEXT); c <= Math.min(maxLen - 1, idx + CONTEXT); c++) {
|
|
28
|
+
toShow.add(c);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sortedIndices = [...toShow].sort((a, b) => a - b);
|
|
32
|
+
let prevIdx = -1;
|
|
33
|
+
for (const i of sortedIndices) {
|
|
34
|
+
// Show separator for gaps
|
|
35
|
+
if (prevIdx !== -1 && i > prevIdx + 1) {
|
|
36
|
+
output.push("@@");
|
|
37
|
+
}
|
|
38
|
+
prevIdx = i;
|
|
39
|
+
const orig = origLines[i];
|
|
40
|
+
const mod = modLines[i];
|
|
41
|
+
if (orig === mod) {
|
|
42
|
+
output.push(` ${orig ?? ""}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if (orig !== undefined)
|
|
46
|
+
output.push(`- ${orig}`);
|
|
47
|
+
if (mod !== undefined)
|
|
48
|
+
output.push(`+ ${mod}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return output.join("\n");
|
|
52
|
+
}
|
|
53
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Rewrite a source file: replace hardcoded strings with t() calls,
|
|
56
|
+
* add the i18n import and hook if missing.
|
|
57
|
+
*
|
|
58
|
+
* Does NOT write to disk — returns the modified source and diff.
|
|
59
|
+
* The CLI layer handles confirmation and calls `applyRewrite` to persist.
|
|
60
|
+
*/
|
|
61
|
+
export async function rewriteFile(filePath, results, config) {
|
|
62
|
+
const absolute = resolve(filePath);
|
|
63
|
+
const originalSource = await readFile(absolute, "utf-8");
|
|
64
|
+
const resolved = results.filter((r) => r.resolvedKey !== null);
|
|
65
|
+
if (resolved.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
file: absolute,
|
|
68
|
+
applied: false,
|
|
69
|
+
changesCount: 0,
|
|
70
|
+
diff: "",
|
|
71
|
+
modifiedSource: originalSource,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const adapter = getAdapter(config.i18nLibrary);
|
|
75
|
+
// Step 1: Replace hardcoded strings with t() calls (positional, text-based)
|
|
76
|
+
const { modified: afterReplacements, count } = applyStringReplacements(originalSource, resolved, adapter);
|
|
77
|
+
// Step 2: Ensure import + hook are present (ts-morph structural edits)
|
|
78
|
+
// Derive namespace from filename: Login.tsx → "login", home.tsx → "home"
|
|
79
|
+
const namespace = basename(filePath, extname(filePath)).toLowerCase();
|
|
80
|
+
const finalSource = ensureTranslationBoilerplate(afterReplacements, filePath, adapter, namespace);
|
|
81
|
+
const diff = generateDiff(originalSource, finalSource, filePath);
|
|
82
|
+
return {
|
|
83
|
+
file: absolute,
|
|
84
|
+
applied: false,
|
|
85
|
+
changesCount: count,
|
|
86
|
+
diff,
|
|
87
|
+
modifiedSource: finalSource,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Write the modified source from a RewriteResult to disk.
|
|
92
|
+
* Call this after the user confirms the diff.
|
|
93
|
+
* Returns the result with `applied: true`.
|
|
94
|
+
*/
|
|
95
|
+
export async function applyRewrite(result) {
|
|
96
|
+
await writeFile(result.file, result.modifiedSource, "utf-8");
|
|
97
|
+
return { ...result, applied: true };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Rewrite multiple files. Each file is processed independently.
|
|
101
|
+
* Results are returned in the same order as the input file list.
|
|
102
|
+
*/
|
|
103
|
+
export async function rewriteFiles(fileResultsMap, config) {
|
|
104
|
+
const rewrites = [];
|
|
105
|
+
for (const [filePath, results] of fileResultsMap) {
|
|
106
|
+
const rewrite = await rewriteFile(filePath, results, config);
|
|
107
|
+
rewrites.push(rewrite);
|
|
108
|
+
}
|
|
109
|
+
return rewrites;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Group a flat ScanResult[] by source file.
|
|
113
|
+
* Useful before calling rewriteFiles.
|
|
114
|
+
*/
|
|
115
|
+
export function groupResultsByFile(results) {
|
|
116
|
+
const map = new Map();
|
|
117
|
+
for (const result of results) {
|
|
118
|
+
const existing = map.get(result.file);
|
|
119
|
+
if (existing) {
|
|
120
|
+
existing.push(result);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
map.set(result.file, [result]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return map;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rewriter/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ScanResult, I18nLibrary } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Encapsulates the library-specific details for import, hook, and call expression.
|
|
4
|
+
* Each supported i18n library has its own adapter.
|
|
5
|
+
*/
|
|
6
|
+
export interface LibraryAdapter {
|
|
7
|
+
/** npm package to import from */
|
|
8
|
+
importSource: string;
|
|
9
|
+
/** Named export to import: "useTranslation", "useTranslations", etc. */
|
|
10
|
+
importSpecifier: string;
|
|
11
|
+
/** The full import statement added to the top of the file */
|
|
12
|
+
importStatement: string;
|
|
13
|
+
/** Hook declaration added at the top of the component body */
|
|
14
|
+
hookStatement: string;
|
|
15
|
+
/**
|
|
16
|
+
* A string that uniquely identifies this hook in source code.
|
|
17
|
+
* Used to detect whether the hook is already present.
|
|
18
|
+
*/
|
|
19
|
+
hookDetectionString: string;
|
|
20
|
+
/** Build the call expression for a given i18n key, e.g. t('auth.login') */
|
|
21
|
+
callFor(key: string): string;
|
|
22
|
+
}
|
|
23
|
+
export declare const LIBRARY_ADAPTERS: Record<I18nLibrary, LibraryAdapter>;
|
|
24
|
+
export declare function getAdapter(library: I18nLibrary): LibraryAdapter;
|
|
25
|
+
/**
|
|
26
|
+
* Apply all resolved string replacements to the source text.
|
|
27
|
+
*
|
|
28
|
+
* Strategy:
|
|
29
|
+
* - Sort results bottom-to-top, right-to-left so earlier replacements
|
|
30
|
+
* don't shift the positions of later ones on the same line.
|
|
31
|
+
* - For each result, locate the exact source span from its NodeType and
|
|
32
|
+
* replace it with the appropriate t() call expression.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyStringReplacements(source: string, results: ScanResult[], adapter: LibraryAdapter): {
|
|
35
|
+
modified: string;
|
|
36
|
+
count: number;
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=transforms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../../src/rewriter/transforms.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI3D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,eAAe,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,WAAW,EAAE,cAAc,CAyChE,CAAC;AAEF,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAE/D;AAID;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,cAAc,GACtB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAgJrC"}
|