@scoutello/i18n-magic 0.54.0 → 0.56.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/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +2 -1
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/sync-locales.d.ts.map +1 -1
- package/dist/commands/sync-locales.js +2 -1
- package/dist/commands/sync-locales.js.map +1 -1
- package/dist/lib/utils.d.ts +27 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +200 -85
- package/dist/lib/utils.js.map +1 -1
- package/dist/mcp-server.js +146 -2
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/scan.ts +2 -1
- package/src/commands/sync-locales.ts +2 -1
- package/src/lib/utils.ts +250 -102
- package/src/mcp-server.ts +168 -2
package/src/lib/utils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Parser } from "i18next-scanner"
|
|
|
3
3
|
import { minimatch } from "minimatch"
|
|
4
4
|
import fs from "node:fs"
|
|
5
5
|
import path from "node:path"
|
|
6
|
+
import { performance } from "node:perf_hooks"
|
|
6
7
|
import type OpenAI from "openai"
|
|
7
8
|
import prompts from "prompts"
|
|
8
9
|
import { languages } from "./languges.js"
|
|
@@ -527,7 +528,9 @@ export const findExistingTranslation = async (
|
|
|
527
528
|
for (const namespace of namespaces) {
|
|
528
529
|
try {
|
|
529
530
|
const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
|
|
530
|
-
|
|
531
|
+
// Use explicit existence check instead of truthy check
|
|
532
|
+
// to handle empty string values correctly
|
|
533
|
+
if (Object.prototype.hasOwnProperty.call(existingKeys, key)) {
|
|
531
534
|
return existingKeys[key]
|
|
532
535
|
}
|
|
533
536
|
} catch (error) {
|
|
@@ -567,7 +570,12 @@ export const findExistingTranslations = async (
|
|
|
567
570
|
for (const key of keys) {
|
|
568
571
|
let found = false
|
|
569
572
|
for (const namespace of namespaces) {
|
|
570
|
-
|
|
573
|
+
// Use explicit existence check instead of truthy check
|
|
574
|
+
// to handle empty string values correctly
|
|
575
|
+
if (
|
|
576
|
+
namespaceKeys[namespace] &&
|
|
577
|
+
Object.prototype.hasOwnProperty.call(namespaceKeys[namespace], key)
|
|
578
|
+
) {
|
|
571
579
|
results[key] = namespaceKeys[namespace][key]
|
|
572
580
|
found = true
|
|
573
581
|
break
|
|
@@ -692,29 +700,49 @@ export class TranslationError extends Error {
|
|
|
692
700
|
* This function adds to the specified language locale, and will also translate
|
|
693
701
|
* and save to other locales if OpenAI is configured.
|
|
694
702
|
*/
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
703
|
+
/**
|
|
704
|
+
* Add multiple translation keys in batch. This is optimized for performance:
|
|
705
|
+
* - Single codebase scan for all keys
|
|
706
|
+
* - Batched file I/O operations
|
|
707
|
+
* - Batched translations per locale
|
|
708
|
+
*/
|
|
709
|
+
export const addTranslationKeys = async ({
|
|
710
|
+
keys,
|
|
699
711
|
config,
|
|
700
712
|
}: {
|
|
701
|
-
key: string
|
|
702
|
-
value: string
|
|
703
|
-
language?: string
|
|
713
|
+
keys: Array<{ key: string; value: string; language?: string }>
|
|
704
714
|
config: Configuration
|
|
705
715
|
}) => {
|
|
716
|
+
const startTime = performance.now()
|
|
706
717
|
const { loadPath, savePath, defaultNamespace, namespaces, globPatterns, defaultLocale, openai, context, model } = config
|
|
707
718
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
719
|
+
if (keys.length === 0) {
|
|
720
|
+
return { results: [], performance: { totalTime: 0, scanTime: 0, translationTime: 0, fileIOTime: 0 } }
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const log = console.log
|
|
724
|
+
log(`🚀 Batch adding ${keys.length} translation key(s)...`)
|
|
725
|
+
|
|
726
|
+
// Step 1: Single codebase scan for all keys (most expensive operation)
|
|
727
|
+
const scanStartTime = performance.now()
|
|
728
|
+
let keysWithNamespaces: Array<{ key: string; namespaces: string[]; file: string }> = []
|
|
711
729
|
try {
|
|
712
|
-
|
|
713
|
-
const keysWithNamespaces = await getKeysWithNamespaces({
|
|
730
|
+
keysWithNamespaces = await getKeysWithNamespaces({
|
|
714
731
|
globPatterns,
|
|
715
732
|
defaultNamespace,
|
|
716
733
|
})
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error(`Warning: Failed to scan codebase for key usage: ${error}`)
|
|
736
|
+
}
|
|
737
|
+
const scanTime = performance.now() - scanStartTime
|
|
738
|
+
log(`⏱️ Codebase scan completed in ${scanTime.toFixed(2)}ms`)
|
|
717
739
|
|
|
740
|
+
// Step 2: Determine namespaces for each key
|
|
741
|
+
const keyToNamespaces = new Map<string, Set<string>>()
|
|
742
|
+
|
|
743
|
+
for (const { key, language = "en" } of keys) {
|
|
744
|
+
const affectedNamespaces: string[] = []
|
|
745
|
+
|
|
718
746
|
// Find entries for this specific key
|
|
719
747
|
const keyEntries = keysWithNamespaces.filter(
|
|
720
748
|
(entry) => entry.key === key || entry.key === `${defaultNamespace}:${key}`
|
|
@@ -730,112 +758,232 @@ export const addTranslationKey = async ({
|
|
|
730
758
|
|
|
731
759
|
if (foundNamespaces.size > 0) {
|
|
732
760
|
affectedNamespaces.push(...Array.from(foundNamespaces))
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
)
|
|
761
|
+
} else {
|
|
762
|
+
// If the key is not found in the codebase, use the default namespace
|
|
763
|
+
affectedNamespaces.push(defaultNamespace)
|
|
736
764
|
}
|
|
737
|
-
} catch (error) {
|
|
738
|
-
// If scanning fails, continue with fallback logic
|
|
739
|
-
console.error(`Warning: Failed to scan codebase for key usage: ${error}`)
|
|
740
|
-
}
|
|
741
765
|
|
|
742
|
-
|
|
743
|
-
// If the key is not found in the codebase, use the default namespace
|
|
744
|
-
affectedNamespaces.push(defaultNamespace)
|
|
745
|
-
console.log(
|
|
746
|
-
`📝 Key "${key}" not found in codebase. Adding to default namespace "${defaultNamespace}".`
|
|
747
|
-
)
|
|
766
|
+
keyToNamespaces.set(key, new Set(affectedNamespaces))
|
|
748
767
|
}
|
|
749
768
|
|
|
750
|
-
//
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// Load existing keys for the specified locale
|
|
763
|
-
let existingKeys: Record<string, string>
|
|
764
|
-
try {
|
|
765
|
-
existingKeys = await loadLocalesFile(loadPath, locale, targetNamespace)
|
|
766
|
-
} catch (error) {
|
|
767
|
-
// If file doesn't exist, start with empty object
|
|
768
|
-
log(`📄 Creating new namespace file for ${locale}/${targetNamespace}`)
|
|
769
|
-
existingKeys = {}
|
|
769
|
+
// Step 3: Group keys by namespace and locale
|
|
770
|
+
const namespaceLocaleToKeys = new Map<string, Array<{ key: string; value: string; language: string }>>()
|
|
771
|
+
|
|
772
|
+
for (const { key, value, language = "en" } of keys) {
|
|
773
|
+
const namespaces = keyToNamespaces.get(key) || new Set([defaultNamespace])
|
|
774
|
+
for (const namespace of namespaces) {
|
|
775
|
+
const mapKey = `${namespace}:${language}`
|
|
776
|
+
if (!namespaceLocaleToKeys.has(mapKey)) {
|
|
777
|
+
namespaceLocaleToKeys.set(mapKey, [])
|
|
778
|
+
}
|
|
779
|
+
namespaceLocaleToKeys.get(mapKey)!.push({ key, value, language })
|
|
770
780
|
}
|
|
781
|
+
}
|
|
771
782
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
783
|
+
// Step 4: Collect all unique namespaces that will be affected
|
|
784
|
+
const affectedNamespaces = new Set<string>()
|
|
785
|
+
for (const namespaces of keyToNamespaces.values()) {
|
|
786
|
+
for (const ns of namespaces) {
|
|
787
|
+
affectedNamespaces.add(ns)
|
|
776
788
|
}
|
|
789
|
+
}
|
|
777
790
|
|
|
778
|
-
|
|
779
|
-
|
|
791
|
+
// Step 5: Batch load ALL locale files for affected namespaces (not just input language)
|
|
792
|
+
// This ensures we preserve existing keys in all locales
|
|
793
|
+
const fileIOStartTime = performance.now()
|
|
794
|
+
const localeFiles = new Map<string, Record<string, string>>()
|
|
795
|
+
const loadPromises: Promise<void>[] = []
|
|
796
|
+
|
|
797
|
+
// Load files for all locales × all affected namespaces
|
|
798
|
+
for (const namespace of affectedNamespaces) {
|
|
799
|
+
for (const locale of config.locales) {
|
|
800
|
+
const fileKey = `${locale}:${namespace}`
|
|
801
|
+
|
|
802
|
+
if (!localeFiles.has(fileKey)) {
|
|
803
|
+
loadPromises.push(
|
|
804
|
+
loadLocalesFile(loadPath, locale, namespace)
|
|
805
|
+
.then((keys) => {
|
|
806
|
+
localeFiles.set(fileKey, keys)
|
|
807
|
+
})
|
|
808
|
+
.catch(() => {
|
|
809
|
+
localeFiles.set(fileKey, {})
|
|
810
|
+
})
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
await Promise.all(loadPromises)
|
|
817
|
+
const fileIOTime = performance.now() - fileIOStartTime
|
|
818
|
+
log(`⏱️ File I/O (load) completed in ${fileIOTime.toFixed(2)}ms`)
|
|
819
|
+
|
|
820
|
+
// Step 6: Add new keys to the input language locale files
|
|
821
|
+
for (const [namespaceLocale, keyValues] of namespaceLocaleToKeys) {
|
|
822
|
+
const [namespace, locale] = namespaceLocale.split(":")
|
|
823
|
+
const fileKey = `${locale}:${namespace}`
|
|
824
|
+
const existingKeys = localeFiles.get(fileKey) || {}
|
|
825
|
+
|
|
826
|
+
for (const { key, value } of keyValues) {
|
|
827
|
+
existingKeys[key] = value
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
localeFiles.set(fileKey, existingKeys)
|
|
831
|
+
}
|
|
780
832
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
833
|
+
// Step 7: Batch translate if OpenAI is configured
|
|
834
|
+
const translationStartTime = performance.now()
|
|
835
|
+
const translationCache = new Map<string, Record<string, string>>()
|
|
836
|
+
|
|
837
|
+
if (openai) {
|
|
838
|
+
// Group keys by input language
|
|
839
|
+
const keysByLanguage = new Map<string, Array<{ key: string; value: string; namespaces: Set<string> }>>()
|
|
840
|
+
|
|
841
|
+
for (const { key, value, language = "en" } of keys) {
|
|
842
|
+
if (!keysByLanguage.has(language)) {
|
|
843
|
+
keysByLanguage.set(language, [])
|
|
844
|
+
}
|
|
845
|
+
keysByLanguage.get(language)!.push({
|
|
846
|
+
key,
|
|
847
|
+
value,
|
|
848
|
+
namespaces: keyToNamespaces.get(key) || new Set([defaultNamespace]),
|
|
849
|
+
})
|
|
850
|
+
}
|
|
784
851
|
|
|
785
|
-
//
|
|
786
|
-
|
|
787
|
-
|
|
852
|
+
// Translate each language group to all other locales
|
|
853
|
+
const translationPromises: Promise<void>[] = []
|
|
854
|
+
|
|
855
|
+
for (const [inputLanguage, keyValues] of keysByLanguage) {
|
|
856
|
+
const otherLocales = config.locales.filter(l => l !== inputLanguage)
|
|
788
857
|
|
|
789
|
-
|
|
790
|
-
|
|
858
|
+
for (const targetLocale of otherLocales) {
|
|
859
|
+
const keysToTranslate = Object.fromEntries(
|
|
860
|
+
keyValues.map(({ key, value }) => [key, value])
|
|
861
|
+
)
|
|
791
862
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
model,
|
|
804
|
-
})
|
|
805
|
-
|
|
806
|
-
// Load existing keys for the target locale
|
|
807
|
-
let targetLocaleKeys: Record<string, string>
|
|
808
|
-
try {
|
|
809
|
-
targetLocaleKeys = await loadLocalesFile(loadPath, targetLocale, targetNamespace)
|
|
810
|
-
} catch (error) {
|
|
811
|
-
log(`📄 Creating new namespace file for ${targetLocale}/${targetNamespace}`)
|
|
812
|
-
targetLocaleKeys = {}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Add the translated key
|
|
816
|
-
targetLocaleKeys[key] = translatedValue[key]
|
|
817
|
-
|
|
818
|
-
// Save the updated keys
|
|
819
|
-
await writeLocalesFile(savePath, targetLocale, targetNamespace, targetLocaleKeys)
|
|
820
|
-
savedLocales.add(targetLocale)
|
|
821
|
-
log(`✅ Successfully translated and saved key to ${targetLocale}/${targetNamespace}`)
|
|
822
|
-
} catch (error) {
|
|
823
|
-
log(`⚠️ Failed to translate key to ${targetLocale}: ${error instanceof Error ? error.message : "Unknown error"}`)
|
|
824
|
-
}
|
|
863
|
+
translationPromises.push(
|
|
864
|
+
translateKey({
|
|
865
|
+
inputLanguage,
|
|
866
|
+
outputLanguage: targetLocale,
|
|
867
|
+
context: context || "",
|
|
868
|
+
object: keysToTranslate,
|
|
869
|
+
openai,
|
|
870
|
+
model,
|
|
871
|
+
})
|
|
872
|
+
.then((translated) => {
|
|
873
|
+
translationCache.set(`${inputLanguage}:${targetLocale}`, translated)
|
|
825
874
|
})
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
875
|
+
.catch((error) => {
|
|
876
|
+
log(`⚠️ Failed to translate ${keyValues.length} key(s) from ${inputLanguage} to ${targetLocale}: ${error instanceof Error ? error.message : "Unknown error"}`)
|
|
877
|
+
})
|
|
878
|
+
)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
await Promise.all(translationPromises)
|
|
883
|
+
|
|
884
|
+
// Add translated keys to locale files (merge with existing keys)
|
|
885
|
+
for (const [inputLanguage, keyValues] of keysByLanguage) {
|
|
886
|
+
const otherLocales = config.locales.filter(l => l !== inputLanguage)
|
|
887
|
+
|
|
888
|
+
for (const targetLocale of otherLocales) {
|
|
889
|
+
const translated = translationCache.get(`${inputLanguage}:${targetLocale}`)
|
|
890
|
+
if (!translated) continue
|
|
891
|
+
|
|
892
|
+
for (const { key, namespaces } of keyValues) {
|
|
893
|
+
if (translated[key]) {
|
|
894
|
+
for (const namespace of namespaces) {
|
|
895
|
+
const fileKey = `${targetLocale}:${namespace}`
|
|
896
|
+
// File should already be loaded from step 5, but ensure it exists
|
|
897
|
+
if (!localeFiles.has(fileKey)) {
|
|
898
|
+
localeFiles.set(fileKey, {})
|
|
899
|
+
}
|
|
900
|
+
// Merge translated key with existing keys (don't overwrite the whole file)
|
|
901
|
+
localeFiles.get(fileKey)![key] = translated[key]
|
|
902
|
+
}
|
|
903
|
+
}
|
|
830
904
|
}
|
|
831
905
|
}
|
|
832
906
|
}
|
|
833
907
|
}
|
|
908
|
+
|
|
909
|
+
const translationTime = performance.now() - translationStartTime
|
|
910
|
+
if (openai && translationTime > 0) {
|
|
911
|
+
log(`⏱️ Translation completed in ${translationTime.toFixed(2)}ms`)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Step 8: Batch write all files
|
|
915
|
+
const writeStartTime = performance.now()
|
|
916
|
+
const writePromises: Promise<void>[] = []
|
|
917
|
+
|
|
918
|
+
for (const [fileKey, keys] of localeFiles) {
|
|
919
|
+
const [locale, namespace] = fileKey.split(":")
|
|
920
|
+
writePromises.push(
|
|
921
|
+
writeLocalesFile(savePath, locale, namespace, keys)
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
await Promise.all(writePromises)
|
|
926
|
+
const writeTime = performance.now() - writeStartTime
|
|
927
|
+
log(`⏱️ File I/O (write) completed in ${writeTime.toFixed(2)}ms`)
|
|
928
|
+
|
|
929
|
+
const totalTime = performance.now() - startTime
|
|
930
|
+
log(`✅ Batch operation completed in ${totalTime.toFixed(2)}ms (${(totalTime / keys.length).toFixed(2)}ms per key)`)
|
|
931
|
+
|
|
932
|
+
// Build results
|
|
933
|
+
const results = keys.map(({ key, value, language = "en" }) => {
|
|
934
|
+
const namespaces = Array.from(keyToNamespaces.get(key) || new Set([defaultNamespace]))
|
|
935
|
+
const savedLocales = new Set<string>([language])
|
|
936
|
+
|
|
937
|
+
if (openai) {
|
|
938
|
+
config.locales.forEach(locale => {
|
|
939
|
+
if (locale !== language) {
|
|
940
|
+
const translated = translationCache.get(`${language}:${locale}`)
|
|
941
|
+
if (translated?.[key]) {
|
|
942
|
+
savedLocales.add(locale)
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
key,
|
|
950
|
+
value,
|
|
951
|
+
namespace: namespaces.join(", "),
|
|
952
|
+
locale: Array.from(savedLocales).sort().join(", "),
|
|
953
|
+
}
|
|
954
|
+
})
|
|
834
955
|
|
|
835
956
|
return {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
957
|
+
results,
|
|
958
|
+
performance: {
|
|
959
|
+
totalTime,
|
|
960
|
+
scanTime,
|
|
961
|
+
translationTime,
|
|
962
|
+
fileIOTime: fileIOTime + writeTime,
|
|
963
|
+
},
|
|
840
964
|
}
|
|
841
965
|
}
|
|
966
|
+
|
|
967
|
+
export const addTranslationKey = async ({
|
|
968
|
+
key,
|
|
969
|
+
value,
|
|
970
|
+
language = "en",
|
|
971
|
+
config,
|
|
972
|
+
}: {
|
|
973
|
+
key: string
|
|
974
|
+
value: string
|
|
975
|
+
language?: string
|
|
976
|
+
config: Configuration
|
|
977
|
+
}) => {
|
|
978
|
+
const startTime = performance.now()
|
|
979
|
+
const result = await addTranslationKeys({
|
|
980
|
+
keys: [{ key, value, language }],
|
|
981
|
+
config,
|
|
982
|
+
})
|
|
983
|
+
const totalTime = performance.now() - startTime
|
|
984
|
+
|
|
985
|
+
const log = console.log
|
|
986
|
+
log(`⏱️ Single key operation completed in ${totalTime.toFixed(2)}ms`)
|
|
987
|
+
|
|
988
|
+
return result.results[0]
|
|
989
|
+
}
|
package/src/mcp-server.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js"
|
|
7
7
|
import { z } from "zod"
|
|
8
|
-
import { addTranslationKey, getMissingKeys, loadConfig, loadLocalesFile } from "./lib/utils.js"
|
|
8
|
+
import { addTranslationKey, addTranslationKeys, getMissingKeys, loadConfig, loadLocalesFile } from "./lib/utils.js"
|
|
9
9
|
import type { Configuration } from "./lib/types.js"
|
|
10
10
|
import path from "path"
|
|
11
11
|
import fs from "fs"
|
|
@@ -71,6 +71,15 @@ const AddTranslationKeySchema = z.object({
|
|
|
71
71
|
language: z.string().optional().describe("The language code of the provided value (e.g., \"en\", \"de\", \"fr\"). Defaults to \"en\" (English) if not specified."),
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
+
// Zod schema for the add_translation_keys (batch) tool parameters
|
|
75
|
+
const AddTranslationKeysSchema = z.object({
|
|
76
|
+
keys: z.array(z.object({
|
|
77
|
+
key: z.string().describe("The translation key to add (e.g., \"welcomeMessage\")"),
|
|
78
|
+
value: z.string().describe("The text value for this translation key"),
|
|
79
|
+
language: z.string().optional().describe("The language code of the provided value (e.g., \"en\", \"de\", \"fr\"). Defaults to \"en\" (English) if not specified."),
|
|
80
|
+
})).describe("Array of translation keys to add in batch. Use this for adding multiple keys at once for better performance."),
|
|
81
|
+
})
|
|
82
|
+
|
|
74
83
|
// Zod schema for the list_untranslated_keys tool parameters
|
|
75
84
|
const ListUntranslatedKeysSchema = z.object({
|
|
76
85
|
namespace: z
|
|
@@ -184,7 +193,7 @@ class I18nMagicServer {
|
|
|
184
193
|
tools: [
|
|
185
194
|
{
|
|
186
195
|
name: "add_translation_key",
|
|
187
|
-
description: "Add a new translation key with a text value. You can optionally specify the language of the value you're providing (defaults to English).",
|
|
196
|
+
description: "Add a new translation key with a text value. You can optionally specify the language of the value you're providing (defaults to English). For adding multiple keys at once, use add_translation_keys instead for better performance.",
|
|
188
197
|
inputSchema: {
|
|
189
198
|
type: "object",
|
|
190
199
|
properties: {
|
|
@@ -207,6 +216,38 @@ class I18nMagicServer {
|
|
|
207
216
|
required: ["key", "value"],
|
|
208
217
|
},
|
|
209
218
|
},
|
|
219
|
+
{
|
|
220
|
+
name: "add_translation_keys",
|
|
221
|
+
description: "Add multiple translation keys in batch. This is optimized for performance - when adding 2 or more keys, prefer this over multiple add_translation_key calls. It performs a single codebase scan, batches file I/O operations, and batches translations for much better performance.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
keys: {
|
|
226
|
+
type: "array",
|
|
227
|
+
description: "Array of translation keys to add in batch",
|
|
228
|
+
items: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
key: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "The translation key to add (e.g., \"welcomeMessage\")",
|
|
234
|
+
},
|
|
235
|
+
value: {
|
|
236
|
+
type: "string",
|
|
237
|
+
description: "The text value for this translation key",
|
|
238
|
+
},
|
|
239
|
+
language: {
|
|
240
|
+
type: "string",
|
|
241
|
+
description: "The language code of the provided value (e.g., \"en\" for English, \"de\" for German, \"fr\" for French). Defaults to \"en\" if not specified.",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
required: ["key", "value"],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
required: ["keys"],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
210
251
|
{
|
|
211
252
|
name: "list_untranslated_keys",
|
|
212
253
|
description:
|
|
@@ -399,6 +440,131 @@ class I18nMagicServer {
|
|
|
399
440
|
}
|
|
400
441
|
}
|
|
401
442
|
|
|
443
|
+
if (request.params.name === "add_translation_keys") {
|
|
444
|
+
try {
|
|
445
|
+
// Validate parameters
|
|
446
|
+
const params = AddTranslationKeysSchema.parse(request.params.arguments)
|
|
447
|
+
|
|
448
|
+
if (!params.keys || params.keys.length === 0) {
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: JSON.stringify(
|
|
454
|
+
{
|
|
455
|
+
success: false,
|
|
456
|
+
error: "No keys provided. The 'keys' array must contain at least one key-value pair.",
|
|
457
|
+
},
|
|
458
|
+
null,
|
|
459
|
+
2,
|
|
460
|
+
),
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
isError: true,
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Ensure config is loaded
|
|
468
|
+
const config = await this.ensureConfig()
|
|
469
|
+
|
|
470
|
+
// Capture console.log output for diagnostics
|
|
471
|
+
const originalConsoleLog = console.log
|
|
472
|
+
const logMessages: string[] = []
|
|
473
|
+
console.log = (...args: any[]) => {
|
|
474
|
+
const message = args.map(arg =>
|
|
475
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
476
|
+
).join(' ')
|
|
477
|
+
logMessages.push(message)
|
|
478
|
+
// Also log to stderr for debugging
|
|
479
|
+
console.error(`[i18n-magic] ${message}`)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let result
|
|
483
|
+
try {
|
|
484
|
+
// Add the translation keys in batch
|
|
485
|
+
result = await addTranslationKeys({
|
|
486
|
+
keys: params.keys.map(k => ({
|
|
487
|
+
key: k.key,
|
|
488
|
+
value: k.value,
|
|
489
|
+
language: k.language || "en",
|
|
490
|
+
})),
|
|
491
|
+
config,
|
|
492
|
+
})
|
|
493
|
+
} finally {
|
|
494
|
+
// Restore console.log
|
|
495
|
+
console.log = originalConsoleLog
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
content: [
|
|
500
|
+
{
|
|
501
|
+
type: "text",
|
|
502
|
+
text: JSON.stringify(
|
|
503
|
+
{
|
|
504
|
+
success: true,
|
|
505
|
+
message: `Successfully added ${result.results.length} translation key(s) in batch`,
|
|
506
|
+
results: result.results,
|
|
507
|
+
performance: result.performance,
|
|
508
|
+
summary: {
|
|
509
|
+
totalKeys: result.results.length,
|
|
510
|
+
totalTime: `${result.performance.totalTime.toFixed(2)}ms`,
|
|
511
|
+
averageTimePerKey: `${(result.performance.totalTime / result.results.length).toFixed(2)}ms`,
|
|
512
|
+
scanTime: `${result.performance.scanTime.toFixed(2)}ms`,
|
|
513
|
+
translationTime: `${result.performance.translationTime.toFixed(2)}ms`,
|
|
514
|
+
fileIOTime: `${result.performance.fileIOTime.toFixed(2)}ms`,
|
|
515
|
+
},
|
|
516
|
+
diagnostics: logMessages.join('\n'),
|
|
517
|
+
},
|
|
518
|
+
null,
|
|
519
|
+
2,
|
|
520
|
+
),
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
const errorMessage =
|
|
526
|
+
error instanceof Error ? error.message : "Unknown error occurred"
|
|
527
|
+
|
|
528
|
+
// Get more detailed error information
|
|
529
|
+
let errorDetails = errorMessage
|
|
530
|
+
if (error instanceof Error) {
|
|
531
|
+
// Check if there's a cause
|
|
532
|
+
const cause = (error as any).cause
|
|
533
|
+
if (cause instanceof Error) {
|
|
534
|
+
errorDetails = `${errorMessage}\nCause: ${cause.message}\nStack: ${cause.stack}`
|
|
535
|
+
} else if (cause) {
|
|
536
|
+
errorDetails = `${errorMessage}\nCause: ${JSON.stringify(cause)}`
|
|
537
|
+
}
|
|
538
|
+
// Include stack trace
|
|
539
|
+
if (error.stack) {
|
|
540
|
+
errorDetails = `${errorDetails}\nStack: ${error.stack}`
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Log detailed error to stderr for debugging
|
|
545
|
+
console.error(`[i18n-magic MCP] Error adding translation keys in batch:`)
|
|
546
|
+
console.error(errorDetails)
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: JSON.stringify(
|
|
553
|
+
{
|
|
554
|
+
success: false,
|
|
555
|
+
error: errorMessage,
|
|
556
|
+
details: errorDetails,
|
|
557
|
+
},
|
|
558
|
+
null,
|
|
559
|
+
2,
|
|
560
|
+
),
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
isError: true,
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
402
568
|
if (request.params.name === "list_untranslated_keys") {
|
|
403
569
|
try {
|
|
404
570
|
// Validate parameters
|