@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
@@ -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":"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"}
@@ -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"}