@scoutello/i18n-magic 0.59.1 ā 0.61.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/README.md +20 -14
- package/dist/cli.js +16 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/remove-key.d.ts +3 -0
- package/dist/commands/remove-key.d.ts.map +1 -0
- package/dist/commands/remove-key.js +93 -0
- package/dist/commands/remove-key.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +94 -65
- package/dist/lib/utils.js.map +1 -1
- package/dist/mcp-server.js +50 -103
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +18 -5
- package/src/commands/remove-key.ts +123 -0
- package/src/lib/utils.ts +102 -74
- package/src/mcp-server.ts +61 -131
package/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@ import dotenv from "dotenv"
|
|
|
3
3
|
import OpenAI from "openai"
|
|
4
4
|
import { checkMissing } from "./commands/check-missing.js"
|
|
5
5
|
import { removeUnusedKeys } from "./commands/clean.js"
|
|
6
|
-
|
|
6
|
+
import { removeKey } from "./commands/remove-key.js"
|
|
7
7
|
import { replaceTranslation } from "./commands/replace.js"
|
|
8
8
|
import { restoreFromNamespaces } from "./commands/restore-from-namespaces.js"
|
|
9
9
|
import { translateMissing } from "./commands/scan.js"
|
|
@@ -59,12 +59,18 @@ const commands: CommandType[] = [
|
|
|
59
59
|
"Restore missing keys by searching for them in other namespace files across all locales.",
|
|
60
60
|
action: restoreFromNamespaces,
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: "remove-key",
|
|
64
|
+
description:
|
|
65
|
+
"Remove a specific translation key from all namespaces and locales.",
|
|
66
|
+
action: removeKey,
|
|
67
|
+
},
|
|
62
68
|
]
|
|
63
69
|
|
|
64
70
|
for (const command of commands) {
|
|
65
71
|
const cmd = program.command(command.name).description(command.description)
|
|
66
72
|
|
|
67
|
-
// Add key option to replace
|
|
73
|
+
// Add key option to replace and remove-key commands
|
|
68
74
|
if (command.name === "replace") {
|
|
69
75
|
cmd
|
|
70
76
|
.option("-k, --key <key>", "translation key to replace")
|
|
@@ -72,8 +78,15 @@ for (const command of commands) {
|
|
|
72
78
|
.argument("[key]", "translation key to replace")
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
if (command.name === "remove-key") {
|
|
82
|
+
cmd
|
|
83
|
+
.option("-k, --key <key>", "translation key to remove")
|
|
84
|
+
.allowExcessArguments(true)
|
|
85
|
+
.argument("[key]", "translation key to remove")
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
cmd.action(async (arg, options) => {
|
|
76
|
-
|
|
89
|
+
dotenv.config({
|
|
77
90
|
path: program.opts().env || ".env",
|
|
78
91
|
})
|
|
79
92
|
|
|
@@ -105,8 +118,8 @@ for (const command of commands) {
|
|
|
105
118
|
}),
|
|
106
119
|
})
|
|
107
120
|
|
|
108
|
-
// For replace
|
|
109
|
-
if (command.name === "replace") {
|
|
121
|
+
// For replace and remove-key commands, check for key in argument or option
|
|
122
|
+
if (command.name === "replace" || command.name === "remove-key") {
|
|
110
123
|
// If key is provided as positional argument, use that first
|
|
111
124
|
const keyToUse = typeof arg === "string" ? arg : options.key
|
|
112
125
|
command.action({ ...config, openai }, keyToUse)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import prompts from "prompts"
|
|
2
|
+
import type { Configuration } from "../lib/types.js"
|
|
3
|
+
import { loadLocalesFile, writeLocalesFile } from "../lib/utils.js"
|
|
4
|
+
|
|
5
|
+
export const removeKey = async (config: Configuration, key?: string) => {
|
|
6
|
+
const { namespaces, locales, loadPath, savePath } = config
|
|
7
|
+
|
|
8
|
+
if (!key) {
|
|
9
|
+
const response = await prompts({
|
|
10
|
+
type: "text",
|
|
11
|
+
name: "key",
|
|
12
|
+
message: "Enter the translation key to remove:",
|
|
13
|
+
validate: (value) => (value ? true : "Key cannot be empty"),
|
|
14
|
+
onState: (state) => {
|
|
15
|
+
if (state.aborted) {
|
|
16
|
+
process.nextTick(() => {
|
|
17
|
+
process.exit(0)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
key = response.key
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!key) {
|
|
27
|
+
console.log("\nā Operation cancelled. No key was provided.")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(
|
|
32
|
+
`\nš Searching for key "${key}" across all namespaces and locales...`,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const foundIn: Array<{ namespace: string; locale: string }> = []
|
|
36
|
+
|
|
37
|
+
for (const namespace of namespaces) {
|
|
38
|
+
for (const locale of locales) {
|
|
39
|
+
try {
|
|
40
|
+
const existingKeys = await loadLocalesFile(
|
|
41
|
+
loadPath,
|
|
42
|
+
locale,
|
|
43
|
+
namespace,
|
|
44
|
+
{
|
|
45
|
+
silent: true,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if (Object.hasOwn(existingKeys, key)) {
|
|
50
|
+
foundIn.push({ namespace, locale })
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Skip if file doesn't exist or can't be read
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (foundIn.length === 0) {
|
|
59
|
+
console.log(`\nā Key "${key}" not found in any namespace or locale.`)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\nā ļø Key "${key}" found in ${foundIn.length} location(s):\n`)
|
|
64
|
+
|
|
65
|
+
const maxToShow = 20
|
|
66
|
+
const itemsToShow = foundIn.slice(0, maxToShow)
|
|
67
|
+
|
|
68
|
+
for (const { namespace, locale } of itemsToShow) {
|
|
69
|
+
console.log(` ⢠${locale}:${namespace}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (foundIn.length > maxToShow) {
|
|
73
|
+
console.log(` ... and ${foundIn.length - maxToShow} more`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log("")
|
|
77
|
+
|
|
78
|
+
const { confirmed } = await prompts({
|
|
79
|
+
type: "confirm",
|
|
80
|
+
name: "confirmed",
|
|
81
|
+
message: `Do you want to remove "${key}" from ${foundIn.length} location(s)?`,
|
|
82
|
+
initial: false,
|
|
83
|
+
onState: (state) => {
|
|
84
|
+
if (state.aborted) {
|
|
85
|
+
process.nextTick(() => {
|
|
86
|
+
process.exit(0)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (!confirmed) {
|
|
93
|
+
console.log("\nā Operation cancelled. No keys were removed.")
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\nšļø Removing key "${key}"...`)
|
|
98
|
+
|
|
99
|
+
let removedCount = 0
|
|
100
|
+
|
|
101
|
+
for (const { namespace, locale } of foundIn) {
|
|
102
|
+
try {
|
|
103
|
+
const existingKeys = await loadLocalesFile(loadPath, locale, namespace, {
|
|
104
|
+
silent: true,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
if (Object.hasOwn(existingKeys, key)) {
|
|
108
|
+
delete existingKeys[key]
|
|
109
|
+
await writeLocalesFile(savePath, locale, namespace, existingKeys)
|
|
110
|
+
removedCount++
|
|
111
|
+
console.log(` ā Removed from ${locale}:${namespace}`)
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(
|
|
115
|
+
` ā Failed to remove from ${locale}:${namespace}: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(
|
|
121
|
+
`\nā
Successfully removed key "${key}" from ${removedCount} location(s)`,
|
|
122
|
+
)
|
|
123
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -872,34 +872,78 @@ export const addTranslationKeys = async ({
|
|
|
872
872
|
log(`ā±ļø Codebase scan completed in ${scanTime.toFixed(2)}ms`)
|
|
873
873
|
|
|
874
874
|
// Step 2: Determine namespaces for each key
|
|
875
|
-
|
|
875
|
+
// Resolution order:
|
|
876
|
+
// 1) Explicit namespace prefix (e.g. "dashboard:welcome")
|
|
877
|
+
// 2) Code usage scan matches
|
|
878
|
+
// 3) Existing key in default locale namespace files
|
|
879
|
+
// 4) Key prefix matching a namespace (e.g. "dashboard.title")
|
|
880
|
+
// 5) Default namespace fallback
|
|
881
|
+
const defaultLocaleKeysByNamespace = new Map<string, Record<string, string>>()
|
|
882
|
+
await Promise.all(
|
|
883
|
+
namespaces.map(async (namespace) => {
|
|
884
|
+
try {
|
|
885
|
+
const nsKeys = await loadLocalesFile(loadPath, defaultLocale, namespace, {
|
|
886
|
+
silent: true,
|
|
887
|
+
})
|
|
888
|
+
defaultLocaleKeysByNamespace.set(namespace, nsKeys)
|
|
889
|
+
} catch {
|
|
890
|
+
defaultLocaleKeysByNamespace.set(namespace, {})
|
|
891
|
+
}
|
|
892
|
+
}),
|
|
893
|
+
)
|
|
876
894
|
|
|
877
|
-
|
|
878
|
-
const
|
|
895
|
+
const preparedKeys = keys.map(({ key, value, language = "en" }) => {
|
|
896
|
+
const splitKey = key.split(":")
|
|
897
|
+
const hasExplicitNamespace =
|
|
898
|
+
splitKey.length > 1 && namespaces.includes(splitKey[0])
|
|
899
|
+
const normalizedKey = hasExplicitNamespace ? splitKey.slice(1).join(":") : key
|
|
900
|
+
const explicitNamespace = hasExplicitNamespace ? splitKey[0] : null
|
|
901
|
+
const foundNamespaces = new Set<string>()
|
|
879
902
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
903
|
+
if (explicitNamespace) {
|
|
904
|
+
foundNamespaces.add(explicitNamespace)
|
|
905
|
+
} else {
|
|
906
|
+
for (const entry of keysWithNamespaces) {
|
|
907
|
+
for (const namespace of entry.namespaces) {
|
|
908
|
+
const pureKey = getPureKey(
|
|
909
|
+
entry.key,
|
|
910
|
+
namespace,
|
|
911
|
+
namespace === defaultNamespace,
|
|
912
|
+
)
|
|
913
|
+
if (entry.key === normalizedKey || pureKey === normalizedKey) {
|
|
914
|
+
foundNamespaces.add(namespace)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
885
918
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
919
|
+
if (foundNamespaces.size === 0) {
|
|
920
|
+
for (const namespace of namespaces) {
|
|
921
|
+
const namespaceKeys = defaultLocaleKeysByNamespace.get(namespace) || {}
|
|
922
|
+
if (Object.hasOwn(namespaceKeys, normalizedKey)) {
|
|
923
|
+
foundNamespaces.add(namespace)
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (foundNamespaces.size === 0) {
|
|
929
|
+
const keyPrefix = normalizedKey.split(".")[0]
|
|
930
|
+
if (namespaces.includes(keyPrefix)) {
|
|
931
|
+
foundNamespaces.add(keyPrefix)
|
|
932
|
+
}
|
|
891
933
|
}
|
|
892
934
|
}
|
|
893
935
|
|
|
894
|
-
if (foundNamespaces.size
|
|
895
|
-
|
|
896
|
-
} else {
|
|
897
|
-
// If the key is not found in the codebase, use the default namespace
|
|
898
|
-
affectedNamespaces.push(defaultNamespace)
|
|
936
|
+
if (foundNamespaces.size === 0) {
|
|
937
|
+
foundNamespaces.add(defaultNamespace)
|
|
899
938
|
}
|
|
900
939
|
|
|
901
|
-
|
|
902
|
-
|
|
940
|
+
return {
|
|
941
|
+
key: normalizedKey,
|
|
942
|
+
value,
|
|
943
|
+
language,
|
|
944
|
+
namespaces: foundNamespaces,
|
|
945
|
+
}
|
|
946
|
+
})
|
|
903
947
|
|
|
904
948
|
// Step 3: Group keys by namespace and locale
|
|
905
949
|
const namespaceLocaleToKeys = new Map<
|
|
@@ -907,21 +951,24 @@ export const addTranslationKeys = async ({
|
|
|
907
951
|
Array<{ key: string; value: string; language: string }>
|
|
908
952
|
>()
|
|
909
953
|
|
|
910
|
-
for (const
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
const mapKey = `${namespace}:${language}`
|
|
954
|
+
for (const keyEntry of preparedKeys) {
|
|
955
|
+
for (const namespace of keyEntry.namespaces) {
|
|
956
|
+
const mapKey = `${namespace}:${keyEntry.language}`
|
|
914
957
|
if (!namespaceLocaleToKeys.has(mapKey)) {
|
|
915
958
|
namespaceLocaleToKeys.set(mapKey, [])
|
|
916
959
|
}
|
|
917
|
-
namespaceLocaleToKeys.get(mapKey)!.push({
|
|
960
|
+
namespaceLocaleToKeys.get(mapKey)!.push({
|
|
961
|
+
key: keyEntry.key,
|
|
962
|
+
value: keyEntry.value,
|
|
963
|
+
language: keyEntry.language,
|
|
964
|
+
})
|
|
918
965
|
}
|
|
919
966
|
}
|
|
920
967
|
|
|
921
968
|
// Step 4: Collect all unique namespaces that will be affected
|
|
922
969
|
const affectedNamespaces = new Set<string>()
|
|
923
|
-
for (const
|
|
924
|
-
for (const ns of namespaces) {
|
|
970
|
+
for (const keyEntry of preparedKeys) {
|
|
971
|
+
for (const ns of keyEntry.namespaces) {
|
|
925
972
|
affectedNamespaces.add(ns)
|
|
926
973
|
}
|
|
927
974
|
}
|
|
@@ -930,32 +977,31 @@ export const addTranslationKeys = async ({
|
|
|
930
977
|
// This ensures we preserve existing keys in all locales
|
|
931
978
|
const fileIOStartTime = performance.now()
|
|
932
979
|
const localeFiles = new Map<string, Record<string, string>>()
|
|
980
|
+
const originalKeyCounts = new Map<string, number>()
|
|
933
981
|
const loadErrors: Array<{ fileKey: string; error: string }> = []
|
|
934
982
|
|
|
935
|
-
// Load files for all locales Ć all affected namespaces
|
|
983
|
+
// Load files for all locales Ć all affected namespaces in parallel (read-only)
|
|
984
|
+
const localeLoadPromises: Promise<void>[] = []
|
|
936
985
|
for (const namespace of affectedNamespaces) {
|
|
937
986
|
for (const locale of config.locales) {
|
|
938
987
|
const fileKey = `${locale}:${namespace}`
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
loadErrors.push({ fileKey, error: errorMsg })
|
|
954
|
-
log(`ā ļø Failed to load ${fileKey}: ${errorMsg}`)
|
|
955
|
-
}
|
|
956
|
-
}
|
|
988
|
+
localeLoadPromises.push(
|
|
989
|
+
loadLocalesFile(loadPath, locale, namespace, { silent: true })
|
|
990
|
+
.then((existingKeys) => {
|
|
991
|
+
localeFiles.set(fileKey, existingKeys)
|
|
992
|
+
originalKeyCounts.set(fileKey, Object.keys(existingKeys).length)
|
|
993
|
+
})
|
|
994
|
+
.catch((error) => {
|
|
995
|
+
// Don't silently ignore - track the error and DO NOT set empty object
|
|
996
|
+
const errorMsg =
|
|
997
|
+
error instanceof Error ? error.message : String(error)
|
|
998
|
+
loadErrors.push({ fileKey, error: errorMsg })
|
|
999
|
+
log(`ā ļø Failed to load ${fileKey}: ${errorMsg}`)
|
|
1000
|
+
}),
|
|
1001
|
+
)
|
|
957
1002
|
}
|
|
958
1003
|
}
|
|
1004
|
+
await Promise.all(localeLoadPromises)
|
|
959
1005
|
|
|
960
1006
|
// If any files failed to load, abort to prevent data loss
|
|
961
1007
|
if (loadErrors.length > 0) {
|
|
@@ -992,14 +1038,14 @@ export const addTranslationKeys = async ({
|
|
|
992
1038
|
Array<{ key: string; value: string; namespaces: Set<string> }>
|
|
993
1039
|
>()
|
|
994
1040
|
|
|
995
|
-
for (const
|
|
996
|
-
if (!keysByLanguage.has(language)) {
|
|
997
|
-
keysByLanguage.set(language, [])
|
|
1041
|
+
for (const keyEntry of preparedKeys) {
|
|
1042
|
+
if (!keysByLanguage.has(keyEntry.language)) {
|
|
1043
|
+
keysByLanguage.set(keyEntry.language, [])
|
|
998
1044
|
}
|
|
999
|
-
keysByLanguage.get(language)!.push({
|
|
1000
|
-
key,
|
|
1001
|
-
value,
|
|
1002
|
-
namespaces:
|
|
1045
|
+
keysByLanguage.get(keyEntry.language)!.push({
|
|
1046
|
+
key: keyEntry.key,
|
|
1047
|
+
value: keyEntry.value,
|
|
1048
|
+
namespaces: keyEntry.namespaces,
|
|
1003
1049
|
})
|
|
1004
1050
|
}
|
|
1005
1051
|
|
|
@@ -1078,22 +1124,6 @@ export const addTranslationKeys = async ({
|
|
|
1078
1124
|
// Step 8: Validate and write all files
|
|
1079
1125
|
// Safety check: ensure we're not accidentally removing keys (only adding)
|
|
1080
1126
|
const writeStartTime = performance.now()
|
|
1081
|
-
const originalKeyCounts = new Map<string, number>()
|
|
1082
|
-
|
|
1083
|
-
// Store original key counts for validation
|
|
1084
|
-
for (const namespace of affectedNamespaces) {
|
|
1085
|
-
for (const locale of config.locales) {
|
|
1086
|
-
const fileKey = `${locale}:${namespace}`
|
|
1087
|
-
try {
|
|
1088
|
-
const original = await loadLocalesFile(loadPath, locale, namespace, {
|
|
1089
|
-
silent: true,
|
|
1090
|
-
})
|
|
1091
|
-
originalKeyCounts.set(fileKey, Object.keys(original).length)
|
|
1092
|
-
} catch {
|
|
1093
|
-
originalKeyCounts.set(fileKey, 0)
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
1127
|
|
|
1098
1128
|
// Validate: new file should have at least as many keys as original
|
|
1099
1129
|
for (const [fileKey, newKeys] of localeFiles) {
|
|
@@ -1123,10 +1153,8 @@ export const addTranslationKeys = async ({
|
|
|
1123
1153
|
)
|
|
1124
1154
|
|
|
1125
1155
|
// Build results
|
|
1126
|
-
const results =
|
|
1127
|
-
const
|
|
1128
|
-
keyToNamespaces.get(key) || new Set([defaultNamespace]),
|
|
1129
|
-
)
|
|
1156
|
+
const results = preparedKeys.map(({ key, value, language, namespaces }) => {
|
|
1157
|
+
const resolvedNamespaces = Array.from(namespaces)
|
|
1130
1158
|
const savedLocales = new Set<string>([language])
|
|
1131
1159
|
|
|
1132
1160
|
if (openai) {
|
|
@@ -1143,7 +1171,7 @@ export const addTranslationKeys = async ({
|
|
|
1143
1171
|
return {
|
|
1144
1172
|
key,
|
|
1145
1173
|
value,
|
|
1146
|
-
namespace:
|
|
1174
|
+
namespace: resolvedNamespaces.join(", "),
|
|
1147
1175
|
locale: Array.from(savedLocales).sort().join(", "),
|
|
1148
1176
|
}
|
|
1149
1177
|
})
|