@brickflow/cli 0.0.6 ā 0.0.8
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/index.mjs +31 -80
- package/package.json +13 -1
- package/src/avif/index.js +79 -0
- package/src/filename/index.js +70 -0
- package/src/graph/index.js +78 -0
- package/src/icon/index.js +162 -0
- package/src/icon-check/index.js +928 -0
- package/src/latest/index.js +105 -0
- package/src/size/index.js +64 -0
- package/src/svg/index.js +97 -0
- package/src/translate/ai-context.js +312 -0
- package/src/translate/ai.js +168 -0
- package/src/translate/gemini.js +142 -0
- package/src/translate/index.js +331 -0
- package/src/translate/models.js +12 -0
- package/src/translate/runtime-config.js +118 -0
- package/src/translate/sync.js +238 -0
- package/src/translate/utils.js +409 -0
- package/src/translate-context/index.js +3 -0
- package/src/translate-sync/index.js +25 -0
- package/src/types/index.js +127 -0
- package/src/upgrade/index.js +168 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export const DEFAULT_CONTEXT_MODEL = 'gemini-3.1-flash-lite-preview'
|
|
2
|
+
|
|
3
|
+
let runtimeConfig = null
|
|
4
|
+
|
|
5
|
+
export function buildTranslateHelp(command = 'brick translate') {
|
|
6
|
+
return `${command}
|
|
7
|
+
|
|
8
|
+
Required options:
|
|
9
|
+
--product-context "<text>"
|
|
10
|
+
--terminology "<text>"
|
|
11
|
+
--tone "<text>"
|
|
12
|
+
--api-key "<key>"
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--context-model "<model>" Default: ${DEFAULT_CONTEXT_MODEL}
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
${command} \\
|
|
19
|
+
--product-context "Creators sell adult content collections with free previews and paid unlocks." \\
|
|
20
|
+
--terminology "Collection=content pack; Unlock=paid access; VIP=paid content" \\
|
|
21
|
+
--tone "Natural, modern, conversion-oriented, explicit when source is explicit." \\
|
|
22
|
+
--api-key "your-gemini-api-key" \\
|
|
23
|
+
--context-model "${DEFAULT_CONTEXT_MODEL}"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getTranslateRuntimeConfig() {
|
|
27
|
+
if (!runtimeConfig) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'Translate runtime config is not initialized. Pass --product-context, --terminology, --tone, and --api-key.',
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return runtimeConfig
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseTranslateRuntimeArgs(rawArgs) {
|
|
37
|
+
const options = {
|
|
38
|
+
apiKey: null,
|
|
39
|
+
contextModel: DEFAULT_CONTEXT_MODEL,
|
|
40
|
+
productContext: null,
|
|
41
|
+
terminology: null,
|
|
42
|
+
tone: null,
|
|
43
|
+
}
|
|
44
|
+
const positional = []
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
47
|
+
const value = rawArgs[index]
|
|
48
|
+
|
|
49
|
+
if (value === '--product-context') {
|
|
50
|
+
options.productContext = rawArgs[index + 1] ?? null
|
|
51
|
+
index += 1
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value === '--terminology') {
|
|
56
|
+
options.terminology = rawArgs[index + 1] ?? null
|
|
57
|
+
index += 1
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (value === '--tone') {
|
|
62
|
+
options.tone = rawArgs[index + 1] ?? null
|
|
63
|
+
index += 1
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (value === '--api-key') {
|
|
68
|
+
options.apiKey = rawArgs[index + 1] ?? null
|
|
69
|
+
index += 1
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (value === '--context-model') {
|
|
74
|
+
options.contextModel = rawArgs[index + 1] ?? DEFAULT_CONTEXT_MODEL
|
|
75
|
+
index += 1
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
positional.push(value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
options,
|
|
84
|
+
positional,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setTranslateRuntimeConfig(config) {
|
|
89
|
+
validateTranslateRuntimeConfig(config)
|
|
90
|
+
runtimeConfig = {
|
|
91
|
+
...config,
|
|
92
|
+
contextModel: config.contextModel || DEFAULT_CONTEXT_MODEL,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function validateTranslateRuntimeConfig(config) {
|
|
97
|
+
const missing = []
|
|
98
|
+
|
|
99
|
+
if (!config.productContext) {
|
|
100
|
+
missing.push('--product-context')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!config.terminology) {
|
|
104
|
+
missing.push('--terminology')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!config.tone) {
|
|
108
|
+
missing.push('--tone')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!config.apiKey) {
|
|
112
|
+
missing.push('--api-key')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (missing.length > 0) {
|
|
116
|
+
throw new Error(`Missing required translate options: ${missing.join(', ')}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
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')
|