feat(runtime-api): serve settings schema labels in the client's ?locale=

GET /v1/settings/schema resolved every label, description and option label
through the Engine's own configured `locale`, falling back to LC_ALL /
LC_MESSAGES / LANG and then English. The desktop resolves its interface
language the same way but ends with the operating system's preferred
languages. A desktop opened from the Finder or the Dock has no LANG, so on a
Chinese macOS with `locale` unset the app drew Chinese chrome around English
Settings rows served by its own Engine child.

The route now takes an optional `?locale=<tag>`. A non-empty tag goes through
`resolve_locale`, so a shipped pack answers, per-key English fallback still
applies, and an unknown tag answers in English. With no tag, or an empty one,
the configured `locale` resolves as before. This is ops launch packet item 18
as recommended ("the desktop passes its UI locale"); codewhale-app sends it in
the matching commit. An older client sends nothing and sees no change.

Evidence:
- Live: target/debug/codewhale app-server --http with an empty HOME and no
  LANG. No query and ?locale=en -> "Compact at" / "Input history" / "Language";
  ?locale=zh-Hans -> 压缩阈值 / 输入历史 / 语言; ?locale=ja -> コンパクトしきい値 /
  入力履歴 / 言語; ?locale= and ?locale=xx-bogus -> English.
- cargo test -p codewhale-tui --lib settings_schema: 3 passed, 0 failed
  (15348 filtered out).
- npm test: node 413 pass / 7 skipped / 0 fail and 7/7, vitest 772/772 in
  74 files; npm run check:web: exit 0; cargo fmt --check: clean.
- CI-form clippy (--workspace --all-targets --all-features --locked, -D
  warnings with the CI allows): clean.

Refs CW-7406

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-16 16:27:13 -07:00
1 parent 5197725295
commit 5265a385dc
2 files changed
+21 -2

No files matched your search

+6
View File
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- `GET /v1/settings/schema` accepts `?locale=<tag>` and resolves row labels,
descriptions and option labels in that locale, with the configured
`locale` as the default. The desktop sends the interface language it
shows, so a desktop opened from the Finder or the Dock on a Chinese
system, which has no `LANG`, no longer lists its Settings rows in English
under Chinese headings (Refs CW-7406).
- `codewhale --continue` reopens a session whose terminal was closed or
killed mid-turn. It used to print "Recovered interrupted session" and then
exit with "session … is open in an interactive Codewhale session": the
+15 -2
View File
@@ -12093,7 +12093,8 @@ async fn set_config(
/// This route projects it for HTTP clients — current values resolved from
/// the owning store (settings.toml via [`crate::settings::Settings`],
/// config.toml, or the notifications table), labels and descriptions
/// resolved through the locale pack. Writes stay on `POST /v1/config`;
/// resolved through the locale pack: the `?locale=` the client displays,
/// else the configured `locale`. Writes stay on `POST /v1/config`;
/// this route never invents a value, an option, or a validator.
#[derive(Debug, Serialize)]
struct SettingsSchemaResponse {
@@ -12210,8 +12211,14 @@ fn toml_value_at_path<'a>(document: &'a toml::Value, segments: &[&str]) -> Optio
Some(current)
}
#[derive(Debug, Deserialize)]
struct SettingsSchemaQuery {
locale: Option<String>,
}
async fn get_settings_schema(
State(state): State<RuntimeApiState>,
Query(query): Query<SettingsSchemaQuery>,
) -> Result<Json<SettingsSchemaResponse>, ApiError> {
use codewhale_config::notifications::NotificationSetting;
use codewhale_config::settings_schema::{
@@ -12221,7 +12228,13 @@ async fn get_settings_schema(
let config = state.config.read().clone();
let settings = crate::settings::Settings::load_persisted().unwrap_or_default();
let locale = resolve_locale(&settings.locale);
let locale = resolve_locale(
query
.locale
.as_deref()
.filter(|tag| !tag.trim().is_empty())
.unwrap_or(&settings.locale),
);
let notifications = config.notifications_config();
// Conditional pairs share the TUI's rule: exactly one member is shown,