@brickflow/cli 0.0.30 ā 0.0.32
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.js +1 -7
- package/src/translate/index.js +48 -63
- package/src/translate/runtime-config.js +15 -33
- package/src/translate/utils.js +9 -9
- package/src/{translate ā translate-context}/ai-context.js +87 -20
- package/src/translate-context/index.js +1 -1
- package/src/translate-sync/index.js +9 -18
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/translate/ai.js
CHANGED
|
@@ -58,7 +58,7 @@ function buildContents(strings, sourceLocale, targetLocales, componentContext) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function buildSystemInstruction() {
|
|
61
|
-
const { productContext
|
|
61
|
+
const { productContext } = getTranslateRuntimeConfig()
|
|
62
62
|
|
|
63
63
|
return [
|
|
64
64
|
'You are a professional localization engine for a paid adult content platform.',
|
|
@@ -66,12 +66,6 @@ function buildSystemInstruction() {
|
|
|
66
66
|
'PRODUCT CONTEXT:',
|
|
67
67
|
productContext,
|
|
68
68
|
'',
|
|
69
|
-
'TERMINOLOGY:',
|
|
70
|
-
terminology,
|
|
71
|
-
'',
|
|
72
|
-
'TONE:',
|
|
73
|
-
tone,
|
|
74
|
-
'',
|
|
75
69
|
'RULES:',
|
|
76
70
|
'- Return only raw JSON.',
|
|
77
71
|
'- Preserve JSON shape exactly.',
|
package/src/translate/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
|
-
import { globSync } from 'glob'
|
|
3
2
|
import { dirname, join, relative } from 'path'
|
|
4
3
|
|
|
5
4
|
import { resolveWorkspaceRoot } from '../shared/workspace-root.js'
|
|
6
|
-
import {
|
|
5
|
+
import { getContextFilePath, readAiContextDescription } from '../translate-context/ai-context.js'
|
|
7
6
|
import { translateBatch } from './ai.js'
|
|
8
7
|
import { buildTranslateHelp, parseTranslateRuntimeArgs, setTranslateRuntimeConfig } from './runtime-config.js'
|
|
9
8
|
import { listTranslationTargets, sortObjectKeys, stringifySortedJson } from './utils.js'
|
|
@@ -17,9 +16,11 @@ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const { options } = parseTranslateRuntimeArgs(rawArgs)
|
|
19
|
+
let requestedLanguageCodes
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
setTranslateRuntimeConfig(options)
|
|
23
|
+
requestedLanguageCodes = parseLanguageCodes(options.locales)
|
|
23
24
|
} catch (error) {
|
|
24
25
|
console.error(error instanceof Error ? error.message : String(error))
|
|
25
26
|
console.error('')
|
|
@@ -27,45 +28,9 @@ try {
|
|
|
27
28
|
process.exit(1)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const DEFAULT_LANGUAGE_CODES = [
|
|
31
|
-
'bn',
|
|
32
|
-
'cz',
|
|
33
|
-
'dk',
|
|
34
|
-
'de',
|
|
35
|
-
'en',
|
|
36
|
-
'es',
|
|
37
|
-
'fi',
|
|
38
|
-
'fr',
|
|
39
|
-
'hi',
|
|
40
|
-
'hu',
|
|
41
|
-
'it',
|
|
42
|
-
'ja',
|
|
43
|
-
'nl',
|
|
44
|
-
'no',
|
|
45
|
-
'pl',
|
|
46
|
-
'pt',
|
|
47
|
-
'ru',
|
|
48
|
-
'si',
|
|
49
|
-
'se',
|
|
50
|
-
'sk',
|
|
51
|
-
]
|
|
52
31
|
const BATCH_MAX_ITEMS = readPositiveInt('TRANSLATE_BATCH_MAX_ITEMS', 30)
|
|
53
32
|
const BATCH_MAX_CHARS = readPositiveInt('TRANSLATE_BATCH_MAX_CHARS', 3500)
|
|
54
|
-
const
|
|
55
|
-
'{packages/brick,apps/*}/{components/**/translate,pages-translate/*,layouts/**,global/*}/generated/*.json',
|
|
56
|
-
{
|
|
57
|
-
absolute: true,
|
|
58
|
-
cwd: workspaceRoot,
|
|
59
|
-
ignore: ['**/node_modules/**', '**/.nuxt/**', '**/dist/**', '**/.output/**', '**/coverage/**', '**/public/**'],
|
|
60
|
-
},
|
|
61
|
-
).sort()
|
|
62
|
-
|
|
63
|
-
const languageCodes = [
|
|
64
|
-
...new Set([
|
|
65
|
-
...languagePaths.map((filePath) => filePath.replace(/.*\/([^/]+)\.json$/, '$1')),
|
|
66
|
-
...DEFAULT_LANGUAGE_CODES,
|
|
67
|
-
]),
|
|
68
|
-
].sort()
|
|
33
|
+
const languageCodes = requestedLanguageCodes
|
|
69
34
|
|
|
70
35
|
const tasks = listTranslationTargets(workspaceRoot).map(({ samplePath, sourceFilePath }) => ({
|
|
71
36
|
sample: readJson(samplePath),
|
|
@@ -141,25 +106,52 @@ function normalizeEol(content) {
|
|
|
141
106
|
return String(content).replace(/\r\n/g, '\n')
|
|
142
107
|
}
|
|
143
108
|
|
|
144
|
-
|
|
109
|
+
function parseLanguageCodes(value) {
|
|
110
|
+
if (value === undefined || value === null) {
|
|
111
|
+
throw new Error('Missing required translate option: --locales')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const localeCodes = [
|
|
115
|
+
...new Set(
|
|
116
|
+
String(value)
|
|
117
|
+
.split(/[\s,]+/)
|
|
118
|
+
.filter(Boolean),
|
|
119
|
+
),
|
|
120
|
+
].map((code) => code.toLowerCase())
|
|
121
|
+
|
|
122
|
+
if (localeCodes.length === 0 || localeCodes.some((code) => !/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/.test(code))) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'Invalid --locales value. Use comma-separated locale codes, for example: --locales en,pl,ru,de',
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!localeCodes.includes('en')) {
|
|
129
|
+
throw new Error('Missing required locale: en. Add it to --locales, for example: --locales en,pl,ru,de')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return localeCodes.sort()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function processSample({ sample, samplePath }) {
|
|
136
|
+
const componentContext = readAiContextDescription(samplePath)
|
|
137
|
+
|
|
138
|
+
if (!componentContext) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Missing AI context: ${relative(workspaceRoot, getContextFilePath(samplePath))}. Run "brick translate-context" first.`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
145
144
|
writeSortedJsonIfNeeded(samplePath, sample)
|
|
146
145
|
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const currentEn =
|
|
146
|
+
const dataPath = join(dirname(samplePath), 'data.json')
|
|
147
|
+
const currentData = existsJson(dataPath) ? readJson(dataPath) : {}
|
|
148
|
+
const currentEn = currentData.en ?? {}
|
|
150
149
|
const currentByLanguage = new Map(
|
|
151
|
-
languageCodes.map((languageCode) => [
|
|
152
|
-
languageCode,
|
|
153
|
-
existsJson(join(generatedDir, `${languageCode}.json`))
|
|
154
|
-
? readJson(join(generatedDir, `${languageCode}.json`))
|
|
155
|
-
: {},
|
|
156
|
-
]),
|
|
150
|
+
languageCodes.map((languageCode) => [languageCode, currentData[languageCode] ?? {}]),
|
|
157
151
|
)
|
|
158
152
|
const resultByLanguage = new Map(languageCodes.map((languageCode) => [languageCode, {}]))
|
|
159
153
|
const pendingEntriesByLocales = new Map()
|
|
160
154
|
|
|
161
|
-
fs.mkdirSync(generatedDir, { recursive: true })
|
|
162
|
-
|
|
163
155
|
for (const [key, sampleValue] of Object.entries(sample)) {
|
|
164
156
|
const missingLocales = []
|
|
165
157
|
|
|
@@ -201,15 +193,6 @@ async function processSample({ sample, samplePath, sourceFilePath }) {
|
|
|
201
193
|
}
|
|
202
194
|
}
|
|
203
195
|
|
|
204
|
-
let componentContext = null
|
|
205
|
-
|
|
206
|
-
if (pendingEntriesByLocales.size > 0) {
|
|
207
|
-
componentContext = getAiContextState({
|
|
208
|
-
samplePath,
|
|
209
|
-
sourceFilePath,
|
|
210
|
-
}).description
|
|
211
|
-
}
|
|
212
|
-
|
|
213
196
|
for (const [localeKey, entries] of pendingEntriesByLocales) {
|
|
214
197
|
const targetLocales = localeKey.split(',').filter(Boolean)
|
|
215
198
|
const chunks = splitIntoBatches(entries, BATCH_MAX_ITEMS, BATCH_MAX_CHARS)
|
|
@@ -243,11 +226,13 @@ async function processSample({ sample, samplePath, sourceFilePath }) {
|
|
|
243
226
|
}
|
|
244
227
|
}
|
|
245
228
|
|
|
229
|
+
const nextData = {}
|
|
230
|
+
|
|
246
231
|
for (const languageCode of languageCodes) {
|
|
247
|
-
|
|
248
|
-
const languageResult = sortObjectKeys(resultByLanguage.get(languageCode) ?? {})
|
|
249
|
-
writeTextPreservingEol(generatedPath, stringifySortedJson(languageResult))
|
|
232
|
+
nextData[languageCode] = sortObjectKeys(resultByLanguage.get(languageCode) ?? {})
|
|
250
233
|
}
|
|
234
|
+
|
|
235
|
+
writeTextPreservingEol(dataPath, stringifySortedJson(nextData))
|
|
251
236
|
}
|
|
252
237
|
|
|
253
238
|
function readJson(filePath) {
|
|
@@ -2,32 +2,29 @@ export const DEFAULT_CONTEXT_MODEL = 'gemini-3.1-flash-lite-preview'
|
|
|
2
2
|
|
|
3
3
|
let runtimeConfig = null
|
|
4
4
|
|
|
5
|
-
export function buildTranslateHelp(command = 'brick translate') {
|
|
5
|
+
export function buildTranslateHelp(command = 'brick translate', { includeLocales = true } = {}) {
|
|
6
|
+
const localesRequired = includeLocales ? '\n --locales "en,pl,ru,de"' : ''
|
|
7
|
+
const localesExample = includeLocales ? ' --locales "en,pl,ru,de" \\\n' : ''
|
|
8
|
+
|
|
6
9
|
return `${command}
|
|
7
10
|
|
|
8
11
|
Required options:
|
|
9
12
|
--product-context "<text>"
|
|
10
|
-
--
|
|
11
|
-
--tone "<text>"
|
|
12
|
-
--api-key "<key>"
|
|
13
|
+
--api-key "<key>"${localesRequired}
|
|
13
14
|
|
|
14
15
|
Optional:
|
|
15
16
|
--context-model "<model>" Default: ${DEFAULT_CONTEXT_MODEL}
|
|
16
17
|
|
|
17
18
|
Example:
|
|
18
19
|
${command} \\
|
|
19
|
-
--product-context "Creators sell
|
|
20
|
-
--terminology "Bundle=content pack; Unlock=paid access; VIP=premium content" \\
|
|
21
|
-
--tone "Natural, modern, conversion-oriented, explicit when source is explicit." \\
|
|
20
|
+
--product-context "Creators sell goods packs." \\
|
|
22
21
|
--api-key "your-gemini-api-key" \\
|
|
23
|
-
--context-model "${DEFAULT_CONTEXT_MODEL}"`
|
|
22
|
+
${localesExample} --context-model "${DEFAULT_CONTEXT_MODEL}"`
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export function getTranslateRuntimeConfig() {
|
|
27
26
|
if (!runtimeConfig) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
'Translate runtime config is not initialized. Pass --product-context, --terminology, --tone, and --api-key.',
|
|
30
|
-
)
|
|
27
|
+
throw new Error('Translate runtime config is not initialized. Pass --product-context and --api-key.')
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
return runtimeConfig
|
|
@@ -37,9 +34,8 @@ export function parseTranslateRuntimeArgs(rawArgs) {
|
|
|
37
34
|
const options = {
|
|
38
35
|
apiKey: null,
|
|
39
36
|
contextModel: DEFAULT_CONTEXT_MODEL,
|
|
37
|
+
locales: undefined,
|
|
40
38
|
productContext: null,
|
|
41
|
-
terminology: null,
|
|
42
|
-
tone: null,
|
|
43
39
|
}
|
|
44
40
|
const positional = []
|
|
45
41
|
|
|
@@ -52,18 +48,6 @@ export function parseTranslateRuntimeArgs(rawArgs) {
|
|
|
52
48
|
continue
|
|
53
49
|
}
|
|
54
50
|
|
|
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
51
|
if (value === '--api-key') {
|
|
68
52
|
options.apiKey = rawArgs[index + 1] ?? null
|
|
69
53
|
index += 1
|
|
@@ -76,6 +60,12 @@ export function parseTranslateRuntimeArgs(rawArgs) {
|
|
|
76
60
|
continue
|
|
77
61
|
}
|
|
78
62
|
|
|
63
|
+
if (value === '--locales') {
|
|
64
|
+
options.locales = rawArgs[index + 1] ?? null
|
|
65
|
+
index += 1
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
79
69
|
positional.push(value)
|
|
80
70
|
}
|
|
81
71
|
|
|
@@ -100,14 +90,6 @@ export function validateTranslateRuntimeConfig(config) {
|
|
|
100
90
|
missing.push('--product-context')
|
|
101
91
|
}
|
|
102
92
|
|
|
103
|
-
if (!config.terminology) {
|
|
104
|
-
missing.push('--terminology')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!config.tone) {
|
|
108
|
-
missing.push('--tone')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
93
|
if (!config.apiKey) {
|
|
112
94
|
missing.push('--api-key')
|
|
113
95
|
}
|
package/src/translate/utils.js
CHANGED
|
@@ -19,7 +19,7 @@ const IGNORED_GLOB_PATTERNS = [
|
|
|
19
19
|
]
|
|
20
20
|
const IGNORED_SOURCE_SEGMENTS = ['/node_modules/', '/.nuxt/', '/dist/', '/.output/', '/coverage/', '/public/']
|
|
21
21
|
const TRANSLATION_SAMPLE_GLOB =
|
|
22
|
-
'{packages/brick,apps/*}/{components/**/translate,pages-translate/*,layouts/**,
|
|
22
|
+
'{packages/brick,apps/*}/{components/**/translate,pages-translate/*,layouts-translate/**,script-translate/*}/sample.json'
|
|
23
23
|
|
|
24
24
|
export function compileVueToJS(code, filePath) {
|
|
25
25
|
const { descriptor } = parseSFC(code)
|
|
@@ -171,6 +171,10 @@ export function getTranslationPaths(id) {
|
|
|
171
171
|
/\.(?:js|ts)$/.test(normalizedId) &&
|
|
172
172
|
!normalizedId.endsWith('.d.ts')
|
|
173
173
|
|
|
174
|
+
if (isComponent && !normalizedId.endsWith('/index.vue')) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
174
178
|
if (!isComponent && !isPage && !isLayout && !isScript) {
|
|
175
179
|
return null
|
|
176
180
|
}
|
|
@@ -204,13 +208,9 @@ export function getTranslationPaths(id) {
|
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
if (isLayout) {
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
.split('/')
|
|
211
|
-
.pop()
|
|
212
|
-
?.replace(/\.\w+$/, '') ?? ''
|
|
213
|
-
const baseDir = join(layoutDir, fileName)
|
|
211
|
+
const layoutsRoot = `${normalizedId.split('/layouts/')[0]}/layouts`
|
|
212
|
+
const layoutPath = normalizedId.split('/layouts/')[1].replace(/\.\w+$/, '')
|
|
213
|
+
const baseDir = join(layoutsRoot, '..', 'layouts-translate', layoutPath)
|
|
214
214
|
|
|
215
215
|
return {
|
|
216
216
|
baseDir,
|
|
@@ -222,7 +222,7 @@ export function getTranslationPaths(id) {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
const baseDir = join(projectRoot, '
|
|
225
|
+
const baseDir = join(projectRoot, 'script-translate', getComponentName(normalizedId))
|
|
226
226
|
|
|
227
227
|
return {
|
|
228
228
|
baseDir,
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'fs'
|
|
4
|
+
import { globSync } from 'glob'
|
|
4
5
|
import { dirname, join, relative, resolve } from 'path'
|
|
5
6
|
import { fileURLToPath } from 'url'
|
|
6
7
|
|
|
7
8
|
import { resolveWorkspaceRoot } from '../shared/workspace-root.js'
|
|
8
|
-
import { generateContentWithLimits } from '
|
|
9
|
+
import { generateContentWithLimits } from '../translate/gemini.js'
|
|
9
10
|
import {
|
|
10
11
|
buildTranslateHelp,
|
|
11
12
|
DEFAULT_CONTEXT_MODEL,
|
|
12
13
|
getTranslateRuntimeConfig,
|
|
13
14
|
parseTranslateRuntimeArgs,
|
|
14
15
|
setTranslateRuntimeConfig,
|
|
15
|
-
} from '
|
|
16
|
-
import { listTranslationTargets, stringifySortedJson } from '
|
|
16
|
+
} from '../translate/runtime-config.js'
|
|
17
|
+
import { listTranslationTargets, stringifySortedJson } from '../translate/utils.js'
|
|
17
18
|
|
|
18
19
|
const workspaceRoot = resolveWorkspaceRoot()
|
|
19
20
|
const CONTEXT_FILE_NAME = 'ai-context.json'
|
|
21
|
+
const CONTEXT_FILE_GLOB =
|
|
22
|
+
'{packages/brick,apps/*}/{components/**/translate,pages-translate/*,layouts-translate/**,script-translate/*}/ai-context.json'
|
|
20
23
|
const CHANGE_THRESHOLD = readFloat('TRANSLATE_CONTEXT_MIN_CHANGE', 0.3)
|
|
21
24
|
const SOURCE_MAX_CHARS = readPositiveInt('TRANSLATE_CONTEXT_SOURCE_MAX_CHARS', 16000)
|
|
22
25
|
|
|
23
26
|
export function buildAiContextHelp(command = 'brick translate-context') {
|
|
24
|
-
return `${buildTranslateHelp(command)}\n\nExtra:\n --force`
|
|
27
|
+
return `${buildTranslateHelp(command, { includeLocales: false })}\n\nExtra:\n --force`
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export function calculateChangeRatio(previousSource, nextSource) {
|
|
@@ -55,13 +58,8 @@ export async function ensureAiContext({ force = false, sample, samplePath, sourc
|
|
|
55
58
|
})
|
|
56
59
|
|
|
57
60
|
const next = {
|
|
58
|
-
changeRatio: state.changeRatio,
|
|
59
61
|
description,
|
|
60
|
-
samplePath: relative(workspaceRoot, samplePath),
|
|
61
|
-
sourceFilePath: relative(workspaceRoot, state.sourceFilePath),
|
|
62
62
|
sourceSnapshot: state.sourceCode,
|
|
63
|
-
updatedAt: new Date().toISOString(),
|
|
64
|
-
version: 1,
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
writeTextPreservingEol(state.contextPath, stringifySortedJson(next))
|
|
@@ -116,6 +114,30 @@ export function getContextFilePath(samplePath) {
|
|
|
116
114
|
return join(dirname(samplePath), CONTEXT_FILE_NAME)
|
|
117
115
|
}
|
|
118
116
|
|
|
117
|
+
export function readAiContextDescription(samplePath) {
|
|
118
|
+
const description = readContextFile(getContextFilePath(samplePath))?.description
|
|
119
|
+
|
|
120
|
+
return typeof description === 'string' && description.length > 0 ? description : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function removeObsoleteAiContexts(targets, rootDir = workspaceRoot) {
|
|
124
|
+
const activeContextPaths = new Set(targets.map(({ samplePath }) => getContextFilePath(samplePath)))
|
|
125
|
+
const contextPaths = globSync(CONTEXT_FILE_GLOB, {
|
|
126
|
+
absolute: true,
|
|
127
|
+
cwd: rootDir,
|
|
128
|
+
nodir: true,
|
|
129
|
+
}).sort()
|
|
130
|
+
|
|
131
|
+
for (const contextPath of contextPaths) {
|
|
132
|
+
if (activeContextPaths.has(contextPath)) {
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fs.rmSync(contextPath)
|
|
137
|
+
console.log(`šļø Context removed (sample not found): ${relative(rootDir, contextPath)}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
export async function runAiContextCli(rawArgs = process.argv.slice(3), command = 'brick translate-context') {
|
|
120
142
|
const helpText = buildAiContextHelp(command)
|
|
121
143
|
|
|
@@ -144,22 +166,41 @@ export async function runAiContextCli(rawArgs = process.argv.slice(3), command =
|
|
|
144
166
|
? targets.filter(({ samplePath }) => requestedSamplePaths.has(samplePath))
|
|
145
167
|
: targets
|
|
146
168
|
|
|
169
|
+
if (!requestedSamplePaths) {
|
|
170
|
+
removeObsoleteAiContexts(targets)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const progress = createProgress(targetEntries.length)
|
|
174
|
+
|
|
147
175
|
for (const { samplePath, sourceFilePath } of targetEntries) {
|
|
148
176
|
if (!sourceFilePath || !fs.existsSync(samplePath)) {
|
|
177
|
+
console.warn(`ā ļø Context skipped (source not found): ${relative(workspaceRoot, samplePath)}`)
|
|
178
|
+
progress(samplePath, 'skipped')
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const state = getAiContextState({ force, samplePath, sourceFilePath })
|
|
183
|
+
|
|
184
|
+
if (!state.shouldRegenerate) {
|
|
185
|
+
progress(samplePath, 'unchanged')
|
|
149
186
|
continue
|
|
150
187
|
}
|
|
151
188
|
|
|
152
|
-
|
|
189
|
+
await ensureAiContext({
|
|
153
190
|
force,
|
|
154
191
|
sample: JSON.parse(fs.readFileSync(samplePath, 'utf-8')),
|
|
155
192
|
samplePath,
|
|
156
193
|
sourceFilePath,
|
|
157
194
|
})
|
|
158
195
|
|
|
159
|
-
|
|
160
|
-
console.log(`š§ Context updated: ${relative(workspaceRoot, samplePath)}`)
|
|
161
|
-
}
|
|
196
|
+
progress(samplePath, 'updated')
|
|
162
197
|
}
|
|
198
|
+
|
|
199
|
+
if (targetEntries.length > 0) {
|
|
200
|
+
process.stdout.write('\n')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(`ā
Done: ${targetEntries.length} context files`)
|
|
163
204
|
}
|
|
164
205
|
|
|
165
206
|
function buildContextContents({ sampleEntries, samplePath, sourceCode, sourceFilePath }) {
|
|
@@ -176,23 +217,19 @@ function buildContextContents({ sampleEntries, samplePath, sourceCode, sourceFil
|
|
|
176
217
|
}
|
|
177
218
|
|
|
178
219
|
function buildContextSystemInstruction() {
|
|
179
|
-
const { productContext
|
|
220
|
+
const { productContext } = getTranslateRuntimeConfig()
|
|
180
221
|
|
|
181
222
|
return [
|
|
182
223
|
'You are generating translation context for a UI component.',
|
|
183
224
|
'Write a compact but informative description for translators.',
|
|
184
225
|
'Product context:',
|
|
185
226
|
productContext,
|
|
186
|
-
'Terminology:',
|
|
187
|
-
terminology,
|
|
188
|
-
'Tone:',
|
|
189
|
-
tone,
|
|
190
227
|
'Focus on:',
|
|
191
228
|
'- what the component or page does',
|
|
192
229
|
'- main user actions',
|
|
193
230
|
'- important entities and domain meaning',
|
|
194
231
|
'- what the shown strings likely refer to',
|
|
195
|
-
'-
|
|
232
|
+
'- UX intent if obvious',
|
|
196
233
|
'Rules:',
|
|
197
234
|
'- Return plain text only.',
|
|
198
235
|
'- Write 4 to 8 short sentences.',
|
|
@@ -209,6 +246,28 @@ function cleanText(text) {
|
|
|
209
246
|
.trim()
|
|
210
247
|
}
|
|
211
248
|
|
|
249
|
+
function createProgress(total) {
|
|
250
|
+
let done = 0
|
|
251
|
+
const start = Date.now()
|
|
252
|
+
|
|
253
|
+
return function update(samplePath, status) {
|
|
254
|
+
done += 1
|
|
255
|
+
|
|
256
|
+
const percent = total === 0 ? 100 : Math.round((done * 100) / total)
|
|
257
|
+
const filled = Math.round(percent / 5)
|
|
258
|
+
const empty = 20 - filled
|
|
259
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
|
260
|
+
const shortName = shortenPath(relative(workspaceRoot, samplePath))
|
|
261
|
+
|
|
262
|
+
process.stdout.write(
|
|
263
|
+
`\rš§ Context: [${'ā'.repeat(filled)}${' '.repeat(empty)}] ` +
|
|
264
|
+
`${percent}% (${done}/${total}) ` +
|
|
265
|
+
`ā± ${elapsed}s ` +
|
|
266
|
+
`\x1b[90m${status} ${shortName}\x1b[0m\x1b[K`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
212
271
|
function detectEol(filePath) {
|
|
213
272
|
if (!fs.existsSync(filePath)) {
|
|
214
273
|
return '\n'
|
|
@@ -301,6 +360,14 @@ function readPositiveInt(name, fallback) {
|
|
|
301
360
|
return Number.isFinite(value) && value > 0 ? value : fallback
|
|
302
361
|
}
|
|
303
362
|
|
|
363
|
+
function shortenPath(filePath, maxLength = 72) {
|
|
364
|
+
if (filePath.length <= maxLength) {
|
|
365
|
+
return filePath
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return `...${filePath.slice(-(maxLength - 3))}`
|
|
369
|
+
}
|
|
370
|
+
|
|
304
371
|
function writeTextPreservingEol(filePath, content) {
|
|
305
372
|
const eol = detectEol(filePath)
|
|
306
373
|
const normalized = String(content).replace(/\r?\n/g, eol)
|
|
@@ -308,5 +375,5 @@ function writeTextPreservingEol(filePath, content) {
|
|
|
308
375
|
}
|
|
309
376
|
|
|
310
377
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
311
|
-
await runAiContextCli(process.argv.slice(2), 'node packages/cli/src/translate/ai-context.js')
|
|
378
|
+
await runAiContextCli(process.argv.slice(2), 'node packages/cli/src/translate-context/ai-context.js')
|
|
312
379
|
}
|
|
@@ -26,11 +26,11 @@ if (args.length > 0) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const workspaceRoot = resolveWorkspaceRoot()
|
|
29
|
-
const
|
|
29
|
+
const activeTranslationDirs = new Set()
|
|
30
30
|
const cleanupRoots = new Set()
|
|
31
31
|
const staticCleanupRoots = [
|
|
32
|
-
path.resolve(workspaceRoot, 'packages/brick/
|
|
33
|
-
...globSync('apps/*/
|
|
32
|
+
path.resolve(workspaceRoot, 'packages/brick/script-translate'),
|
|
33
|
+
...globSync('apps/*/script-translate', {
|
|
34
34
|
absolute: true,
|
|
35
35
|
cwd: workspaceRoot,
|
|
36
36
|
}),
|
|
@@ -52,12 +52,12 @@ for (const file of filtered) {
|
|
|
52
52
|
progress(file)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
cleanupTranslationDirs()
|
|
56
56
|
|
|
57
57
|
process.stdout.write('\n')
|
|
58
58
|
console.log('ā
Done')
|
|
59
59
|
|
|
60
|
-
function
|
|
60
|
+
function cleanupTranslationDirs() {
|
|
61
61
|
for (const rootDir of cleanupRoots) {
|
|
62
62
|
if (!fs.existsSync(rootDir)) {
|
|
63
63
|
continue
|
|
@@ -70,7 +70,7 @@ function cleanupGeneratedDirs() {
|
|
|
70
70
|
|
|
71
71
|
const targetDir = path.resolve(rootDir, entry.name)
|
|
72
72
|
|
|
73
|
-
if (!
|
|
73
|
+
if (!activeTranslationDirs.has(targetDir)) {
|
|
74
74
|
removeDir(targetDir)
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -127,16 +127,7 @@ function ensureSortedJsonFile(filePath) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
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
|
-
}
|
|
130
|
+
const filesToCheck = [samplePath, path.resolve(baseDir, 'ai-context.json'), path.resolve(baseDir, 'data.json')]
|
|
140
131
|
|
|
141
132
|
for (const filePath of filesToCheck) {
|
|
142
133
|
try {
|
|
@@ -165,7 +156,7 @@ Usage:
|
|
|
165
156
|
|
|
166
157
|
Notes:
|
|
167
158
|
Scans source files and regenerates translation sample.json files
|
|
168
|
-
Removes obsolete
|
|
159
|
+
Removes obsolete translation directories
|
|
169
160
|
Verifies and normalizes translation JSON key ordering`)
|
|
170
161
|
}
|
|
171
162
|
|
|
@@ -241,7 +232,7 @@ function writeTranslations(id, strings) {
|
|
|
241
232
|
}
|
|
242
233
|
|
|
243
234
|
if (isPage || isLayout || isScript) {
|
|
244
|
-
|
|
235
|
+
activeTranslationDirs.add(baseDir)
|
|
245
236
|
}
|
|
246
237
|
|
|
247
238
|
fs.mkdirSync(baseDir, { recursive: true })
|