@scoutello/i18n-magic 0.65.0 ā 0.66.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 +13 -7
- package/dist/cli.js +20 -15
- package/dist/cli.js.map +1 -1
- package/dist/commands/check-missing.d.ts +11 -1
- package/dist/commands/check-missing.d.ts.map +1 -1
- package/dist/commands/check-missing.js +56 -96
- package/dist/commands/check-missing.js.map +1 -1
- package/dist/commands/replace.d.ts +6 -0
- package/dist/commands/replace.d.ts.map +1 -1
- package/dist/commands/replace.js +48 -29
- package/dist/commands/replace.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +114 -80
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/sync-locales.d.ts.map +1 -1
- package/dist/commands/sync-locales.js +21 -28
- package/dist/commands/sync-locales.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/locale-storage.d.ts +7 -0
- package/dist/lib/locale-storage.d.ts.map +1 -0
- package/dist/lib/locale-storage.js +49 -0
- package/dist/lib/locale-storage.js.map +1 -0
- package/dist/lib/translation-response.d.ts +8 -0
- package/dist/lib/translation-response.d.ts.map +1 -0
- package/dist/lib/translation-response.js +43 -0
- package/dist/lib/translation-response.js.map +1 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/update-translation-key.d.ts +16 -0
- package/dist/lib/update-translation-key.d.ts.map +1 -0
- package/dist/lib/update-translation-key.js +67 -0
- package/dist/lib/update-translation-key.js.map +1 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +69 -51
- package/dist/lib/utils.js.map +1 -1
- package/dist/mcp-server.js +28 -79
- package/dist/mcp-server.js.map +1 -1
- package/package.json +5 -9
- package/src/cli.ts +26 -18
- package/src/commands/check-missing.ts +83 -137
- package/src/commands/replace.ts +81 -43
- package/src/commands/scan.ts +130 -108
- package/src/commands/sync-locales.ts +41 -63
- package/src/index.ts +6 -1
- package/src/lib/locale-storage.ts +76 -0
- package/src/lib/translation-response.ts +59 -0
- package/src/lib/types.ts +1 -1
- package/src/lib/update-translation-key.ts +119 -0
- package/src/lib/utils.ts +92 -76
- package/src/mcp-server.ts +31 -112
|
@@ -1,162 +1,108 @@
|
|
|
1
1
|
import type { Configuration } from "../lib/types.js"
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
getKeysWithNamespaces,
|
|
4
|
+
getPureKey,
|
|
5
5
|
loadLocalesFile,
|
|
6
|
-
writeLocalesFile,
|
|
7
|
-
translateKey,
|
|
8
6
|
} from "../lib/utils.js"
|
|
9
7
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
namespaces,
|
|
16
|
-
locales,
|
|
17
|
-
context,
|
|
18
|
-
openai,
|
|
19
|
-
model,
|
|
20
|
-
disableTranslationDuringScan,
|
|
21
|
-
} = config
|
|
22
|
-
|
|
23
|
-
const newKeys = await getMissingKeys(config)
|
|
8
|
+
export interface MissingTranslationsReport {
|
|
9
|
+
missing: Record<string, Record<string, string[]>>
|
|
10
|
+
totalExpected: number
|
|
11
|
+
totalMissing: number
|
|
12
|
+
}
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
export class MissingTranslationsError extends Error {
|
|
15
|
+
constructor(public report: MissingTranslationsReport) {
|
|
16
|
+
super(`Found ${report.totalMissing} missing translation(s).`)
|
|
17
|
+
this.name = "MissingTranslationsError"
|
|
28
18
|
}
|
|
19
|
+
}
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
21
|
+
export const collectMissingTranslations = async (
|
|
22
|
+
config: Configuration,
|
|
23
|
+
): Promise<MissingTranslationsReport> => {
|
|
24
|
+
const keysWithNamespaces = await getKeysWithNamespaces({
|
|
25
|
+
globPatterns: config.globPatterns,
|
|
26
|
+
defaultNamespace: config.defaultNamespace,
|
|
27
|
+
})
|
|
28
|
+
const expectedByNamespace = Object.fromEntries(
|
|
29
|
+
config.namespaces.map((namespace) => [namespace, new Set<string>()]),
|
|
30
|
+
) as Record<string, Set<string>>
|
|
31
|
+
|
|
32
|
+
for (const { key, namespaces } of keysWithNamespaces) {
|
|
33
|
+
for (const namespace of namespaces) {
|
|
34
|
+
if (!expectedByNamespace[namespace]) continue
|
|
35
|
+
|
|
36
|
+
const pureKey = getPureKey(
|
|
37
|
+
key,
|
|
38
|
+
namespace,
|
|
39
|
+
namespace === config.defaultNamespace,
|
|
40
|
+
)
|
|
41
|
+
const expectedKey = pureKey || (!key.includes(":") ? key : null)
|
|
42
|
+
if (expectedKey !== null) expectedByNamespace[namespace].add(expectedKey)
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
const loadedFiles = await Promise.all(
|
|
47
|
+
config.locales.flatMap((locale) =>
|
|
48
|
+
config.namespaces.map(async (namespace) => ({
|
|
49
|
+
locale,
|
|
50
|
+
namespace,
|
|
51
|
+
translations: await loadLocalesFile(
|
|
52
|
+
config.loadPath,
|
|
53
|
+
locale,
|
|
54
|
+
namespace,
|
|
55
|
+
{ silent: true },
|
|
56
|
+
),
|
|
57
|
+
})),
|
|
58
|
+
),
|
|
57
59
|
)
|
|
58
60
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
namespaces: string[]
|
|
62
|
-
value: string | null
|
|
63
|
-
}> = []
|
|
61
|
+
const missing: Record<string, Record<string, string[]>> = {}
|
|
62
|
+
let totalMissing = 0
|
|
64
63
|
|
|
65
|
-
const
|
|
66
|
-
|
|
64
|
+
for (const { locale, namespace, translations } of loadedFiles) {
|
|
65
|
+
const missingKeys = Array.from(expectedByNamespace[namespace]).filter(
|
|
66
|
+
(key) => !Object.hasOwn(translations, key),
|
|
67
|
+
)
|
|
68
|
+
if (missingKeys.length === 0) continue
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (existingValue !== null) {
|
|
72
|
-
reusedKeys.push(newKey.key)
|
|
73
|
-
keysWithValues.push({
|
|
74
|
-
key: newKey.key,
|
|
75
|
-
namespaces: newKey.namespaces,
|
|
76
|
-
value: existingValue,
|
|
77
|
-
})
|
|
78
|
-
} else {
|
|
79
|
-
stillMissingKeys.push(newKey.key)
|
|
80
|
-
keysWithValues.push({
|
|
81
|
-
key: newKey.key,
|
|
82
|
-
namespaces: newKey.namespaces,
|
|
83
|
-
value: null,
|
|
84
|
-
})
|
|
85
|
-
}
|
|
70
|
+
missing[locale] ??= {}
|
|
71
|
+
missing[locale][namespace] = missingKeys.sort()
|
|
72
|
+
totalMissing += missingKeys.length
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const allLocales = disableTranslationDuringScan ? [defaultLocale] : locales
|
|
75
|
+
return {
|
|
76
|
+
missing,
|
|
77
|
+
totalExpected: Object.values(expectedByNamespace).reduce(
|
|
78
|
+
(total, keys) => total + keys.size,
|
|
79
|
+
0,
|
|
80
|
+
),
|
|
81
|
+
totalMissing,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
99
84
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
85
|
+
const printMissingTranslationsReport = (report: MissingTranslationsReport) => {
|
|
86
|
+
if (report.totalMissing === 0) {
|
|
87
|
+
console.log("ā
No missing translations found.")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
104
90
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
nonDefaultLocales.map(async (locale) => {
|
|
110
|
-
const translatedValues = await translateKey({
|
|
111
|
-
inputLanguage: defaultLocale,
|
|
112
|
-
outputLanguage: locale,
|
|
113
|
-
context,
|
|
114
|
-
object: newKeysObject,
|
|
115
|
-
openai,
|
|
116
|
-
model,
|
|
117
|
-
})
|
|
118
|
-
translationCache[locale] = translatedValues
|
|
119
|
-
})
|
|
120
|
-
)
|
|
91
|
+
console.error(`ā Found ${report.totalMissing} missing translation(s):`)
|
|
92
|
+
for (const [locale, namespaces] of Object.entries(report.missing)) {
|
|
93
|
+
for (const [namespace, keys] of Object.entries(namespaces)) {
|
|
94
|
+
console.error(` ${locale}/${namespace}: ${keys.join(", ")}`)
|
|
121
95
|
}
|
|
122
|
-
|
|
123
|
-
// Process all locale/namespace combinations in parallel
|
|
124
|
-
await Promise.all(
|
|
125
|
-
allLocales.flatMap((locale) =>
|
|
126
|
-
namespaces.map(async (namespace) => {
|
|
127
|
-
const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
|
|
128
|
-
|
|
129
|
-
const relevantKeys = keysToWrite.filter((key) =>
|
|
130
|
-
key.namespaces?.includes(namespace),
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
if (relevantKeys.length === 0) {
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const translatedValues = translationCache[locale]
|
|
138
|
-
for (const key of relevantKeys) {
|
|
139
|
-
existingKeys[key.key] = translatedValues[key.key]
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
await writeLocalesFile(savePath, locale, namespace, existingKeys)
|
|
143
|
-
console.log(` ā
Updated ${locale}/${namespace}.json with ${relevantKeys.length} key(s)`)
|
|
144
|
-
})
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
console.log(`\nā
Successfully auto-filled ${reusedKeys.length} key(s) from existing translations.`)
|
|
149
96
|
}
|
|
97
|
+
}
|
|
150
98
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
console.error(`\nPlease run 'i18n-magic scan' to provide values for these keys.`)
|
|
158
|
-
process.exit(1)
|
|
99
|
+
export const checkMissing = async (config: Configuration) => {
|
|
100
|
+
const report = await collectMissingTranslations(config)
|
|
101
|
+
printMissingTranslationsReport(report)
|
|
102
|
+
|
|
103
|
+
if (report.totalMissing > 0) {
|
|
104
|
+
throw new MissingTranslationsError(report)
|
|
159
105
|
}
|
|
160
106
|
|
|
161
|
-
|
|
107
|
+
return report
|
|
162
108
|
}
|
package/src/commands/replace.ts
CHANGED
|
@@ -27,20 +27,88 @@ const getKeyToReplace = async (
|
|
|
27
27
|
return { key: keyToReplace, namespaces }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export const replaceTranslationValue = async ({
|
|
31
|
+
config,
|
|
32
|
+
key,
|
|
33
|
+
value,
|
|
34
|
+
targetNamespaces,
|
|
35
|
+
}: {
|
|
36
|
+
config: Configuration
|
|
37
|
+
key: string
|
|
38
|
+
value: string
|
|
39
|
+
targetNamespaces: string[]
|
|
40
|
+
}) => {
|
|
41
|
+
const { loadPath, savePath, defaultLocale, locales, context, openai } = config
|
|
42
|
+
const stagedFiles = new Map<string, Record<string, string>>()
|
|
43
|
+
await Promise.all(
|
|
44
|
+
targetNamespaces.flatMap((namespace) =>
|
|
45
|
+
locales.map(async (locale) => {
|
|
46
|
+
const existingKeys = await loadLocalesFile(
|
|
47
|
+
loadPath,
|
|
48
|
+
locale,
|
|
49
|
+
namespace,
|
|
50
|
+
)
|
|
51
|
+
stagedFiles.set(JSON.stringify([locale, namespace]), {
|
|
52
|
+
...existingKeys,
|
|
53
|
+
})
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const translationCache: Record<string, string> = {
|
|
59
|
+
[defaultLocale]: value,
|
|
60
|
+
}
|
|
61
|
+
await Promise.all(
|
|
62
|
+
locales
|
|
63
|
+
.filter((locale) => locale !== defaultLocale)
|
|
64
|
+
.map(async (locale) => {
|
|
65
|
+
const translation = await translateKey({
|
|
66
|
+
context,
|
|
67
|
+
inputLanguage: defaultLocale,
|
|
68
|
+
outputLanguage: locale,
|
|
69
|
+
object: { [key]: value },
|
|
70
|
+
openai,
|
|
71
|
+
model: config.model,
|
|
72
|
+
})
|
|
73
|
+
translationCache[locale] = translation[key]
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
for (const namespace of targetNamespaces) {
|
|
78
|
+
for (const locale of locales) {
|
|
79
|
+
const staged = stagedFiles.get(JSON.stringify([locale, namespace]))
|
|
80
|
+
if (!staged) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Internal error: no staged snapshot for ${locale}/${namespace}.`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
staged[key] = translationCache[locale]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const namespace of targetNamespaces) {
|
|
90
|
+
for (const locale of locales) {
|
|
91
|
+
await writeLocalesFile(
|
|
92
|
+
savePath,
|
|
93
|
+
locale,
|
|
94
|
+
namespace,
|
|
95
|
+
stagedFiles.get(JSON.stringify([locale, namespace]))!,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
30
101
|
export const replaceTranslation = async (
|
|
31
102
|
config: Configuration,
|
|
32
103
|
key?: string,
|
|
33
104
|
) => {
|
|
34
105
|
const {
|
|
35
106
|
loadPath,
|
|
36
|
-
savePath,
|
|
37
107
|
defaultLocale,
|
|
38
108
|
defaultNamespace,
|
|
39
109
|
namespaces,
|
|
40
110
|
locales,
|
|
41
111
|
globPatterns,
|
|
42
|
-
context,
|
|
43
|
-
openai,
|
|
44
112
|
} = config
|
|
45
113
|
|
|
46
114
|
// Find all keys with their namespaces from the codebase
|
|
@@ -59,7 +127,7 @@ export const replaceTranslation = async (
|
|
|
59
127
|
namespaces.map(async (namespace) => ({
|
|
60
128
|
namespace,
|
|
61
129
|
keys: await loadLocalesFile(loadPath, defaultLocale, namespace),
|
|
62
|
-
}))
|
|
130
|
+
})),
|
|
63
131
|
)
|
|
64
132
|
|
|
65
133
|
for (const { namespace, keys } of namespaceKeysResults) {
|
|
@@ -115,7 +183,7 @@ export const replaceTranslation = async (
|
|
|
115
183
|
targetNamespaces.map(async (namespace) => {
|
|
116
184
|
const keys = await loadLocalesFile(loadPath, defaultLocale, namespace)
|
|
117
185
|
return { namespace, value: keys[keyToReplace] }
|
|
118
|
-
})
|
|
186
|
+
}),
|
|
119
187
|
)
|
|
120
188
|
|
|
121
189
|
for (const { namespace, value } of currentTranslations) {
|
|
@@ -127,44 +195,14 @@ export const replaceTranslation = async (
|
|
|
127
195
|
}
|
|
128
196
|
|
|
129
197
|
const newTranslation = await getTextInput("Enter the new translation: ")
|
|
198
|
+
await replaceTranslationValue({
|
|
199
|
+
config,
|
|
200
|
+
key: keyToReplace,
|
|
201
|
+
value: newTranslation,
|
|
202
|
+
targetNamespaces,
|
|
203
|
+
})
|
|
130
204
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
[defaultLocale]: newTranslation,
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const nonDefaultLocales = locales.filter((l) => l !== defaultLocale)
|
|
137
|
-
if (nonDefaultLocales.length > 0) {
|
|
138
|
-
await Promise.all(
|
|
139
|
-
nonDefaultLocales.map(async (locale) => {
|
|
140
|
-
const translation = await translateKey({
|
|
141
|
-
context,
|
|
142
|
-
inputLanguage: defaultLocale,
|
|
143
|
-
outputLanguage: locale,
|
|
144
|
-
object: {
|
|
145
|
-
[keyToReplace]: newTranslation,
|
|
146
|
-
},
|
|
147
|
-
openai,
|
|
148
|
-
model: config.model,
|
|
149
|
-
})
|
|
150
|
-
translationCache[locale] = translation[keyToReplace]
|
|
151
|
-
})
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Update the key in all relevant namespaces and locales in parallel
|
|
156
|
-
await Promise.all(
|
|
157
|
-
targetNamespaces.flatMap((namespace) =>
|
|
158
|
-
locales.map(async (locale) => {
|
|
159
|
-
const newValue = translationCache[locale]
|
|
160
|
-
const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
|
|
161
|
-
existingKeys[keyToReplace] = newValue
|
|
162
|
-
await writeLocalesFile(savePath, locale, namespace, existingKeys)
|
|
163
|
-
|
|
164
|
-
console.log(
|
|
165
|
-
`Updated "${keyToReplace}" in ${locale} (${namespace}): "${newValue}"`,
|
|
166
|
-
)
|
|
167
|
-
})
|
|
168
|
-
)
|
|
205
|
+
console.log(
|
|
206
|
+
`Updated "${keyToReplace}" in ${targetNamespaces.length} namespace(s) and ${locales.length} locale(s).`,
|
|
169
207
|
)
|
|
170
208
|
}
|