@scoutello/i18n-magic 0.15.2 → 0.18.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 (63) hide show
  1. package/README.md +178 -17
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +101 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/check-missing.d.ts +1 -0
  7. package/dist/commands/check-missing.d.ts.map +1 -0
  8. package/dist/commands/check-missing.js +13 -0
  9. package/dist/commands/check-missing.js.map +1 -0
  10. package/dist/commands/clean.d.ts +3 -0
  11. package/dist/commands/clean.d.ts.map +1 -0
  12. package/dist/commands/clean.js +81 -0
  13. package/dist/commands/clean.js.map +1 -0
  14. package/dist/commands/create-pruned-namespace-automated.d.ts +20 -0
  15. package/dist/commands/create-pruned-namespace-automated.d.ts.map +1 -0
  16. package/dist/commands/create-pruned-namespace-automated.js +98 -0
  17. package/dist/commands/create-pruned-namespace-automated.js.map +1 -0
  18. package/dist/commands/create-pruned-namespace.d.ts +3 -0
  19. package/dist/commands/create-pruned-namespace.d.ts.map +1 -0
  20. package/dist/commands/create-pruned-namespace.js +122 -0
  21. package/dist/commands/create-pruned-namespace.js.map +1 -0
  22. package/dist/commands/replace.d.ts +1 -0
  23. package/dist/commands/replace.d.ts.map +1 -0
  24. package/dist/commands/replace.js +58 -0
  25. package/dist/commands/replace.js.map +1 -0
  26. package/dist/commands/scan.d.ts +1 -0
  27. package/dist/commands/scan.d.ts.map +1 -0
  28. package/dist/commands/scan.js +70 -0
  29. package/dist/commands/scan.js.map +1 -0
  30. package/dist/commands/sync-locales.d.ts +1 -0
  31. package/dist/commands/sync-locales.d.ts.map +1 -0
  32. package/dist/commands/sync-locales.js +78 -0
  33. package/dist/commands/sync-locales.js.map +1 -0
  34. package/dist/i18n-magic.cjs.development.js +458 -126
  35. package/dist/i18n-magic.cjs.development.js.map +1 -1
  36. package/dist/i18n-magic.cjs.production.min.js +1 -1
  37. package/dist/i18n-magic.cjs.production.min.js.map +1 -1
  38. package/dist/i18n-magic.esm.js +449 -126
  39. package/dist/i18n-magic.esm.js.map +1 -1
  40. package/dist/index.d.ts +11 -1
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +22 -9
  43. package/dist/index.js.map +1 -0
  44. package/dist/lib/languges.d.ts +1 -0
  45. package/dist/lib/languges.d.ts.map +1 -0
  46. package/dist/lib/languges.js +146 -0
  47. package/dist/lib/languges.js.map +1 -0
  48. package/dist/lib/types.d.ts +8 -0
  49. package/dist/lib/types.d.ts.map +1 -0
  50. package/dist/lib/types.js +3 -0
  51. package/dist/lib/types.js.map +1 -0
  52. package/dist/lib/utils.d.ts +1 -0
  53. package/dist/lib/utils.d.ts.map +1 -0
  54. package/dist/lib/utils.js +220 -0
  55. package/dist/lib/utils.js.map +1 -0
  56. package/package.json +37 -14
  57. package/src/cli.ts +117 -0
  58. package/src/commands/clean.ts +105 -0
  59. package/src/commands/create-pruned-namespace-automated.ts +165 -0
  60. package/src/commands/create-pruned-namespace.ts +165 -0
  61. package/src/commands/scan.ts +12 -0
  62. package/src/index.ts +23 -106
  63. package/src/lib/types.ts +8 -0
@@ -1,6 +1,11 @@
1
1
  import type OpenAI from "openai";
2
2
  import type { ChatModel } from "openai/resources/chat/chat";
3
3
  type Model = ChatModel | "gemini-2.5-pro-exp-03-25" | "gemini-2.0-flash" | "gemini-2.0-flash-lite";
4
+ export interface NamespacePruneConfig {
5
+ sourceNamespace: string;
6
+ newNamespace: string;
7
+ globPatterns: string[];
8
+ }
4
9
  export interface Configuration {
5
10
  loadPath: string | ((locale: string, namespace: string) => Promise<Record<string, string>>);
6
11
  savePath: string | ((locale: string, namespace: string, data: Record<string, string>) => Promise<void>);
@@ -11,10 +16,12 @@ export interface Configuration {
11
16
  globPatterns: string[];
12
17
  context?: string;
13
18
  disableTranslation?: boolean;
19
+ autoClear?: boolean;
14
20
  OPENAI_API_KEY?: string;
15
21
  GEMINI_API_KEY?: string;
16
22
  model?: Model;
17
23
  openai?: OpenAI;
24
+ pruneNamespaces?: NamespacePruneConfig[];
18
25
  }
19
26
  export interface CommandType {
20
27
  name: string;
@@ -22,3 +29,4 @@ export interface CommandType {
22
29
  action: (config: Configuration, ...args: any[]) => Promise<void>;
23
30
  }
24
31
  export {};
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAA;AAChC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAE3D,KAAK,KAAK,GACN,SAAS,GACT,0BAA0B,GAC1B,kBAAkB,GAClB,uBAAuB,CAAA;AAE3B,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EACJ,MAAM,GACN,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC5E,QAAQ,EACJ,MAAM,GACN,CAAC,CACC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACzB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAA;CACzC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACjE"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":""}
@@ -24,3 +24,4 @@ export declare class TranslationError extends Error {
24
24
  cause?: Error;
25
25
  constructor(message: string, locale?: string, namespace?: string, cause?: Error);
26
26
  }
27
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAA;AAGhC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,eAAO,MAAM,UAAU,GAAI,iBAExB;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,QAgBxB,CAAA;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAE1D;AAED,eAAO,MAAM,YAAY,GAAU,oEAOhC;IACD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,oCAyDA,CAAA;AAED,eAAO,MAAM,eAAe,GAC1B,UACI,MAAM,GACN,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAC5E,QAAQ,MAAM,EACd,WAAW,MAAM,oCAsBlB,CAAA;AAED,eAAO,MAAM,gBAAgB,GAC3B,UACI,MAAM,GACN,CAAC,CACC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACzB,OAAO,CAAC,IAAI,CAAC,CAAC,EACvB,QAAQ,MAAM,EACd,WAAW,MAAM,EACjB,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,kBAa7B,CAAA;AAED,eAAO,MAAM,UAAU,GACrB,KAAK,MAAM,EACX,YAAY,MAAM,EAClB,YAAY,OAAO,WAiBpB,CAAA;AAED,eAAO,MAAM,cAAc,GAAU,0EAMlC,aAAa,mBA4Cf,CAAA;AAED,eAAO,MAAM,YAAY,GAAU,QAAQ,MAAM,oBAehD,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAU,yGAUrC,aAAa,kBAsDf,CAAA;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IAGhC,MAAM,CAAC,EAAE,MAAM;IACf,SAAS,CAAC,EAAE,MAAM;IAClB,KAAK,CAAC,EAAE,KAAK;gBAHpB,OAAO,EAAE,MAAM,EACR,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,KAAK;CAKvB"}
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TranslationError = exports.checkAllKeysExist = exports.getTextInput = exports.getMissingKeys = exports.getPureKey = exports.writeLocalesFile = exports.loadLocalesFile = exports.translateKey = exports.loadConfig = void 0;
7
+ exports.removeDuplicatesFromArray = removeDuplicatesFromArray;
8
+ const fast_glob_1 = __importDefault(require("fast-glob"));
9
+ const i18next_scanner_1 = require("i18next-scanner");
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const prompts_1 = __importDefault(require("prompts"));
13
+ const languges_1 = require("./languges");
14
+ const loadConfig = ({ configPath = "i18n-magic.js", }) => {
15
+ const filePath = node_path_1.default.join(process.cwd(), configPath);
16
+ if (!node_fs_1.default.existsSync(filePath)) {
17
+ console.error("Config file does not exist:", filePath);
18
+ process.exit(1);
19
+ }
20
+ try {
21
+ const config = require(filePath);
22
+ // Validate config if needed
23
+ return config;
24
+ }
25
+ catch (error) {
26
+ console.error("Error while loading config:", error);
27
+ process.exit(1);
28
+ }
29
+ };
30
+ exports.loadConfig = loadConfig;
31
+ function removeDuplicatesFromArray(arr) {
32
+ return arr.filter((item, index) => arr.indexOf(item) === index);
33
+ }
34
+ const translateKey = async ({ inputLanguage, context, object, openai, outputLanguage, model, }) => {
35
+ // Split object into chunks of 100 keys
36
+ const entries = Object.entries(object);
37
+ const chunks = [];
38
+ for (let i = 0; i < entries.length; i += 100) {
39
+ chunks.push(entries.slice(i, i + 100));
40
+ }
41
+ let result = {};
42
+ const existingInput = languges_1.languages.find((l) => l.value === inputLanguage);
43
+ const existingOutput = languges_1.languages.find((l) => l.value === outputLanguage);
44
+ const input = existingInput?.label || inputLanguage;
45
+ const output = existingOutput?.label || outputLanguage;
46
+ // Translate each chunk
47
+ for (const chunk of chunks) {
48
+ const chunkObject = Object.fromEntries(chunk);
49
+ const completion = await openai.beta.chat.completions.parse({
50
+ model,
51
+ messages: [
52
+ {
53
+ content: `You are a bot that translates the values of a locales JSON. ${context
54
+ ? `The user provided some additional context or guidelines about what to fill in the blanks: \"${context}\". `
55
+ : ""}The user provides you a JSON with a field named "inputLanguage", which defines the language the values of the JSON are defined in. It also has a field named "outputLanguage", which defines the language you should translate the values to. The last field is named "data", which includes the object with the values to translate. The keys of the values should never be changed. You output only a JSON, which has the same keys as the input, but with translated values. I give you an example input: {"inputLanguage": "English", outputLanguage: "German", "keys": {"hello": "Hello", "world": "World"}}. The output should be {"hello": "Hallo", "world": "Welt"}.`,
56
+ role: "system",
57
+ },
58
+ {
59
+ content: JSON.stringify({
60
+ inputLanguage: input,
61
+ outputLanguage: output,
62
+ data: chunkObject,
63
+ }),
64
+ role: "user",
65
+ },
66
+ ],
67
+ response_format: {
68
+ type: "json_object",
69
+ },
70
+ });
71
+ const translatedChunk = JSON.parse(completion.choices[0].message.content);
72
+ // Merge translated chunk with result
73
+ result = { ...result, ...translatedChunk };
74
+ // Optional: Add a small delay between chunks to avoid rate limiting
75
+ await new Promise((resolve) => setTimeout(resolve, 100));
76
+ }
77
+ return result;
78
+ };
79
+ exports.translateKey = translateKey;
80
+ const loadLocalesFile = async (loadPath, locale, namespace) => {
81
+ if (typeof loadPath === "string") {
82
+ const resolvedPath = loadPath
83
+ .replace("{{lng}}", locale)
84
+ .replace("{{ns}}", namespace);
85
+ const content = node_fs_1.default.readFileSync(resolvedPath, "utf-8");
86
+ try {
87
+ const json = JSON.parse(content);
88
+ return json;
89
+ }
90
+ catch (error) {
91
+ throw new TranslationError(`Invalid JSON in locale file for ${locale}:${namespace}. Path: ${resolvedPath}`, locale, namespace, error instanceof Error ? error : undefined);
92
+ }
93
+ }
94
+ return loadPath(locale, namespace);
95
+ };
96
+ exports.loadLocalesFile = loadLocalesFile;
97
+ const writeLocalesFile = async (savePath, locale, namespace, data) => {
98
+ if (typeof savePath === "string") {
99
+ const resolvedSavePath = savePath
100
+ .replace("{{lng}}", locale)
101
+ .replace("{{ns}}", namespace);
102
+ node_fs_1.default.writeFileSync(resolvedSavePath, JSON.stringify(data, null, 2));
103
+ return;
104
+ }
105
+ await savePath(locale, namespace, data);
106
+ };
107
+ exports.writeLocalesFile = writeLocalesFile;
108
+ const getPureKey = (key, namespace, isDefault) => {
109
+ const splitted = key.split(":");
110
+ if (splitted.length === 1) {
111
+ if (isDefault) {
112
+ return key;
113
+ }
114
+ return null;
115
+ }
116
+ if (splitted[0] === namespace) {
117
+ return splitted[1];
118
+ }
119
+ return null;
120
+ };
121
+ exports.getPureKey = getPureKey;
122
+ const getMissingKeys = async ({ globPatterns, namespaces, defaultNamespace, defaultLocale, loadPath, }) => {
123
+ const parser = new i18next_scanner_1.Parser({
124
+ nsSeparator: false,
125
+ keySeparator: false,
126
+ });
127
+ const files = await (0, fast_glob_1.default)([...globPatterns, "!**/node_modules/**"]);
128
+ const keys = [];
129
+ for (const file of files) {
130
+ const content = node_fs_1.default.readFileSync(file, "utf-8");
131
+ parser.parseFuncFromString(content, { list: ["t"] }, (key) => {
132
+ keys.push(key);
133
+ });
134
+ }
135
+ const uniqueKeys = removeDuplicatesFromArray(keys);
136
+ const newKeys = [];
137
+ for (const namespace of namespaces) {
138
+ const existingKeys = await (0, exports.loadLocalesFile)(loadPath, defaultLocale, namespace);
139
+ console.log(Object.keys(existingKeys).length, "existing keys");
140
+ for (const key of uniqueKeys) {
141
+ const pureKey = (0, exports.getPureKey)(key, namespace, namespace === defaultNamespace);
142
+ if (!pureKey) {
143
+ continue;
144
+ }
145
+ if (!existingKeys[pureKey]) {
146
+ newKeys.push({ key: pureKey, namespace });
147
+ }
148
+ }
149
+ }
150
+ return newKeys;
151
+ };
152
+ exports.getMissingKeys = getMissingKeys;
153
+ const getTextInput = async (prompt) => {
154
+ const input = await (0, prompts_1.default)({
155
+ name: "value",
156
+ type: "text",
157
+ message: prompt,
158
+ onState: (state) => {
159
+ if (state.aborted) {
160
+ process.nextTick(() => {
161
+ process.exit(0);
162
+ });
163
+ }
164
+ },
165
+ });
166
+ return input.value;
167
+ };
168
+ exports.getTextInput = getTextInput;
169
+ const checkAllKeysExist = async ({ namespaces, defaultLocale, loadPath, locales, context, openai, savePath, disableTranslation, model, }) => {
170
+ if (disableTranslation) {
171
+ return;
172
+ }
173
+ for (const namespace of namespaces) {
174
+ const defaultLocaleKeys = await (0, exports.loadLocalesFile)(loadPath, defaultLocale, namespace);
175
+ for (const locale of locales) {
176
+ if (locale === defaultLocale)
177
+ continue;
178
+ const localeKeys = await (0, exports.loadLocalesFile)(loadPath, locale, namespace);
179
+ const missingKeys = {};
180
+ // Check which keys from default locale are missing in current locale
181
+ for (const [key, value] of Object.entries(defaultLocaleKeys)) {
182
+ if (!localeKeys[key]) {
183
+ missingKeys[key] = value;
184
+ }
185
+ }
186
+ // If there are missing keys, translate them
187
+ if (Object.keys(missingKeys).length > 0) {
188
+ console.log(`Found ${Object.keys(missingKeys).length} missing keys in ${locale} (namespace: ${namespace})`);
189
+ const translatedValues = await (0, exports.translateKey)({
190
+ inputLanguage: defaultLocale,
191
+ outputLanguage: locale,
192
+ context,
193
+ object: missingKeys,
194
+ openai,
195
+ model,
196
+ });
197
+ // Merge translated values with existing ones
198
+ const updatedLocaleKeys = {
199
+ ...localeKeys,
200
+ ...translatedValues,
201
+ };
202
+ // Save the updated translations
203
+ (0, exports.writeLocalesFile)(savePath, locale, namespace, updatedLocaleKeys);
204
+ console.log(`✓ Translated and saved missing keys for ${locale} (namespace: ${namespace})`);
205
+ }
206
+ }
207
+ }
208
+ };
209
+ exports.checkAllKeysExist = checkAllKeysExist;
210
+ class TranslationError extends Error {
211
+ constructor(message, locale, namespace, cause) {
212
+ super(message);
213
+ this.locale = locale;
214
+ this.namespace = namespace;
215
+ this.cause = cause;
216
+ this.name = "TranslationError";
217
+ }
218
+ }
219
+ exports.TranslationError = TranslationError;
220
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":";;;;;;AA6BA,8DAEC;AA/BD,0DAA4B;AAC5B,qDAAwC;AACxC,sDAAwB;AACxB,0DAA4B;AAE5B,sDAA6B;AAC7B,yCAAsC;AAG/B,MAAM,UAAU,GAAG,CAAC,EACzB,UAAU,GAAG,eAAe,GACL,EAAE,EAAE;IAC3B,MAAM,QAAQ,GAAG,mBAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAA;IAErD,IAAI,CAAC,iBAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,QAAQ,CAAC,CAAA;QACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;QAChC,4BAA4B;QAC5B,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAA;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC,CAAA;AAlBY,QAAA,UAAU,cAkBtB;AAED,SAAgB,yBAAyB,CAAI,GAAQ;IACnD,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,CAAA;AACjE,CAAC;AAEM,MAAM,YAAY,GAAG,KAAK,EAAE,EACjC,aAAa,EACb,OAAO,EACP,MAAM,EACN,MAAM,EACN,cAAc,EACd,KAAK,GAQN,EAAE,EAAE;IACH,uCAAuC;IACvC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IACtC,MAAM,MAAM,GAA8B,EAAE,CAAA;IAE5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;IACxC,CAAC;IAED,IAAI,MAAM,GAA2B,EAAE,CAAA;IAEvC,MAAM,aAAa,GAAG,oBAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,aAAa,CAAC,CAAA;IACtE,MAAM,cAAc,GAAG,oBAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,CAAA;IAExE,MAAM,KAAK,GAAG,aAAa,EAAE,KAAK,IAAI,aAAa,CAAA;IACnD,MAAM,MAAM,GAAG,cAAc,EAAE,KAAK,IAAI,cAAc,CAAA;IAEtD,uBAAuB;IACvB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;QAC7C,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;YAC1D,KAAK;YACL,QAAQ,EAAE;gBACR;oBACE,OAAO,EAAE,+DACP,OAAO;wBACL,CAAC,CAAC,+FAA+F,OAAO,MAAM;wBAC9G,CAAC,CAAC,EACN,8oBAA8oB;oBAC9oB,IAAI,EAAE,QAAQ;iBACf;gBACD;oBACE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;wBACtB,aAAa,EAAE,KAAK;wBACpB,cAAc,EAAE,MAAM;wBACtB,IAAI,EAAE,WAAW;qBAClB,CAAC;oBACF,IAAI,EAAE,MAAM;iBACb;aACF;YACD,eAAe,EAAE;gBACf,IAAI,EAAE,aAAa;aACpB;SACF,CAAC,CAAA;QAEF,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAChC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CACZ,CAAA;QAE3B,qCAAqC;QACrC,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,GAAG,eAAe,EAAE,CAAA;QAE1C,oEAAoE;QACpE,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAA;IAC1D,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAvEY,QAAA,YAAY,gBAuExB;AAEM,MAAM,eAAe,GAAG,KAAK,EAClC,QAE4E,EAC5E,MAAc,EACd,SAAiB,EACjB,EAAE;IACF,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,YAAY,GAAG,QAAQ;aAC1B,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;aAC1B,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAE/B,MAAM,OAAO,GAAG,iBAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;QACtD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAChC,OAAO,IAA8B,CAAA;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,gBAAgB,CACxB,mCAAmC,MAAM,IAAI,SAAS,WAAW,YAAY,EAAE,EAC/E,MAAM,EACN,SAAS,EACT,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAC3C,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;AACpC,CAAC,CAAA;AA3BY,QAAA,eAAe,mBA2B3B;AAEM,MAAM,gBAAgB,GAAG,KAAK,EACnC,QAMuB,EACvB,MAAc,EACd,SAAiB,EACjB,IAA4B,EAC5B,EAAE;IACF,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,gBAAgB,GAAG,QAAQ;aAC9B,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;aAC1B,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAE/B,iBAAE,CAAC,aAAa,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;QAEjE,OAAM;IACR,CAAC;IAED,MAAM,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;AACzC,CAAC,CAAA;AAvBY,QAAA,gBAAgB,oBAuB5B;AAEM,MAAM,UAAU,GAAG,CACxB,GAAW,EACX,SAAkB,EAClB,SAAmB,EACnB,EAAE;IACF,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAE/B,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAA;IACpB,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AApBY,QAAA,UAAU,cAoBtB;AAEM,MAAM,cAAc,GAAG,KAAK,EAAE,EACnC,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,QAAQ,GACM,EAAE,EAAE;IAClB,MAAM,MAAM,GAAG,IAAI,wBAAM,CAAC;QACxB,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,KAAK;KACpB,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,MAAM,IAAA,mBAAI,EAAC,CAAC,GAAG,YAAY,EAAE,qBAAqB,CAAC,CAAC,CAAA;IAElE,MAAM,IAAI,GAAG,EAAE,CAAA;IAEf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,iBAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QAC9C,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAW,EAAE,EAAE;YACnE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAChB,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAA;IAElD,MAAM,OAAO,GAAG,EAAE,CAAA;IAElB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,YAAY,GAAG,MAAM,IAAA,uBAAe,EACxC,QAAQ,EACR,aAAa,EACb,SAAS,CACV,CAAA;QAED,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;QAE9D,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,IAAA,kBAAU,EAAC,GAAG,EAAE,SAAS,EAAE,SAAS,KAAK,gBAAgB,CAAC,CAAA;YAE1E,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,SAAQ;YACV,CAAC;YAED,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3B,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAA;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAlDY,QAAA,cAAc,kBAkD1B;AAEM,MAAM,YAAY,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;IACnD,MAAM,KAAK,GAAG,MAAM,IAAA,iBAAO,EAAC;QAC1B,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBAClB,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE;oBACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;gBACjB,CAAC,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,KAAK,CAAC,KAAe,CAAA;AAC9B,CAAC,CAAA;AAfY,QAAA,YAAY,gBAexB;AAEM,MAAM,iBAAiB,GAAG,KAAK,EAAE,EACtC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,OAAO,EACP,OAAO,EACP,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,KAAK,GACS,EAAE,EAAE;IAClB,IAAI,kBAAkB,EAAE,CAAC;QACvB,OAAM;IACR,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,iBAAiB,GAAG,MAAM,IAAA,uBAAe,EAC7C,QAAQ,EACR,aAAa,EACb,SAAS,CACV,CAAA;QAED,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,MAAM,KAAK,aAAa;gBAAE,SAAQ;YAEtC,MAAM,UAAU,GAAG,MAAM,IAAA,uBAAe,EAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;YACrE,MAAM,WAAW,GAA2B,EAAE,CAAA;YAE9C,qEAAqE;YACrE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBAC7D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACrB,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;gBAC1B,CAAC;YACH,CAAC;YAED,4CAA4C;YAC5C,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxC,OAAO,CAAC,GAAG,CACT,SAAS,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,oBAAoB,MAAM,gBAAgB,SAAS,GAAG,CAC/F,CAAA;gBAED,MAAM,gBAAgB,GAAG,MAAM,IAAA,oBAAY,EAAC;oBAC1C,aAAa,EAAE,aAAa;oBAC5B,cAAc,EAAE,MAAM;oBACtB,OAAO;oBACP,MAAM,EAAE,WAAW;oBACnB,MAAM;oBACN,KAAK;iBACN,CAAC,CAAA;gBAEF,6CAA6C;gBAC7C,MAAM,iBAAiB,GAAG;oBACxB,GAAG,UAAU;oBACb,GAAG,gBAAgB;iBACpB,CAAA;gBAED,gCAAgC;gBAChC,IAAA,wBAAgB,EAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAA;gBAChE,OAAO,CAAC,GAAG,CACT,2CAA2C,MAAM,gBAAgB,SAAS,GAAG,CAC9E,CAAA;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAhEY,QAAA,iBAAiB,qBAgE7B;AAED,MAAa,gBAAiB,SAAQ,KAAK;IACzC,YACE,OAAe,EACR,MAAe,EACf,SAAkB,EAClB,KAAa;QAEpB,KAAK,CAAC,OAAO,CAAC,CAAA;QAJP,WAAM,GAAN,MAAM,CAAS;QACf,cAAS,GAAT,SAAS,CAAS;QAClB,UAAK,GAAL,KAAK,CAAQ;QAGpB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF;AAVD,4CAUC"}
package/package.json CHANGED
@@ -1,41 +1,62 @@
1
1
  {
2
2
  "name": "@scoutello/i18n-magic",
3
- "version": "0.15.2",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
- "description": "CLI to help you manage your locales JSON with translations, replacements, etc. with OpenAI or Gemini",
6
+ "description": "Intelligent CLI toolkit that automates internationalization workflows with AI-powered translations for JavaScript/TypeScript projects",
7
+ "keywords": [
8
+ "i18n",
9
+ "internationalization",
10
+ "translation",
11
+ "localization",
12
+ "cli",
13
+ "ai",
14
+ "openai",
15
+ "gemini",
16
+ "react-i18next",
17
+ "next-i18next",
18
+ "vue-i18n",
19
+ "automation",
20
+ "typescript",
21
+ "javascript"
22
+ ],
23
+ "homepage": "https://github.com/BjoernRave/i18n-magic#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/BjoernRave/i18n-magic/issues"
26
+ },
7
27
  "repository": {
8
28
  "type": "git",
9
29
  "url": "https://github.com/BjoernRave/i18n-magic"
10
30
  },
11
- "author": "scoutello",
31
+ "author": {
32
+ "name": "scoutello",
33
+ "url": "https://github.com/BjoernRave"
34
+ },
12
35
  "main": "dist/index.js",
13
- "module": "dist/i18n-magic.esm.js",
14
36
  "typings": "dist/index.d.ts",
15
37
  "types": "dist/index.d.ts",
16
- "bin": "dist/index.js",
38
+ "bin": "dist/cli.js",
17
39
  "exports": {
18
40
  ".": {
19
41
  "require": "./dist/index.js",
20
- "import": "./dist/i18n-magic.esm.js",
21
42
  "types": "./dist/index.d.ts"
22
43
  }
23
44
  },
24
45
  "files": ["dist", "src"],
25
46
  "scripts": {
26
47
  "analyze": "size-limit --why",
27
- "build": "dts build && echo '#!/usr/bin/env node' | cat - dist/index.js > temp && mv temp dist/index.js",
28
- "lint": "dts lint",
29
- "prepare": "dts build && echo '#!/usr/bin/env node' | cat - dist/index.js > temp && mv temp dist/index.js",
48
+ "build": "tsc && echo '#!/usr/bin/env node' | cat - dist/cli.js > temp && mv temp dist/cli.js",
49
+ "lint": "biome check src/",
50
+ "prepare": "npm run build",
30
51
  "size": "size-limit",
31
- "start": "dts watch",
32
- "test": "dts test"
52
+ "start": "tsc --watch",
53
+ "test": "jest"
33
54
  },
34
55
  "jest": {
35
56
  "testEnvironment": "node"
36
57
  },
37
58
  "engines": {
38
- "node": ">=18"
59
+ "node": ">=16"
39
60
  },
40
61
  "peerDependencies": {
41
62
  "openai": "^4.94.0"
@@ -54,9 +75,11 @@
54
75
  "@tsconfig/recommended": "^1.0.8",
55
76
  "@types/node": "^20.12.12",
56
77
  "@types/prompts": "^2.4.9",
57
- "dts-cli": "^2.0.5",
78
+ "jest": "^29.7.0",
79
+ "@types/jest": "^29.5.12",
58
80
  "openai": "^4.94.0",
59
81
  "tslib": "^2.8.1",
60
82
  "typescript": "^5.8.3"
61
- }
83
+ },
84
+ "packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92"
62
85
  }
package/src/cli.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { Command } from "commander"
2
+ import dotenv from "dotenv"
3
+ import OpenAI from "openai"
4
+ import { checkMissing } from "./commands/check-missing"
5
+ import { removeUnusedKeys } from "./commands/clean"
6
+ import { createPrunedNamespace } from "./commands/create-pruned-namespace"
7
+ import { replaceTranslation } from "./commands/replace"
8
+ import { translateMissing } from "./commands/scan"
9
+ import { syncLocales } from "./commands/sync-locales"
10
+ import type { CommandType, Configuration } from "./lib/types"
11
+ import { loadConfig } from "./lib/utils"
12
+
13
+ const program = new Command()
14
+
15
+ program
16
+ .name("i18n-magic")
17
+ .description(
18
+ "CLI to help you manage your locales JSON with translations, replacements, etc. with OpenAI.",
19
+ )
20
+ .version("0.2.0")
21
+ .option("-c, --config <path>", "path to config file")
22
+ .option("-e, --env <path>", "path to .env file")
23
+
24
+ const commands: CommandType[] = [
25
+ {
26
+ name: "scan",
27
+ description:
28
+ "Scan for missing translations, get prompted for each, translate it to the other locales and save it to the JSON file.",
29
+ action: translateMissing,
30
+ },
31
+ {
32
+ name: "replace",
33
+ description:
34
+ "Replace a translation based on the key, and translate it to the other locales and save it to the JSON file.",
35
+ action: replaceTranslation,
36
+ },
37
+ {
38
+ name: "check-missing",
39
+ description:
40
+ "Check if there are any missing translations. Useful for a CI/CD pipeline or husky hook.",
41
+ action: checkMissing,
42
+ },
43
+ {
44
+ name: "sync",
45
+ description:
46
+ "Sync the translations from the default locale to the other locales. Useful for a CI/CD pipeline or husky hook.",
47
+ action: syncLocales,
48
+ },
49
+ {
50
+ name: "clean",
51
+ description:
52
+ "Remove unused translations from all locales. Useful for a CI/CD pipeline or husky hook.",
53
+ action: removeUnusedKeys,
54
+ },
55
+ {
56
+ name: "prune",
57
+ description: "Create a pruned namespace from the other namespaces.",
58
+ action: createPrunedNamespace,
59
+ },
60
+ ]
61
+
62
+ for (const command of commands) {
63
+ const cmd = program.command(command.name).description(command.description)
64
+
65
+ // Add key option to replace command
66
+ if (command.name === "replace") {
67
+ cmd
68
+ .option("-k, --key <key>", "translation key to replace")
69
+ .allowExcessArguments(true)
70
+ .argument("[key]", "translation key to replace")
71
+ }
72
+
73
+ cmd.action(async (arg, options) => {
74
+ const res = dotenv.config({
75
+ path: program.opts().env || ".env",
76
+ })
77
+
78
+ const config: Configuration = await loadConfig({
79
+ configPath: program.opts().config,
80
+ })
81
+
82
+ const isGemini = (config.model as string)?.includes("gemini")
83
+
84
+ // Get API key from environment or config
85
+ const openaiKey = res.parsed.OPENAI_API_KEY || config.OPENAI_API_KEY
86
+ const geminiKey = res.parsed.GEMINI_API_KEY || config.GEMINI_API_KEY
87
+
88
+ // Select appropriate key based on model type
89
+ const key = isGemini ? geminiKey : openaiKey
90
+
91
+ if (!key) {
92
+ const keyType = isGemini ? "GEMINI_API_KEY" : "OPENAI_API_KEY"
93
+ console.error(
94
+ `Please provide a${isGemini ? " Gemini" : "n OpenAI"} API key in your .env file or config, called ${keyType}.`,
95
+ )
96
+ process.exit(1)
97
+ }
98
+
99
+ const openai = new OpenAI({
100
+ apiKey: key,
101
+ ...(isGemini && {
102
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
103
+ }),
104
+ })
105
+
106
+ // For replace command, check for key in argument or option
107
+ if (command.name === "replace") {
108
+ // If key is provided as positional argument, use that first
109
+ const keyToUse = typeof arg === "string" ? arg : options.key
110
+ command.action({ ...config, openai }, keyToUse)
111
+ } else {
112
+ command.action({ ...config, openai })
113
+ }
114
+ })
115
+ }
116
+
117
+ program.parse(process.argv)
@@ -0,0 +1,105 @@
1
+ import glob from "fast-glob"
2
+ import { Parser } from "i18next-scanner"
3
+ import fs from "node:fs"
4
+ import type { Configuration } from "../lib/types"
5
+ import {
6
+ getPureKey,
7
+ loadLocalesFile,
8
+ removeDuplicatesFromArray,
9
+ writeLocalesFile,
10
+ } from "../lib/utils"
11
+
12
+ export const removeUnusedKeys = async (config: Configuration) => {
13
+ const {
14
+ globPatterns,
15
+ namespaces,
16
+ defaultNamespace,
17
+ locales,
18
+ loadPath,
19
+ savePath,
20
+ } = config
21
+
22
+ // Set up the parser
23
+ const parser = new Parser({
24
+ nsSeparator: false,
25
+ keySeparator: false,
26
+ })
27
+
28
+ // Find all files to scan
29
+ const files = await glob([...globPatterns, "!**/node_modules/**"])
30
+
31
+ // Extract all translation keys from the codebase
32
+ const extractedKeys = []
33
+ for (const file of files) {
34
+ const content = fs.readFileSync(file, "utf-8")
35
+ parser.parseFuncFromString(content, { list: ["t"] }, (key: string) => {
36
+ extractedKeys.push(key)
37
+ })
38
+ }
39
+
40
+ // Remove duplicates
41
+ const uniqueExtractedKeys = removeDuplicatesFromArray(extractedKeys)
42
+
43
+ // Track stats for reporting
44
+ const stats = {
45
+ total: 0,
46
+ removed: 0,
47
+ }
48
+
49
+ // Process each namespace and locale
50
+ for (const namespace of namespaces) {
51
+ // Build a set of pure keys that are actually used in the codebase for this namespace
52
+ const usedKeysSet = new Set<string>()
53
+
54
+ for (const key of uniqueExtractedKeys) {
55
+ const pureKey = getPureKey(key, namespace, namespace === defaultNamespace)
56
+ if (pureKey) {
57
+ usedKeysSet.add(pureKey)
58
+ }
59
+ }
60
+
61
+ // Process each locale
62
+ for (const locale of locales) {
63
+ // Load existing keys for this locale and namespace
64
+ const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
65
+ const existingKeysCount = Object.keys(existingKeys).length
66
+ stats.total += existingKeysCount
67
+
68
+ // Create a new object with only the keys that are used
69
+ const cleanedKeys: Record<string, string> = {}
70
+ let removedCount = 0
71
+
72
+ for (const [key, value] of Object.entries(existingKeys)) {
73
+ if (usedKeysSet.has(key)) {
74
+ cleanedKeys[key] = value
75
+ } else {
76
+ removedCount++
77
+ }
78
+ }
79
+
80
+ stats.removed += removedCount
81
+
82
+ // Only write the file if keys were removed
83
+ if (removedCount > 0) {
84
+ await writeLocalesFile(savePath, locale, namespace, cleanedKeys)
85
+ console.log(
86
+ `✓ Removed ${removedCount} unused keys from ${locale}:${namespace} (${
87
+ Object.keys(cleanedKeys).length
88
+ } keys remaining)`,
89
+ )
90
+ } else {
91
+ console.log(`No unused keys found in ${locale}:${namespace}`)
92
+ }
93
+ }
94
+ }
95
+
96
+ if (stats.removed > 0) {
97
+ console.log(
98
+ `✅ Removed ${stats.removed} unused keys (out of ${stats.total} total keys)`,
99
+ )
100
+ } else {
101
+ console.log(
102
+ `✅ No unused keys found in the project (${stats.total} total keys)`,
103
+ )
104
+ }
105
+ }