@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Configuration } from "./types.js"
|
|
2
|
+
import { resolveConfiguredLocale } from "./locale-storage.js"
|
|
3
|
+
import {
|
|
4
|
+
loadLocalesFile,
|
|
5
|
+
translateKey,
|
|
6
|
+
writeLocalesFile,
|
|
7
|
+
} from "./utils.js"
|
|
8
|
+
|
|
9
|
+
export interface UpdateTranslationKeyResult {
|
|
10
|
+
key: string
|
|
11
|
+
value: string
|
|
12
|
+
sourceLocale: string
|
|
13
|
+
targetNamespaces: string[]
|
|
14
|
+
updatedLocales: string[]
|
|
15
|
+
pendingLocales: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const updateTranslationKeyOperation = async ({
|
|
19
|
+
config,
|
|
20
|
+
key,
|
|
21
|
+
value,
|
|
22
|
+
language,
|
|
23
|
+
}: {
|
|
24
|
+
config: Configuration
|
|
25
|
+
key: string
|
|
26
|
+
value: string
|
|
27
|
+
language?: string
|
|
28
|
+
}): Promise<UpdateTranslationKeyResult> => {
|
|
29
|
+
const sourceLocale = resolveConfiguredLocale(config, language)
|
|
30
|
+
const sourceFiles = await Promise.all(
|
|
31
|
+
config.namespaces.map(async (namespace) => ({
|
|
32
|
+
namespace,
|
|
33
|
+
translations: await loadLocalesFile(
|
|
34
|
+
config.loadPath,
|
|
35
|
+
sourceLocale,
|
|
36
|
+
namespace,
|
|
37
|
+
{ silent: true },
|
|
38
|
+
),
|
|
39
|
+
})),
|
|
40
|
+
)
|
|
41
|
+
const targetNamespaces = sourceFiles
|
|
42
|
+
.filter(({ translations }) => Object.hasOwn(translations, key))
|
|
43
|
+
.map(({ namespace }) => namespace)
|
|
44
|
+
|
|
45
|
+
if (targetNamespaces.length === 0) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Key "${key}" does not exist in the ${sourceLocale} translation files. Use add_translation_key to create it.`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const pendingLocales = config.openai
|
|
52
|
+
? []
|
|
53
|
+
: config.locales.filter((locale) => locale !== sourceLocale)
|
|
54
|
+
const updatedLocales = config.openai ? [...config.locales] : [sourceLocale]
|
|
55
|
+
const translatedValues: Record<string, string> = {
|
|
56
|
+
[sourceLocale]: value,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.openai) {
|
|
60
|
+
await Promise.all(
|
|
61
|
+
config.locales
|
|
62
|
+
.filter((locale) => locale !== sourceLocale)
|
|
63
|
+
.map(async (locale) => {
|
|
64
|
+
const translation = await translateKey({
|
|
65
|
+
context: config.context || "",
|
|
66
|
+
inputLanguage: sourceLocale,
|
|
67
|
+
outputLanguage: locale,
|
|
68
|
+
object: { [key]: value },
|
|
69
|
+
openai: config.openai,
|
|
70
|
+
model: config.model,
|
|
71
|
+
})
|
|
72
|
+
translatedValues[locale] = translation[key]
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const snapshots = new Map<string, Record<string, string>>()
|
|
78
|
+
await Promise.all(
|
|
79
|
+
updatedLocales.flatMap((locale) =>
|
|
80
|
+
targetNamespaces.map(async (namespace) => {
|
|
81
|
+
const sourceSnapshot =
|
|
82
|
+
locale === sourceLocale
|
|
83
|
+
? sourceFiles.find((entry) => entry.namespace === namespace)
|
|
84
|
+
?.translations
|
|
85
|
+
: undefined
|
|
86
|
+
const existingKeys =
|
|
87
|
+
sourceSnapshot ??
|
|
88
|
+
(await loadLocalesFile(config.loadPath, locale, namespace, {
|
|
89
|
+
silent: true,
|
|
90
|
+
}))
|
|
91
|
+
snapshots.set(
|
|
92
|
+
JSON.stringify([locale, namespace]),
|
|
93
|
+
{ ...existingKeys, [key]: translatedValues[locale] },
|
|
94
|
+
)
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
for (const locale of updatedLocales) {
|
|
100
|
+
for (const namespace of targetNamespaces) {
|
|
101
|
+
const snapshot = snapshots.get(JSON.stringify([locale, namespace]))
|
|
102
|
+
if (!snapshot) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Internal error: no staged snapshot for ${locale}/${namespace}.`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
await writeLocalesFile(config.savePath, locale, namespace, snapshot)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
key,
|
|
113
|
+
value,
|
|
114
|
+
sourceLocale,
|
|
115
|
+
targetNamespaces,
|
|
116
|
+
updatedLocales,
|
|
117
|
+
pendingLocales,
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -7,6 +7,15 @@ import { minimatch } from "minimatch"
|
|
|
7
7
|
import type OpenAI from "openai"
|
|
8
8
|
import prompts from "prompts"
|
|
9
9
|
import { languages } from "./languges.js"
|
|
10
|
+
import {
|
|
11
|
+
resolveConfiguredLocale,
|
|
12
|
+
resolveLocaleTemplatePath,
|
|
13
|
+
} from "./locale-storage.js"
|
|
14
|
+
import {
|
|
15
|
+
parseTranslationResponse,
|
|
16
|
+
TranslationProviderUnavailableError,
|
|
17
|
+
TranslationResponseError,
|
|
18
|
+
} from "./translation-response.js"
|
|
10
19
|
import type { Configuration, GlobPatternConfig } from "./types.js"
|
|
11
20
|
|
|
12
21
|
export const loadConfig = async ({
|
|
@@ -17,8 +26,7 @@ export const loadConfig = async ({
|
|
|
17
26
|
const filePath = path.join(process.cwd(), configPath)
|
|
18
27
|
|
|
19
28
|
if (!fs.existsSync(filePath)) {
|
|
20
|
-
|
|
21
|
-
process.exit(1)
|
|
29
|
+
throw new Error(`Config file does not exist: ${filePath}`)
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
try {
|
|
@@ -33,32 +41,10 @@ export const loadConfig = async ({
|
|
|
33
41
|
errorMessage.includes("MODULE_NOT_FOUND") ||
|
|
34
42
|
errorMessage.includes("Cannot find module")
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.error(
|
|
41
|
-
" 1. Your config file imports dependencies that aren't installed",
|
|
42
|
-
)
|
|
43
|
-
console.error(
|
|
44
|
-
" 2. Dependencies are installed but have missing peer dependencies",
|
|
45
|
-
)
|
|
46
|
-
console.error(
|
|
47
|
-
" 3. You're using a custom storage solution (e.g., AWS S3) without proper dependencies",
|
|
48
|
-
)
|
|
49
|
-
console.error("\n To fix:")
|
|
50
|
-
console.error(
|
|
51
|
-
" - Check if your config file imports any external packages",
|
|
52
|
-
)
|
|
53
|
-
console.error(
|
|
54
|
-
" - Ensure all dependencies are properly installed: pnpm install",
|
|
55
|
-
)
|
|
56
|
-
console.error(
|
|
57
|
-
" - If using AWS SDK, ensure all @smithy/* peer dependencies are installed",
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
process.exit(1)
|
|
44
|
+
const hint = isModuleNotFound
|
|
45
|
+
? " Check the config imports and installed dependencies."
|
|
46
|
+
: ""
|
|
47
|
+
throw new Error(`Error while loading config: ${errorMessage}.${hint}`)
|
|
62
48
|
}
|
|
63
49
|
}
|
|
64
50
|
|
|
@@ -109,6 +95,8 @@ export const translateKey = async ({
|
|
|
109
95
|
}) => {
|
|
110
96
|
// Split object into chunks of 100 keys
|
|
111
97
|
const entries = Object.entries(object)
|
|
98
|
+
if (entries.length === 0) return {}
|
|
99
|
+
if (!openai) throw new TranslationProviderUnavailableError()
|
|
112
100
|
const chunks: Array<[string, string][]> = []
|
|
113
101
|
|
|
114
102
|
for (let i = 0; i < entries.length; i += 100) {
|
|
@@ -129,34 +117,57 @@ export const translateKey = async ({
|
|
|
129
117
|
|
|
130
118
|
for (const chunk of chunks) {
|
|
131
119
|
const chunkObject = Object.fromEntries(chunk)
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
120
|
+
const requestedKeys = chunk.map(([key]) => key)
|
|
121
|
+
let translatedChunk: Record<string, string> | undefined
|
|
122
|
+
|
|
123
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
124
|
+
const correctivePrompt =
|
|
125
|
+
attempt === 1
|
|
126
|
+
? ` The previous response was invalid. Return exactly these keys with string values and no additional keys: ${JSON.stringify(requestedKeys)}.`
|
|
127
|
+
: ""
|
|
128
|
+
const completion = await openai.chat.completions.create({
|
|
129
|
+
model,
|
|
130
|
+
messages: [
|
|
131
|
+
{
|
|
132
|
+
content: `You are a bot that translates the values of a locales JSON. ${
|
|
133
|
+
context
|
|
134
|
+
? `The user provided some additional context or guidelines about what to fill in the blanks: "${context}". `
|
|
135
|
+
: ""
|
|
136
|
+
}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"}.${correctivePrompt}`,
|
|
137
|
+
role: "system",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
content: JSON.stringify({
|
|
141
|
+
inputLanguage: input,
|
|
142
|
+
outputLanguage: output,
|
|
143
|
+
data: chunkObject,
|
|
144
|
+
}),
|
|
145
|
+
role: "user",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
response_format: {
|
|
149
|
+
type: "json_object",
|
|
150
150
|
},
|
|
151
|
-
|
|
152
|
-
response_format: {
|
|
153
|
-
type: "json_object",
|
|
154
|
-
},
|
|
155
|
-
})
|
|
151
|
+
})
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
try {
|
|
154
|
+
translatedChunk = parseTranslationResponse(
|
|
155
|
+
completion.choices[0]?.message.content ?? null,
|
|
156
|
+
requestedKeys,
|
|
157
|
+
)
|
|
158
|
+
break
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!(error instanceof TranslationResponseError) || attempt === 1) {
|
|
161
|
+
throw error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!translatedChunk) {
|
|
167
|
+
throw new TranslationResponseError(
|
|
168
|
+
"Translation provider did not return a valid response.",
|
|
169
|
+
)
|
|
170
|
+
}
|
|
160
171
|
|
|
161
172
|
// Merge translated chunk with result
|
|
162
173
|
result = { ...result, ...translatedChunk }
|
|
@@ -185,9 +196,7 @@ export const loadLocalesFile = async (
|
|
|
185
196
|
const silent = options?.silent ?? false
|
|
186
197
|
|
|
187
198
|
if (typeof loadPath === "string") {
|
|
188
|
-
const resolvedPath = loadPath
|
|
189
|
-
.replace("{{lng}}", locale)
|
|
190
|
-
.replace("{{ns}}", namespace)
|
|
199
|
+
const resolvedPath = resolveLocaleTemplatePath(loadPath, locale, namespace)
|
|
191
200
|
|
|
192
201
|
// Check if file exists, return empty object if it doesn't
|
|
193
202
|
if (!fs.existsSync(resolvedPath)) {
|
|
@@ -227,9 +236,11 @@ export const writeLocalesFile = async (
|
|
|
227
236
|
data: Record<string, string>,
|
|
228
237
|
) => {
|
|
229
238
|
if (typeof savePath === "string") {
|
|
230
|
-
const resolvedSavePath =
|
|
231
|
-
|
|
232
|
-
|
|
239
|
+
const resolvedSavePath = resolveLocaleTemplatePath(
|
|
240
|
+
savePath,
|
|
241
|
+
locale,
|
|
242
|
+
namespace,
|
|
243
|
+
)
|
|
233
244
|
|
|
234
245
|
// Ensure directory exists
|
|
235
246
|
const dir = path.dirname(resolvedSavePath)
|
|
@@ -876,6 +887,12 @@ export const addTranslationKeys = async ({
|
|
|
876
887
|
}
|
|
877
888
|
}
|
|
878
889
|
|
|
890
|
+
const validatedKeys = keys.map(({ key, value, language }) => ({
|
|
891
|
+
key,
|
|
892
|
+
value,
|
|
893
|
+
language: resolveConfiguredLocale(config, language),
|
|
894
|
+
}))
|
|
895
|
+
|
|
879
896
|
const log = console.log
|
|
880
897
|
const namespaceResolutionDebug =
|
|
881
898
|
process.env.DEBUG_NAMESPACE_RESOLUTION === "1" ||
|
|
@@ -923,7 +940,7 @@ export const addTranslationKeys = async ({
|
|
|
923
940
|
}),
|
|
924
941
|
)
|
|
925
942
|
|
|
926
|
-
const preparedKeys =
|
|
943
|
+
const preparedKeys = validatedKeys.map(({ key, value, language }) => {
|
|
927
944
|
const splitKey = key.split(":")
|
|
928
945
|
const hasNamespacedKey = splitKey.length > 1 && namespaces.includes(splitKey[0])
|
|
929
946
|
const normalizedKey = hasNamespacedKey ? splitKey.slice(1).join(":") : key
|
|
@@ -1029,6 +1046,7 @@ export const addTranslationKeys = async ({
|
|
|
1029
1046
|
const fileIOStartTime = performance.now()
|
|
1030
1047
|
const localeFiles = new Map<string, Record<string, string>>()
|
|
1031
1048
|
const originalKeyCounts = new Map<string, number>()
|
|
1049
|
+
const changedFiles = new Set<string>()
|
|
1032
1050
|
const loadErrors: Array<{ fileKey: string; error: string }> = []
|
|
1033
1051
|
|
|
1034
1052
|
// Load files for all locales × all affected namespaces in parallel (read-only)
|
|
@@ -1076,6 +1094,7 @@ export const addTranslationKeys = async ({
|
|
|
1076
1094
|
}
|
|
1077
1095
|
|
|
1078
1096
|
localeFiles.set(fileKey, existingKeys)
|
|
1097
|
+
changedFiles.add(fileKey)
|
|
1079
1098
|
}
|
|
1080
1099
|
|
|
1081
1100
|
// Step 7: Batch translate if OpenAI is configured
|
|
@@ -1119,18 +1138,12 @@ export const addTranslationKeys = async ({
|
|
|
1119
1138
|
object: keysToTranslate,
|
|
1120
1139
|
openai,
|
|
1121
1140
|
model,
|
|
1122
|
-
})
|
|
1123
|
-
.
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
})
|
|
1129
|
-
.catch((error) => {
|
|
1130
|
-
log(
|
|
1131
|
-
`⚠️ Failed to translate ${keyValues.length} key(s) from ${inputLanguage} to ${targetLocale}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1132
|
-
)
|
|
1133
|
-
}),
|
|
1141
|
+
}).then((translated) => {
|
|
1142
|
+
translationCache.set(
|
|
1143
|
+
`${inputLanguage}:${targetLocale}`,
|
|
1144
|
+
translated,
|
|
1145
|
+
)
|
|
1146
|
+
}),
|
|
1134
1147
|
)
|
|
1135
1148
|
}
|
|
1136
1149
|
}
|
|
@@ -1148,7 +1161,7 @@ export const addTranslationKeys = async ({
|
|
|
1148
1161
|
if (!translated) continue
|
|
1149
1162
|
|
|
1150
1163
|
for (const { key, namespaces } of keyValues) {
|
|
1151
|
-
if (translated
|
|
1164
|
+
if (Object.hasOwn(translated, key)) {
|
|
1152
1165
|
for (const namespace of namespaces) {
|
|
1153
1166
|
const fileKey = `${targetLocale}:${namespace}`
|
|
1154
1167
|
// File MUST already be loaded from step 5 - if not, something went wrong
|
|
@@ -1160,6 +1173,7 @@ export const addTranslationKeys = async ({
|
|
|
1160
1173
|
}
|
|
1161
1174
|
// Merge translated key with existing keys (don't overwrite the whole file)
|
|
1162
1175
|
localeFiles.get(fileKey)![key] = translated[key]
|
|
1176
|
+
changedFiles.add(fileKey)
|
|
1163
1177
|
}
|
|
1164
1178
|
}
|
|
1165
1179
|
}
|
|
@@ -1191,6 +1205,7 @@ export const addTranslationKeys = async ({
|
|
|
1191
1205
|
|
|
1192
1206
|
// All validations passed, now write files sequentially to avoid race conditions
|
|
1193
1207
|
for (const [fileKey, keys] of localeFiles) {
|
|
1208
|
+
if (!changedFiles.has(fileKey)) continue
|
|
1194
1209
|
const [locale, namespace] = fileKey.split(":")
|
|
1195
1210
|
await writeLocalesFile(savePath, locale, namespace, keys)
|
|
1196
1211
|
}
|
|
@@ -1212,7 +1227,7 @@ export const addTranslationKeys = async ({
|
|
|
1212
1227
|
config.locales.forEach((locale) => {
|
|
1213
1228
|
if (locale !== language) {
|
|
1214
1229
|
const translated = translationCache.get(`${language}:${locale}`)
|
|
1215
|
-
if (translated
|
|
1230
|
+
if (translated && Object.hasOwn(translated, key)) {
|
|
1216
1231
|
savedLocales.add(locale)
|
|
1217
1232
|
}
|
|
1218
1233
|
}
|
|
@@ -1222,6 +1237,7 @@ export const addTranslationKeys = async ({
|
|
|
1222
1237
|
return {
|
|
1223
1238
|
key,
|
|
1224
1239
|
value,
|
|
1240
|
+
providedLanguage: language,
|
|
1225
1241
|
namespace: resolvedNamespaces.join(", "),
|
|
1226
1242
|
locale: Array.from(savedLocales).sort().join(", "),
|
|
1227
1243
|
}
|
|
@@ -1241,7 +1257,7 @@ export const addTranslationKeys = async ({
|
|
|
1241
1257
|
export const addTranslationKey = async ({
|
|
1242
1258
|
key,
|
|
1243
1259
|
value,
|
|
1244
|
-
language
|
|
1260
|
+
language,
|
|
1245
1261
|
config,
|
|
1246
1262
|
}: {
|
|
1247
1263
|
key: string
|
package/src/mcp-server.ts
CHANGED
|
@@ -10,6 +10,7 @@ import path from "path"
|
|
|
10
10
|
import { fileURLToPath } from "url"
|
|
11
11
|
import { z } from "zod"
|
|
12
12
|
import type { Configuration } from "./lib/types.js"
|
|
13
|
+
import { updateTranslationKeyOperation } from "./lib/update-translation-key.js"
|
|
13
14
|
import {
|
|
14
15
|
addTranslationKey,
|
|
15
16
|
addTranslationKeys,
|
|
@@ -162,7 +163,7 @@ const AddTranslationKeySchema = z.object({
|
|
|
162
163
|
.string()
|
|
163
164
|
.optional()
|
|
164
165
|
.describe(
|
|
165
|
-
|
|
166
|
+
"The configured language code of the provided value. Defaults to config.defaultLocale if not specified.",
|
|
166
167
|
),
|
|
167
168
|
})
|
|
168
169
|
|
|
@@ -181,7 +182,7 @@ const AddTranslationKeysSchema = z.object({
|
|
|
181
182
|
.string()
|
|
182
183
|
.optional()
|
|
183
184
|
.describe(
|
|
184
|
-
|
|
185
|
+
"The configured language code of the provided value. Defaults to config.defaultLocale if not specified.",
|
|
185
186
|
),
|
|
186
187
|
}),
|
|
187
188
|
)
|
|
@@ -210,7 +211,7 @@ const UpdateTranslationKeySchema = z.object({
|
|
|
210
211
|
.string()
|
|
211
212
|
.optional()
|
|
212
213
|
.describe(
|
|
213
|
-
|
|
214
|
+
"The configured language code of the provided value. Defaults to config.defaultLocale if not specified.",
|
|
214
215
|
),
|
|
215
216
|
})
|
|
216
217
|
|
|
@@ -299,7 +300,7 @@ class I18nMagicServer {
|
|
|
299
300
|
{
|
|
300
301
|
name: "add_translation_key",
|
|
301
302
|
description:
|
|
302
|
-
"Add a new translation key with a text value. The destination translation file is resolved automatically from project configuration and code usage.
|
|
303
|
+
"Add a new translation key with a text value. The destination translation file is resolved automatically from project configuration and code usage. The language defaults to the configured defaultLocale. For adding multiple keys at once, use add_translation_keys instead for better performance. NOTE: This tool can only ADD keys, it will NEVER remove any existing keys.",
|
|
303
304
|
inputSchema: {
|
|
304
305
|
type: "object",
|
|
305
306
|
properties: {
|
|
@@ -315,7 +316,7 @@ class I18nMagicServer {
|
|
|
315
316
|
language: {
|
|
316
317
|
type: "string",
|
|
317
318
|
description:
|
|
318
|
-
|
|
319
|
+
"The configured language code of the provided value. Defaults to config.defaultLocale if not specified.",
|
|
319
320
|
},
|
|
320
321
|
},
|
|
321
322
|
required: ["key", "value"],
|
|
@@ -346,7 +347,7 @@ class I18nMagicServer {
|
|
|
346
347
|
language: {
|
|
347
348
|
type: "string",
|
|
348
349
|
description:
|
|
349
|
-
|
|
350
|
+
"The configured language code of the provided value. Defaults to config.defaultLocale if not specified.",
|
|
350
351
|
},
|
|
351
352
|
},
|
|
352
353
|
required: ["key", "value"],
|
|
@@ -385,7 +386,7 @@ class I18nMagicServer {
|
|
|
385
386
|
{
|
|
386
387
|
name: "update_translation_key",
|
|
387
388
|
description:
|
|
388
|
-
"Update an existing translation key with a new text value.
|
|
389
|
+
"Update an existing translation key with a new text value. The language defaults to the configured defaultLocale. With a provider, every locale is translated before writes begin. Without a provider, only the supplied/default locale is updated and other locales are returned as pending. NOTE: This tool can only UPDATE existing keys, it will NEVER remove any keys.",
|
|
389
390
|
inputSchema: {
|
|
390
391
|
type: "object",
|
|
391
392
|
properties: {
|
|
@@ -401,7 +402,7 @@ class I18nMagicServer {
|
|
|
401
402
|
language: {
|
|
402
403
|
type: "string",
|
|
403
404
|
description:
|
|
404
|
-
|
|
405
|
+
"The configured language code of the provided value. Defaults to config.defaultLocale if not specified.",
|
|
405
406
|
},
|
|
406
407
|
},
|
|
407
408
|
required: ["key", "value"],
|
|
@@ -448,7 +449,7 @@ class I18nMagicServer {
|
|
|
448
449
|
return await addTranslationKey({
|
|
449
450
|
key: params.key,
|
|
450
451
|
value: params.value,
|
|
451
|
-
language: params.language
|
|
452
|
+
language: params.language,
|
|
452
453
|
config,
|
|
453
454
|
})
|
|
454
455
|
})
|
|
@@ -467,7 +468,7 @@ class I18nMagicServer {
|
|
|
467
468
|
message: `Successfully added translation key "${result.key}"`,
|
|
468
469
|
key: result.key,
|
|
469
470
|
value: result.value,
|
|
470
|
-
providedLanguage:
|
|
471
|
+
providedLanguage: result.providedLanguage,
|
|
471
472
|
locales: result.locale,
|
|
472
473
|
nextStep: result.locale.includes(",")
|
|
473
474
|
? "Key was automatically translated to multiple locales"
|
|
@@ -564,7 +565,7 @@ class I18nMagicServer {
|
|
|
564
565
|
keys: params.keys.map((k) => ({
|
|
565
566
|
key: k.key,
|
|
566
567
|
value: k.value,
|
|
567
|
-
language: k.language
|
|
568
|
+
language: k.language,
|
|
568
569
|
})),
|
|
569
570
|
config,
|
|
570
571
|
})
|
|
@@ -892,102 +893,14 @@ class I18nMagicServer {
|
|
|
892
893
|
console.log = () => {}
|
|
893
894
|
|
|
894
895
|
try {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const keys = await loadLocalesFile(
|
|
904
|
-
config.loadPath,
|
|
905
|
-
"en",
|
|
906
|
-
namespace,
|
|
907
|
-
)
|
|
908
|
-
if (Object.hasOwn(keys, params.key)) {
|
|
909
|
-
targetNamespaces.push(namespace)
|
|
910
|
-
}
|
|
911
|
-
} catch (error) {
|
|
912
|
-
// Namespace file doesn't exist, continue
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (targetNamespaces.length === 0) {
|
|
917
|
-
throw new Error(
|
|
918
|
-
`Key "${params.key}" does not exist in translation files. Use add_translation_key to create it.`,
|
|
919
|
-
)
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// Build translation cache with new value
|
|
923
|
-
const inputLanguage = params.language || "en"
|
|
924
|
-
const translationCache: Record<string, string> = {
|
|
925
|
-
[inputLanguage]: params.value,
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// Translate to all other locales
|
|
929
|
-
const otherLocales = config.locales.filter(
|
|
930
|
-
(l) => l !== inputLanguage,
|
|
931
|
-
)
|
|
932
|
-
if (otherLocales.length > 0 && config.openai) {
|
|
933
|
-
const { translateKey } = await import("./lib/utils.js")
|
|
934
|
-
|
|
935
|
-
await Promise.all(
|
|
936
|
-
otherLocales.map(async (locale) => {
|
|
937
|
-
const translation = await translateKey({
|
|
938
|
-
context: config.context || "",
|
|
939
|
-
inputLanguage: inputLanguage,
|
|
940
|
-
outputLanguage: locale,
|
|
941
|
-
object: {
|
|
942
|
-
[params.key]: params.value,
|
|
943
|
-
},
|
|
944
|
-
openai: config.openai!,
|
|
945
|
-
model: config.model,
|
|
946
|
-
})
|
|
947
|
-
translationCache[locale] = translation[params.key]
|
|
948
|
-
}),
|
|
949
|
-
)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Update the key in all relevant namespaces and locales
|
|
953
|
-
// Process sequentially to avoid race conditions
|
|
954
|
-
const { writeLocalesFile } = await import("./lib/utils.js")
|
|
955
|
-
|
|
956
|
-
for (const namespace of targetNamespaces) {
|
|
957
|
-
for (const locale of config.locales) {
|
|
958
|
-
const newValue = translationCache[locale] || params.value
|
|
959
|
-
|
|
960
|
-
// Load existing keys
|
|
961
|
-
const existingKeys = await loadLocalesFile(
|
|
962
|
-
config.loadPath,
|
|
963
|
-
locale,
|
|
964
|
-
namespace,
|
|
965
|
-
)
|
|
966
|
-
|
|
967
|
-
const originalKeyCount = Object.keys(existingKeys).length
|
|
968
|
-
|
|
969
|
-
// Update the specific key
|
|
970
|
-
existingKeys[params.key] = newValue
|
|
971
|
-
|
|
972
|
-
const newKeyCount = Object.keys(existingKeys).length
|
|
973
|
-
|
|
974
|
-
// Safety check: we should only be updating, not removing
|
|
975
|
-
// Key count should stay same (update) or increase by 1 (add)
|
|
976
|
-
if (newKeyCount < originalKeyCount) {
|
|
977
|
-
throw new Error(
|
|
978
|
-
`Safety check failed: Updating locale "${locale}" would reduce keys from ${originalKeyCount} to ${newKeyCount}. Aborting.`,
|
|
979
|
-
)
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
await writeLocalesFile(
|
|
983
|
-
config.savePath,
|
|
984
|
-
locale,
|
|
985
|
-
namespace,
|
|
986
|
-
existingKeys,
|
|
987
|
-
)
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
})
|
|
896
|
+
const result = await translationMutex.withLock(async () =>
|
|
897
|
+
updateTranslationKeyOperation({
|
|
898
|
+
config,
|
|
899
|
+
key: params.key,
|
|
900
|
+
value: params.value,
|
|
901
|
+
language: params.language,
|
|
902
|
+
}),
|
|
903
|
+
)
|
|
991
904
|
|
|
992
905
|
return {
|
|
993
906
|
content: [
|
|
@@ -996,11 +909,17 @@ class I18nMagicServer {
|
|
|
996
909
|
text: JSON.stringify(
|
|
997
910
|
{
|
|
998
911
|
success: true,
|
|
999
|
-
message: `Successfully updated translation key "${
|
|
1000
|
-
key:
|
|
1001
|
-
newValue:
|
|
1002
|
-
providedLanguage:
|
|
1003
|
-
|
|
912
|
+
message: `Successfully updated translation key "${result.key}" in ${result.updatedLocales.length} locale(s)`,
|
|
913
|
+
key: result.key,
|
|
914
|
+
newValue: result.value,
|
|
915
|
+
providedLanguage: result.sourceLocale,
|
|
916
|
+
updatedLocales: result.updatedLocales,
|
|
917
|
+
pendingLocales: result.pendingLocales,
|
|
918
|
+
locales: result.updatedLocales,
|
|
919
|
+
nextStep:
|
|
920
|
+
result.pendingLocales.length > 0
|
|
921
|
+
? "Run 'i18n-magic sync' with a configured provider to translate pending locales."
|
|
922
|
+
: "All configured locales were updated.",
|
|
1004
923
|
},
|
|
1005
924
|
null,
|
|
1006
925
|
2,
|