@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.
- 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 +90 -50
- 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/languges.d.ts.map +1 -1
- package/dist/lib/languges.js +20 -0
- package/dist/lib/languges.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 +125 -86
- package/src/index.ts +6 -1
- package/src/lib/languges.ts +20 -0
- 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
package/src/commands/scan.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Configuration } from "../lib/types.js"
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
85
|
-
if (reusedKeys.length > 0) {
|
|
79
|
+
if (missingDefaultKeys.size > 0) {
|
|
86
80
|
console.log(
|
|
87
|
-
|
|
81
|
+
`${missingDefaultKeys.size} keys are missing. Please provide the values for the following keys in ${defaultLocale}:`,
|
|
88
82
|
)
|
|
89
83
|
}
|
|
90
84
|
|
|
91
|
-
const
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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:
|
|
145
|
+
object: keysToTranslate,
|
|
113
146
|
openai,
|
|
114
147
|
model: config.model,
|
|
115
148
|
})
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] = {
|
|
62
|
+
localeResults[locale] = {
|
|
63
|
+
status: "pending",
|
|
64
|
+
translated: 0,
|
|
65
|
+
translationCompleted: 0,
|
|
66
|
+
translationTotal: 0,
|
|
67
|
+
reused: 0,
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
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}% | {
|
|
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
|
-
|
|
90
|
+
const localeProgressBar = progressBars.create(localesToProcess.length, 0, {
|
|
91
|
+
label: "Languages ",
|
|
79
92
|
status: "",
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
109
|
+
// Helper to update progress bars with currently processing languages
|
|
116
110
|
const updateProgressDisplay = () => {
|
|
117
111
|
const processingArray = Array.from(currentlyProcessing)
|
|
118
|
-
const
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
status,
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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"
|
package/src/lib/languges.ts
CHANGED
|
@@ -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
|
]
|