Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/config/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Interactive terminal use: run with no flags to enter the TUI form.`,
cmd.Flags().StringVar(&opts.AppID, "app-id", "", "App ID to bind (required for OpenClaw multi-account)")
cmd.Flags().StringVar(&opts.Identity, "identity", "", "identity preset (bot-only|user-default); defaults to bot-only in flag mode (safer: no impersonation)")
cmd.Flags().BoolVar(&opts.Force, "force", false, "confirm a risky transition (currently: bot-only → user-default identity change in flag mode)")
cmd.Flags().StringVar(&opts.Lang, "lang", "", "language preference (e.g. zh or zh_cn)")
cmd.Flags().StringVar(&opts.Lang, "lang", "", "language preference (zh, en, or ja)")
cmdutil.SetRisk(cmd, "write")

return cmd
Expand Down
2 changes: 2 additions & 0 deletions cmd/config/bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ func TestConfigBindRun_InvalidLang(t *testing.T) {
{"removed code ar", "ar"},
{"unknown xx", "xx"},
{"hyphen form zh-CN", "zh-CN"},
{"dropped short code ko", "ko"},
{"dropped locale ko_kr", "ko_kr"},
}

for _, tc := range cases {
Expand Down
14 changes: 14 additions & 0 deletions cmd/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ func TestConfigInitCmd_InvalidLang(t *testing.T) {
{"removed code ar", "ar"},
{"unknown xx", "xx"},
{"hyphen form zh-CN", "zh-CN"},
{"dropped short code ko", "ko"},
{"dropped locale ko_kr", "ko_kr"},
}

for _, tc := range cases {
Expand Down Expand Up @@ -509,6 +511,18 @@ func TestValidateInitLang(t *testing.T) {
}
}
})
t.Run("dropped short code ko errors", func(t *testing.T) {
opts := &ConfigInitOptions{Lang: "ko", langExplicit: true}
if err := validateInitLang(opts); err == nil {
t.Fatal("expected validation error for --lang ko, got nil")
}
})
t.Run("dropped locale ko_kr errors", func(t *testing.T) {
opts := &ConfigInitOptions{Lang: "ko_kr", langExplicit: true}
if err := validateInitLang(opts); err == nil {
t.Fatal("expected validation error for --lang ko_kr, got nil")
}
})
}

// TestPrintLangPreferenceConfirmation covers the confirmation helper: it prints
Expand Down
2 changes: 1 addition & 1 deletion cmd/config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ if the user explicitly wants a separate app inside the Agent workspace.`,
cmd.Flags().StringVar(&opts.AppID, "app-id", "", "App ID (non-interactive)")
cmd.Flags().BoolVar(&opts.AppSecretStdin, "app-secret-stdin", false, "Read App Secret from stdin to avoid process list exposure")
cmd.Flags().StringVar(&opts.Brand, "brand", "feishu", "feishu or lark (non-interactive, default feishu)")
cmd.Flags().StringVar(&opts.Lang, "lang", "", "language preference (e.g. zh or zh_cn)")
cmd.Flags().StringVar(&opts.Lang, "lang", "", "language preference (zh, en, or ja)")
cmd.Flags().StringVar(&opts.ProfileName, "name", "", "create or update a named profile (append instead of replace)")
cmd.Flags().BoolVar(&opts.ForceInit, "force-init", false, "allow init inside an Agent workspace (OPENCLAW_HOME / HERMES_HOME); use config bind instead unless you really want a separate app")
cmdutil.SetRisk(cmd, "write")
Expand Down
2 changes: 1 addition & 1 deletion cmd/profile/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
cmd.Flags().StringVar(&appID, "app-id", "", "App ID (required)")
cmd.Flags().BoolVar(&appSecretStdin, "app-secret-stdin", false, "read App Secret from stdin")
cmd.Flags().StringVar(&brand, "brand", "feishu", "feishu or lark")
cmd.Flags().StringVar(&lang, "lang", "", "language preference (e.g. zh or zh_cn)")
cmd.Flags().StringVar(&lang, "lang", "", "language preference (zh, en, or ja)")

Check warning on line 44 in cmd/profile/add.go

View check run for this annotation

Codecov / codecov/patch

cmd/profile/add.go#L44

Added line #L44 was not covered by tests
cmd.Flags().BoolVar(&use, "use", false, "switch to this profile after adding")

_ = cmd.MarkFlagRequired("name")
Expand Down
14 changes: 14 additions & 0 deletions cmd/profile/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ func TestProfileAddRun_Lang(t *testing.T) {
t.Fatalf("expected ExitValidation, got %T: %v", err, err)
}
})

t.Run("dropped code ko errors", func(t *testing.T) {
setupProfileConfigDir(t)
f, _, _, _ := cmdutil.TestFactory(t, nil)
f.IOStreams.In = strings.NewReader("secret\n")
err := profileAddRun(f, "p", "app-p", true, "feishu", "ko", false)
if err == nil {
t.Fatal("expected validation error for --lang ko, got nil")
}
exitErr, ok := err.(*output.ExitError)
if !ok || exitErr.Code != output.ExitValidation {
t.Fatalf("expected ExitValidation, got %T: %v", err, err)
}
})
}

func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
Expand Down
21 changes: 6 additions & 15 deletions internal/i18n/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@ const (
LangZhCN Lang = "zh_cn"
LangEnUS Lang = "en_us"
LangJaJP Lang = "ja_jp"
LangKoKR Lang = "ko_kr"
LangFrFR Lang = "fr_fr"
LangDeDE Lang = "de_de"
LangEsES Lang = "es_es"
LangItIT Lang = "it_it"
LangRuRU Lang = "ru_ru"
LangPtBR Lang = "pt_br"
LangThTH Lang = "th_th"
LangViVN Lang = "vi_vn"
LangIdID Lang = "id_id"
LangMsMY Lang = "ms_my"
)

type langEntry struct {
Expand All @@ -29,11 +18,13 @@ type langEntry struct {
}

// catalog is the single source of truth; order drives --help and error listing.
// Locked to {zh, en, ja} as of 2026-05-28: TUI bundles only ship for zh/en
// (ja falls back to the zh bundle), and Lark API client code only branches on
// these three for localization. Adding more entries here is meaningful only
// after the downstream codepaths (mail signature locale, TUI bundle) gain
// branches for them.
var catalog = []langEntry{
{LangZhCN, "zh"}, {LangEnUS, "en"}, {LangJaJP, "ja"}, {LangKoKR, "ko"},
{LangFrFR, "fr"}, {LangDeDE, "de"}, {LangEsES, "es"}, {LangItIT, "it"},
{LangRuRU, "ru"}, {LangPtBR, "pt"}, {LangThTH, "th"}, {LangViVN, "vi"},
{LangIdID, "id"}, {LangMsMY, "ms"},
{LangZhCN, "zh"}, {LangEnUS, "en"}, {LangJaJP, "ja"},
}

// find matches a short code or Feishu locale against the catalog (case-sensitive).
Expand Down
20 changes: 12 additions & 8 deletions internal/i18n/lang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

package i18n

import "testing"
import (
"slices"
"testing"
)

func TestParse(t *testing.T) {
tests := []struct {
Expand All @@ -16,14 +19,17 @@ func TestParse(t *testing.T) {
{"en", LangEnUS, true}, // short code
{"en_us", LangEnUS, true}, // canonical locale
{"ja", LangJaJP, true}, // short code
{"pt", LangPtBR, true}, // pt → pt_br, not pt_pt
{"ms", LangMsMY, true}, // ms → ms_my
{"ja_jp", LangJaJP, true}, // canonical locale
{"", "", false}, // unset
{"ZH", "", false}, // case-sensitive
{"zh-CN", "", false}, // hyphen form not accepted
{"zh_CN", "", false}, // case-sensitive region
{"ar", "", false}, // not in the supported set
{"xx", "", false}, // unknown
{"ko", "", false}, // dropped in 2026-05-28 catalog shrink
{"ko_kr", "", false}, // dropped: legacy Feishu locale
{"fr_fr", "", false}, // dropped: legacy Feishu locale
{"de_de", "", false}, // dropped: legacy Feishu locale
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
Expand Down Expand Up @@ -81,11 +87,9 @@ func TestBase(t *testing.T) {

func TestCodes(t *testing.T) {
codes := Codes()
if len(codes) != 14 {
t.Fatalf("len(Codes()) = %d, want 14", len(codes))
}
if codes[0] != "zh_cn" {
t.Errorf("Codes()[0] = %q, want %q (catalog order)", codes[0], "zh_cn")
want := []string{"zh_cn", "en_us", "ja_jp"}
if !slices.Equal(codes, want) {
t.Fatalf("Codes() = %v, want %v", codes, want)
}
// Every code must round-trip through Parse to itself (canonical).
for _, c := range codes {
Expand Down
17 changes: 13 additions & 4 deletions shortcuts/common/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,20 @@ func (ctx *RuntimeContext) IsBot() bool {
// UserOpenId returns the current user's open_id from config.
func (ctx *RuntimeContext) UserOpenId() string { return ctx.Config.UserOpenId }

// Lang returns the user's preference as a canonical locale, or "" if unset or
// unrecognized; callers choose their own fallback.
// Lang returns the user's preference as a canonical locale.
// Empty stays empty (unset). Any non-empty stored value that does not resolve
// via i18n.Parse (e.g. legacy ko_kr / fr_fr from before the catalog was
// shrunk to zh/en/ja) is silently coerced to LangZhCN — existing configs
// stay readable, just behave as zh.
func (ctx *RuntimeContext) Lang() i18n.Lang {
lang, _ := i18n.Parse(string(ctx.Config.Lang))
return lang
raw := string(ctx.Config.Lang)
if raw == "" {
return ""
}
if lang, ok := i18n.Parse(raw); ok {
return lang
}
return i18n.LangZhCN
}

// BotInfo holds bot identity metadata fetched lazily from /bot/v3/info.
Expand Down
8 changes: 7 additions & 1 deletion shortcuts/common/runner_lang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ func TestRuntimeContext_Lang(t *testing.T) {
{"legacy short value normalizes", "ja", i18n.LangJaJP},
{"legacy short zh normalizes", "zh", i18n.LangZhCN},
{"unset stays empty", "", ""},
{"unrecognized stays empty", "klingon", ""},
// Flipped semantics: unrecognized non-empty values are now treated
// as legacy storage from the pre-2026-05-28 14-language catalog
// and silently coerced to LangZhCN, not left empty.
{"unrecognized garbage coerces to zh", "klingon", i18n.LangZhCN},
{"legacy ko_kr coerces to zh", "ko_kr", i18n.LangZhCN},
{"legacy fr_fr coerces to zh", "fr_fr", i18n.LangZhCN},
{"legacy short ko coerces to zh", "ko", i18n.LangZhCN},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/mail/mail_signature_lang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestResolveLang(t *testing.T) {
{"japanese", i18n.LangJaJP, "ja_jp"},
{"chinese", i18n.LangZhCN, "zh_cn"},
{"legacy short en", "en", "en_us"},
{"unsupported-by-mail falls back to zh_cn", i18n.LangFrFR, "zh_cn"},
{"legacy fr_fr falls back to zh_cn", i18n.Lang("fr_fr"), "zh_cn"},
{"unset falls back to zh_cn", "", "zh_cn"},
}
for _, tt := range tests {
Expand Down
Loading