Skip to content

Commit 28007f0

Browse files
committed
feat: add glossary presets and init CLI
1 parent 4aeb58d commit 28007f0

5 files changed

Lines changed: 311 additions & 3 deletions

File tree

packages/i18n-migrate-cli/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pnpm exec tmigrate --help
2222
# 1. 初始化 .tmigrate 配置目录
2323
pnpm exec tmigrate init
2424

25+
# 1.5. 可选:初始化内置高频词库,减少机器翻译误译
26+
pnpm exec tmigrate glossary init --preset all
27+
2528
# 2. 扫描源码并生成 .tmigrate/maps 分片映射
2629
pnpm exec tmigrate scan src --to en
2730

@@ -93,6 +96,28 @@ tmigrate scan src --incremental --clean-deprecated
9396
| `--incremental` | 只扫描 hash 发生变化的文件 |
9497
| `--clean-deprecated` | 清理已废弃条目 |
9598

99+
### `glossary init`
100+
101+
把内置高频词库合并到 `.tmigrate/glossary.json`。默认使用项目配置里的 `sourceLocale` / `targetLocale`,目前内置支持 `zh -> en``en -> zh`
102+
103+
```bash
104+
tmigrate glossary init
105+
tmigrate glossary init --preset business
106+
tmigrate glossary init --preset all --dry-run
107+
tmigrate glossary init --from en --to zh --preset ui
108+
tmigrate glossary init --preset all --overwrite
109+
```
110+
111+
常用选项:
112+
113+
| 选项 | 说明 |
114+
|---|---|
115+
| `--preset <name>` | 内置词库:`ui``business``all`,默认 `ui` |
116+
| `--from <locale>` | 覆盖源语言,如 `zh``en` |
117+
| `--to <locale>` | 覆盖目标语言,如 `en``zh` |
118+
| `--dry-run` | 只预览新增/更新/跳过数量,不写入文件 |
119+
| `--overwrite` | 覆盖已有冲突词条;默认保留现有人工词条 |
120+
96121
### `apply`
97122

98123
把映射文件中已确认的译文回写到源文件。
@@ -372,11 +397,13 @@ API 请求体格式:
372397
```ts
373398
import {
374399
applyTranslations,
400+
initGlossary,
375401
initProject,
376402
scanProject,
377403
} from '@translation-master/i18n-migrate-cli'
378404

379405
await initProject({ cwd: process.cwd(), from: 'zh', to: 'en', overwrite: false })
406+
await initGlossary({ cwd: process.cwd(), preset: 'all' })
380407

381408
await scanProject({
382409
cwd: process.cwd(),
@@ -391,6 +418,7 @@ await applyTranslations({ cwd: process.cwd(), dryRun: true })
391418
| 导出 | 说明 |
392419
|---|---|
393420
| `initProject()` | 初始化 `.tmigrate` |
421+
| `initGlossary()` | 合并内置词库到 `.tmigrate/glossary.json` |
394422
| `scanProject()` | 扫描源码并生成 map |
395423
| `applyTranslations()` | 回写已确认译文 |
396424
| `restoreBackups()` | 从备份恢复 |

packages/i18n-migrate-cli/src/__tests__/workflow.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
33
import os from 'node:os'
44
import path from 'node:path'
55
import { afterEach, describe, expect, it } from 'vitest'
6-
import { applyTranslations, approveTranslations, initProject, restoreBackups, scanProject } from '../index'
6+
import { applyTranslations, approveTranslations, initGlossary, initProject, restoreBackups, scanProject } from '../index'
77
import { collectMapStats, formatMapStatsReport } from '../stats'
88

99
const tempDirs: string[] = []
@@ -153,6 +153,42 @@ describe('i18n migrate workflow', () => {
153153
expect(overriddenAppMap.entries['旧文案']?.approved).toBe(true)
154154
})
155155

156+
it('seeds glossary presets without clobbering existing manual terms by default', async () => {
157+
const cwd = await createTempProject()
158+
await initProject({ cwd, overwrite: false, from: 'zh', to: 'en' })
159+
const glossaryPath = path.join(cwd, '.tmigrate', 'glossary.json')
160+
await writeFile(glossaryPath, JSON.stringify({ 提交: 'Send', 自定义: 'Custom' }, null, 2), 'utf8')
161+
162+
const preview = await initGlossary({ cwd, preset: 'business', dryRun: true })
163+
expect(preview.entries.订单).toBe('Order')
164+
expect(JSON.parse(await readFile(glossaryPath, 'utf8')).订单).toBeUndefined()
165+
166+
const seeded = await initGlossary({ cwd, preset: 'all' })
167+
expect(seeded.added).toBeGreaterThan(0)
168+
expect(seeded.skipped).toBe(1)
169+
expect(seeded.entries.提交).toBe('Send')
170+
expect(seeded.entries.自定义).toBe('Custom')
171+
172+
const saved = JSON.parse(await readFile(glossaryPath, 'utf8')) as Record<string, string>
173+
expect(saved.订单).toBe('Order')
174+
expect(saved.提交).toBe('Send')
175+
176+
const overwritten = await initGlossary({ cwd, preset: 'ui', overwrite: true })
177+
expect(overwritten.updated).toBe(1)
178+
expect(overwritten.entries.提交).toBe('Submit')
179+
})
180+
181+
it('supports English to Chinese glossary presets from project config', async () => {
182+
const cwd = await createTempProject()
183+
await initProject({ cwd, overwrite: false, from: 'en', to: 'zh' })
184+
185+
const seeded = await initGlossary({ cwd, preset: 'ui' })
186+
expect(seeded.sourceLocale).toBe('en')
187+
expect(seeded.targetLocale).toBe('zh')
188+
expect(seeded.entries.Submit).toBe('提交')
189+
expect(seeded.entries.Search).toBe('搜索')
190+
})
191+
156192
it('summarizes map progress and flags orphaned files', async () => {
157193
const cwd = await createTempProject()
158194
const appPath = path.join(cwd, 'src', 'App.vue')

packages/i18n-migrate-cli/src/cli.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { ScanProgressEvent, WorkflowProgressEvent } from './types'
2+
import path from 'node:path'
23
import process from 'node:process'
34
import { Command } from 'commander'
45
import pc from 'picocolors'
56
import { applyTranslations, restoreBackups } from './apply'
67
import { approveTranslations } from './approve'
8+
import { initGlossary } from './glossary'
79
import { initProject } from './init'
810
import { createSpinner } from './prompts'
911
import { scanProject } from './scanner'
@@ -97,6 +99,39 @@ export function createCli(options: CreateCliOptions): Command {
9799
console.log(formatMapStatsReport(report))
98100
})
99101

102+
const glossary = program
103+
.command('glossary')
104+
.description('Manage .tmigrate/glossary.json.')
105+
106+
glossary
107+
.command('init')
108+
.description('Seed .tmigrate/glossary.json with built-in terminology presets.')
109+
.option('--preset <name>', 'preset glossary: ui, business, all', 'ui')
110+
.option('--from <locale>', 'source locale')
111+
.option('--to <locale>', 'target locale')
112+
.option('--overwrite', 'overwrite conflicting existing glossary entries')
113+
.option('--dry-run', 'preview changes without writing glossary.json')
114+
.action(async (command: {
115+
preset?: string
116+
from?: string
117+
to?: string
118+
overwrite?: boolean
119+
dryRun?: boolean
120+
}) => {
121+
const result = await initGlossary({
122+
preset: command.preset,
123+
from: command.from,
124+
to: command.to,
125+
overwrite: command.overwrite,
126+
dryRun: command.dryRun,
127+
})
128+
const verb = result.dryRun ? 'Would seed' : 'Seeded'
129+
console.log(pc.green(`${verb} ${pathRelativeToCwd(result.glossaryPath)} using ${result.preset} (${result.sourceLocale}->${result.targetLocale}).`))
130+
console.log(pc.dim(`Added ${result.added}, updated ${result.updated}, unchanged ${result.unchanged}, skipped ${result.skipped}.`))
131+
if (result.skipped > 0 && !command.overwrite)
132+
console.log(pc.dim('Use --overwrite to replace conflicting existing entries.'))
133+
})
134+
100135
program
101136
.command('apply')
102137
.description('Apply approved translations back to source files.')
@@ -303,3 +338,7 @@ function formatModelLoadMessage(
303338
const fileProgress = currentFileTotal > 0 ? ` (${currentFileIndex}/${currentFileTotal})` : ''
304339
return `${filePrefix}${fileProgress} · loading local model`
305340
}
341+
342+
function pathRelativeToCwd(filePath: string): string {
343+
return path.relative(process.cwd(), filePath) || filePath
344+
}

0 commit comments

Comments
 (0)