@scoutello/i18n-magic 0.64.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.
Files changed (58) hide show
  1. package/README.md +13 -7
  2. package/dist/cli.js +20 -15
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/check-missing.d.ts +11 -1
  5. package/dist/commands/check-missing.d.ts.map +1 -1
  6. package/dist/commands/check-missing.js +56 -96
  7. package/dist/commands/check-missing.js.map +1 -1
  8. package/dist/commands/replace.d.ts +6 -0
  9. package/dist/commands/replace.d.ts.map +1 -1
  10. package/dist/commands/replace.js +48 -29
  11. package/dist/commands/replace.js.map +1 -1
  12. package/dist/commands/scan.d.ts.map +1 -1
  13. package/dist/commands/scan.js +114 -80
  14. package/dist/commands/scan.js.map +1 -1
  15. package/dist/commands/sync-locales.d.ts.map +1 -1
  16. package/dist/commands/sync-locales.js +90 -50
  17. package/dist/commands/sync-locales.js.map +1 -1
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/languges.d.ts.map +1 -1
  23. package/dist/lib/languges.js +20 -0
  24. package/dist/lib/languges.js.map +1 -1
  25. package/dist/lib/locale-storage.d.ts +7 -0
  26. package/dist/lib/locale-storage.d.ts.map +1 -0
  27. package/dist/lib/locale-storage.js +49 -0
  28. package/dist/lib/locale-storage.js.map +1 -0
  29. package/dist/lib/translation-response.d.ts +8 -0
  30. package/dist/lib/translation-response.d.ts.map +1 -0
  31. package/dist/lib/translation-response.js +43 -0
  32. package/dist/lib/translation-response.js.map +1 -0
  33. package/dist/lib/types.d.ts +1 -1
  34. package/dist/lib/types.d.ts.map +1 -1
  35. package/dist/lib/update-translation-key.d.ts +16 -0
  36. package/dist/lib/update-translation-key.d.ts.map +1 -0
  37. package/dist/lib/update-translation-key.js +67 -0
  38. package/dist/lib/update-translation-key.js.map +1 -0
  39. package/dist/lib/utils.d.ts +2 -0
  40. package/dist/lib/utils.d.ts.map +1 -1
  41. package/dist/lib/utils.js +69 -51
  42. package/dist/lib/utils.js.map +1 -1
  43. package/dist/mcp-server.js +28 -79
  44. package/dist/mcp-server.js.map +1 -1
  45. package/package.json +5 -9
  46. package/src/cli.ts +26 -18
  47. package/src/commands/check-missing.ts +83 -137
  48. package/src/commands/replace.ts +81 -43
  49. package/src/commands/scan.ts +130 -108
  50. package/src/commands/sync-locales.ts +125 -86
  51. package/src/index.ts +6 -1
  52. package/src/lib/languges.ts +20 -0
  53. package/src/lib/locale-storage.ts +76 -0
  54. package/src/lib/translation-response.ts +59 -0
  55. package/src/lib/types.ts +1 -1
  56. package/src/lib/update-translation-key.ts +119 -0
  57. package/src/lib/utils.ts +92 -76
  58. package/src/mcp-server.ts +31 -112
@@ -1,8 +1,7 @@
1
1
  import type { Configuration } from "../lib/types.js"
2
2
  import {
3
- checkAllKeysExist,
4
- findExistingTranslations,
5
- getMissingKeys,
3
+ getKeysWithNamespaces,
4
+ getPureKey,
6
5
  getTextInput,
7
6
  loadLocalesFile,
8
7
  translateKey,
@@ -10,11 +9,15 @@ import {
10
9
  } from "../lib/utils.js"
11
10
  import { findUnusedKeys, removeUnusedKeys } from "./clean.js"
12
11
 
12
+ const fileKey = (locale: string, namespace: string) =>
13
+ JSON.stringify([locale, namespace])
14
+
13
15
  export const translateMissing = async (config: Configuration) => {
14
16
  const {
15
17
  loadPath,
16
18
  savePath,
17
19
  defaultLocale,
20
+ defaultNamespace,
18
21
  namespaces,
19
22
  locales,
20
23
  context,
@@ -22,138 +25,157 @@ export const translateMissing = async (config: Configuration) => {
22
25
  disableTranslationDuringScan,
23
26
  autoClear,
24
27
  } = config
25
-
26
- if (autoClear) {
27
- console.log("🧹 Checking for unused translations before scanning...")
28
- const report = await findUnusedKeys(config)
29
-
30
- if (report.unusedCount > 0) {
31
- await removeUnusedKeys(config)
32
- console.log("")
33
- } else {
34
- console.log("✅ No unused keys found.\n")
28
+ const activeLocales = disableTranslationDuringScan
29
+ ? [defaultLocale]
30
+ : locales
31
+ const keysWithNamespaces = await getKeysWithNamespaces({
32
+ globPatterns: config.globPatterns,
33
+ defaultNamespace,
34
+ })
35
+ const expectedByNamespace = Object.fromEntries(
36
+ namespaces.map((namespace) => [namespace, new Set<string>()]),
37
+ ) as Record<string, Set<string>>
38
+
39
+ for (const { key, namespaces: keyNamespaces } of keysWithNamespaces) {
40
+ for (const namespace of keyNamespaces) {
41
+ if (!expectedByNamespace[namespace]) continue
42
+ const pureKey = getPureKey(
43
+ key,
44
+ namespace,
45
+ namespace === defaultNamespace,
46
+ )
47
+ const expectedKey = pureKey || (!key.includes(":") ? key : null)
48
+ if (expectedKey !== null) expectedByNamespace[namespace].add(expectedKey)
35
49
  }
36
50
  }
37
51
 
38
- const newKeys = await getMissingKeys(config)
39
-
40
- if (newKeys.length === 0) {
41
- console.log("No new keys found.")
42
-
43
- await checkAllKeysExist(config)
44
-
45
- return
46
- }
47
-
48
- console.log(
49
- `${newKeys.length} keys are missing. Please provide the values for the following keys in ${defaultLocale}:`,
50
- )
51
-
52
- const newKeysWithDefaultLocale = []
53
-
54
- // Check for existing translations in parallel
55
- const keysList = newKeys.map((k) => k.key)
56
- const existingTranslationResults = await findExistingTranslations(
57
- keysList,
58
- namespaces,
59
- defaultLocale,
60
- loadPath,
52
+ const stagedFiles = new Map<string, Record<string, string>>()
53
+ await Promise.all(
54
+ activeLocales.flatMap((locale) =>
55
+ namespaces.map(async (namespace) => {
56
+ const translations = await loadLocalesFile(
57
+ loadPath,
58
+ locale,
59
+ namespace,
60
+ { silent: true },
61
+ )
62
+ stagedFiles.set(fileKey(locale, namespace), { ...translations })
63
+ }),
64
+ ),
61
65
  )
62
66
 
63
- const reusedKeys: string[] = []
64
- for (const newKey of newKeys) {
65
- const existingValue = existingTranslationResults[newKey.key]
66
-
67
- let answer: string
68
- // Use explicit null check instead of truthy check to handle empty string values
69
- if (existingValue !== null) {
70
- reusedKeys.push(newKey.key)
71
- answer = existingValue
72
- } else {
73
- answer = await getTextInput(newKey.key, newKey.namespaces)
67
+ const changedFiles = new Set<string>()
68
+ const missingDefaultKeys = new Map<string, Set<string>>()
69
+ for (const namespace of namespaces) {
70
+ const translations = stagedFiles.get(fileKey(defaultLocale, namespace))!
71
+ for (const key of expectedByNamespace[namespace]) {
72
+ if (!Object.hasOwn(translations, key)) {
73
+ missingDefaultKeys.get(key)?.add(namespace) ??
74
+ missingDefaultKeys.set(key, new Set([namespace]))
75
+ }
74
76
  }
75
-
76
- newKeysWithDefaultLocale.push({
77
- key: newKey.key,
78
- namespace: newKey.namespace,
79
- namespaces: newKey.namespaces,
80
- value: answer,
81
- })
82
77
  }
83
78
 
84
- // Batch log reused keys
85
- if (reusedKeys.length > 0) {
79
+ if (missingDefaultKeys.size > 0) {
86
80
  console.log(
87
- `🔄 Auto-reused ${reusedKeys.length} existing values from other namespaces`,
81
+ `${missingDefaultKeys.size} keys are missing. Please provide the values for the following keys in ${defaultLocale}:`,
88
82
  )
89
83
  }
90
84
 
91
- const newKeysObject = newKeysWithDefaultLocale.reduce((prev, next) => {
92
- prev[next.key] = next.value
85
+ for (const [key, keyNamespaces] of missingDefaultKeys) {
86
+ let value: string | undefined
87
+ for (const namespace of namespaces) {
88
+ const translations = stagedFiles.get(fileKey(defaultLocale, namespace))!
89
+ if (Object.hasOwn(translations, key)) {
90
+ value = translations[key]
91
+ break
92
+ }
93
+ }
93
94
 
94
- return prev
95
- }, {})
95
+ value ??= await getTextInput(key, Array.from(keyNamespaces))
96
+ for (const namespace of keyNamespaces) {
97
+ stagedFiles.get(fileKey(defaultLocale, namespace))![key] = value
98
+ changedFiles.add(fileKey(defaultLocale, namespace))
99
+ }
100
+ }
96
101
 
97
- const allLocales = disableTranslationDuringScan ? [defaultLocale] : locales
102
+ await Promise.all(
103
+ activeLocales
104
+ .filter((locale) => locale !== defaultLocale)
105
+ .map(async (locale) => {
106
+ const missingByKey = new Map<string, Set<string>>()
107
+ for (const namespace of namespaces) {
108
+ const translations = stagedFiles.get(fileKey(locale, namespace))!
109
+ for (const key of expectedByNamespace[namespace]) {
110
+ if (!Object.hasOwn(translations, key)) {
111
+ missingByKey.get(key)?.add(namespace) ??
112
+ missingByKey.set(key, new Set([namespace]))
113
+ }
114
+ }
115
+ }
98
116
 
99
- // Batch translate for all non-default locales in parallel
100
- const translationCache: Record<string, Record<string, string>> = {
101
- [defaultLocale]: newKeysObject,
102
- }
117
+ const reused: Record<string, string> = {}
118
+ const keysToTranslate: Record<string, string> = {}
119
+ for (const key of missingByKey.keys()) {
120
+ let existingValue: string | undefined
121
+ for (const namespace of namespaces) {
122
+ const translations = stagedFiles.get(fileKey(locale, namespace))!
123
+ if (Object.hasOwn(translations, key)) {
124
+ existingValue = translations[key]
125
+ break
126
+ }
127
+ }
128
+
129
+ if (existingValue !== undefined) {
130
+ reused[key] = existingValue
131
+ continue
132
+ }
133
+
134
+ const sourceNamespace = Array.from(missingByKey.get(key)!)[0]
135
+ const sourceTranslations = stagedFiles.get(
136
+ fileKey(defaultLocale, sourceNamespace),
137
+ )!
138
+ keysToTranslate[key] = sourceTranslations[key]
139
+ }
103
140
 
104
- const nonDefaultLocales = allLocales.filter((l) => l !== defaultLocale)
105
- if (nonDefaultLocales.length > 0) {
106
- await Promise.all(
107
- nonDefaultLocales.map(async (locale) => {
108
- const translatedValues = await translateKey({
141
+ const translated = await translateKey({
109
142
  inputLanguage: defaultLocale,
110
143
  outputLanguage: locale,
111
144
  context,
112
- object: newKeysObject,
145
+ object: keysToTranslate,
113
146
  openai,
114
147
  model: config.model,
115
148
  })
116
- translationCache[locale] = translatedValues
117
- }),
118
- )
119
- }
120
-
121
- // Process all locale/namespace combinations in parallel
122
- const writeResults: Array<{
123
- locale: string
124
- namespace: string
125
- keyCount: number
126
- }> = []
127
- await Promise.all(
128
- allLocales.flatMap((locale) =>
129
- namespaces.map(async (namespace) => {
130
- const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
131
-
132
- const relevantKeys = newKeysWithDefaultLocale.filter((key) =>
133
- key.namespaces?.includes(namespace),
134
- )
135
-
136
- if (relevantKeys.length === 0) {
137
- return
138
- }
139
-
140
- const translatedValues = translationCache[locale]
141
- for (const key of relevantKeys) {
142
- existingKeys[key.key] = translatedValues[key.key]
149
+ const resolvedValues = { ...reused, ...translated }
150
+
151
+ for (const [key, keyNamespaces] of missingByKey) {
152
+ for (const namespace of keyNamespaces) {
153
+ stagedFiles.get(fileKey(locale, namespace))![key] =
154
+ resolvedValues[key]
155
+ changedFiles.add(fileKey(locale, namespace))
156
+ }
143
157
  }
144
-
145
- await writeLocalesFile(savePath, locale, namespace, existingKeys)
146
- writeResults.push({ locale, namespace, keyCount: relevantKeys.length })
147
158
  }),
148
- ),
149
159
  )
150
160
 
151
- // Log where keys were written
152
- for (const { locale, namespace, keyCount } of writeResults) {
153
- console.log(` 📝 Wrote ${keyCount} key(s) to ${locale}/${namespace}.json`)
161
+ for (const locale of activeLocales) {
162
+ for (const namespace of namespaces) {
163
+ const key = fileKey(locale, namespace)
164
+ if (!changedFiles.has(key)) continue
165
+ await writeLocalesFile(savePath, locale, namespace, stagedFiles.get(key)!)
166
+ console.log(` 📝 Updated ${locale}/${namespace}.json`)
167
+ }
154
168
  }
155
169
 
156
- await checkAllKeysExist(config)
170
+ if (autoClear) {
171
+ console.log("🧹 Checking for unused translations after scanning...")
172
+ const report = await findUnusedKeys(config)
173
+ if (report.unusedCount > 0) await removeUnusedKeys(config)
174
+ }
157
175
 
158
- console.log(`Successfully translated ${newKeys.length} keys.`)
176
+ if (changedFiles.size === 0) {
177
+ console.log("No missing translations found.")
178
+ } else {
179
+ console.log(`Successfully updated ${changedFiles.size} locale file(s).`)
180
+ }
159
181
  }
@@ -1,10 +1,8 @@
1
- import fs from "node:fs"
2
1
  import chalk from "chalk"
3
2
  import cliProgress from "cli-progress"
4
3
  import { languages } from "../lib/languges.js"
5
4
  import type { Configuration } from "../lib/types.js"
6
5
  import {
7
- findExistingTranslations,
8
6
  loadLocalesFile,
9
7
  TranslationError,
10
8
  translateKey,
@@ -51,34 +49,52 @@ export const syncLocales = async (config: Configuration) => {
51
49
  const localeResults: Record<
52
50
  string,
53
51
  {
54
- status: "pending" | "processing" | "done" | "error"
52
+ status: "pending" | "processing" | "translating" | "done" | "error"
55
53
  translated: number
54
+ translationCompleted: number
55
+ translationTotal: number
56
56
  reused: number
57
57
  error?: string
58
58
  }
59
59
  > = {}
60
60
 
61
61
  for (const locale of localesToProcess) {
62
- localeResults[locale] = { status: "pending", translated: 0, reused: 0 }
62
+ localeResults[locale] = {
63
+ status: "pending",
64
+ translated: 0,
65
+ translationCompleted: 0,
66
+ translationTotal: 0,
67
+ reused: 0,
68
+ }
63
69
  }
64
70
 
65
- // Create a single progress bar for overall progress
66
- const progressBar = new cliProgress.SingleBar(
71
+ const stagedFiles = new Map<
72
+ string,
73
+ { locale: string; namespace: string; translations: Record<string, string> }
74
+ >()
75
+
76
+ const progressBars = new cliProgress.MultiBar(
67
77
  {
68
- format: ` {bar} {percentage}% | {current}/{total} | {lang} {status}`,
78
+ format: ` {label} {bar} {percentage}% | {value}/{total} | {duration_formatted} | {status}`,
69
79
  barCompleteChar: chalk.green("█"),
70
80
  barIncompleteChar: chalk.gray("░"),
71
81
  hideCursor: true,
72
82
  clearOnComplete: false,
83
+ emptyOnZero: true,
84
+ etaAsynchronousUpdate: true,
73
85
  barsize: 30,
74
86
  },
75
87
  cliProgress.Presets.shades_classic,
76
88
  )
77
89
 
78
- progressBar.start(localesToProcess.length, 0, {
90
+ const localeProgressBar = progressBars.create(localesToProcess.length, 0, {
91
+ label: "Languages ",
79
92
  status: "",
80
- current: 0,
81
- lang: "",
93
+ })
94
+
95
+ const translationProgressBar = progressBars.create(0, 0, {
96
+ label: "Translations",
97
+ status: "finding keys to translate...",
82
98
  })
83
99
 
84
100
  // Suppress console.log during progress bar display
@@ -86,63 +102,72 @@ export const syncLocales = async (config: Configuration) => {
86
102
  const suppressedLog = () => {}
87
103
 
88
104
  try {
89
- // Helper to ensure a locale/namespace file exists before we add keys
90
- const ensureLocaleNamespaceFile = async (
91
- locale: string,
92
- namespace: string,
93
- ) => {
94
- if (typeof savePath === "string") {
95
- const filePath = savePath
96
- .replace("{{lng}}", locale)
97
- .replace("{{ns}}", namespace)
98
- if (!fs.existsSync(filePath)) {
99
- await writeLocalesFile(savePath, locale, namespace, {})
100
- }
101
- }
102
- }
103
-
104
- // Ensure all default-locale namespace files exist first
105
- await Promise.all(
106
- namespaces.map((namespace) =>
107
- ensureLocaleNamespaceFile(defaultLocale, namespace),
108
- ),
109
- )
110
-
111
105
  // Process locales in parallel batches
112
106
  let completedCount = 0
113
107
  const currentlyProcessing: Set<string> = new Set()
114
108
 
115
- // Helper to update progress bar with currently processing languages
109
+ // Helper to update progress bars with currently processing languages
116
110
  const updateProgressDisplay = () => {
117
111
  const processingArray = Array.from(currentlyProcessing)
118
- const langs = processingArray
119
- .slice(0, 3)
120
- .map((l) => getLanguageLabel(l).substring(0, 8))
121
- .join(", ")
122
- const status = processingArray.length > 0 ? `processing ${langs}...` : ""
123
- progressBar.update(completedCount, {
124
- status,
125
- current: completedCount,
126
- lang: "",
112
+ const localeStatus =
113
+ processingArray.length > 0
114
+ ? `processing ${processingArray.join(", ")}`
115
+ : ""
116
+ localeProgressBar.update(completedCount, {
117
+ label: "Languages ",
118
+ status: localeStatus,
119
+ })
120
+
121
+ const translationTotals = localesToProcess.reduce(
122
+ (totals, locale) => {
123
+ const result = localeResults[locale]
124
+ totals.completed += result.translationCompleted
125
+ totals.total += result.translationTotal
126
+ return totals
127
+ },
128
+ { completed: 0, total: 0 },
129
+ )
130
+ const translationTotal = translationTotals.total
131
+ const translationCurrent =
132
+ translationTotal > 0
133
+ ? Math.min(translationTotals.completed, translationTotal)
134
+ : 0
135
+ const activeTranslations = localesToProcess.filter((locale) => {
136
+ const result = localeResults[locale]
137
+ return (
138
+ currentlyProcessing.has(locale) &&
139
+ result.translationTotal > 0 &&
140
+ result.translationCompleted < result.translationTotal
141
+ )
142
+ })
143
+ const translationStatus =
144
+ activeTranslations.length > 0
145
+ ? `translating ${activeTranslations
146
+ .map((locale) => {
147
+ const result = localeResults[locale]
148
+ return `${locale} ${result.translationCompleted}/${result.translationTotal}`
149
+ })
150
+ .join(", ")}`
151
+ : processingArray.length > 0
152
+ ? "waiting for translation work..."
153
+ : translationTotal > 0
154
+ ? "translations complete"
155
+ : "finding keys to translate..."
156
+
157
+ translationProgressBar.setTotal(translationTotal)
158
+ translationProgressBar.update(translationCurrent, {
159
+ label: "Translations",
160
+ status: translationStatus,
127
161
  })
128
162
  }
129
163
 
130
164
  // Process a single locale
131
165
  const processLocale = async (locale: string) => {
132
- const langLabel = getLanguageLabel(locale)
133
- const shortLang = langLabel.substring(0, 10).padEnd(10)
134
166
  localeResults[locale].status = "processing"
135
167
  currentlyProcessing.add(locale)
136
168
  updateProgressDisplay()
137
169
 
138
170
  try {
139
- // Ensure all namespace files for this locale exist
140
- await Promise.all(
141
- namespaces.map((namespace) =>
142
- ensureLocaleNamespaceFile(locale, namespace),
143
- ),
144
- )
145
-
146
171
  // Collect all missing keys for this locale across all namespaces
147
172
  const allMissingKeys: Record<
148
173
  string,
@@ -181,7 +206,7 @@ export const syncLocales = async (config: Configuration) => {
181
206
  namespaceKeys[namespace] = localeKeys
182
207
 
183
208
  for (const [key, value] of Object.entries(defaultLocaleKeys)) {
184
- if (!localeKeys[key]) {
209
+ if (!Object.hasOwn(localeKeys, key)) {
185
210
  if (allMissingKeys[key]) {
186
211
  allMissingKeys[key].namespaces.push(namespace)
187
212
  } else {
@@ -208,18 +233,12 @@ export const syncLocales = async (config: Configuration) => {
208
233
  const keysToTranslate: Record<string, string> = {}
209
234
  const existingTranslations: Record<string, string> = {}
210
235
 
211
- const existingTranslationResults = await findExistingTranslations(
212
- missingKeysList,
213
- namespaces,
214
- locale,
215
- loadPath,
216
- { silent: true },
217
- )
218
-
219
236
  for (const key of missingKeysList) {
220
- const existingValue = existingTranslationResults[key]
221
- if (existingValue !== null) {
222
- existingTranslations[key] = existingValue
237
+ const existingNamespace = namespaces.find((namespace) =>
238
+ Object.hasOwn(namespaceKeys[namespace], key),
239
+ )
240
+ if (existingNamespace !== undefined) {
241
+ existingTranslations[key] = namespaceKeys[existingNamespace][key]
223
242
  localeResults[locale].reused++
224
243
  } else {
225
244
  keysToTranslate[key] = allMissingKeys[key].value
@@ -231,6 +250,12 @@ export const syncLocales = async (config: Configuration) => {
231
250
  // Translate if needed
232
251
  if (Object.keys(keysToTranslate).length > 0) {
233
252
  try {
253
+ localeResults[locale].status = "translating"
254
+ localeResults[locale].translationCompleted = 0
255
+ localeResults[locale].translationTotal =
256
+ Object.keys(keysToTranslate).length
257
+ updateProgressDisplay()
258
+
234
259
  translatedValues = await translateKey({
235
260
  inputLanguage: defaultLocale,
236
261
  outputLanguage: locale,
@@ -239,9 +264,13 @@ export const syncLocales = async (config: Configuration) => {
239
264
  openai,
240
265
  model: config.model,
241
266
  onProgress: (completed, total) => {
242
- // Progress callback still works but doesn't update UI during parallel processing
267
+ localeResults[locale].translationCompleted = completed
268
+ localeResults[locale].translationTotal = total
269
+ updateProgressDisplay()
243
270
  },
244
271
  })
272
+ localeResults[locale].translationCompleted =
273
+ Object.keys(keysToTranslate).length
245
274
  localeResults[locale].translated =
246
275
  Object.keys(translatedValues).length
247
276
  } catch (error) {
@@ -258,24 +287,25 @@ export const syncLocales = async (config: Configuration) => {
258
287
  // Combine and save
259
288
  const allTranslations = { ...existingTranslations, ...translatedValues }
260
289
 
261
- await Promise.all(
262
- namespaces.map(async (namespace) => {
263
- let hasChanges = false
264
- const updatedKeys = { ...namespaceKeys[namespace] }
265
-
266
- for (const key of missingKeysList) {
267
- if (allMissingKeys[key].namespaces.includes(namespace)) {
268
- const translation = allTranslations[key] || ""
269
- updatedKeys[key] = translation
270
- hasChanges = true
271
- }
272
- }
290
+ for (const namespace of namespaces) {
291
+ let hasChanges = false
292
+ const updatedKeys = { ...namespaceKeys[namespace] }
273
293
 
274
- if (hasChanges) {
275
- await writeLocalesFile(savePath, locale, namespace, updatedKeys)
294
+ for (const key of missingKeysList) {
295
+ if (allMissingKeys[key].namespaces.includes(namespace)) {
296
+ updatedKeys[key] = allTranslations[key]
297
+ hasChanges = true
276
298
  }
277
- }),
278
- )
299
+ }
300
+
301
+ if (hasChanges) {
302
+ stagedFiles.set(JSON.stringify([locale, namespace]), {
303
+ locale,
304
+ namespace,
305
+ translations: updatedKeys,
306
+ })
307
+ }
308
+ }
279
309
 
280
310
  localeResults[locale].status = "done"
281
311
  currentlyProcessing.delete(locale)
@@ -303,7 +333,20 @@ export const syncLocales = async (config: Configuration) => {
303
333
  // Restore console.log
304
334
  console.log = originalConsoleLog
305
335
 
306
- progressBar.stop()
336
+ progressBars.stop()
337
+
338
+ const errorLocales = localesToProcess.filter(
339
+ (locale) => localeResults[locale].status === "error",
340
+ )
341
+ if (errorLocales.length > 0) {
342
+ throw new TranslationError(
343
+ `Sync failed for ${errorLocales.length} locale(s); no translations were written.`,
344
+ )
345
+ }
346
+
347
+ for (const { locale, namespace, translations } of stagedFiles.values()) {
348
+ await writeLocalesFile(savePath, locale, namespace, translations)
349
+ }
307
350
 
308
351
  // Print summary
309
352
  console.log("")
@@ -328,10 +371,6 @@ export const syncLocales = async (config: Configuration) => {
328
371
  const doneLocales = localesToProcess.filter(
329
372
  (l) => localeResults[l].status === "done",
330
373
  )
331
- const errorLocales = localesToProcess.filter(
332
- (l) => localeResults[l].status === "error",
333
- )
334
-
335
374
  // Show successful languages in a compact grid
336
375
  if (doneLocales.length > 0) {
337
376
  const columns = 3
@@ -381,7 +420,7 @@ export const syncLocales = async (config: Configuration) => {
381
420
  } catch (error) {
382
421
  // Restore console.log
383
422
  console.log = originalConsoleLog
384
- progressBar.stop()
423
+ progressBars.stop()
385
424
  if (error instanceof TranslationError) {
386
425
  throw error
387
426
  }
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  // Export command functions for programmatic usage
2
- export { checkMissing } from "./commands/check-missing.js"
2
+ export {
3
+ checkMissing,
4
+ collectMissingTranslations,
5
+ MissingTranslationsError,
6
+ } from "./commands/check-missing.js"
7
+ export type { MissingTranslationsReport } from "./commands/check-missing.js"
3
8
  export { removeUnusedKeys } from "./commands/clean.js"
4
9
 
5
10
  export { replaceTranslation } from "./commands/replace.js"
@@ -19,11 +19,21 @@ export const languages = [
19
19
  name: "French",
20
20
  value: "fr",
21
21
  },
22
+ {
23
+ label: "Dansk",
24
+ name: "Danish",
25
+ value: "da",
26
+ },
22
27
  {
23
28
  label: "Dansk",
24
29
  name: "Danish",
25
30
  value: "dk",
26
31
  },
32
+ {
33
+ label: "中文",
34
+ name: "Chinese",
35
+ value: "zh",
36
+ },
27
37
  {
28
38
  label: "中文",
29
39
  name: "Chinese",
@@ -144,4 +154,14 @@ export const languages = [
144
154
  name: "Thai",
145
155
  value: "th",
146
156
  },
157
+ {
158
+ label: "العربية",
159
+ name: "Arabic",
160
+ value: "ar",
161
+ },
162
+ {
163
+ label: "فارسی",
164
+ name: "Persian",
165
+ value: "fa",
166
+ },
147
167
  ]