Skip to content

feat: decouple --lang preference from TUI display language#1132

Open
luozhixiong01 wants to merge 21 commits into
mainfrom
feat/cliconfig-lang-multilang
Open

feat: decouple --lang preference from TUI display language#1132
luozhixiong01 wants to merge 21 commits into
mainfrom
feat/cliconfig-lang-multilang

Conversation

@luozhixiong01
Copy link
Copy Markdown
Collaborator

@luozhixiong01 luozhixiong01 commented May 27, 2026

Summary

The config init / config bind --lang flag was overloaded: it both selected the TUI display language and set a persistent preference, sharing one opts.Lang field. 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: --lang becomes 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

  • Add UILang field to BindOptions / ConfigInitOptions (cmd/config/bind.go, cmd/config/init.go); TUI rendering reads UILang (default zh, picker-only), preference writes read Lang
  • Strictly validate --lang against i18n.ValidLanguages in validateBindFlags and a new validateInitLang — empty string, wrong case, typos, and removed codes all exit 2 via output.ErrValidation
  • Collapse getBindMsg / getInitMsg to a bilingual switch and delete the 12 non-zh/en message structs (cmd/config/bind_messages.go, cmd/config/init_messages.go)
  • Shrink promptLangSelection to 2 options (中文 / English); the picker now writes both Lang and UILang
  • Print a language-preference confirmation to stderr only when --lang is explicit (LangPreferenceSet message field)
  • JSON envelope message field follows Lang (preference) for AI-agent consumption, while stderr TUI text follows UILang
  • Remove the now-unused i18n.NormalizeLang; trim ValidLanguages to 14 and update its count assertion
  • Update --lang help text to describe preference semantics instead of "interactive prompts"

Test Plan

  • make unit-test passed (per-package; the aggregate run is resource-constrained in the sandbox, every package passes in isolation — cmd/config, internal/i18n, plus 16/16 sequential ok)
  • validate passed (build / vet / integration; unit-test caveat above)
  • local-eval skipped: lite mode (E2E sandbox not run); skillave N/A (no shortcut/skill/meta changes)
  • acceptance-reviewer passed (2/2 scenarios — core happy-path + invalid-lang exit-2)
  • manual verification: 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 --help lists exactly 14 codes

Related Issues

N/A

Summary by CodeRabbit

  • New Features

    • Persistent language preference via --lang and a separate per-invocation TUI display language (defaults to zh)
    • Interactive language picker sets the session TUI language; TUI copy uses session language while machine-readable output uses the persisted preference
    • Stderr confirmation shown on successful change only when --lang was explicitly provided
  • Bug Fixes / Validation

    • Case-sensitive validation of allowed language codes with clear, enumerated errors
  • Tests

    • Expanded tests for validation, fallback, picker behavior, and confirmation output

Review Change Stack

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)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62359b15-6bcb-47ee-a11f-e9491eb8aadd

📥 Commits

Reviewing files that changed from the base of the PR and between 1574074 and 0794bc9.

📒 Files selected for processing (1)
  • cmd/config/config_test.go

📝 Walkthrough

Walkthrough

The PR separates persistent language preference (Lang) from per-invocation TUI display language (UILang), adds an i18n validation module, updates message templates and confirmation behavior, and expands tests for bind and init commands.

Changes

Language Preference Separation in Config Commands

Layer / File(s) Summary
i18n language validation module
internal/i18n/lang.go, internal/i18n/lang_test.go
New module exports ValidLanguages and IsValidLang; tests verify case-sensitive validation and exact supported-language count.
Core config model updates
internal/core/config.go
CliConfig gains Lang field for resolved UI language, populated from multi-app config in ResolveConfigFromMulti.
Config bind options and flags
cmd/config/bind.go
BindOptions extended with Lang (persistent), langExplicit (flag tracking), and UILang (TUI rendering); i18n import added; NewCmdConfigBind sets default UILang; --lang flag help updated.
Config bind validation and TUI rendering
cmd/config/bind.go
validateBindFlags enforces --lang validation against ValidLanguages and normalizes empty Lang to "zh" when non-explicit; TUI picker updates both opts.Lang and opts.UILang when appropriate; TUI messages and brand displays use UILang.
Config bind messages, JSON envelope, and templates
cmd/config/bind_messages.go, cmd/config/bind.go
bindMsg adds LangPreferenceSet; TUI success banner uses UILang and conditionally prints confirmation when --lang was explicit; non-TUI JSON messages use Lang; localized templates updated.
Config init options and flags
cmd/config/init.go
ConfigInitOptions extended with UILang; NewCmdConfigInit defaults UILang to "zh"; validateInitLang added and invoked early in RunE.
Config init validation and confirmation across paths
cmd/config/init.go
printLangPreferenceConfirmation prints stderr confirmation only when --lang explicit; confirmation invoked in non-interactive, --new, interactive TUI, and readline fallback success paths; interactive picker sets both Lang and UILang.
Config init message templates and picker refactor
cmd/config/init_messages.go, cmd/config/init_messages_test.go
initMsg gains LangPreferenceSet; promptLangSelection refactored to no-arg defaulting to "zh"; tests updated and new tests added for bilingual collapse and ValidLanguages validation.
Config init invalid language validation test
cmd/config/config_test.go
TestConfigInitCmd_InvalidLang table-driven test verifies config init --lang rejects invalid inputs with ExitValidation error and an error message containing "invalid --lang".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • liangshuo-1

🐰 Two tongues now split, one deep, one bright—
Lang persists, while UILang lights the sight,
Picker whispers zh when no flag is shown,
Confirmations printed when choices are known.
Tests hop in to guard each bilingual line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: decoupling the persistent language preference from TUI display language is the core architectural change reflected across the codebase.
Description check ✅ Passed The description comprehensively covers all required sections: Summary explains motivation and scope; Changes lists main modifications; Test Plan documents verification approach with all checks (unit tests, validation, acceptance, manual verification); Related Issues section included.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cliconfig-lang-multilang

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 27, 2026

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label May 27, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

❌ Patch coverage is 67.79661% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.32%. Comparing base (aea9f37) to head (0794bc9).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
cmd/config/bind.go 60.71% 11 Missing ⚠️
cmd/config/init.go 75.00% 6 Missing ⚠️
cmd/config/init_messages.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1132      +/-   ##
==========================================
+ Coverage   67.85%   68.32%   +0.47%     
==========================================
  Files         592      620      +28     
  Lines       55373    57461    +2088     
==========================================
+ Hits        37574    39262    +1688     
- Misses      14685    14959     +274     
- Partials     3114     3240     +126     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@0794bc981ef7b362cf777d864192c0f455f7762e

🧩 Skill update

npx skills add larksuite/cli#feat/cliconfig-lang-multilang -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Include LangPreferenceSet in the non-empty message field assertions.

The new initMsg.LangPreferenceSet field isn’t validated by assertAllFieldsNonEmpty, 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 win

Cover 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 ValidLanguages and verifies each returns true to 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 win

Clarify CliConfig.Lang as 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

📥 Commits

Reviewing files that changed from the base of the PR and between 70081f6 and 066543c.

📒 Files selected for processing (10)
  • cmd/config/bind.go
  • cmd/config/bind_messages.go
  • cmd/config/bind_test.go
  • cmd/config/config_test.go
  • cmd/config/init.go
  • cmd/config/init_messages.go
  • cmd/config/init_messages_test.go
  • internal/core/config.go
  • internal/i18n/lang.go
  • internal/i18n/lang_test.go

Comment thread cmd/config/config_test.go
Comment thread cmd/config/init.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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
cmd/config/config_test.go (1)

460-468: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Isolate config state in the newly added tests.

Please set LARKSUITE_CLI_CONFIG_DIR to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3d3d42c and 1574074.

📒 Files selected for processing (2)
  • cmd/config/bind_test.go
  • cmd/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).
@luozhixiong01
Copy link
Copy Markdown
Collaborator Author

Reviewer summary

This PR decouples the --lang flag from the command's TUI display language, so --lang is now a pure, strictly-validated preference while the on-screen language is controlled separately.

What changed (behavior)

  • --lang = preference only. It sets the persisted Lang and the JSON envelope message (consumed by downstream AI agents). It no longer changes what language the interactive prompts render in.
  • TUI display language = UILang (new field, default zh). Only the interactive picker writes it; the picker is reduced to 2 options (中文 / English).
  • Strict validation. Invalid --lang (empty / wrong case / not in the 14-code i18n.ValidLanguages) now exits 2 with a structured validation error, instead of silently falling back.
  • Confirmation line. When --lang is passed explicitly, a "language preference set" line is printed to stderr.
  • The JSON envelope message intentionally follows Lang (preference), while the stderr banner follows UILang — different audiences (agent vs human).

Key design point

message (stdout JSON, for agents) → follows Lang. The stderr banner (for humans) → follows UILang. This split is the whole point of the refactor; message-follows-Lang is pre-existing behavior, preserved.

Verification

  • All CI green: unit-test, lint, security, CodeQL Analyze, e2e-live, license/cla.
  • codecov/patch 67.79% (target 60%); codecov/project 68.32% (+0.47%).
  • CodeRabbit review addressed: picker errors wrapped in output.Errorf (bind + init), test isolation, LangPreferenceSet non-empty assertion, ValidLanguages drift guard, Lang comment clarified.
  • Remaining uncovered patch lines are interactive picker / TUI form paths that unit tests can't exercise.

Ready for review — the only gate left is approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants