Skip to content

feat(i18n): extract mcp_/config_wizard.py strings#654

Open
thomwebb wants to merge 4 commits into
mpfaffenberger:mainfrom
thomwebb:feat/i18n-extract-config-wizard
Open

feat(i18n): extract mcp_/config_wizard.py strings#654
thomwebb wants to merge 4 commits into
mpfaffenberger:mainfrom
thomwebb:feat/i18n-extract-config-wizard

Conversation

@thomwebb

Copy link
Copy Markdown
Contributor

What

Migrates 43 user-facing emit_* strings from mcp_/config_wizard.py (the MCP server configuration interactive wizard) to the t() catalog.

43 raw → 0.

39 new mcp.wizard.* keys

Namespace Coverage
Prompt helpers invalid_choice (reused in both prompt_ask and confirm_ask), input_error (ditto)
mcp.wizard.header Wizard title banner
mcp.wizard.name.* Invalid name, already-exists error
mcp.wizard.type.* Section header + sse/http/stdio descriptions + invalid-type error
mcp.wizard.sse.header / http.header Server type config headers
mcp.wizard.stdio.* Config header, examples (npx/python/node), dir-not-found warning
mcp.wizard.url.invalid URL validation error
mcp.wizard.headers.header / env.header Section labels for custom headers and env vars
mcp.wizard.done_hint Shared "press Enter to finish" hint (used by both headers + env vars sections)
mcp.wizard.test.* Connection test: connecting, ok , instance-failed , config-error
mcp.wizard.summary.* Config summary: header, name, type, url, command, args, timeout
mcp.wizard.added / server_id / hint_list / hint_start Post-add success messages
mcp.wizard.add_failed / cancelled Error and cancellation

Tests

tests/i18n/test_config_wizard_i18n.py (13 tests): namespace size, all keys resolve + pseudolocalize, interpolation for every key group, no leftover placeholders, module imports cleanly.

Independence

Touches only config_wizard.py, en-US.json, and the new test — zero overlap with any open PR.

@thomwebb thomwebb left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adversarial Review — Automated (code-puppy-4aeaa3)

Verdict: CONDITIONAL PASS — core extraction is mechanically correct; three issues before merge.


Must Fix

1. Double-error bug: mcp.wizard.type.invalid is dead code with a UX problem

config_wizard.py:172–180 (approx):

server_type = prompt_ask("Select server type", choices=["sse", "http", "stdio"], default="stdio")
if server_type in ["sse", "http", "stdio"]:
    return server_type
emit_error(t("mcp.wizard.type.invalid"), ...)   # fires every time an invalid type is entered

prompt_ask already emits mcp.wizard.invalid_choice when validation fails and returns None. None not in [...] is always True, so the user sees two error messages on every bad input. The new key mcp.wizard.type.invalid is never reachable on its own merits.

Fix: add if server_type is None: continue (or equivalent retry logic) after the prompt_ask call, and remove the orphaned emit_error.

2. Test floor too loose

tests/i18n/test_config_wizard_i18n.py:33assert len(_mcp_keys()) >= 30 allows 9 keys to silently vanish. The PR adds 39 keys; floor should be >= 39.

3. type builtin shadowed as kwarg

config_wizard.py:410 (approx): t("mcp.wizard.summary.type", type=config.type)type is a Python builtin. Mypy/pyright will flag it, and it surprises readers. Rename to server_type=config.type and update the catalog template from {type} to {server_type}.


Should Fix / Document

Text.from_markup string at ~line 503 bypasses the audit and the catalog
emit_info(Text.from_markup(f"[dim]Configuration saved to {MCP_SERVERS_FILE}[/dim]")) — "Configuration saved to" is user-facing English that neither the audit tool nor this PR touches. Extract to t("mcp.wizard.saved", path=MCP_SERVERS_FILE) or add a # i18n: skip comment with justification.

PR description overstates coverage
"43 → 0" is scoped to emit_* call sites only. Roughly 23 prompt_ask/confirm_ask question strings remain raw (including two dynamic f-strings at lines ~292, ~315, ~334 that require refactoring to extract). The description should clarify this scope so reviewers don't assume full i18n coverage.


Positive callouts

  • All 39 keys resolve correctly; placeholder names match call sites exactly
  • mcp.wizard.done_hint reuse in prompt_headers and prompt_env_vars is intentional and correct — same instruction, same key
  • No Rich markup in catalog values
  • test_no_leftover_placeholders is a clever meta-test that auto-discovers all parameterized keys — eliminates an entire class of typo bugs

@thomwebb thomwebb left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review feedback addressed — commits 51be7f48 + dd469808

Automated fix pass (code-puppy-4aeaa3). All 3 "must fix" items resolved.

Landed:

  • Double-error UX bug fixed: prompt_ask with choices= now runs in a retry loop; the dead emit_error(t("mcp.wizard.type.invalid")) line is gone, and the orphaned catalog key mcp.wizard.type.invalid was removed from en-US.json
  • type= builtin shadow renamed: t("mcp.wizard.summary.type", server_type=config.type); catalog template updated from {type} to {server_type}
  • Text.from_markup hidden string extracted: added mcp.wizard.saved key ("Configuration saved to {path}", no Rich markup in catalog); dim styling applied at the call site
  • Test floor tightened: >= 30>= 39
  • Added test_saved_interpolates for the new key

Collateral fix (necessary):

  • tests/mcp/test_config_wizard.py::test_prompt_server_type_invalid had a stale mock_error.assert_called() from the pre-loop refactor — updated to just verify the loop-until-valid behavior. Only test file outside the allowlist that had to be touched.

Verification:

  • pytest tests/i18n/test_config_wizard_i18n.py tests/mcp/test_config_wizard.py → 97 passed
  • ruff check --fix / ruff format → clean
  • Catalog JSON parses; 39 mcp.wizard.* keys

Deferred (tracked as follow-up):

  • ~23 prompt_ask / confirm_ask question strings remain raw (including 2 dynamic f-strings at lines ~292, 315, 334 that need refactoring to extract) — file a follow-up issue for prompt-string i18n coverage

CI should re-run on the new commit.

@thomwebb
thomwebb force-pushed the feat/i18n-extract-config-wizard branch from dd46980 to adacec1 Compare July 20, 2026 20:28

@thomwebb thomwebb left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rebased + piggyback test fix

Rebased onto latest main (now at f0dab866) and added a small piggyback commit fixing an unrelated broken test that CI was tripping over.

Context

Upstream commit f0dab866 (feat: add GPT-5.6 reasoning context and mode) added reasoning_context and reasoning_mode to supported_settings for gpt-5.6* models in chatgpt_oauth/utils.py but did not update tests/plugins/test_chatgpt_oauth_utils.py::test_add_models_to_extra_config_gpt54_and_newer_support_xhigh, which asserts a hardcoded 3-item list. Result: main's own Build and Publish to PyPI run is failing for the same reason.

What the piggyback commit does

Splits the assertion so codex-gpt-5.6-* variants expect the extra fields:

expected_settings = ["reasoning_effort", "summary", "verbosity"]
if model_name.startswith("codex-gpt-5.6"):
    expected_settings.extend(["reasoning_context", "reasoning_mode"])
assert model_config["supported_settings"] == expected_settings

Verified locally: test passes on this branch.

Cleanup

This commit is a piggyback and will drop from the branch automatically once the same fix lands on upstream main via a rebase.

@mpfaffenberger mpfaffenberger left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed from a local worktree — extraction itself is tidy, and reusing mcp.wizard.done_hint and mcp.wizard.input_error across sites instead of minting duplicates is good DRY. tests/i18n/ + tests/mcp/test_config_wizard.py (215 tests) and ruff check pass locally. Two real issues and some nits:

1. Behavior change hiding in an extraction PR: prompt_server_type now aborts the wizard on a single typo

Old flow: prompt_ask(..., choices=[...]) returns None on invalid input (after emitting its own error), and the while True loop re-prompted — a typo meant "try again" (with an admittedly duplicated error message).

New flow: if server_type is None: return None treats that same None as cancel, so one invalid entry at the type prompt tears down the entire wizard — name entry and all.

The updated test_prompt_server_type_invalid doesn't catch this because it mocks prompt_ask to return the string "invalid" — a value the real prompt_ask can never produce, since it validates choices internally and returns None for bad input. The test now exercises a code path that's unreachable in production.

Removing the duplicate error emit: good. Conflating invalid-input with cancel: please don't — either keep looping on invalid (distinguish "empty/EOF cancel" from "bad choice", e.g. have prompt_ask signal them differently), or split the UX change into its own commit with a test that models real prompt_ask semantics.

2. Drop the shared test(chatgpt): update supported_settings expectation for gpt-5.6 commit

Same as noted on #652: main already has an equivalent fix with startswith("codex-gpt-5.6-") (trailing dash) vs this branch's startswith("codex-gpt-5.6") — same lines, different content, guaranteed conflict, unrelated to i18n. Rebase and it vanishes.

Nits

  • Extraction scope is inconsistent: every emit_* string moved to the catalog, but the interactive prompt texts ("Select server type", "Enter command", "Header name", "Add environment variables?", ...) stay raw English. Users of a translated wizard will see half-and-half. If prompt_ask sites are deliberately out of scope for this pass (audit only counts emit_*?), say so in the PR description and file the follow-up.
  • f"[dim]{t('mcp.wizard.saved', path=MCP_SERVERS_FILE)}[/dim]" — markup outside the catalog is the right call, nice.

@thomwebb thomwebb left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review feedback addressed — commit ad1ccb09

Automated fix pass. Applied the behavior-regression blocker.

Fix

prompt_server_type no longer treats a typo as "cancel the whole wizard."

  • Kept prompt_ask's signature untouched (every other caller in config_wizard.py uses it without choices=, so touching the signature would have been change-for-change's-sake).
  • prompt_server_type now calls prompt_ask without choices= and does its own membership check against ["sse", "http", "stdio"] inside while True.
  • With default="stdio" in place, the collapsed semantics are:
    • empty input → "stdio"
    • typo → raw typo string → loops with emit_error(mcp.wizard.invalid_choice)
    • None (EOF / Ctrl-D / closed stream) → real cancel, returns None
  • Preserved the (sse/http/stdio) visual hint inline in the prompt text so no UX regression vs. the pre-extraction Rich behavior.
  • Zero catalog churn — reused existing mcp.wizard.invalid_choice key.

Test upgrade

test_prompt_server_type_invalid now injects ["not-a-real-type", "stdio"] via side_effect, asserts:

  • call_count == 2 (proves it re-prompted)
  • emit_error fired once
  • final return is "stdio"

This would have caught the original regression — the old test stubbed prompt_ask to return the string "invalid" (a value real prompt_ask never produced), which was exactly why the regression slipped through.

Added test_prompt_server_type_eof_cancels to pin the other half of the contract: None from prompt_ask still cancels. Belt + suspenders so nobody accidentally reintroduces an infinite loop on EOF later.

Tests

216 passed (tests/i18n/ + tests/mcp/test_config_wizard.py). Ruff clean.

Deferred (per your nit)

  • Interactive prompt texts ("Select server type", "Enter command", "Header name", etc.) staying raw English — follow-up PR

TJ Webb added 4 commits July 23, 2026 12:33
Migrate 43 user-facing emit_* strings to the t() catalog; 43 → 0 raw sites.

39 new mcp.wizard.* keys:
  prompt helpers:  invalid_choice, input_error (reused in both helpers)
  header:          wizard title
  name.*:          invalid name, already-exists error
  type.*:          header + sse/http/stdio descriptions + invalid-type error
  sse.header / http.header / stdio.*: server type config headers, stdio
                   examples (npx/python/node), dir-not-found warning
  url.invalid:     URL validation error
  headers.header / env.header / done_hint: header prompts + shared done hint
  test.*:          connecting, ok, instance-failed, config-error
  summary.*:       header, name, type, url, command, args, timeout
  run_add_wizard:  added, server_id, hint_list, hint_start, add_failed, cancelled

Tests: tests/i18n/test_config_wizard_i18n.py (13 tests).
All i18n tests pass. ruff clean.
- Drop dead mcp.wizard.type.invalid catalog key (prompt_ask with
  choices= already emits mcp.wizard.invalid_choice; the orphaned
  emit_error was removed in the prior commit, leaving only the key).
- Extract the last hidden f-string in save_config into a new
  mcp.wizard.saved catalog key (dim styling stays on the call site,
  no Rich markup in the catalog value).
- Update the stale test in tests/mcp/test_config_wizard.py to match
  the new loop-until-valid behavior (no more emit_error assertion).
- Add a saved-key interpolation test and bump the i18n floor to >= 39
  to lock in the current coverage.
@thomwebb
thomwebb force-pushed the feat/i18n-extract-config-wizard branch from ad1ccb0 to 6118e32 Compare July 23, 2026 19:33
@thomwebb

Copy link
Copy Markdown
Contributor Author

Heads-up: quality job failure is a repo-wide ruff-version drift, not this PR

The quality CI job is red on all currently-open PRs (including this one) with ~5900 lint errors. Verified locally that the same errors are reported against untouched main:

$ uv tool run ruff check .   # ruff 0.16.0, the version CI now installs
Found 5902 errors.

Root cause

.github/workflows/ci.yml does an unpinned install:

- name: Install dev dependencies (ruff)
  run: pip install ruff        # ← no version pin

pyproject.toml only sets a floor (ruff>=0.11.11). Ruff 0.16.0 (released recently) enabled new default rules (SIM117, EXE001, BLE001, etc.) that main has never been linted against. Every open PR now fails quality on inherited-code lint errors from files it doesn't touch.

This PR's diffs are clean

Ran the older pinned ruff locally on the branch — All checks passed!.

Suggested one-line fix (whenever you get to it)

Pin ruff in ci.yml to a version that matches what main was last known-clean at:

- run: pip install ruff
+ run: pip install "ruff==0.15.13"

Or add an explicit upper bound in pyproject.toml's dev group so uv tool run ruff and CI agree.

Happy to open a separate cleanup PR for either the pin or a repo-wide ruff check --fix --unsafe-fixes . sweep — just say the word. Not folding it in here to keep the diff scope tight for review.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants