@brickflow/cli 0.0.5 ā 0.0.7
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 +321 -0
- package/src/translate/ai.js +168 -0
- package/src/translate/gemini.js +142 -0
- package/src/translate/index.js +362 -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 +378 -0
- package/src/translate-sync/index.js +25 -0
- package/src/types/index.js +127 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { globSync } from 'glob'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(3)
|
|
8
|
+
|
|
9
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
10
|
+
printHelp()
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (args.length > 0) {
|
|
15
|
+
printHelp()
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const workspaceRoot = path.resolve(currentDir, '../../../..')
|
|
21
|
+
const packageJsonPaths = globSync('**/package.json', {
|
|
22
|
+
absolute: true,
|
|
23
|
+
cwd: workspaceRoot,
|
|
24
|
+
ignore: ['**/node_modules/**'],
|
|
25
|
+
}).sort()
|
|
26
|
+
|
|
27
|
+
console.log('\x1b[32m', '\rPackage update')
|
|
28
|
+
|
|
29
|
+
let index = 0
|
|
30
|
+
|
|
31
|
+
for await (const packageJsonPath of packageJsonPaths) {
|
|
32
|
+
index += 1
|
|
33
|
+
|
|
34
|
+
const packageJson = JSON.parse(
|
|
35
|
+
fs.readFileSync(packageJsonPath, {
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
await updateJson(packageJson)
|
|
41
|
+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`)
|
|
42
|
+
|
|
43
|
+
const percent = Math.round((index * 100) / packageJsonPaths.length)
|
|
44
|
+
process.stdout.write(`\rš Updating: [${'š©'.repeat((percent * 10) / 100)}] ${percent}%`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.stdout.write('\n')
|
|
48
|
+
console.log('\x1b[0m', '')
|
|
49
|
+
|
|
50
|
+
function isWorkspaceVersion(value) {
|
|
51
|
+
return value === 'workspace:*' || value === 'workspace: *'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log(`brick latest
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
brick latest
|
|
59
|
+
|
|
60
|
+
Notes:
|
|
61
|
+
Scans all package.json files in the repository, including the root one
|
|
62
|
+
Updates dependencies and devDependencies to the latest npm versions
|
|
63
|
+
Skips workspace:* versions`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readLatestVersion(packageName) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
exec(`npm view ${packageName} version`, (error, stdout) => {
|
|
69
|
+
if (error) {
|
|
70
|
+
console.info(error.message)
|
|
71
|
+
resolve(null)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
resolve(String(stdout).trim().replace('\n', ''))
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function updateJson(jsonData) {
|
|
81
|
+
await Promise.all([
|
|
82
|
+
...Object.entries(jsonData.dependencies || {}).map(async ([key, value]) => {
|
|
83
|
+
if (isWorkspaceVersion(value)) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const version = await readLatestVersion(key)
|
|
88
|
+
|
|
89
|
+
if (version) {
|
|
90
|
+
jsonData.dependencies[key] = version
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
...Object.entries(jsonData.devDependencies || {}).map(async ([key, value]) => {
|
|
94
|
+
if (isWorkspaceVersion(value)) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const version = await readLatestVersion(key)
|
|
99
|
+
|
|
100
|
+
if (version) {
|
|
101
|
+
jsonData.devDependencies[key] = version
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
])
|
|
105
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { globSync } from 'glob'
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(3)
|
|
5
|
+
|
|
6
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
7
|
+
printHelp()
|
|
8
|
+
process.exit(0)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (args.length > 0) {
|
|
12
|
+
printHelp()
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log('\x1b[32m', '\rAssets analyse')
|
|
17
|
+
const imgList = globSync('./**/*.{webp,svg}')
|
|
18
|
+
|
|
19
|
+
if (imgList.length === 0) {
|
|
20
|
+
console.log('\nNothing find!')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function bytesToSize(bytes, decimals = 2) {
|
|
24
|
+
if (!Number(bytes)) {
|
|
25
|
+
return '0 Bytes'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const kbToBytes = 1000
|
|
29
|
+
const dm = decimals < 0 ? 0 : decimals
|
|
30
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
31
|
+
|
|
32
|
+
const index = Math.floor(Math.log(bytes) / Math.log(kbToBytes))
|
|
33
|
+
|
|
34
|
+
return `${parseFloat((bytes / kbToBytes ** index).toFixed(dm))} ${sizes[index]}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let result = ''
|
|
38
|
+
for (let i = 0; i < imgList.length; i++) {
|
|
39
|
+
const imgPath = imgList[i]
|
|
40
|
+
const size = fs.statSync(imgPath).size
|
|
41
|
+
if (size > 400000 && !/-animation.webp$/.test(imgPath)) {
|
|
42
|
+
result = `${result}
|
|
43
|
+
š¢ ${bytesToSize(size)} šŗļø ${imgPath}
|
|
44
|
+
`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (result.length > 0) {
|
|
48
|
+
console.log('\x1b[0m', '')
|
|
49
|
+
throw result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('\x1b[0m', '')
|
|
53
|
+
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log(`brick size
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
brick size
|
|
59
|
+
|
|
60
|
+
Notes:
|
|
61
|
+
Scans .webp and .svg files under the current working directory
|
|
62
|
+
Fails when a file is larger than 400000 bytes
|
|
63
|
+
Ignores files ending with -animation.webp`)
|
|
64
|
+
}
|
package/src/svg/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { globSync } from 'glob'
|
|
3
|
+
import { optimize } from 'svgo'
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(3)
|
|
6
|
+
|
|
7
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
+
printHelp()
|
|
9
|
+
process.exit(0)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (args.length > 0) {
|
|
13
|
+
printHelp()
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('\x1b[32m', '\rSVG Optimize')
|
|
18
|
+
const svgList = globSync('./**/*.svg')
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < svgList.length; i++) {
|
|
21
|
+
const svgPath = svgList[i]
|
|
22
|
+
const svg = fs.readFileSync(svgPath, 'utf8')
|
|
23
|
+
const svgOptimized = optimize(svg, {
|
|
24
|
+
js2svg: {
|
|
25
|
+
indent: '',
|
|
26
|
+
pretty: true,
|
|
27
|
+
},
|
|
28
|
+
path: svgPath,
|
|
29
|
+
plugins: [
|
|
30
|
+
// "addAttributesToSVGElement", // adds attributes to an outer <svg> element
|
|
31
|
+
// "addClassesToSVGElement", // add classnames to an outer <svg> element
|
|
32
|
+
'cleanupAttrs', // cleanup attributes from newlines, trailing, and repeating spaces Yes
|
|
33
|
+
'cleanupEnableBackground', // remove or cleanup enable-background attribute when possible Yes
|
|
34
|
+
'cleanupIds', // remove unused and minify used IDs Yes
|
|
35
|
+
'cleanupListOfValues', // round numeric values in attributes that take a list of numbers (like viewBox or enable-background)
|
|
36
|
+
'cleanupNumericValues', // round numeric values to the fixed precision, remove default px units Yes
|
|
37
|
+
'collapseGroups', // collapse useless groups Yes
|
|
38
|
+
'convertColors', // convert colors (from rgb() to #rrggbb, from #rrggbb to #rgb) Yes
|
|
39
|
+
'convertEllipseToCircle', // convert non-eccentric <ellipse> to <circle> Yes
|
|
40
|
+
'convertPathData', // convert Path data to relative or absolute (whichever is shorter), convert one segment to another, trim useless delimiters, smart rounding, and much more Yes
|
|
41
|
+
'convertShapeToPath', // convert some basic shapes to <path> Yes
|
|
42
|
+
'convertStyleToAttrs', // convert styles into attributes
|
|
43
|
+
'convertTransform', // collapse multiple transforms into one, convert matrices to the short aliases, and much more Yes
|
|
44
|
+
'inlineStyles', // move and merge styles from <style> elements to element style attributes Yes
|
|
45
|
+
'mergePaths', // merge multiple Paths into one Yes
|
|
46
|
+
'mergeStyles', // merge multiple style elements into one Yes
|
|
47
|
+
'minifyStyles', // minify <style> elements content with CSSO Yes
|
|
48
|
+
'moveElemsAttrsToGroup', // move elements' attributes to their enclosing group Yes
|
|
49
|
+
'moveGroupAttrsToElems', // move some group attributes to the contained elements Yes
|
|
50
|
+
'prefixIds', // prefix IDs and classes with the SVG filename or an arbitrary string
|
|
51
|
+
// "removeAttributesBySelector", // removes attributes of elements that match a CSS selector
|
|
52
|
+
// "removeAttrs", // remove attributes by pattern
|
|
53
|
+
'removeComments', // remove comments Yes
|
|
54
|
+
'removeDesc', // remove <desc> Yes
|
|
55
|
+
'removeDimensions', // remove width/height and add viewBox if it's missing (opposite to removeViewBox, disable it first)
|
|
56
|
+
'removeDoctype', // remove doctype declaration Yes
|
|
57
|
+
'removeEditorsNSData', // remove editors namespaces, elements, and attributes Yes
|
|
58
|
+
'removeElementsByAttr', // remove arbitrary elements by ID or className
|
|
59
|
+
'removeEmptyAttrs', // remove empty attributes Yes
|
|
60
|
+
'removeEmptyContainers', // remove empty Container elements Yes
|
|
61
|
+
'removeEmptyText', // remove empty Text elements Yes
|
|
62
|
+
'removeHiddenElems', // remove hidden elements Yes
|
|
63
|
+
'removeMetadata', // remove <metadata> Yes
|
|
64
|
+
'removeNonInheritableGroupAttrs', // remove non-inheritable group's "presentation" attributes Yes
|
|
65
|
+
'removeOffCanvasPaths', // removes elements that are drawn outside of the viewbox
|
|
66
|
+
'removeRasterImages', // remove raster images
|
|
67
|
+
'removeScripts', // remove <script> elements
|
|
68
|
+
// "removeStyleElement", // remove <style> elements
|
|
69
|
+
'removeTitle', // remove <title> Yes
|
|
70
|
+
'removeUnknownsAndDefaults', // remove unknown elements content and attributes, remove attributes with default values Yes
|
|
71
|
+
'removeUnusedNS', // remove unused namespaces declaration Yes
|
|
72
|
+
'removeUselessDefs', // remove elements of <defs> without id Yes
|
|
73
|
+
'removeUselessStrokeAndFill', // remove useless stroke and fill attributes Yes
|
|
74
|
+
// "removeViewBox", // remove viewBox attribute when possible Yes
|
|
75
|
+
// "removeXMLNS", // removes the xmlns attribute (for inline SVG)
|
|
76
|
+
'removeXMLProcInst', // remove XML processing instructions Yes
|
|
77
|
+
'reusePaths', // Find duplicated elements and replace them with links
|
|
78
|
+
'sortAttrs', // sort element attributes for epic readability Yes
|
|
79
|
+
'sortDefsChildren', // sort children of <defs> in order to improve compression Yes
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
fs.writeFileSync(svgPath, svgOptimized.data)
|
|
83
|
+
const percent = Math.round(((i + 1) * 100) / svgList.length)
|
|
84
|
+
process.stdout.write(`\rš
Optimizing: [${'š¹'.repeat((percent * 10) / 100)}] ${percent}%`)
|
|
85
|
+
}
|
|
86
|
+
console.log('\x1b[0m', '')
|
|
87
|
+
|
|
88
|
+
function printHelp() {
|
|
89
|
+
console.log(`brick svg
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
brick svg
|
|
93
|
+
|
|
94
|
+
Notes:
|
|
95
|
+
Scans .svg files under the current working directory
|
|
96
|
+
Optimizes files in place with svgo`)
|
|
97
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import { dirname, join, relative, resolve } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
import { generateContentWithLimits } from './gemini.js'
|
|
8
|
+
import {
|
|
9
|
+
buildTranslateHelp,
|
|
10
|
+
DEFAULT_CONTEXT_MODEL,
|
|
11
|
+
getTranslateRuntimeConfig,
|
|
12
|
+
parseTranslateRuntimeArgs,
|
|
13
|
+
setTranslateRuntimeConfig,
|
|
14
|
+
} from './runtime-config.js'
|
|
15
|
+
import { getTranslationPaths, listWorkspaceFiles, stringifySortedJson } from './utils.js'
|
|
16
|
+
|
|
17
|
+
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const workspaceRoot = resolve(currentDir, '../../../..')
|
|
19
|
+
const CONTEXT_FILE_NAME = 'ai-context.json'
|
|
20
|
+
const CHANGE_THRESHOLD = readFloat('TRANSLATE_CONTEXT_MIN_CHANGE', 0.3)
|
|
21
|
+
const SOURCE_MAX_CHARS = readPositiveInt('TRANSLATE_CONTEXT_SOURCE_MAX_CHARS', 16000)
|
|
22
|
+
|
|
23
|
+
export function calculateChangeRatio(previousSource, nextSource) {
|
|
24
|
+
const before = normalizeSource(previousSource)
|
|
25
|
+
const after = normalizeSource(nextSource)
|
|
26
|
+
|
|
27
|
+
if (before === after) {
|
|
28
|
+
return 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const beforeLines = before.split('\n')
|
|
32
|
+
const afterLines = after.split('\n')
|
|
33
|
+
const commonLines = longestCommonSubsequenceLength(beforeLines, afterLines)
|
|
34
|
+
const maxLines = Math.max(beforeLines.length, afterLines.length, 1)
|
|
35
|
+
|
|
36
|
+
return 1 - commonLines / maxLines
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function ensureAiContext({ force = false, sample, samplePath, sourceFilePath }) {
|
|
40
|
+
const state = getAiContextState({ force, samplePath, sourceFilePath })
|
|
41
|
+
|
|
42
|
+
if (!state.shouldRegenerate) {
|
|
43
|
+
return state.description
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const description = await generateContextDescription({
|
|
47
|
+
sample,
|
|
48
|
+
samplePath,
|
|
49
|
+
sourceCode: state.sourceCode,
|
|
50
|
+
sourceFilePath: state.sourceFilePath,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const next = {
|
|
54
|
+
changeRatio: state.changeRatio,
|
|
55
|
+
description,
|
|
56
|
+
samplePath: relative(workspaceRoot, samplePath),
|
|
57
|
+
sourceFilePath: relative(workspaceRoot, state.sourceFilePath),
|
|
58
|
+
sourceSnapshot: state.sourceCode,
|
|
59
|
+
updatedAt: new Date().toISOString(),
|
|
60
|
+
version: 1,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writeTextPreservingEol(state.contextPath, stringifySortedJson(next))
|
|
64
|
+
|
|
65
|
+
return description
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getAiContextState({ force = false, samplePath, sourceFilePath }) {
|
|
69
|
+
if (!samplePath || !sourceFilePath || !fs.existsSync(sourceFilePath)) {
|
|
70
|
+
return {
|
|
71
|
+
changeRatio: 0,
|
|
72
|
+
contextPath: samplePath ? getContextFilePath(samplePath) : null,
|
|
73
|
+
description: null,
|
|
74
|
+
existing: null,
|
|
75
|
+
reason: 'missing_source',
|
|
76
|
+
shouldRegenerate: false,
|
|
77
|
+
sourceCode: null,
|
|
78
|
+
sourceFilePath,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const contextPath = getContextFilePath(samplePath)
|
|
83
|
+
const sourceCode = fs.readFileSync(sourceFilePath, 'utf-8')
|
|
84
|
+
const existing = readContextFile(contextPath)
|
|
85
|
+
const changeRatio = existing?.sourceSnapshot ? calculateChangeRatio(existing.sourceSnapshot, sourceCode) : 1
|
|
86
|
+
const hasDescription = typeof existing?.description === 'string' && existing.description.length > 0
|
|
87
|
+
const shouldRegenerate = force || !hasDescription || changeRatio >= CHANGE_THRESHOLD
|
|
88
|
+
|
|
89
|
+
let reason = 'reuse'
|
|
90
|
+
|
|
91
|
+
if (force) {
|
|
92
|
+
reason = 'force'
|
|
93
|
+
} else if (!hasDescription) {
|
|
94
|
+
reason = 'missing_context'
|
|
95
|
+
} else if (changeRatio >= CHANGE_THRESHOLD) {
|
|
96
|
+
reason = 'changed_30_percent'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
changeRatio,
|
|
101
|
+
contextPath,
|
|
102
|
+
description: hasDescription ? existing.description : null,
|
|
103
|
+
existing,
|
|
104
|
+
reason,
|
|
105
|
+
shouldRegenerate,
|
|
106
|
+
sourceCode,
|
|
107
|
+
sourceFilePath,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getContextFilePath(samplePath) {
|
|
112
|
+
return join(dirname(samplePath), CONTEXT_FILE_NAME)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildContextContents({ sampleEntries, samplePath, sourceCode, sourceFilePath }) {
|
|
116
|
+
return [
|
|
117
|
+
`Sample path: ${samplePath}`,
|
|
118
|
+
`Source file: ${sourceFilePath}`,
|
|
119
|
+
'',
|
|
120
|
+
'Known translation keys and sample texts:',
|
|
121
|
+
JSON.stringify(sampleEntries, null, 2),
|
|
122
|
+
'',
|
|
123
|
+
'Source code:',
|
|
124
|
+
sourceCode,
|
|
125
|
+
].join('\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildContextSystemInstruction() {
|
|
129
|
+
const { productContext, terminology, tone } = getTranslateRuntimeConfig()
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
'You are generating translation context for a UI component.',
|
|
133
|
+
'Write a compact but informative description for translators.',
|
|
134
|
+
'Product context:',
|
|
135
|
+
productContext,
|
|
136
|
+
'Terminology:',
|
|
137
|
+
terminology,
|
|
138
|
+
'Tone:',
|
|
139
|
+
tone,
|
|
140
|
+
'Focus on:',
|
|
141
|
+
'- what the component or page does',
|
|
142
|
+
'- main user actions',
|
|
143
|
+
'- important entities and domain meaning',
|
|
144
|
+
'- what the shown strings likely refer to',
|
|
145
|
+
'- tone or UX intent if obvious',
|
|
146
|
+
'Rules:',
|
|
147
|
+
'- Return plain text only.',
|
|
148
|
+
'- Write 4 to 8 short sentences.',
|
|
149
|
+
'- Be concrete, not generic.',
|
|
150
|
+
'- Do not repeat raw code.',
|
|
151
|
+
'- Mention adult-content context only if it is actually visible in the code or strings.',
|
|
152
|
+
].join('\n')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cleanText(text) {
|
|
156
|
+
return text
|
|
157
|
+
.replace(/```text/gi, '')
|
|
158
|
+
.replace(/```/g, '')
|
|
159
|
+
.trim()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function detectEol(filePath) {
|
|
163
|
+
if (!fs.existsSync(filePath)) {
|
|
164
|
+
return '\n'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const text = fs.readFileSync(filePath, 'utf-8')
|
|
168
|
+
return text.includes('\r\n') ? '\r\n' : '\n'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function generateContextDescription({ sample, samplePath, sourceCode, sourceFilePath }) {
|
|
172
|
+
const sampleEntries = Object.entries(sample ?? {})
|
|
173
|
+
.slice(0, 40)
|
|
174
|
+
.map(([key, text]) => ({
|
|
175
|
+
key,
|
|
176
|
+
text,
|
|
177
|
+
}))
|
|
178
|
+
const responseText = await generateContentWithLimits({
|
|
179
|
+
apiKey: getTranslateRuntimeConfig().apiKey,
|
|
180
|
+
config: {
|
|
181
|
+
systemInstruction: buildContextSystemInstruction(),
|
|
182
|
+
},
|
|
183
|
+
contents: buildContextContents({
|
|
184
|
+
sampleEntries,
|
|
185
|
+
samplePath: relative(workspaceRoot, samplePath),
|
|
186
|
+
sourceCode: sourceCode.slice(0, SOURCE_MAX_CHARS),
|
|
187
|
+
sourceFilePath: relative(workspaceRoot, sourceFilePath),
|
|
188
|
+
}),
|
|
189
|
+
model: getTranslateRuntimeConfig().contextModel || DEFAULT_CONTEXT_MODEL,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return cleanText(responseText)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function longestCommonSubsequenceLength(left, right) {
|
|
196
|
+
const rows = left.length + 1
|
|
197
|
+
const cols = right.length + 1
|
|
198
|
+
const dp = Array.from({ length: rows }, () => Array(cols).fill(0))
|
|
199
|
+
|
|
200
|
+
for (let i = 1; i < rows; i++) {
|
|
201
|
+
for (let j = 1; j < cols; j++) {
|
|
202
|
+
if (left[i - 1] === right[j - 1]) {
|
|
203
|
+
dp[i][j] = dp[i - 1][j - 1] + 1
|
|
204
|
+
} else {
|
|
205
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return dp[left.length][right.length]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeSource(source) {
|
|
214
|
+
return String(source)
|
|
215
|
+
.replace(/\r\n/g, '\n')
|
|
216
|
+
.replace(/[ \t]+$/gm, '')
|
|
217
|
+
.trim()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readContextFile(filePath) {
|
|
221
|
+
if (!fs.existsSync(filePath)) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
227
|
+
} catch {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function readFloat(name, fallback) {
|
|
233
|
+
const raw = process.env[name]
|
|
234
|
+
|
|
235
|
+
if (!raw) {
|
|
236
|
+
return fallback
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const value = Number.parseFloat(raw)
|
|
240
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function readPositiveInt(name, fallback) {
|
|
244
|
+
const raw = process.env[name]
|
|
245
|
+
|
|
246
|
+
if (!raw) {
|
|
247
|
+
return fallback
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const value = Number.parseInt(raw, 10)
|
|
251
|
+
return Number.isFinite(value) && value > 0 ? value : fallback
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runCli() {
|
|
255
|
+
const rawArgs = process.argv.slice(2)
|
|
256
|
+
|
|
257
|
+
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
258
|
+
console.log(`${buildTranslateHelp('node packages/cli/src/translate/ai-context.js')}\n\nExtra:\n --force`)
|
|
259
|
+
process.exit(0)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const force = rawArgs.includes('--force')
|
|
263
|
+
const filteredArgs = rawArgs.filter((arg) => arg !== '--force')
|
|
264
|
+
const { options, positional } = parseTranslateRuntimeArgs(filteredArgs)
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
setTranslateRuntimeConfig(options)
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
270
|
+
console.error('')
|
|
271
|
+
console.error(`${buildTranslateHelp('node packages/cli/src/translate/ai-context.js')}\n\nExtra:\n --force`)
|
|
272
|
+
process.exit(1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const sourceFiles = listWorkspaceFiles(workspaceRoot).filter(
|
|
276
|
+
(filePath) => /\.(?:js|ts|vue)$/.test(filePath) && !filePath.endsWith('.d.ts'),
|
|
277
|
+
)
|
|
278
|
+
const sampleToSource = new Map()
|
|
279
|
+
|
|
280
|
+
for (const sourceFilePath of sourceFiles) {
|
|
281
|
+
const samplePath = getTranslationPaths(sourceFilePath)?.samplePath
|
|
282
|
+
|
|
283
|
+
if (samplePath && fs.existsSync(samplePath) && !sampleToSource.has(samplePath)) {
|
|
284
|
+
sampleToSource.set(samplePath, sourceFilePath)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const targetSamplePaths =
|
|
289
|
+
positional.length > 0
|
|
290
|
+
? positional.map((samplePath) => resolve(workspaceRoot, samplePath))
|
|
291
|
+
: [...sampleToSource.keys()].sort()
|
|
292
|
+
|
|
293
|
+
for (const samplePath of targetSamplePaths) {
|
|
294
|
+
const sourceFilePath = sampleToSource.get(samplePath)
|
|
295
|
+
|
|
296
|
+
if (!sourceFilePath || !fs.existsSync(samplePath)) {
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const description = await ensureAiContext({
|
|
301
|
+
force,
|
|
302
|
+
sample: JSON.parse(fs.readFileSync(samplePath, 'utf-8')),
|
|
303
|
+
samplePath,
|
|
304
|
+
sourceFilePath,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (description) {
|
|
308
|
+
console.log(`š§ Context updated: ${relative(workspaceRoot, samplePath)}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function writeTextPreservingEol(filePath, content) {
|
|
314
|
+
const eol = detectEol(filePath)
|
|
315
|
+
const normalized = String(content).replace(/\r?\n/g, eol)
|
|
316
|
+
fs.writeFileSync(filePath, normalized, 'utf-8')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
320
|
+
await runCli()
|
|
321
|
+
}
|