Files
2026-07-13 13:00:08 +08:00

156 lines
3.6 KiB
Go

package cli
import (
"fmt"
"os"
"path/filepath"
"strings"
"reasonix/internal/config"
"reasonix/internal/i18n"
)
func (m *chatTUI) runLanguageSubcommand(input string) {
args := tokenizeArgs(input)
if len(args) < 2 {
cfg, err := config.Load()
if err != nil {
m.notice("language: " + err.Error())
return
}
saved := languageDisplay(cfg.Language)
resolved := i18n.DetectLanguage(cfg.Language)
m.notice(i18n.M.LanguageHeader + "\n" + describeLanguages(saved, resolved) + "\n" + i18n.M.LanguageHint)
return
}
if len(args) > 2 {
m.notice(i18n.M.LanguageHint)
return
}
lang, err := normalizeLanguageArg(args[1])
if err != nil {
m.notice(err.Error())
return
}
path := config.SourcePath()
if path == "" {
path = config.UserConfigPath()
}
if path == "" {
m.notice("language: cannot resolve config path")
return
}
// Lock the whole load-modify-save cycle, including the user-scope override
// cleanup — its own read-modify-write on the user config. path may be a
// project config here; holding the user-config lock for that case is
// harmless. The controller update below runs off-lock.
if err := func() error {
unlock := config.LockUserConfigEdits()
defer unlock()
edit := config.LoadForEdit(path)
if err := edit.SetLanguage(lang); err != nil {
return err
}
if err := edit.SaveTo(path); err != nil {
return err
}
if lang == "" {
return clearUserLanguageOverride(path)
}
return nil
}(); err != nil {
m.notice("language: " + err.Error())
return
}
resolved := i18n.DetectLanguage(lang)
if m.ctrl != nil {
m.ctrl.SetResponseLanguage(lang)
}
m.notice(fmt.Sprintf(i18n.M.LanguageChangedFmt, languageDisplay(lang), resolved))
}
// clearUserLanguageOverride drops a stale user-level language override after a
// project-level write. Callers must hold LockUserConfigEdits: this is its own
// load-modify-save on the user config and must not take the lock itself.
func clearUserLanguageOverride(primaryPath string) error {
userPath := config.UserConfigPath()
if userPath == "" || sameConfigPath(primaryPath, userPath) {
return nil
}
if _, err := os.Stat(userPath); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
edit := config.LoadForEdit(userPath)
if strings.TrimSpace(edit.Language) == "" {
return nil
}
if err := edit.SetLanguage(""); err != nil {
return err
}
return edit.SaveTo(userPath)
}
func sameConfigPath(a, b string) bool {
aa, errA := filepath.Abs(a)
bb, errB := filepath.Abs(b)
if errA == nil {
a = aa
}
if errB == nil {
b = bb
}
return filepath.Clean(a) == filepath.Clean(b)
}
func normalizeLanguageArg(s string) (string, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "auto", "detect", "default":
return "", nil
case "en", "english":
return "en", nil
case "zh", "cn", "chinese", "中文":
return "zh", nil
default:
return "", fmt.Errorf("usage: /language auto|en|zh")
}
}
func languageDisplay(lang string) string {
if strings.TrimSpace(lang) == "" {
return "auto"
}
return lang
}
func describeLanguages(current, resolved string) string {
items := []struct {
tag string
hint string
}{
{"auto", i18n.M.ArgLanguageAuto},
{"en", i18n.M.ArgLanguageEn},
{"zh", i18n.M.ArgLanguageZh},
}
var b strings.Builder
for _, it := range items {
marker := " "
if it.tag == current {
marker = "• "
}
hint := it.hint
if it.tag == current {
hint += " · " + i18n.M.ArgThemeCurrent
}
if it.tag == "auto" && current == "auto" {
hint += " · " + resolved
}
fmt.Fprintf(&b, "%s%-6s %s\n", marker, it.tag, hint)
}
return strings.TrimRight(b.String(), "\n")
}