Skip to content

Commit cfd99fc

Browse files
committed
feat: Add 'codei18n init' subcommand --story=1
1 parent dd2596e commit cfd99fc

File tree

16 files changed

+905
-366
lines changed

16 files changed

+905
-366
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ release/
6565
# C/C++ artifacts (for CGO)
6666
*.o
6767
*.a
68-
coverage-data
68+
coverage-data
69+
./codei18n

cmd/codei18n/hook.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,16 @@ func init() {
3636
}
3737

3838
func runHookInstall() {
39+
if err := installHook(); err != nil {
40+
log.Fatal("安装 hook 失败: %v", err)
41+
}
42+
log.Success("Hook 安装成功")
43+
}
44+
45+
func installHook() error {
3946
gitDir := ".git"
4047
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
41-
log.Fatal("当前目录不是 Git 仓库根目录")
48+
return os.ErrNotExist
4249
}
4350

4451
hookPath := filepath.Join(gitDir, "hooks", "pre-commit")
@@ -91,10 +98,9 @@ exit 0
9198
`
9299

93100
if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
94-
log.Fatal("安装 hook 失败: %v", err)
101+
return err
95102
}
96-
97-
log.Success("Hook 安装成功: %s", hookPath)
103+
return nil
98104
}
99105

100106
func runHookUninstall() {

cmd/codei18n/init.go

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
package main
22

33
import (
4+
"os"
5+
"strings"
6+
47
"github.com/spf13/cobra"
58
"github.com/studyzy/codei18n/core/config"
9+
"github.com/studyzy/codei18n/core/workflow"
610
"github.com/studyzy/codei18n/internal/log"
711
)
812

913
var (
10-
initSourceLang string
11-
initTargetLang string
12-
initProvider string
14+
initSourceLang string
15+
initTargetLang string
16+
initProvider string
17+
initWithTranslate bool
18+
initForce bool
1319
)
1420

1521
var initCmd = &cobra.Command{
1622
Use: "init",
1723
Short: "初始化项目配置",
24+
Long: `初始化项目配置,继承全局设置,并自动配置 Git 环境。可选执行初次扫描和翻译。`,
1825
Run: func(cmd *cobra.Command, args []string) {
19-
runInit()
26+
runInit(cmd)
2027
},
2128
}
2229

@@ -25,19 +32,112 @@ func init() {
2532

2633
initCmd.Flags().StringVar(&initSourceLang, "source-lang", "en", "源码语言")
2734
initCmd.Flags().StringVar(&initTargetLang, "target-lang", "zh-CN", "本地目标语言")
28-
// Default to using LLM-based remote translation (OpenAI-compatible protocol)
29-
initCmd.Flags().StringVar(&initProvider, "provider", "openai", "翻译提供商 (openai/llm/ollama)")
35+
initCmd.Flags().StringVar(&initProvider, "provider", "", "翻译提供商 (openai/llm/ollama)")
36+
initCmd.Flags().BoolVar(&initWithTranslate, "with-translate", false, "初始化后立即执行翻译")
37+
initCmd.Flags().BoolVar(&initForce, "force", false, "如果配置已存在,强制覆盖")
3038
}
3139

32-
func runInit() {
33-
cfg := config.DefaultConfig()
34-
cfg.SourceLanguage = initSourceLang
35-
cfg.LocalLanguage = initTargetLang
36-
cfg.TranslationProvider = initProvider
40+
func runInit(cmd *cobra.Command) {
41+
// 1. Load effective config (Global + Defaults)
42+
cfg, err := config.LoadConfig()
43+
if err != nil {
44+
log.Warn("无法加载配置,使用默认值: %v", err)
45+
cfg = config.DefaultConfig()
46+
}
47+
48+
// 2. Apply flags
49+
// Note: Flags defined in init() act as overrides or defaults.
50+
// Cobra flags don't automatically populate cfg, we map them here.
51+
// But we need to distinguish between "default value" and "user provided value".
52+
// Since we set default values in flags, we might overwrite global config if we are not careful.
53+
// Actually, `initSourceLang` defaults to "en". If global config has "fr", `initSourceLang` will still be "en" if user didn't provide it?
54+
// No, cobra flags have default values.
55+
// We should only override if user EXPLICITLY provided the flag.
56+
// How to check if flag was changed? cmd.Flags().Changed("source-lang")
57+
58+
if cmd.Flags().Changed("source-lang") {
59+
cfg.SourceLanguage = initSourceLang
60+
}
61+
if cmd.Flags().Changed("target-lang") {
62+
cfg.LocalLanguage = initTargetLang
63+
}
64+
if cmd.Flags().Changed("provider") {
65+
cfg.TranslationProvider = initProvider
66+
}
67+
68+
// 3. Sanitize (Strip secrets inherited from global)
69+
// We create a new sanitized config object to save locally
70+
projectCfg := cfg.Sanitize()
3771

38-
if err := config.SaveConfig(cfg); err != nil {
72+
// 4. Save Config
73+
if _, err := os.Stat(".codei18n/config.json"); err == nil && !initForce {
74+
log.Fatal("配置文件已存在 (使用 --force 覆盖)")
75+
}
76+
77+
if err := config.SaveConfig(projectCfg); err != nil {
3978
log.Fatal("保存配置失败: %v", err)
4079
}
80+
log.Success("项目配置文件已生成")
4181

42-
log.Success("项目初始化成功")
82+
// 5. Git Integration
83+
setupGitIntegration()
84+
85+
// 6. Map Update (Auto Scan)
86+
log.Info("正在初始化注释映射...")
87+
mapResult, err := workflow.MapUpdate(projectCfg, ".", false)
88+
if err != nil {
89+
log.Warn("映射初始化失败: %v", err)
90+
} else {
91+
log.Success("已扫描 %d 条注释,生成初始映射", mapResult.TotalComments)
92+
}
93+
94+
// 7. Conditional Translation
95+
if initWithTranslate {
96+
log.Info("正在执行初次翻译...")
97+
transResult, err := workflow.Translate(projectCfg, workflow.TranslateOptions{
98+
Concurrency: 5,
99+
// Use provider/model from config unless overridden (which we handled in step 2)
100+
})
101+
if err != nil {
102+
log.Warn("翻译失败: %v", err)
103+
} else {
104+
log.Success("翻译完成!共处理 %d 条注释", transResult.SuccessCount)
105+
}
106+
} else {
107+
log.Info("提示: 运行 'codei18n translate' 可生成翻译")
108+
}
109+
110+
log.Success("项目初始化完成!")
111+
}
112+
113+
func setupGitIntegration() {
114+
if _, err := os.Stat(".git"); os.IsNotExist(err) {
115+
return // Not a git repo
116+
}
117+
118+
// 1. Update .gitignore
119+
gitignorePath := ".gitignore"
120+
content, _ := os.ReadFile(gitignorePath) // ignore error, treat as empty if missing
121+
sContent := string(content)
122+
if !strings.Contains(sContent, ".codei18n/") {
123+
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
124+
if err != nil {
125+
log.Warn("更新 .gitignore 失败: %v", err)
126+
} else {
127+
if len(content) > 0 && !strings.HasSuffix(sContent, "\n") {
128+
f.WriteString("\n")
129+
}
130+
f.WriteString(".codei18n/\n")
131+
f.Close()
132+
log.Success("已将 .codei18n/ 添加到 .gitignore")
133+
}
134+
}
135+
136+
// 2. Install hook
137+
// Assuming installHook is available in package main (from hook.go)
138+
if err := installHook(); err != nil {
139+
log.Warn("安装 git hook 失败: %v", err)
140+
} else {
141+
log.Success("Git Pre-commit Hook 已自动安装")
142+
}
43143
}

cmd/codei18n/map.go

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/spf13/cobra"
99
"github.com/studyzy/codei18n/core/config"
1010
"github.com/studyzy/codei18n/core/mapping"
11-
"github.com/studyzy/codei18n/core/utils"
11+
"github.com/studyzy/codei18n/core/workflow"
1212
"github.com/studyzy/codei18n/internal/log"
1313
)
1414

@@ -64,63 +64,17 @@ func runMapUpdate() {
6464
cfg = config.DefaultConfig()
6565
}
6666

67-
// 1. Scan for comments
68-
log.Info("正在扫描目录: %s", mapScanDir)
69-
comments, err := scanDirectory(mapScanDir)
67+
result, err := workflow.MapUpdate(cfg, mapScanDir, mapDryRun)
7068
if err != nil {
71-
log.Fatal("扫描失败: %v", err)
69+
log.Fatal("Map update failed: %v", err)
7270
}
7371

74-
// 2. Generate IDs
75-
for _, c := range comments {
76-
if c.ID == "" {
77-
c.ID = utils.GenerateCommentID(c)
78-
}
79-
}
80-
81-
// 3. Load Store
82-
storePath := filepath.Join(".codei18n", "mappings.json")
83-
store := mapping.NewStore(storePath)
84-
if err := store.Load(); err != nil {
85-
log.Fatal("加载映射文件失败: %v", err)
86-
}
87-
88-
// 4. Update
89-
m := store.GetMapping()
90-
m.SourceLanguage = cfg.SourceLanguage
91-
m.TargetLanguage = cfg.LocalLanguage
92-
93-
addedCount := 0
94-
for _, c := range comments {
95-
// Check if ID exists
96-
if _, exists := m.Comments[c.ID]; !exists {
97-
// [MOCK zh-CN->en] // Intelligently detect comment language
98-
detectedLang := utils.DetectLanguage(c.SourceText)
99-
100-
if detectedLang == cfg.LocalLanguage {
101-
// [MOCK zh-CN->en] // The comment is in the local language (Chinese), stored as LocalLanguage
102-
store.Set(c.ID, cfg.LocalLanguage, c.SourceText)
103-
log.Info("检测到中文注释: ID=%s, Text=%s", c.ID, c.SourceText)
104-
} else {
105-
// [MOCK zh-CN->en] // The comment is in the source language (English), stored as SourceLanguage
106-
store.Set(c.ID, cfg.SourceLanguage, c.SourceText)
107-
}
108-
addedCount++
109-
}
110-
}
111-
112-
log.Success("发现 %d 条注释,新增 %d 条映射", len(comments), addedCount)
113-
72+
log.Success("发现 %d 条注释,新增 %d 条映射", result.TotalComments, result.AddedCount)
11473
if mapDryRun {
11574
log.Info("Dry run 模式,不保存文件")
116-
return
117-
}
118-
119-
// 5. Save
120-
if err := store.Save(); err != nil {
121-
log.Fatal("保存映射文件失败: %v", err)
75+
} else {
76+
log.Success("映射文件已更新: %s", result.StorePath)
12277
}
123-
log.Success("映射文件已更新: %s", storePath)
12478
}
12579

12680
func runMapGet(commentID string) {

cmd/codei18n/scan.go

Lines changed: 4 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ package main
33
import (
44
"encoding/json"
55
"fmt"
6-
"io"
76
"os"
87
"path/filepath"
98

109
"github.com/spf13/cobra"
11-
"github.com/studyzy/codei18n/adapters"
1210
"github.com/studyzy/codei18n/core/config"
1311
"github.com/studyzy/codei18n/core/domain"
1412
"github.com/studyzy/codei18n/core/mapping"
13+
"github.com/studyzy/codei18n/core/scanner"
1514
"github.com/studyzy/codei18n/core/utils"
1615
"github.com/studyzy/codei18n/internal/log"
1716
)
@@ -60,11 +59,11 @@ func runScan() {
6059

6160
// Determine strategy
6261
if scanStdin {
63-
comments, err = scanFromStdin(scanFile)
62+
comments, err = scanner.FromStdin(scanFile)
6463
} else if scanFile != "" {
65-
comments, err = scanSingleFile(scanFile)
64+
comments, err = scanner.SingleFile(scanFile)
6665
} else {
67-
comments, err = scanDirectory(scanDir)
66+
comments, err = scanner.Directory(scanDir)
6867
}
6968

7069
if err != nil {
@@ -114,85 +113,6 @@ func runScan() {
114113
}
115114
}
116115

117-
func scanFromStdin(filename string) ([]*domain.Comment, error) {
118-
// Read stdin
119-
src, err := io.ReadAll(os.Stdin)
120-
if err != nil {
121-
return nil, fmt.Errorf("读取 stdin 失败: %w", err)
122-
}
123-
124-
adapter, err := adapters.GetAdapter(filename)
125-
if err != nil {
126-
return nil, err
127-
}
128-
return adapter.Parse(filename, src)
129-
}
130-
131-
func scanSingleFile(filename string) ([]*domain.Comment, error) {
132-
adapter, err := adapters.GetAdapter(filename)
133-
if err != nil {
134-
return nil, err
135-
}
136-
137-
// Read file content manually to ensure consistency with directory scan
138-
// and to debug potential read issues
139-
content, err := os.ReadFile(filename)
140-
if err != nil {
141-
return nil, fmt.Errorf("read file failed: %w", err)
142-
}
143-
144-
return adapter.Parse(filename, content)
145-
}
146-
147-
func scanDirectory(dir string) ([]*domain.Comment, error) {
148-
var comments []*domain.Comment
149-
var walkErr error
150-
151-
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
152-
if err != nil {
153-
return err
154-
}
155-
if info.IsDir() {
156-
// Skip .git, vendor, .codei18n
157-
if info.Name() == ".git" || info.Name() == "vendor" || info.Name() == ".codei18n" {
158-
return filepath.SkipDir
159-
}
160-
return nil
161-
}
162-
163-
// Try to get adapter for file
164-
adapter, err := adapters.GetAdapter(path)
165-
if err == nil {
166-
// Supported file
167-
// Calculate relative path for ID stability
168-
relPath, err := filepath.Rel(dir, path)
169-
if err != nil {
170-
relPath = path
171-
}
172-
173-
// Read file content manually to ensure we access the correct file
174-
content, err := os.ReadFile(path)
175-
if err != nil {
176-
log.Warn("读取文件 %s 失败: %v", path, err)
177-
return nil
178-
}
179-
180-
fileComments, err := adapter.Parse(relPath, content)
181-
if err != nil {
182-
log.Warn("解析文件 %s 失败: %v", path, err)
183-
return nil // Continue scanning other files
184-
}
185-
comments = append(comments, fileComments...)
186-
}
187-
return nil
188-
})
189-
190-
if walkErr != nil {
191-
return nil, walkErr
192-
}
193-
return comments, err
194-
}
195-
196116
func outputResults(comments []*domain.Comment) error {
197117
// Prepare output data
198118
type Output struct {

0 commit comments

Comments
 (0)