@scoutello/i18n-magic 0.51.0 → 0.53.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.
@@ -55,8 +55,14 @@ export const replaceTranslation = async (
55
55
  { namespace: string; value: string }[]
56
56
  > = {}
57
57
 
58
- for (const namespace of namespaces) {
59
- const keys = await loadLocalesFile(loadPath, defaultLocale, namespace)
58
+ const namespaceKeysResults = await Promise.all(
59
+ namespaces.map(async (namespace) => ({
60
+ namespace,
61
+ keys: await loadLocalesFile(loadPath, defaultLocale, namespace),
62
+ }))
63
+ )
64
+
65
+ for (const { namespace, keys } of namespaceKeysResults) {
60
66
  for (const [keyName, value] of Object.entries(keys)) {
61
67
  if (!allAvailableKeys[keyName]) {
62
68
  allAvailableKeys[keyName] = []
@@ -105,24 +111,32 @@ export const replaceTranslation = async (
105
111
  }
106
112
 
107
113
  // Show current translations across namespaces
108
- for (const namespace of targetNamespaces) {
109
- const keys = await loadLocalesFile(loadPath, defaultLocale, namespace)
110
- if (keys[keyToReplace]) {
114
+ const currentTranslations = await Promise.all(
115
+ targetNamespaces.map(async (namespace) => {
116
+ const keys = await loadLocalesFile(loadPath, defaultLocale, namespace)
117
+ return { namespace, value: keys[keyToReplace] }
118
+ })
119
+ )
120
+
121
+ for (const { namespace, value } of currentTranslations) {
122
+ if (value) {
111
123
  console.log(
112
- `Current translation in ${defaultLocale} (${namespace}): "${keys[keyToReplace]}"`,
124
+ `Current translation in ${defaultLocale} (${namespace}): "${value}"`,
113
125
  )
114
126
  }
115
127
  }
116
128
 
117
129
  const newTranslation = await getTextInput("Enter the new translation: ")
118
130
 
119
- // Update the key in all relevant namespaces and locales
120
- for (const namespace of targetNamespaces) {
121
- for (const locale of locales) {
122
- let newValue = ""
123
- if (locale === defaultLocale) {
124
- newValue = newTranslation
125
- } else {
131
+ // Batch translate for all non-default locales first
132
+ const translationCache: Record<string, string> = {
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) => {
126
140
  const translation = await translateKey({
127
141
  context,
128
142
  inputLanguage: defaultLocale,
@@ -133,17 +147,24 @@ export const replaceTranslation = async (
133
147
  openai,
134
148
  model: config.model,
135
149
  })
136
-
137
- newValue = translation[keyToReplace]
138
- }
139
-
140
- const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
141
- existingKeys[keyToReplace] = newValue
142
- await writeLocalesFile(savePath, locale, namespace, existingKeys)
143
-
144
- console.log(
145
- `Updated "${keyToReplace}" in ${locale} (${namespace}): "${newValue}"`,
146
- )
147
- }
150
+ translationCache[locale] = translation[keyToReplace]
151
+ })
152
+ )
148
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
+ )
169
+ )
149
170
  }
@@ -3,7 +3,6 @@ import {
3
3
  checkAllKeysExist,
4
4
  getMissingKeys,
5
5
  getTextInput,
6
- findExistingTranslation,
7
6
  findExistingTranslations,
8
7
  loadLocalesFile,
9
8
  translateKey,
@@ -93,40 +92,51 @@ export const translateMissing = async (config: Configuration) => {
93
92
 
94
93
  const allLocales = disableTranslationDuringScan ? [defaultLocale] : locales
95
94
 
96
- for (const locale of allLocales) {
97
- let translatedValues = {}
95
+ // Batch translate for all non-default locales in parallel
96
+ const translationCache: Record<string, Record<string, string>> = {
97
+ [defaultLocale]: newKeysObject,
98
+ }
98
99
 
99
- if (locale === defaultLocale) {
100
- translatedValues = newKeysObject
101
- } else {
102
- translatedValues = await translateKey({
103
- inputLanguage: defaultLocale,
104
- outputLanguage: locale,
105
- context,
106
- object: newKeysObject,
107
- openai,
108
- model: config.model,
100
+ const nonDefaultLocales = allLocales.filter((l) => l !== defaultLocale)
101
+ if (nonDefaultLocales.length > 0) {
102
+ await Promise.all(
103
+ nonDefaultLocales.map(async (locale) => {
104
+ const translatedValues = await translateKey({
105
+ inputLanguage: defaultLocale,
106
+ outputLanguage: locale,
107
+ context,
108
+ object: newKeysObject,
109
+ openai,
110
+ model: config.model,
111
+ })
112
+ translationCache[locale] = translatedValues
109
113
  })
110
- }
114
+ )
115
+ }
111
116
 
112
- for (const namespace of namespaces) {
113
- const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
117
+ // Process all locale/namespace combinations in parallel
118
+ await Promise.all(
119
+ allLocales.flatMap((locale) =>
120
+ namespaces.map(async (namespace) => {
121
+ const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
114
122
 
115
- const relevantKeys = newKeysWithDefaultLocale.filter((key) =>
116
- key.namespaces?.includes(namespace),
117
- )
123
+ const relevantKeys = newKeysWithDefaultLocale.filter((key) =>
124
+ key.namespaces?.includes(namespace),
125
+ )
118
126
 
119
- if (relevantKeys.length === 0) {
120
- continue
121
- }
127
+ if (relevantKeys.length === 0) {
128
+ return
129
+ }
122
130
 
123
- for (const key of relevantKeys) {
124
- existingKeys[key.key] = translatedValues[key.key]
125
- }
131
+ const translatedValues = translationCache[locale]
132
+ for (const key of relevantKeys) {
133
+ existingKeys[key.key] = translatedValues[key.key]
134
+ }
126
135
 
127
- writeLocalesFile(savePath, locale, namespace, existingKeys)
128
- }
129
- }
136
+ await writeLocalesFile(savePath, locale, namespace, existingKeys)
137
+ })
138
+ )
139
+ )
130
140
 
131
141
  await checkAllKeysExist(config)
132
142
 
package/src/lib/utils.ts CHANGED
@@ -703,45 +703,24 @@ export const addTranslationKey = async ({
703
703
  }) => {
704
704
  const { loadPath, savePath, defaultNamespace, namespaces, globPatterns, defaultLocale, openai, context, model } = config
705
705
 
706
- // Use console.error for logging when called from MCP server (console.log is suppressed)
707
- const log = console.log
708
-
709
- // Validate that config has required fields
710
- if (!loadPath || !savePath) {
711
- throw new Error("Config must have loadPath and savePath defined")
712
- }
713
-
714
- if (!namespaces || namespaces.length === 0) {
715
- throw new Error("Config must have at least one namespace defined")
716
- }
717
-
718
706
  // Try to find which namespaces this key is used in by scanning the codebase
719
707
  const affectedNamespaces: string[] = []
720
708
 
721
709
  try {
722
- log(`🔍 Scanning codebase for key "${key}"...`)
723
- log(` Glob patterns: ${extractGlobPatterns(globPatterns).length} pattern(s)`)
724
- log(` Default namespace: ${defaultNamespace}`)
725
-
726
710
  // Scan the codebase to find where this key is already being used
727
711
  const keysWithNamespaces = await getKeysWithNamespaces({
728
712
  globPatterns,
729
713
  defaultNamespace,
730
714
  })
731
715
 
732
- log(` Scanned ${keysWithNamespaces.length} total key instances across all files`)
733
-
734
716
  // Find entries for this specific key
735
717
  const keyEntries = keysWithNamespaces.filter(
736
- (entry) => entry.key === key || entry.key === `${defaultNamespace}:${key}` || entry.key.endsWith(`:${key}`)
718
+ (entry) => entry.key === key || entry.key === `${defaultNamespace}:${key}`
737
719
  )
738
720
 
739
- log(` Found ${keyEntries.length} instance(s) of key "${key}"`)
740
-
741
721
  // Collect unique namespaces where this key is used
742
722
  const foundNamespaces = new Set<string>()
743
723
  for (const entry of keyEntries) {
744
- log(` File: ${entry.file} → Namespaces: ${entry.namespaces.join(", ")}`)
745
724
  for (const ns of entry.namespaces) {
746
725
  foundNamespaces.add(ns)
747
726
  }
@@ -749,25 +728,19 @@ export const addTranslationKey = async ({
749
728
 
750
729
  if (foundNamespaces.size > 0) {
751
730
  affectedNamespaces.push(...Array.from(foundNamespaces))
752
- log(
753
- `✓ Found key "${key}" in use across ${affectedNamespaces.length} namespace(s): ${affectedNamespaces.join(", ")}`
731
+ console.log(
732
+ `🔍 Found key "${key}" in use across ${affectedNamespaces.length} namespace(s): ${affectedNamespaces.join(", ")}`
754
733
  )
755
734
  }
756
735
  } catch (error) {
757
- // If scanning fails, log the error with details but continue with fallback logic
758
- log(`⚠️ Warning: Failed to scan codebase for key usage`)
759
- if (error instanceof Error) {
760
- log(` Error: ${error.message}`)
761
- if (error.stack) {
762
- log(` Stack trace: ${error.stack}`)
763
- }
764
- }
736
+ // If scanning fails, continue with fallback logic
737
+ console.error(`Warning: Failed to scan codebase for key usage: ${error}`)
765
738
  }
766
739
 
767
740
  if (affectedNamespaces.length === 0) {
768
741
  // If the key is not found in the codebase, use the default namespace
769
742
  affectedNamespaces.push(defaultNamespace)
770
- log(
743
+ console.log(
771
744
  `📝 Key "${key}" not found in codebase. Adding to default namespace "${defaultNamespace}".`
772
745
  )
773
746
  }
@@ -775,6 +748,9 @@ export const addTranslationKey = async ({
775
748
  // Always use "en" as the locale for adding keys (English)
776
749
  const locale = "en"
777
750
 
751
+ // Use console.error for logging when called from MCP server (console.log is suppressed)
752
+ const log = console.log
753
+
778
754
  for (const targetNamespace of affectedNamespaces) {
779
755
  log(`➕ Adding translation key "${key}" to namespace "${targetNamespace}" (${locale})`)
780
756
 
@@ -782,7 +758,6 @@ export const addTranslationKey = async ({
782
758
  let existingKeys: Record<string, string>
783
759
  try {
784
760
  existingKeys = await loadLocalesFile(loadPath, locale, targetNamespace)
785
- log(` Loaded ${Object.keys(existingKeys).length} existing keys from ${locale}/${targetNamespace}`)
786
761
  } catch (error) {
787
762
  // If file doesn't exist, start with empty object
788
763
  log(`📄 Creating new namespace file for ${locale}/${targetNamespace}`)
@@ -803,51 +778,39 @@ export const addTranslationKey = async ({
803
778
  log(`✅ Successfully saved key to ${locale}/${targetNamespace}`)
804
779
 
805
780
  // If defaultLocale is different from "en", translate and save to defaultLocale
806
- if (defaultLocale !== "en") {
807
- if (!openai) {
808
- log(`⚠️ No OpenAI client configured. Skipping translation to ${defaultLocale}.`)
809
- log(` You can run 'i18n-magic sync' to translate this key later`)
810
- } else if (!model) {
811
- log(`⚠️ No model specified in config. Skipping translation to ${defaultLocale}.`)
812
- log(` You can run 'i18n-magic sync' to translate this key later`)
813
- } else {
814
- log(`🌐 Translating key "${key}" to ${defaultLocale}...`)
815
- log(` Using model: ${model}`)
816
-
817
- try {
818
- // Translate the single key
819
- const translatedValue = await translateKey({
820
- inputLanguage: "en",
821
- outputLanguage: defaultLocale,
822
- context: context || "",
823
- object: { [key]: value },
824
- openai,
825
- model: model as string,
826
- })
827
-
828
- // Load existing keys for the default locale
829
- let defaultLocaleKeys: Record<string, string>
830
- try {
831
- defaultLocaleKeys = await loadLocalesFile(loadPath, defaultLocale, targetNamespace)
832
- } catch (error) {
833
- // If file doesn't exist, start with empty object
834
- log(`📄 Creating new namespace file for ${defaultLocale}/${targetNamespace}`)
835
- defaultLocaleKeys = {}
836
- }
837
-
838
- // Add the translated key
839
- defaultLocaleKeys[key] = translatedValue[key]
781
+ if (defaultLocale !== "en" && openai) {
782
+ log(`🌐 Translating key "${key}" to ${defaultLocale}...`)
783
+
784
+ try {
785
+ // Translate the single key
786
+ const translatedValue = await translateKey({
787
+ inputLanguage: "en",
788
+ outputLanguage: defaultLocale,
789
+ context: context || "",
790
+ object: { [key]: value },
791
+ openai,
792
+ model,
793
+ })
840
794
 
841
- // Save the updated keys
842
- await writeLocalesFile(savePath, defaultLocale, targetNamespace, defaultLocaleKeys)
843
- log(`✅ Successfully translated and saved key to ${defaultLocale}/${targetNamespace}`)
795
+ // Load existing keys for the default locale
796
+ let defaultLocaleKeys: Record<string, string>
797
+ try {
798
+ defaultLocaleKeys = await loadLocalesFile(loadPath, defaultLocale, targetNamespace)
844
799
  } catch (error) {
845
- log(`⚠️ Failed to translate key to ${defaultLocale}: ${error instanceof Error ? error.message : "Unknown error"}`)
846
- if (error instanceof Error && error.stack) {
847
- log(` Stack: ${error.stack}`)
848
- }
849
- log(` You can run 'i18n-magic sync' to translate this key later`)
800
+ // If file doesn't exist, start with empty object
801
+ log(`📄 Creating new namespace file for ${defaultLocale}/${targetNamespace}`)
802
+ defaultLocaleKeys = {}
850
803
  }
804
+
805
+ // Add the translated key
806
+ defaultLocaleKeys[key] = translatedValue[key]
807
+
808
+ // Save the updated keys
809
+ await writeLocalesFile(savePath, defaultLocale, targetNamespace, defaultLocaleKeys)
810
+ log(`✅ Successfully translated and saved key to ${defaultLocale}/${targetNamespace}`)
811
+ } catch (error) {
812
+ log(`⚠️ Failed to translate key to ${defaultLocale}: ${error instanceof Error ? error.message : "Unknown error"}`)
813
+ log(` You can run 'i18n-magic sync' to translate this key later`)
851
814
  }
852
815
  }
853
816
  }