feat: decouple --lang preference from TUI display language#1132
Conversation
…non-zh/en message structs
…essage follows opts.Lang preference
Empty string (explicit), wrong case, typos, and removed language codes all exit with ExitValidation (code 2). Empty Lang on bypass-cobra test paths normalizes to default 'zh' to preserve test ergonomics while keeping the user-facing contract strict.
After the --lang refactor, the only callers of NormalizeLang were the picker pre-select (now hardcoded zh) and getBindMsg/getInitMsg default branches (now bilingual collapse). Strict validation at the flag-parse boundary makes silent normalization unnecessary.
The help text still said 'language for interactive prompts', which contradicts the refactor — --lang no longer controls TUI language. Updated to describe it as a downstream API/output preference that does not change this command's TUI language. (acceptance-reviewer finding)
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdd typed i18n (internal/i18n), ParseLangFlag, persist AppConfig.Lang, and separate persisted Lang from per-run UILang; update config init/bind flows, message bundles, mail signature runtime, login/profile handling, and many tests to canonicalize/validate language flags. ChangesLanguage Preference Separation & i18n rollout
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1132 +/- ##
==========================================
+ Coverage 68.31% 68.70% +0.39%
==========================================
Files 619 627 +8
Lines 57431 58449 +1018
==========================================
+ Hits 39232 40158 +926
- Misses 14959 15002 +43
- Partials 3240 3289 +49 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@6595ffc6a3d9645c99c630918c31b38e911629a1🧩 Skill updatenpx skills add larksuite/cli#feat/cliconfig-lang-multilang -y -g |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cmd/config/init_messages_test.go (1)
50-67:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInclude
LangPreferenceSetin the non-empty message field assertions.The new
initMsg.LangPreferenceSetfield isn’t validated byassertAllFieldsNonEmpty, so an empty confirmation template would pass tests unnoticed.Suggested patch
fields := map[string]string{ "SelectAction": msg.SelectAction, "CreateNewApp": msg.CreateNewApp, "ConfigExistingApp": msg.ConfigExistingApp, "Platform": msg.Platform, "SelectPlatform": msg.SelectPlatform, "Feishu": msg.Feishu, "ScanQRCode": msg.ScanQRCode, "ScanOrOpenLink": msg.ScanOrOpenLink, "WaitingForScan": msg.WaitingForScan, "OpenLinkNonTTY": msg.OpenLinkNonTTY, "WaitingForScanNonTTY": msg.WaitingForScanNonTTY, "DetectedLarkTenant": msg.DetectedLarkTenant, "AppCreated": msg.AppCreated, "ConfigSaved": msg.ConfigSaved, + "LangPreferenceSet": msg.LangPreferenceSet, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmd/config/init_messages_test.go` around lines 50 - 67, The assertion helper assertAllFieldsNonEmpty is missing the new initMsg field LangPreferenceSet, so add "LangPreferenceSet": msg.LangPreferenceSet to the fields map inside assertAllFieldsNonEmpty to ensure the LangPreferenceSet template is validated as non-empty during tests; locate the fields map in assertAllFieldsNonEmpty and include the LangPreferenceSet entry alongside the other message keys.
🧹 Nitpick comments (2)
internal/i18n/lang_test.go (1)
8-28: ⚡ Quick winCover all declared valid language codes in the positive path.
At Line 13, the table only checks a subset of valid codes. Add an assertion that iterates
ValidLanguagesand verifies each returnstrueto prevent list/function drift.Proposed test hardening
func TestIsValidLang(t *testing.T) { tests := []struct { lang string expected bool }{ {"zh", true}, {"en", true}, {"ja", true}, {"ko", true}, {"invalid", false}, {"", false}, {"ZH", false}, // case sensitive } for _, tt := range tests { t.Run(tt.lang, func(t *testing.T) { if got := IsValidLang(tt.lang); got != tt.expected { t.Errorf("IsValidLang(%q) = %v, want %v", tt.lang, got, tt.expected) } }) } + for _, lang := range ValidLanguages { + if !IsValidLang(lang) { + t.Errorf("IsValidLang(%q) = false, want true", lang) + } + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/i18n/lang_test.go` around lines 8 - 28, The test only checks a subset of valid language codes; update TestIsValidLang to also iterate over the ValidLanguages slice and assert IsValidLang(code) returns true for each entry so the test will catch any drift between ValidLanguages and IsValidLang. Locate TestIsValidLang and add a subtest or loop that goes through ValidLanguages (the symbol ValidLanguages) and calls IsValidLang for each value, failing the test if any valid code returns false.internal/core/config.go (1)
162-163: ⚡ Quick winClarify
CliConfig.Langas preference, not display language.At Line 162, “UI language” can be misread as TUI render language. This PR separates preference (
Lang) from display (UILang), so tightening this comment will reduce future misuse.Suggested wording update
- Lang string // UI language (zh, en, ja, ko, etc.) + Lang string // Persistent language preference (e.g. zh, en, ja, ko)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/core/config.go` around lines 162 - 163, Update the comment for the CliConfig.Lang field to state that it is the user's language preference for content/translation (e.g., "preferred language for prompts, responses, and content") and not the terminal/UI render language; reference the separate UILang field (e.g., "use UILang for TUI rendering/localization") so future readers don't confuse CliConfig.Lang with display rendering. Ensure the comment mentions CliConfig.Lang and UILang by name so maintainers can find them easily.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/config/config_test.go`:
- Around line 186-188: In TestConfigInitCmd_InvalidLang add isolation for config
state by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start
of the test (before clearAgentEnv and any test table iterations) so the test
uses a unique temporary config directory; update any related new table tests in
the same file to do the same to avoid cross-test interference.
In `@cmd/config/init.go`:
- Around line 338-344: The picker error handling in the init command currently
returns raw errors from promptLangSelection (only huh.ErrUserAborted is
wrapped), which can leak huh errors into RunE; replace the bare return err with
a structured output error (e.g., use output.Errorf or output.ErrWithHint) so all
non-abort failures are returned as output.* errors—keep the huh.ErrUserAborted
branch returning output.ErrBare(1) and for other errors wrap err using
output.Errorf("failed to select language: %v", err) or output.ErrWithHint as
appropriate; update the block around promptLangSelection to use these output.*
wrappers so RunE only receives structured output errors.
---
Outside diff comments:
In `@cmd/config/init_messages_test.go`:
- Around line 50-67: The assertion helper assertAllFieldsNonEmpty is missing the
new initMsg field LangPreferenceSet, so add "LangPreferenceSet":
msg.LangPreferenceSet to the fields map inside assertAllFieldsNonEmpty to ensure
the LangPreferenceSet template is validated as non-empty during tests; locate
the fields map in assertAllFieldsNonEmpty and include the LangPreferenceSet
entry alongside the other message keys.
---
Nitpick comments:
In `@internal/core/config.go`:
- Around line 162-163: Update the comment for the CliConfig.Lang field to state
that it is the user's language preference for content/translation (e.g.,
"preferred language for prompts, responses, and content") and not the
terminal/UI render language; reference the separate UILang field (e.g., "use
UILang for TUI rendering/localization") so future readers don't confuse
CliConfig.Lang with display rendering. Ensure the comment mentions
CliConfig.Lang and UILang by name so maintainers can find them easily.
In `@internal/i18n/lang_test.go`:
- Around line 8-28: The test only checks a subset of valid language codes;
update TestIsValidLang to also iterate over the ValidLanguages slice and assert
IsValidLang(code) returns true for each entry so the test will catch any drift
between ValidLanguages and IsValidLang. Locate TestIsValidLang and add a subtest
or loop that goes through ValidLanguages (the symbol ValidLanguages) and calls
IsValidLang for each value, failing the test if any valid code returns false.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e71fe396-b371-4aa4-9366-d7ba958f06aa
📒 Files selected for processing (10)
cmd/config/bind.gocmd/config/bind_messages.gocmd/config/bind_test.gocmd/config/config_test.gocmd/config/init.gocmd/config/init_messages.gocmd/config/init_messages_test.gointernal/core/config.gointernal/i18n/lang.gointernal/i18n/lang_test.go
Bare picker errors leaked through RunE, breaking structured stderr JSON for agent callers. Wrap non-abort huh failures in output.Errorf in both bind and init. Also isolate the InvalidLang test config dir, assert the new LangPreferenceSet field is non-empty, guard ValidLanguages drift, and clarify the CliConfig.Lang comment as preference (not TUI language). Addresses CodeRabbit review on PR #1132.
Add unit tests for the flag-mode language-preference confirmation line (bind + init), the printLangPreferenceConfirmation helper (both explicit and implicit branches), and validateInitLang's empty-implicit normalize path. Raises patch coverage on the new --lang preference code; the remaining uncovered lines are interactive picker/TUI paths.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
cmd/config/config_test.go (1)
460-468:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winIsolate config state in the newly added tests.
Please set
LARKSUITE_CLI_CONFIG_DIRto a temp dir at test start so these tests remain hermetic and cannot leak/read shared config state.Suggested patch
func TestValidateInitLang_NormalizesEmptyImplicitToZh(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) opts := &ConfigInitOptions{Lang: "", langExplicit: false} if err := validateInitLang(opts); err != nil { t.Fatalf("expected nil error, got %v", err) } @@ func TestPrintLangPreferenceConfirmation(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) t.Run("explicit prints confirmation", func(t *testing.T) { f, _, stderr, _ := cmdutil.TestFactory(t, nil) printLangPreferenceConfirmation(&ConfigInitOptions{Factory: f, Lang: "en", UILang: "zh", langExplicit: true})As per coding guidelines,
**/*_test.go:Use t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) to isolate config state in tests.Also applies to: 472-488
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmd/config/config_test.go` around lines 460 - 468, The tests modify/inspect config and must isolate config state: at the start of TestValidateInitLang_NormalizesEmptyImplicitToZh and the other test covering lines 472-488, call t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) so each test uses a fresh temp config directory; add this single line as the first statement in those test functions (referencing the test function names TestValidateInitLang_NormalizesEmptyImplicitToZh and the adjacent test) to ensure hermetic behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@cmd/config/config_test.go`:
- Around line 460-468: The tests modify/inspect config and must isolate config
state: at the start of TestValidateInitLang_NormalizesEmptyImplicitToZh and the
other test covering lines 472-488, call t.Setenv("LARKSUITE_CLI_CONFIG_DIR",
t.TempDir()) so each test uses a fresh temp config directory; add this single
line as the first statement in those test functions (referencing the test
function names TestValidateInitLang_NormalizesEmptyImplicitToZh and the adjacent
test) to ensure hermetic behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 43a5d826-997b-4e01-9eea-2ca3f811b619
📒 Files selected for processing (2)
cmd/config/bind_test.gocmd/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- cmd/config/bind_test.go
Set LARKSUITE_CLI_CONFIG_DIR to a temp dir in the two newly added tests to follow the repo test-isolation convention (addresses CodeRabbit).
Reviewer summaryThis PR decouples the What changed (behavior)
Key design point
Verification
Ready for review — the only gate left is approval. |
…& profile - i18n.Lang string enum with a single-source catalog (Parse/IsEnglish/Base/Codes) - --lang accepts a short code or Feishu locale (zh|zh_cn), stores canonical; empty is unset (no-op) and never clobbers an existing preference - expose RuntimeContext.Lang() and collapse mail resolveLang onto it - share cmdutil.ParseLangFlag across config bind/init and profile add Change-Id: Iede1a375c3d962440ad3ccb6fd4161bad7d62f49
…ultilang Change-Id: I70f918998ec8454ec6e80e14d6867d780d969561
…ract - bind/init rebuild the config fresh, so an unset --lang dropped a previously stored preference; inherit it instead (regression tests added) - express the empty-never-clobbers rule once via preferredLang, used by every bind/init write site - i18n: Parse/Base/IsEnglish share one find() over the catalog Change-Id: I094e470be0942195f36eb6af355dd8c15123ed18
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/config/bind.go`:
- Around line 457-459: The stdout JSON message is being rendered with
i18n.Lang(opts.Lang) instead of the resolved language returned by
applyPreferences, so when --lang is unset but a prior config language exists the
wrong language is used; update the code that builds prefMsg and brand (currently
calling getBindMsg and brandDisplay with i18n.Lang(opts.Lang)) to use the
resolved language value produced by applyPreferences (use that resolvedLang
variable or field) when calling getBindMsg(...) and brandDisplay(...), ensuring
the envelope message matches the persisted/resolved language rather than
opts.Lang.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d370ca0d-bd1f-4fc9-8531-c201a7c8aedf
📒 Files selected for processing (20)
cmd/auth/login.gocmd/auth/login_messages.gocmd/auth/login_messages_test.gocmd/config/bind.gocmd/config/bind_messages.gocmd/config/bind_test.gocmd/config/config_test.gocmd/config/init.gocmd/config/init_messages.gocmd/config/init_messages_test.gocmd/profile/add.gocmd/profile/profile_test.gointernal/cmdutil/lang.gointernal/core/config.gointernal/i18n/lang.gointernal/i18n/lang_test.goshortcuts/common/runner.goshortcuts/common/runner_lang_test.goshortcuts/mail/mail_signature.goshortcuts/mail/mail_signature_lang_test.go
💤 Files with no reviewable changes (3)
- shortcuts/mail/mail_signature_lang_test.go
- shortcuts/mail/mail_signature.go
- shortcuts/common/runner_lang_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
- internal/core/config.go
- cmd/config/bind_test.go
- cmd/config/init.go
The re-bind inheritance contract introduced in 6604b22 was incomplete: appConfig.Lang inherited the prior preference, but commitBinding still rendered the JSON envelope "message" via opts.Lang (raw --lang). An AI agent that set --lang en on first bind would see Chinese envelope text on every subsequent re-bind even though en_us stayed on disk. Route prefMsg + brandDisplay through appConfig.Lang (the resolved preference) instead. Regression test pins envelope.message to the inherited language. Change-Id: I8e3d2a1f4b9c5a6e7d8f9a0b1c2d3e4f5a6b7c8d
…ntApp in priorLang Two issues caught in review of #1132 that the existing tests missed because they constructed RuntimeContext/CliConfig directly, bypassing the credential edge where the bug lives. P1 — Lang dropped at credential boundary credential.Account had no Lang field, so AccountFromCliConfig and ToCliConfig silently dropped cfg.Lang. The production Factory builds CliConfig via acct.ToCliConfig() (factory_default.go Phase 3), which meant RuntimeContext.Lang() always returned "" in production and shortcuts/mail/mail_signature.go always fell back to zh_cn — defeating the whole point of persisting --lang. Fix: add Lang i18n.Lang to Account and copy it in both directions. Regression test: TestFullChain_LangSurvivesProductionPath walks the real path (SaveMultiAppConfig -> DefaultAccountProvider.ResolveAccount -> ToCliConfig) and asserts Lang survives, so any future field added to CliConfig forces the same audit. P2 — priorLang ignored CurrentApp in multi-profile workspaces priorLang scanned all Apps and returned the first non-empty Lang. If a user had multiple profiles and the active one disagreed with Apps[0], a re-bind without --lang would silently inherit the wrong profile's preference. Fix: read multi.CurrentAppConfig("").Lang instead. Regression tests cover CurrentApp wins over Apps[0], single-app fallback, and malformed bytes. Change-Id: If7a276605f84f398cec329c2c942b471b4c32749
…ntApp in priorLang (#1157) Two issues caught in review of #1132 that the existing tests missed because they constructed RuntimeContext/CliConfig directly, bypassing the credential edge where the bug lives. P1 — Lang dropped at credential boundary credential.Account had no Lang field, so AccountFromCliConfig and ToCliConfig silently dropped cfg.Lang. The production Factory builds CliConfig via acct.ToCliConfig() (factory_default.go Phase 3), which meant RuntimeContext.Lang() always returned "" in production and shortcuts/mail/mail_signature.go always fell back to zh_cn — defeating the whole point of persisting --lang. Fix: add Lang i18n.Lang to Account and copy it in both directions. Regression test: TestFullChain_LangSurvivesProductionPath walks the real path (SaveMultiAppConfig -> DefaultAccountProvider.ResolveAccount -> ToCliConfig) and asserts Lang survives, so any future field added to CliConfig forces the same audit. P2 — priorLang ignored CurrentApp in multi-profile workspaces priorLang scanned all Apps and returned the first non-empty Lang. If a user had multiple profiles and the active one disagreed with Apps[0], a re-bind without --lang would silently inherit the wrong profile's preference. Fix: read multi.CurrentAppConfig("").Lang instead. Regression tests cover CurrentApp wins over Apps[0], single-app fallback, and malformed bytes. Change-Id: If7a276605f84f398cec329c2c942b471b4c32749
…ntApp in priorLang (larksuite#1157) Two issues caught in review of larksuite#1132 that the existing tests missed because they constructed RuntimeContext/CliConfig directly, bypassing the credential edge where the bug lives. P1 — Lang dropped at credential boundary credential.Account had no Lang field, so AccountFromCliConfig and ToCliConfig silently dropped cfg.Lang. The production Factory builds CliConfig via acct.ToCliConfig() (factory_default.go Phase 3), which meant RuntimeContext.Lang() always returned "" in production and shortcuts/mail/mail_signature.go always fell back to zh_cn — defeating the whole point of persisting --lang. Fix: add Lang i18n.Lang to Account and copy it in both directions. Regression test: TestFullChain_LangSurvivesProductionPath walks the real path (SaveMultiAppConfig -> DefaultAccountProvider.ResolveAccount -> ToCliConfig) and asserts Lang survives, so any future field added to CliConfig forces the same audit. P2 — priorLang ignored CurrentApp in multi-profile workspaces priorLang scanned all Apps and returned the first non-empty Lang. If a user had multiple profiles and the active one disagreed with Apps[0], a re-bind without --lang would silently inherit the wrong profile's preference. Fix: read multi.CurrentAppConfig("").Lang instead. Regression tests cover CurrentApp wins over Apps[0], single-app fallback, and malformed bytes. Change-Id: If7a276605f84f398cec329c2c942b471b4c32749
Summary
The
config init/config bind--langflag was overloaded: it both selected the TUI display language and set a persistent preference, sharing oneopts.Langfield. This produced a contradictory UX (the picker offered languages the TUI then fell back to Chinese for) and let unvalidated values reach disk. This PR splits the two concerns:--langbecomes a strictly-validated persistent preference, while the TUI is bilingual (zh/en) driven by a separate field. The supported preference set is aligned with the Feishu client UI (14 languages).Changes
UILangfield toBindOptions/ConfigInitOptions(cmd/config/bind.go,cmd/config/init.go); TUI rendering readsUILang(default zh, picker-only), preference writes readLang--langagainsti18n.ValidLanguagesinvalidateBindFlagsand a newvalidateInitLang— empty string, wrong case, typos, and removed codes all exit 2 viaoutput.ErrValidationgetBindMsg/getInitMsgto a bilingual switch and delete the 12 non-zh/en message structs (cmd/config/bind_messages.go,cmd/config/init_messages.go)promptLangSelectionto 2 options (中文 / English); the picker now writes bothLangandUILang--langis explicit (LangPreferenceSetmessage field)messagefield followsLang(preference) for AI-agent consumption, while stderr TUI text followsUILangi18n.NormalizeLang; trimValidLanguagesto 14 and update its count assertion--langhelp text to describe preference semantics instead of "interactive prompts"Test Plan
make unit-testpassed (per-package; the aggregate run is resource-constrained in the sandbox, every package passes in isolation —cmd/config,internal/i18n, plus 16/16 sequentialok)config init --lang fr ...→ exit 0,lang:"fr"on disk, zh confirmation语言偏好已设置:fr;--lang frr|ZH|""|ar→ exit 2 with structured validation error;config init --helplists exactly 14 codesRelated Issues
N/A
Summary by CodeRabbit