@brickflow/cli 0.0.11 โ 0.0.13
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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/translate/ai-context.js +2 -2
- package/src/translate/index.js +3 -4
- package/src/translate-sync/index.js +252 -1
- package/src/types/index.js +31 -13
- package/src/translate/sync.js +0 -238
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs'
|
|
|
4
4
|
import { dirname, join, relative, resolve } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
6
|
|
|
7
|
+
import { resolveWorkspaceRoot } from '../shared/workspace-root.js'
|
|
7
8
|
import { generateContentWithLimits } from './gemini.js'
|
|
8
9
|
import {
|
|
9
10
|
buildTranslateHelp,
|
|
@@ -14,8 +15,7 @@ import {
|
|
|
14
15
|
} from './runtime-config.js'
|
|
15
16
|
import { listTranslationTargets, stringifySortedJson } from './utils.js'
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
const workspaceRoot = resolve(currentDir, '../../../..')
|
|
18
|
+
const workspaceRoot = resolveWorkspaceRoot()
|
|
19
19
|
const CONTEXT_FILE_NAME = 'ai-context.json'
|
|
20
20
|
const CHANGE_THRESHOLD = readFloat('TRANSLATE_CONTEXT_MIN_CHANGE', 0.3)
|
|
21
21
|
const SOURCE_MAX_CHARS = readPositiveInt('TRANSLATE_CONTEXT_SOURCE_MAX_CHARS', 16000)
|
package/src/translate/index.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import { globSync } from 'glob'
|
|
3
|
-
import { dirname, join, relative
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
3
|
+
import { dirname, join, relative } from 'path'
|
|
5
4
|
|
|
5
|
+
import { resolveWorkspaceRoot } from '../shared/workspace-root.js'
|
|
6
6
|
import { getAiContextState } from './ai-context.js'
|
|
7
7
|
import { translateBatch } from './ai.js'
|
|
8
8
|
import { buildTranslateHelp, parseTranslateRuntimeArgs, setTranslateRuntimeConfig } from './runtime-config.js'
|
|
9
9
|
import { listTranslationTargets, sortObjectKeys, stringifySortedJson } from './utils.js'
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const workspaceRoot = resolve(currentDir, '../../../..')
|
|
11
|
+
const workspaceRoot = resolveWorkspaceRoot()
|
|
13
12
|
const rawArgs = process.argv.slice(3)
|
|
14
13
|
|
|
15
14
|
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { globSync } from 'glob'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
compileVueToJS,
|
|
7
|
+
extractStrings,
|
|
8
|
+
getTranslationPaths,
|
|
9
|
+
listWorkspaceFiles,
|
|
10
|
+
sortObjectKeys,
|
|
11
|
+
stringifySortedJson,
|
|
12
|
+
} from '../translate/utils.js'
|
|
13
|
+
|
|
14
|
+
const WORKSPACE_MARKERS = ['.git', 'pnpm-workspace.yaml', 'lerna.json', 'turbo.json']
|
|
15
|
+
|
|
1
16
|
const args = process.argv.slice(3)
|
|
2
17
|
|
|
3
18
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -10,7 +25,137 @@ if (args.length > 0) {
|
|
|
10
25
|
process.exit(1)
|
|
11
26
|
}
|
|
12
27
|
|
|
13
|
-
|
|
28
|
+
const workspaceRoot = resolveWorkspaceRoot()
|
|
29
|
+
const activeGeneratedDirs = new Set()
|
|
30
|
+
const cleanupRoots = new Set()
|
|
31
|
+
const staticCleanupRoots = [
|
|
32
|
+
path.resolve(workspaceRoot, 'packages/brick/global'),
|
|
33
|
+
...globSync('apps/*/global', {
|
|
34
|
+
absolute: true,
|
|
35
|
+
cwd: workspaceRoot,
|
|
36
|
+
}),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const files = listWorkspaceFiles(workspaceRoot).filter(
|
|
40
|
+
(file) => /\.(?:js|ts|vue)$/.test(file) && !file.endsWith('.d.ts'),
|
|
41
|
+
)
|
|
42
|
+
const filtered = files.filter((file) => getTranslationPaths(file))
|
|
43
|
+
|
|
44
|
+
for (const rootDir of staticCleanupRoots) {
|
|
45
|
+
cleanupRoots.add(rootDir)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const progress = createProgress(filtered.length)
|
|
49
|
+
|
|
50
|
+
for (const file of filtered) {
|
|
51
|
+
processFile(file)
|
|
52
|
+
progress(file)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cleanupGeneratedDirs()
|
|
56
|
+
|
|
57
|
+
process.stdout.write('\n')
|
|
58
|
+
console.log('โ
Done')
|
|
59
|
+
|
|
60
|
+
function cleanupGeneratedDirs() {
|
|
61
|
+
for (const rootDir of cleanupRoots) {
|
|
62
|
+
if (!fs.existsSync(rootDir)) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
67
|
+
if (!entry.isDirectory()) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const targetDir = path.resolve(rootDir, entry.name)
|
|
72
|
+
|
|
73
|
+
if (!activeGeneratedDirs.has(targetDir)) {
|
|
74
|
+
removeDir(targetDir)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createProgress(total) {
|
|
81
|
+
let done = 0
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
|
|
84
|
+
return function update(currentFile) {
|
|
85
|
+
done += 1
|
|
86
|
+
|
|
87
|
+
const percent = total === 0 ? 100 : Math.round((done * 100) / total)
|
|
88
|
+
const filled = Math.round(percent / 5)
|
|
89
|
+
const empty = 20 - filled
|
|
90
|
+
|
|
91
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
|
92
|
+
const shortName = currentFile.split('/').slice(-3).join('/')
|
|
93
|
+
|
|
94
|
+
process.stdout.write(
|
|
95
|
+
`\rโ๏ธ Processing: [${'โ'.repeat(filled)}${' '.repeat(empty)}] ` +
|
|
96
|
+
`${percent}% (${done}/${total}) ` +
|
|
97
|
+
`โฑ ${elapsed}s ` +
|
|
98
|
+
`\x1b[90m${shortName}\x1b[0m\x1b[K`,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function detectEol(filePath) {
|
|
104
|
+
if (!fs.existsSync(filePath)) {
|
|
105
|
+
return '\n'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const text = fs.readFileSync(filePath, 'utf-8')
|
|
109
|
+
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ensureSortedJsonFile(filePath) {
|
|
113
|
+
if (!fs.existsSync(filePath)) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentText = fs.readFileSync(filePath, 'utf-8')
|
|
118
|
+
const parsed = JSON.parse(currentText)
|
|
119
|
+
const sortedText = stringifySortedJson(parsed)
|
|
120
|
+
|
|
121
|
+
if (normalizeJsonEol(currentText) === normalizeJsonEol(sortedText)) {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
writeTextPreservingEol(filePath, sortedText)
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ensureSortedTranslationJsons(baseDir, samplePath) {
|
|
130
|
+
const filesToCheck = [samplePath, path.resolve(baseDir, 'ai-context.json')]
|
|
131
|
+
const generatedDir = path.resolve(baseDir, 'generated')
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(generatedDir)) {
|
|
134
|
+
for (const entry of fs.readdirSync(generatedDir, { withFileTypes: true })) {
|
|
135
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
136
|
+
filesToCheck.push(path.resolve(generatedDir, entry.name))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const filePath of filesToCheck) {
|
|
142
|
+
try {
|
|
143
|
+
if (ensureSortedJsonFile(filePath)) {
|
|
144
|
+
console.log('\n๐ค Sorted keys:', filePath)
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('\nโ invalid translation json:', filePath, error)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasAnyFile(directoryPath, fileNames) {
|
|
153
|
+
return fileNames.some((fileName) => fs.existsSync(path.join(directoryPath, fileName)))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeJsonEol(content) {
|
|
157
|
+
return String(content).replace(/\r\n/g, '\n')
|
|
158
|
+
}
|
|
14
159
|
|
|
15
160
|
function printHelp() {
|
|
16
161
|
console.log(`brick translate-sync
|
|
@@ -23,3 +168,109 @@ Notes:
|
|
|
23
168
|
Removes obsolete generated translation directories
|
|
24
169
|
Verifies and normalizes translation JSON key ordering`)
|
|
25
170
|
}
|
|
171
|
+
|
|
172
|
+
function processFile(filePath) {
|
|
173
|
+
let code = fs.readFileSync(filePath, 'utf-8')
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
if (filePath.endsWith('.vue')) {
|
|
177
|
+
code = compileVueToJS(code, filePath)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const strings = extractStrings(code, filePath)
|
|
181
|
+
|
|
182
|
+
writeTranslations(filePath, strings)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('\nโ error:', filePath, error)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removeDir(dir) {
|
|
189
|
+
if (fs.existsSync(dir)) {
|
|
190
|
+
fs.rmSync(dir, { force: true, recursive: true })
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
195
|
+
let currentDir = path.resolve(startDir)
|
|
196
|
+
let packageRoot = null
|
|
197
|
+
|
|
198
|
+
while (true) {
|
|
199
|
+
if (hasAnyFile(currentDir, WORKSPACE_MARKERS)) {
|
|
200
|
+
return currentDir
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
204
|
+
packageRoot = currentDir
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const parentDir = path.dirname(currentDir)
|
|
208
|
+
|
|
209
|
+
if (parentDir === currentDir) {
|
|
210
|
+
return packageRoot || startDir
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
currentDir = parentDir
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writeTextPreservingEol(filePath, content) {
|
|
218
|
+
const eol = detectEol(filePath)
|
|
219
|
+
const normalized = String(content).replace(/\r?\n/g, eol)
|
|
220
|
+
fs.writeFileSync(filePath, normalized, 'utf-8')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function writeTranslations(id, strings) {
|
|
224
|
+
const paths = getTranslationPaths(id)
|
|
225
|
+
|
|
226
|
+
if (!paths) {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { baseDir, isComponent, isLayout, isPage, isScript, samplePath } = paths
|
|
231
|
+
|
|
232
|
+
if (isPage || isLayout || isScript) {
|
|
233
|
+
cleanupRoots.add(path.dirname(baseDir))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (strings.size === 0) {
|
|
237
|
+
if (isComponent || isPage || isLayout || isScript) {
|
|
238
|
+
removeDir(baseDir)
|
|
239
|
+
}
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isPage || isLayout || isScript) {
|
|
244
|
+
activeGeneratedDirs.add(baseDir)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fs.mkdirSync(baseDir, { recursive: true })
|
|
248
|
+
|
|
249
|
+
let prev = {}
|
|
250
|
+
|
|
251
|
+
if (fs.existsSync(samplePath)) {
|
|
252
|
+
try {
|
|
253
|
+
prev = JSON.parse(fs.readFileSync(samplePath, 'utf-8'))
|
|
254
|
+
} catch {
|
|
255
|
+
prev = {}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const next = {}
|
|
260
|
+
|
|
261
|
+
for (const [key, value] of strings) {
|
|
262
|
+
next[key] = value
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const sortedNext = sortObjectKeys(next)
|
|
266
|
+
const isSame =
|
|
267
|
+
Object.keys(prev).length === Object.keys(sortedNext).length &&
|
|
268
|
+
Object.keys(prev).every((key) => prev[key] === sortedNext[key])
|
|
269
|
+
|
|
270
|
+
if (!isSame) {
|
|
271
|
+
writeTextPreservingEol(samplePath, stringifySortedJson(sortedNext))
|
|
272
|
+
console.log('\n๐งช Updated:', samplePath)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
ensureSortedTranslationJsons(baseDir, samplePath)
|
|
276
|
+
}
|
package/src/types/index.js
CHANGED
|
@@ -10,29 +10,41 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
10
10
|
|
|
11
11
|
const options = parseArgs(args)
|
|
12
12
|
|
|
13
|
-
if (
|
|
13
|
+
if (options.paths.length === 0 || !options.typeName || !options.outputFile || !options.replacePattern) {
|
|
14
14
|
printHelp()
|
|
15
15
|
process.exit(1)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const inputDirs = options.paths.map((inputPath) => path.resolve(process.cwd(), inputPath))
|
|
19
19
|
const outputFile = path.resolve(process.cwd(), options.outputFile)
|
|
20
20
|
const replacePattern = parseReplacePattern(options.replacePattern)
|
|
21
|
-
const entries =
|
|
22
|
-
.
|
|
23
|
-
|
|
21
|
+
const entries = inputDirs
|
|
22
|
+
.flatMap((inputDir) =>
|
|
23
|
+
readdirSync(inputDir)
|
|
24
|
+
.filter((entry) => statSync(path.join(inputDir, entry)).isFile())
|
|
25
|
+
.map((entry) => ({ entry, inputDir })),
|
|
26
|
+
)
|
|
27
|
+
.sort((first, second) => {
|
|
28
|
+
const entryComparison = first.entry.localeCompare(second.entry)
|
|
29
|
+
|
|
30
|
+
if (entryComparison !== 0) {
|
|
31
|
+
return entryComparison
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return first.inputDir.localeCompare(second.inputDir)
|
|
35
|
+
})
|
|
24
36
|
|
|
25
|
-
const typeValues = new Set(entries.map((entry) => applyReplacePattern(entry, replacePattern)).filter(Boolean))
|
|
37
|
+
const typeValues = new Set(entries.map(({ entry }) => applyReplacePattern(entry, replacePattern)).filter(Boolean))
|
|
26
38
|
|
|
27
39
|
if (typeValues.size === 0) {
|
|
28
|
-
throw new Error(`No type values generated from: ${
|
|
40
|
+
throw new Error(`No type values generated from: ${inputDirs.join(', ')}`)
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
const declaration = `declare type ${options.typeName} = ${[...typeValues].map((value) => `'${value}'`).join(' | ')}\n`
|
|
32
44
|
writeFileSync(outputFile, declaration, 'utf8')
|
|
33
45
|
|
|
34
46
|
console.log(`โ
Types generated for ${options.typeName}`)
|
|
35
|
-
console.log(` input: ${
|
|
47
|
+
console.log(` input: ${inputDirs.join(', ')}`)
|
|
36
48
|
console.log(` output: ${outputFile}`)
|
|
37
49
|
|
|
38
50
|
function applyReplacePattern(value, pattern) {
|
|
@@ -46,7 +58,7 @@ function applyReplacePattern(value, pattern) {
|
|
|
46
58
|
function parseArgs(rawArgs) {
|
|
47
59
|
const parsedOptions = {
|
|
48
60
|
outputFile: null,
|
|
49
|
-
|
|
61
|
+
paths: [],
|
|
50
62
|
replacePattern: null,
|
|
51
63
|
typeName: null,
|
|
52
64
|
}
|
|
@@ -56,7 +68,12 @@ function parseArgs(rawArgs) {
|
|
|
56
68
|
const value = rawArgs[index]
|
|
57
69
|
|
|
58
70
|
if (value === '--path') {
|
|
59
|
-
|
|
71
|
+
const inputPath = rawArgs[index + 1] ?? null
|
|
72
|
+
|
|
73
|
+
if (inputPath) {
|
|
74
|
+
parsedOptions.paths.push(inputPath)
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
index += 1
|
|
61
78
|
continue
|
|
62
79
|
}
|
|
@@ -82,8 +99,8 @@ function parseArgs(rawArgs) {
|
|
|
82
99
|
positional.push(value)
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
if (
|
|
86
|
-
parsedOptions.
|
|
102
|
+
if (parsedOptions.paths.length === 0 && positional[0]) {
|
|
103
|
+
parsedOptions.paths.push(positional[0])
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
if (!parsedOptions.typeName) {
|
|
@@ -118,9 +135,10 @@ Usage:
|
|
|
118
135
|
brick types ./path/to/files IconName ./types/icon.d.ts .svg
|
|
119
136
|
brick types ./path/to/files IconName ./types/icon.d.ts /\\.svg$/
|
|
120
137
|
brick types --path ./path/to/files --type-name IconName --output-file ./types/icon.d.ts --replace-pattern /\\.svg$/
|
|
138
|
+
brick types --path ./path/to/files --path ./path/to/more-files --type-name IconName --output-file ./types/icon.d.ts --replace-pattern /\\.svg$/
|
|
121
139
|
|
|
122
140
|
Notes:
|
|
123
|
-
path: directory with source files
|
|
141
|
+
path: directory with source files, can be passed multiple times
|
|
124
142
|
typeName: generated TypeScript type name
|
|
125
143
|
outputFile: file to write declaration into
|
|
126
144
|
replacePattern: string or regex literal used in replace(..., '') for each filename`)
|
package/src/translate/sync.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
4
|
-
import { globSync } from 'glob'
|
|
5
|
-
import { dirname, resolve } from 'path'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
compileVueToJS,
|
|
10
|
-
extractStrings,
|
|
11
|
-
getTranslationPaths,
|
|
12
|
-
listWorkspaceFiles,
|
|
13
|
-
sortObjectKeys,
|
|
14
|
-
stringifySortedJson,
|
|
15
|
-
} from './utils.js'
|
|
16
|
-
|
|
17
|
-
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
18
|
-
const workspaceRoot = resolve(currentDir, '../../../..')
|
|
19
|
-
const activeGeneratedDirs = new Set()
|
|
20
|
-
const cleanupRoots = new Set()
|
|
21
|
-
const STATIC_CLEANUP_ROOTS = [
|
|
22
|
-
resolve(workspaceRoot, 'packages/brick/global'),
|
|
23
|
-
...globSync('apps/*/global', {
|
|
24
|
-
absolute: true,
|
|
25
|
-
cwd: workspaceRoot,
|
|
26
|
-
}),
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
function cleanupGeneratedDirs() {
|
|
30
|
-
for (const rootDir of cleanupRoots) {
|
|
31
|
-
if (!existsSync(rootDir)) {
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
|
|
36
|
-
if (!entry.isDirectory()) {
|
|
37
|
-
continue
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const targetDir = resolve(rootDir, entry.name)
|
|
41
|
-
|
|
42
|
-
if (!activeGeneratedDirs.has(targetDir)) {
|
|
43
|
-
removeDir(targetDir)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function createProgress(total) {
|
|
50
|
-
let done = 0
|
|
51
|
-
const start = Date.now()
|
|
52
|
-
|
|
53
|
-
return function update(currentFile) {
|
|
54
|
-
done += 1
|
|
55
|
-
|
|
56
|
-
const percent = Math.round((done * 100) / total)
|
|
57
|
-
const filled = Math.round(percent / 5)
|
|
58
|
-
const empty = 20 - filled
|
|
59
|
-
|
|
60
|
-
const elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
|
61
|
-
|
|
62
|
-
const shortName = currentFile.split('/').slice(-3).join('/')
|
|
63
|
-
|
|
64
|
-
process.stdout.write(
|
|
65
|
-
`\rโ๏ธ Processing: [${'โ'.repeat(filled)}${' '.repeat(empty)}] ` +
|
|
66
|
-
`${percent}% (${done}/${total}) ` +
|
|
67
|
-
`โฑ ${elapsed}s ` +
|
|
68
|
-
`\x1b[90m${shortName}\x1b[0m\x1b[K`,
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------- progress ----------
|
|
74
|
-
|
|
75
|
-
function detectEol(filePath) {
|
|
76
|
-
if (!existsSync(filePath)) {
|
|
77
|
-
return '\n'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const text = readFileSync(filePath, 'utf-8')
|
|
81
|
-
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---------- core ----------
|
|
85
|
-
|
|
86
|
-
function ensureSortedJsonFile(filePath) {
|
|
87
|
-
if (!existsSync(filePath)) {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const currentText = readFileSync(filePath, 'utf-8')
|
|
92
|
-
const parsed = JSON.parse(currentText)
|
|
93
|
-
const sortedText = stringifySortedJson(parsed)
|
|
94
|
-
|
|
95
|
-
if (normalizeJsonEol(currentText) === normalizeJsonEol(sortedText)) {
|
|
96
|
-
return false
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
writeTextPreservingEol(filePath, sortedText)
|
|
100
|
-
return true
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function ensureSortedTranslationJsons(baseDir, samplePath) {
|
|
104
|
-
const filesToCheck = [samplePath, resolve(baseDir, 'ai-context.json')]
|
|
105
|
-
const generatedDir = resolve(baseDir, 'generated')
|
|
106
|
-
|
|
107
|
-
if (existsSync(generatedDir)) {
|
|
108
|
-
for (const entry of readdirSync(generatedDir, { withFileTypes: true })) {
|
|
109
|
-
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
110
|
-
filesToCheck.push(resolve(generatedDir, entry.name))
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for (const filePath of filesToCheck) {
|
|
116
|
-
try {
|
|
117
|
-
if (ensureSortedJsonFile(filePath)) {
|
|
118
|
-
console.log('\n๐ค Sorted keys:', filePath)
|
|
119
|
-
}
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error('\nโ invalid translation json:', filePath, error)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeJsonEol(content) {
|
|
127
|
-
return String(content).replace(/\r\n/g, '\n')
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ---------- core ----------
|
|
131
|
-
|
|
132
|
-
function processFile(filePath) {
|
|
133
|
-
let code = readFileSync(filePath, 'utf-8')
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
if (filePath.endsWith('.vue')) {
|
|
137
|
-
code = compileVueToJS(code, filePath)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const strings = extractStrings(code, filePath)
|
|
141
|
-
|
|
142
|
-
writeTranslations(filePath, strings)
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.error('\nโ error:', filePath, e)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ---------- translations ----------
|
|
149
|
-
|
|
150
|
-
function removeDir(dir) {
|
|
151
|
-
if (existsSync(dir)) {
|
|
152
|
-
rmSync(dir, { force: true, recursive: true })
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function writeTextPreservingEol(filePath, content) {
|
|
157
|
-
const eol = detectEol(filePath)
|
|
158
|
-
const normalized = String(content).replace(/\r?\n/g, eol)
|
|
159
|
-
writeFileSync(filePath, normalized, 'utf-8')
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function writeTranslations(id, strings) {
|
|
163
|
-
const paths = getTranslationPaths(id)
|
|
164
|
-
|
|
165
|
-
if (!paths) {
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const { baseDir, isComponent, isLayout, isPage, isScript, samplePath } = paths
|
|
170
|
-
|
|
171
|
-
if (isPage || isLayout || isScript) {
|
|
172
|
-
cleanupRoots.add(dirname(baseDir))
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (strings.size === 0) {
|
|
176
|
-
if (isComponent || isPage || isLayout || isScript) {
|
|
177
|
-
removeDir(baseDir)
|
|
178
|
-
}
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (isPage || isLayout || isScript) {
|
|
183
|
-
activeGeneratedDirs.add(baseDir)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
mkdirSync(baseDir, { recursive: true })
|
|
187
|
-
|
|
188
|
-
let prev = {}
|
|
189
|
-
|
|
190
|
-
if (existsSync(samplePath)) {
|
|
191
|
-
try {
|
|
192
|
-
prev = JSON.parse(readFileSync(samplePath, 'utf-8'))
|
|
193
|
-
} catch {
|
|
194
|
-
prev = {}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const next = {}
|
|
199
|
-
|
|
200
|
-
for (const [k, v] of strings) {
|
|
201
|
-
next[k] = v
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const sortedNext = sortObjectKeys(next)
|
|
205
|
-
const isSame =
|
|
206
|
-
Object.keys(prev).length === Object.keys(sortedNext).length &&
|
|
207
|
-
Object.keys(prev).every((k) => prev[k] === sortedNext[k])
|
|
208
|
-
|
|
209
|
-
if (!isSame) {
|
|
210
|
-
writeTextPreservingEol(samplePath, stringifySortedJson(sortedNext))
|
|
211
|
-
console.log('\n๐งช Updated:', samplePath)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
ensureSortedTranslationJsons(baseDir, samplePath)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ---------- run ----------
|
|
218
|
-
|
|
219
|
-
const files = listWorkspaceFiles(workspaceRoot).filter(
|
|
220
|
-
(file) => /\.(?:js|ts|vue)$/.test(file) && !file.endsWith('.d.ts'),
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
const filtered = files.filter((file) => getTranslationPaths(file))
|
|
224
|
-
for (const rootDir of STATIC_CLEANUP_ROOTS) {
|
|
225
|
-
cleanupRoots.add(rootDir)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const progress = createProgress(filtered.length)
|
|
229
|
-
|
|
230
|
-
for (const file of filtered) {
|
|
231
|
-
processFile(file)
|
|
232
|
-
progress(file)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
cleanupGeneratedDirs()
|
|
236
|
-
|
|
237
|
-
process.stdout.write('\n')
|
|
238
|
-
console.log('โ
Done')
|