返回 DeepSeek-Reasonix
language.go
根目录 / internal / cli / language.go
1 package cli
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8
9 tea "charm.land/bubbletea/v2"
10
11 "reasonix/internal/config"
12 "reasonix/internal/i18n"
13 )
14
15 func (m *chatTUI) runLanguageSubcommand(input string) tea.Cmd {
16 args := tokenizeArgs(input)
17 if len(args) < 2 {
18 cfg, err := config.Load()
19 if err != nil {
20 m.notice("language: " + err.Error())
21 return nil
22 }
23 saved := languageDisplay(cfg.Language)
24 resolved := i18n.DetectLanguage(cfg.Language)
25 m.notice(i18n.M.LanguageHeader + "\n" + describeLanguages(saved, resolved) + "\n" + i18n.M.LanguageHint)
26 return nil
27 }
28 if len(args) > 2 {
29 m.notice(i18n.M.LanguageHint)
30 return nil
31 }
32
33 lang, err := normalizeLanguageArg(args[1])
34 if err != nil {
35 m.notice(err.Error())
36 return nil
37 }
38 if !m.runtimeSettingChangeReady() {
39 return nil
40 }
41 path := config.SourcePath()
42 if path == "" {
43 path = config.UserConfigPath()
44 }
45 if path == "" {
46 m.notice("language: cannot resolve config path")
47 return nil
48 }
49 // Lock both possible write targets before resolving either one. This keeps
50 // a project language update and user-override cleanup atomic even when the
51 // files are reached through aliases.
52 if err := func() error {
53 userPath := config.UserConfigPath()
54 lockPaths := []string{path}
55 if userPath != "" && !sameConfigPath(path, userPath) {
56 lockPaths = append(lockPaths, userPath)
57 }
58 unlock, err := config.LockConfigFilesEdits(lockPaths...)
59 if err != nil {
60 return err
61 }
62 defer unlock()
63 edit, err := config.LoadForEditReadOnlyStrict(path)
64 if err != nil {
65 return err
66 }
67 if err := edit.SetLanguage(lang); err != nil {
68 return err
69 }
70 if err := edit.SaveTo(path); err != nil {
71 return err
72 }
73 if lang == "" {
74 return clearUserLanguageOverride(path)
75 }
76 return nil
77 }(); err != nil {
78 m.notice("language: " + err.Error())
79 return nil
80 }
81
82 resolved := i18n.DetectLanguage(lang)
83 m.refreshInputPlaceholder()
84 if m.ctrl != nil {
85 m.ctrl.SetResponseLanguage(lang)
86 }
87 success := fmt.Sprintf(i18n.M.LanguageChangedFmt, languageDisplay(lang), resolved)
88 if m.ctrl == nil {
89 m.notice(success)
90 return nil
91 }
92 return m.scheduleCurrentControllerRebuild("language", success)
93 }
94
95 // clearUserLanguageOverride drops a stale user-level language override after a
96 // project-level write. Callers must hold LockUserConfigEdits: this is its own
97 // load-modify-save on the user config and must not take the lock itself.
98 func clearUserLanguageOverride(primaryPath string) error {
99 userPath := config.UserConfigPath()
100 if userPath == "" || sameConfigPath(primaryPath, userPath) {
101 return nil
102 }
103 if _, err := os.Stat(userPath); err != nil {
104 if os.IsNotExist(err) {
105 return nil
106 }
107 return err
108 }
109 edit, err := config.LoadForEditReadOnlyStrict(userPath)
110 if err != nil {
111 return err
112 }
113 if strings.TrimSpace(edit.Language) == "" {
114 return nil
115 }
116 if err := edit.SetLanguage(""); err != nil {
117 return err
118 }
119 return edit.SaveTo(userPath)
120 }
121
122 func sameConfigPath(a, b string) bool {
123 aa, errA := filepath.Abs(a)
124 bb, errB := filepath.Abs(b)
125 if errA == nil {
126 a = aa
127 }
128 if errB == nil {
129 b = bb
130 }
131 return filepath.Clean(a) == filepath.Clean(b)
132 }
133
134 func normalizeLanguageArg(s string) (string, error) {
135 switch strings.ToLower(strings.TrimSpace(s)) {
136 case "", "auto", "detect", "default":
137 return "", nil
138 case "en", "english":
139 return "en", nil
140 case "zh", "cn", "chinese", "中文":
141 return "zh", nil
142 default:
143 return "", fmt.Errorf("usage: /language auto|en|zh")
144 }
145 }
146
147 func languageDisplay(lang string) string {
148 if strings.TrimSpace(lang) == "" {
149 return "auto"
150 }
151 return lang
152 }
153
154 func describeLanguages(current, resolved string) string {
155 items := []struct {
156 tag string
157 hint string
158 }{
159 {"auto", i18n.M.ArgLanguageAuto},
160 {"en", i18n.M.ArgLanguageEn},
161 {"zh", i18n.M.ArgLanguageZh},
162 }
163 var b strings.Builder
164 for _, it := range items {
165 marker := " "
166 if it.tag == current {
167 marker = "• "
168 }
169 hint := it.hint
170 if it.tag == current {
171 hint += " · " + i18n.M.ArgThemeCurrent
172 }
173 if it.tag == "auto" && current == "auto" {
174 hint += " · " + resolved
175 }
176 fmt.Fprintf(&b, "%s%-6s %s\n", marker, it.tag, hint)
177 }
178 return strings.TrimRight(b.String(), "\n")
179 }
180
180 lines GO