From 930d8ea1ab1ec660dc8a4570a963357f015ffcac Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Mon, 20 Jul 2026 10:31:17 -0700 Subject: [PATCH 1/8] feat(i18n): extract claude_code_oauth/register_callbacks.py strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate 49 user-facing emit_* strings to the t() catalog; 49 → 0 raw sites. Key groupings (48 new oauth.* keys): oauth.server.* — callback server startup, ports, redirect-URI, paste hint oauth.pasteback.* — paste-back parse/provider errors, no-code, no-state oauth.state_mismatch / oauth.callback.* — state check, callback error/timeout oauth.browser.* — headless mode, opening, fallback URL, manual URL, open-failed oauth.auth.* — token exchange, save, success, model discovery oauth.reauth.* — refresh-failed, restored, no-token oauth.cmd.auth.* — starting, overwrite-warning oauth.cmd.status.* — authenticated, expires, models, no-models, not-authenticated, hint oauth.cmd.fast.* — wrong-model, enabled, enabled-detail, disabled oauth.cmd.logout.* — tokens-removed, models-removed, success oauth.model.* — no-api-key Tests: tests/i18n/test_claude_oauth_i18n.py (14 tests). All i18n tests pass. ruff clean. --- code_puppy/i18n/locales/en-US.json | 76 ++++++++-- .../claude_code_oauth/register_callbacks.py | 117 +++++++-------- tests/i18n/test_claude_oauth_i18n.py | 133 ++++++++++++++++++ 3 files changed, 243 insertions(+), 83 deletions(-) create mode 100644 tests/i18n/test_claude_oauth_i18n.py diff --git a/code_puppy/i18n/locales/en-US.json b/code_puppy/i18n/locales/en-US.json index 485e58bba..24cf9a408 100644 --- a/code_puppy/i18n/locales/en-US.json +++ b/code_puppy/i18n/locales/en-US.json @@ -1,6 +1,6 @@ { "startup.welcome": "Welcome to Code Puppy, {name}!", - "startup.ready": "Ready to fetch some code. \ud83d\udc36", + "startup.ready": "Ready to fetch some code. 🐶", "locale.set": "Locale set to {locale}.", "locale.current": "Current locale: {locale}", "files.deleted": { @@ -22,21 +22,21 @@ "version.undetected": "Could not detect current version, using fallback", "confirm.yes": "Yes", "confirm.no": "No", - "cli.loading": "\ud83d\udc36 Code Puppy is Loading...", + "cli.loading": "🐶 Code Puppy is Loading...", "cli.error.no_ports": "No available ports in range 8090-9010!", - "cli.error.model_transient": "\ud83d\udd0c The model connection hit a transient error ({error_type}) and didn't recover after auto-retries. This is almost always a VPN/WiFi/provider blip \u2014 just re-run your last prompt. Your session history is intact.", + "cli.error.model_transient": "🔌 The model connection hit a transient error ({error_type}) and didn't recover after auto-retries. This is almost always a VPN/WiFi/provider blip — just re-run your last prompt. Your session history is intact.", "cli.version.update_disabled": "Update phase disabled because NO_VERSION_UPDATE is set to 1 or true", "cli.model.not_found": "Model '{model}' not found", "cli.model.available": "Available models: {models}", - "cli.model.using": "\ud83c\udfaf Using model: {model}", + "cli.model.using": "🎯 Using model: {model}", "cli.model.validate_error": "Error validating model: {error}", "cli.agent.not_found": "Agent '{agent}' not found", "cli.agent.available": "Available agents: {agents}", - "cli.agent.using": "\ud83e\udd16 Using agent: {agent}", + "cli.agent.using": "🤖 Using agent: {agent}", "cli.agent.set_error": "Error setting agent: {error}", "cli.agent.cancelling": "Cancelling running agent task...", "cli.agent.task_cancelled": "Agent task cancelled", - "cli.resume.quick_searching": "\ud83d\udd0d Quick Resume selected - finding latest session for {scope}", + "cli.resume.quick_searching": "🔍 Quick Resume selected - finding latest session for {scope}", "cli.resume.none_found": "No previous session found for this scope; starting fresh.", "cli.resume.available_sessions": "Available sessions: {sessions}", "cli.resume.created": "Created new session: {session}", @@ -47,7 +47,7 @@ "cli.help.commands": "Type /help to view all commands", "cli.help.completion": "Type @ for path completion, or /model to pick a model. Toggle multiline with Alt+M or F2; newline: Shift+Enter.", "cli.help.paste_images": "Paste images: Ctrl+V (even on Mac!), F3, or /paste command.", - "cli.help.macos_paste": "\ud83d\udca1 macOS tip: Use Ctrl+V (not Cmd+V) to paste images in terminal.", + "cli.help.macos_paste": "💡 macOS tip: Use Ctrl+V (not Cmd+V) to paste images in terminal.", "cli.help.cancel_key": "Press {cancel_key} during processing to cancel the current task or inference.", "cli.help.editor_shortcuts": "Use Ctrl+X Ctrl+E to open $EDITOR (Notepad on Windows); Ctrl+X Ctrl+B to background running shell commands; Ctrl+X Ctrl+X to kill running shell commands.", "cli.help.autosave_load": "Use /autosave_load to manually load a previous autosave session.", @@ -56,20 +56,20 @@ "cli.help.shell_passthrough": "! to run shell commands directly (e.g., !git status)", "cli.prompt.enter_task": "Enter your coding task:", "cli.initial_command.processing": "Processing initial command: {command}", - "cli.initial_command.continuing": "\ud83d\udc36 Continuing in Interactive Mode", + "cli.initial_command.continuing": "🐶 Continuing in Interactive Mode", "cli.initial_command.preserved": "Your command and response are preserved in the conversation history.", "cli.initial_command.error": "Error processing initial command: {error}", "cli.prompt_toolkit.installing": "Warning: prompt_toolkit not installed. Installing now...", "cli.prompt_toolkit.installed": "Successfully installed prompt_toolkit", "cli.prompt_toolkit.install_error": "Error installing prompt_toolkit: {error}", "cli.prompt_toolkit.fallback": "Falling back to basic input without tab completion", - "cli.queue.running": "\u23ed running queued prompt", + "cli.queue.running": "⏭ running queued prompt", "cli.input.cancelled": "Input cancelled", "cli.goodbye_ctrld": "Goodbye! (Ctrl+D)", "cli.goodbye": "Goodbye!", "cli.command.error": "Command error: {error}", "cli.autosave.load_cancelled": "Autosave load cancelled", - "cli.autosave.loaded": "\u2705 Autosave loaded: {messages} messages ({tokens} tokens)", + "cli.autosave.loaded": "✅ Autosave loaded: {messages} messages ({tokens} tokens)", "cli.autosave.load_failed": "Failed to load autosave: {error}", "cli.autosave.headless_unsupported": "/autosave_load requires interactive mode; use -r SESSION in headless mode.", "cli.turn.cancelled": "Cancelled", @@ -83,7 +83,7 @@ "cli.headless.cancelled": "Execution cancelled by user", "cli.headless.error": "Error executing prompt: {error}", "cli.headless.save_failed": "Failed to save session {session}: {error}", - "cli.autosave.loaded_path": "\ud83d\udcc1 From: {path}", + "cli.autosave.loaded_path": "📁 From: {path}", "cfg.set.key_required": "You must supply a key.", "cfg.set.yolo_config_unchanged": "Using YOLO mode from puppy.cfg; configuration unchanged.", "cfg.set.success": "Set {key} = \"{value}\" in puppy.cfg!", @@ -106,9 +106,9 @@ "cfg.unpin.reload_failed": "Model unpinned but reload failed: {error}", "cfg.unpin.failed": "Failed to unpin model from agent '{agent}': {error}", "cfg.diff.apply_failed": "Failed to apply diff settings: {error}", - "cfg.colors.saved": "Banner colors saved! \ud83c\udfa8", + "cfg.colors.saved": "Banner colors saved! 🎨", "cfg.colors.apply_failed": "Failed to apply banner color settings: {error}", - "cfg.colors.all_available": "\ud83c\udfa8 All Available Rich Colors:", + "cfg.colors.all_available": "🎨 All Available Rich Colors:", "cfg.colors.usage": "Usage: /diff {{color_type}} ", "cfg.colors.white_text_note": "All diffs use white text on your chosen background colors", "cfg.colors.hex_note": "You can also use hex colors like #ff0000 or rgb(255,0,0)", @@ -123,5 +123,53 @@ "codex.imagegen.usage": "Usage: /codex-imagegen ", "codex.imagegen.generating": "Generating image with Codex OAuth...", "codex.imagegen.saved": "Generated image saved to {path}", - "codex.logout.reload_failed": "ChatGPT logout: agent reload failed: {error}" + "codex.logout.reload_failed": "ChatGPT logout: agent reload failed: {error}", + "oauth.server.no_ports": "Could not start OAuth callback server; all candidate ports are in use", + "oauth.server.redirect_uri_error": "Failed to assign redirect URI for OAuth flow: {error}", + "oauth.server.pasteback_mode": "Continuing Claude Code OAuth in paste-back mode.", + "oauth.server.no_redirect_uri": "Failed to assign redirect URI for OAuth flow", + "oauth.server.listening": "Listening for callback on {uri}", + "oauth.server.pasteback_uri": "Using redirect URI for paste-back: {uri}", + "oauth.server.paste_hint": "If localhost cannot be reached, paste the full callback URL or authorization code here and press Enter.", + "oauth.pasteback.parse_error": "Could not parse pasted OAuth input: {error}", + "oauth.pasteback.provider_error": "OAuth provider returned an error: {message}", + "oauth.pasteback.no_code": "Pasted OAuth input did not contain an authorization code.", + "oauth.pasteback.no_state": "Pasted OAuth input did not include state; continuing with this login attempt.", + "oauth.state_mismatch": "State mismatch detected; aborting authentication.", + "oauth.callback.error": "OAuth callback error: {error}", + "oauth.callback.timeout": "OAuth callback timed out. Please try again.", + "oauth.browser.headless": "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…", + "oauth.browser.headless_url": "In normal mode, would visit: {url}", + "oauth.browser.opening": "Opening browser for Claude Code OAuth…", + "oauth.browser.fallback_url": "If it doesn't open automatically, visit: {url}", + "oauth.browser.open_failed": "Failed to open browser automatically: {error}", + "oauth.browser.manual_url": "Please open the URL manually: {url}", + "oauth.auth.exchanging": "Exchanging authorization code for tokens…", + "oauth.auth.exchange_failed": "Token exchange failed. Please retry the authentication flow.", + "oauth.auth.save_failed": "Tokens retrieved but failed to save locally. Check file permissions.", + "oauth.auth.success": "Claude Code OAuth authentication successful!", + "oauth.auth.no_access_token": "No access token returned; skipping model discovery.", + "oauth.auth.fetching_models": "Fetching available Claude Code models…", + "oauth.auth.no_models": "Claude Code authentication succeeded but no models were returned.", + "oauth.auth.discovered_models": "Discovered {count} models: {models}", + "oauth.auth.models_added": "Claude Code models added to your configuration. Use the `claude-code-` prefix!", + "oauth.reauth.refresh_failed": "Claude Code OAuth refresh failed; launching the browser sign-in flow again.", + "oauth.reauth.restored": "Claude Code OAuth restored. Retrying the failed request…", + "oauth.reauth.no_token": "Claude Code OAuth reauthentication did not produce a usable token.", + "oauth.cmd.auth.starting": "Starting Claude Code OAuth authentication…", + "oauth.cmd.auth.overwrite_warning": "Existing Claude Code tokens found. Continuing will overwrite them.", + "oauth.cmd.status.authenticated": "Claude Code OAuth: Authenticated", + "oauth.cmd.status.expires": "Token expires in ~{hours}h {minutes}m", + "oauth.cmd.status.models": "Configured Claude Code models: {models}", + "oauth.cmd.status.no_models": "No Claude Code models configured yet.", + "oauth.cmd.status.not_authenticated": "Claude Code OAuth: Not authenticated", + "oauth.cmd.status.hint": "Run /claude-code-auth to begin the browser sign-in flow.", + "oauth.cmd.fast.wrong_model": "Fast mode only applies to Claude Code models. Switch to a claude-code-* model first.", + "oauth.cmd.fast.enabled": "Fast mode ENABLED for {model}", + "oauth.cmd.fast.enabled_detail": "Injecting speed=fast into payloads and fast-mode-2026-02-01 beta header.", + "oauth.cmd.fast.disabled": "Fast mode DISABLED for {model}", + "oauth.cmd.logout.tokens_removed": "Removed Claude Code OAuth tokens", + "oauth.cmd.logout.models_removed": "Removed {count} Claude Code models from configuration", + "oauth.cmd.logout.success": "Claude Code logout complete", + "oauth.model.no_api_key": "API key is not set for Claude Code endpoint; skipping model '{model}'." } diff --git a/code_puppy/plugins/claude_code_oauth/register_callbacks.py b/code_puppy/plugins/claude_code_oauth/register_callbacks.py index d2b1695bc..a68efee5b 100644 --- a/code_puppy/plugins/claude_code_oauth/register_callbacks.py +++ b/code_puppy/plugins/claude_code_oauth/register_callbacks.py @@ -15,6 +15,7 @@ from urllib.parse import parse_qs, urlparse from code_puppy.callbacks import register_callback +from code_puppy.i18n import t from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning from code_puppy.model_switching import set_model_and_reload_agent from code_puppy.provider_identity import ( @@ -146,17 +147,15 @@ def _parse_pasted_callback(context: OAuthContext, raw_input: str) -> Optional[st return None if not parsed.code: - emit_error("Pasted OAuth input did not contain an authorization code.") + emit_error(t("oauth.pasteback.no_code")) return None if parsed.state: if parsed.state != context.state: - emit_error("State mismatch detected; aborting authentication.") + emit_error(t("oauth.state_mismatch")) return None else: - emit_warning( - "Pasted OAuth input did not include state; continuing with this login attempt." - ) + emit_warning(t("oauth.pasteback.no_state")) return parsed.code @@ -174,11 +173,11 @@ def _wait_for_callback_or_paste( while elapsed < timeout: if event and event.is_set() and result: if result.error: - emit_error(f"OAuth callback error: {result.error}") + emit_error(t("oauth.callback.error", error=result.error)) return None if result.state != context.state: - emit_error("State mismatch detected; aborting authentication.") + emit_error(t("oauth.state_mismatch")) return None return result.code @@ -192,7 +191,7 @@ def _wait_for_callback_or_paste( time.sleep(interval) elapsed += interval - emit_error("OAuth callback timed out. Please try again.") + emit_error(t("oauth.callback.timeout")) return None @@ -206,13 +205,13 @@ def _await_callback(context: OAuthContext) -> Optional[str]: if started: server, result, event = started else: - emit_warning("Continuing Claude Code OAuth in paste-back mode.") + emit_warning(t("oauth.server.pasteback_mode")) if not _assign_manual_redirect_uri(context): return None redirect_uri = context.redirect_uri if not redirect_uri: - emit_error("Failed to assign redirect URI for OAuth flow") + emit_error(t("oauth.server.no_redirect_uri")) if server: server.shutdown() return None @@ -227,27 +226,22 @@ def _await_callback(context: OAuthContext) -> Optional[str]: suppress_browser = should_suppress_browser() if suppress_browser: - emit_info( - "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…" - ) - emit_info(f"In normal mode, would visit: {auth_url}") + emit_info(t("oauth.browser.headless")) + emit_info(t("oauth.browser.headless_url", url=auth_url)) else: - emit_info("Opening browser for Claude Code OAuth…") + emit_info(t("oauth.browser.opening")) webbrowser.open(auth_url) - emit_info(f"If it doesn't open automatically, visit: {auth_url}") + emit_info(t("oauth.browser.fallback_url", url=auth_url)) except Exception as exc: # pragma: no cover if not suppress_browser: - emit_warning(f"Failed to open browser automatically: {exc}") - emit_info(f"Please open the URL manually: {auth_url}") + emit_warning(t("oauth.browser.open_failed", error=exc)) + emit_info(t("oauth.browser.manual_url", url=auth_url)) if server: - emit_info(f"Listening for callback on {redirect_uri}") + emit_info(t("oauth.server.listening", uri=redirect_uri)) else: - emit_info(f"Using redirect URI for paste-back: {redirect_uri}") - emit_info( - "If localhost cannot be reached, paste the full callback URL or " - "authorization code here and press Enter." - ) + emit_info(t("oauth.server.pasteback_uri", uri=redirect_uri)) + emit_info(t("oauth.server.paste_hint")) code = _wait_for_callback_or_paste( context=context, @@ -286,38 +280,34 @@ def _perform_authentication() -> None: if not code: return - emit_info("Exchanging authorization code for tokens…") + emit_info(t("oauth.auth.exchanging")) tokens = exchange_code_for_tokens(code, context) if not tokens: - emit_error("Token exchange failed. Please retry the authentication flow.") + emit_error(t("oauth.auth.exchange_failed")) return if not save_tokens(tokens): - emit_error( - "Tokens retrieved but failed to save locally. Check file permissions." - ) + emit_error(t("oauth.auth.save_failed")) return - emit_success("Claude Code OAuth authentication successful!") + emit_success(t("oauth.auth.success")) access_token = tokens.get("access_token") if not access_token: - emit_warning("No access token returned; skipping model discovery.") + emit_warning(t("oauth.auth.no_access_token")) return - emit_info("Fetching available Claude Code models…") + emit_info(t("oauth.auth.fetching_models")) models = fetch_claude_code_models(access_token) if not models: - emit_warning( - "Claude Code authentication succeeded but no models were returned." - ) + emit_warning(t("oauth.auth.no_models")) return - emit_info(f"Discovered {len(models)} models: {', '.join(models)}") + emit_info( + t("oauth.auth.discovered_models", count=len(models), models=", ".join(models)) + ) if add_models_to_extra_config(models): - emit_success( - "Claude Code models added to your configuration. Use the `claude-code-` prefix!" - ) + emit_success(t("oauth.auth.models_added")) def _reauthenticate_after_expired_oauth(model_name: str) -> Optional[str]: @@ -329,17 +319,15 @@ def _reauthenticate_after_expired_oauth(model_name: str) -> Optional[str]: ) return None - emit_warning( - "Claude Code OAuth refresh failed; launching the browser sign-in flow again." - ) + emit_warning(t("oauth.reauth.refresh_failed")) _perform_authentication() access_token = get_valid_access_token() if access_token: - emit_success("Claude Code OAuth restored. Retrying the failed request…") + emit_success(t("oauth.reauth.restored")) return access_token - emit_error("Claude Code OAuth reauthentication did not produce a usable token.") + emit_error(t("oauth.reauth.no_token")) return None @@ -348,12 +336,10 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: return None if name == "claude-code-auth": - emit_info("Starting Claude Code OAuth authentication…") + emit_info(t("oauth.cmd.auth.starting")) tokens = load_stored_tokens() if tokens and tokens.get("access_token"): - emit_warning( - "Existing Claude Code tokens found. Continuing will overwrite them." - ) + emit_warning(t("oauth.cmd.auth.overwrite_warning")) _perform_authentication() set_model_and_reload_agent("claude-code-claude-opus-4-8-long") return True @@ -361,12 +347,12 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: if name == "claude-code-status": tokens = load_stored_tokens() if tokens and tokens.get("access_token"): - emit_success("Claude Code OAuth: Authenticated") + emit_success(t("oauth.cmd.status.authenticated")) expires_at = tokens.get("expires_at") if expires_at: remaining = max(0, int(expires_at - time.time())) hours, minutes = divmod(remaining // 60, 60) - emit_info(f"Token expires in ~{hours}h {minutes}m") + emit_info(t("oauth.cmd.status.expires", hours=hours, minutes=minutes)) claude_models = [ name @@ -374,12 +360,12 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: if cfg.get("oauth_source") == "claude-code-plugin" ] if claude_models: - emit_info(f"Configured Claude Code models: {', '.join(claude_models)}") + emit_info(t("oauth.cmd.status.models", models=", ".join(claude_models))) else: - emit_warning("No Claude Code models configured yet.") + emit_warning(t("oauth.cmd.status.no_models")) else: - emit_warning("Claude Code OAuth: Not authenticated") - emit_info("Run /claude-code-auth to begin the browser sign-in flow.") + emit_warning(t("oauth.cmd.status.not_authenticated")) + emit_info(t("oauth.cmd.status.hint")) return True if name == "claude-code-fast": @@ -390,10 +376,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: active_model = get_global_model_name() or "" if not active_model.startswith(CLAUDE_CODE_OAUTH_CONFIG["prefix"]): - emit_warning( - "Fast mode only applies to Claude Code models. " - "Switch to a claude-code-* model first." - ) + emit_warning(t("oauth.cmd.fast.wrong_model")) return True currently_on = is_fast_mode_enabled(active_model) @@ -402,12 +385,10 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: set_model_setting(active_model, FAST_SETTING_KEY, str(new_value).lower()) if new_value: - emit_success(f"Fast mode ENABLED for {active_model}") - emit_info( - "Injecting speed=fast into payloads and fast-mode-2026-02-01 beta header." - ) + emit_success(t("oauth.cmd.fast.enabled", model=active_model)) + emit_info(t("oauth.cmd.fast.enabled_detail")) else: - emit_info(f"Fast mode DISABLED for {active_model}") + emit_info(t("oauth.cmd.fast.disabled", model=active_model)) # Reload agent so the anthropic-beta header update (set at client # construction time) takes effect. Payload side is live either way. @@ -418,13 +399,13 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: token_path = get_token_storage_path() if token_path.exists(): token_path.unlink() - emit_info("Removed Claude Code OAuth tokens") + emit_info(t("oauth.cmd.logout.tokens_removed")) removed = remove_claude_code_models() if removed: - emit_info(f"Removed {removed} Claude Code models from configuration") + emit_info(t("oauth.cmd.logout.models_removed", count=removed)) - emit_success("Claude Code logout complete") + emit_success(t("oauth.cmd.logout.success")) return True return None @@ -469,9 +450,7 @@ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) custom_endpoint["api_key"] = refreshed_token if not api_key: - emit_warning( - f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'." - ) + emit_warning(t("oauth.model.no_api_key", model=model_config.get("name"))) return None # Check if interleaved thinking is enabled (defaults to True for OAuth models). diff --git a/tests/i18n/test_claude_oauth_i18n.py b/tests/i18n/test_claude_oauth_i18n.py new file mode 100644 index 000000000..2b86d93cb --- /dev/null +++ b/tests/i18n/test_claude_oauth_i18n.py @@ -0,0 +1,133 @@ +"""i18n coverage for the claude_code_oauth/register_callbacks extraction. + +Validates the ``oauth.*`` catalog namespace introduced by this extraction. +Integration with the live OAuth flow is tested elsewhere; this suite +focuses purely on catalog correctness and interpolation. +""" + +import re + +import pytest + +from code_puppy.i18n import catalog, pseudo, translate + +_PLACEHOLDER = re.compile(r"\{(\w+)\}") + +OAUTH_PREFIXES = ( + "oauth.server.", + "oauth.pasteback.", + "oauth.state_mismatch", + "oauth.callback.", + "oauth.browser.", + "oauth.auth.", + "oauth.reauth.", + "oauth.cmd.", + "oauth.model.", +) + + +@pytest.fixture(autouse=True) +def _reset_locale(): + translate.get_translator().set_locale("en-US") + catalog.reset() + yield + translate.get_translator().set_locale("en-US") + catalog.reset() + + +def _oauth_keys(): + src = catalog.load_catalog("en-US") + return [k for k in src if any(k.startswith(p) for p in OAUTH_PREFIXES)] + + +def test_oauth_namespace_is_populated(): + assert len(_oauth_keys()) >= 40 + + +def test_every_oauth_key_resolves(): + translate.set_locale("en-US") + offenders = [k for k in _oauth_keys() if not translate.t(k) or translate.t(k) == k] + assert not offenders, f"oauth.* keys not resolving: {offenders}" + + +def test_every_oauth_key_pseudolocalizes(): + translate.set_locale(pseudo.PSEUDO_LOCALE) + offenders = [k for k in _oauth_keys() if not translate.t(k).startswith("\u27e6")] + assert not offenders, f"oauth.* keys not pseudolocalized: {offenders}" + + +def test_server_keys_interpolate(): + translate.set_locale("en-US") + assert "boom" in translate.t("oauth.server.redirect_uri_error", error="boom") + assert "http://x" in translate.t("oauth.server.listening", uri="http://x") + assert "http://x" in translate.t("oauth.server.pasteback_uri", uri="http://x") + + +def test_pasteback_keys_interpolate(): + translate.set_locale("en-US") + assert "boom" in translate.t("oauth.pasteback.parse_error", error="boom") + assert "boom" in translate.t("oauth.pasteback.provider_error", message="boom") + + +def test_callback_keys_interpolate(): + translate.set_locale("en-US") + assert "500" in translate.t("oauth.callback.error", error="500") + + +def test_browser_keys_interpolate(): + translate.set_locale("en-US") + for key in ( + "oauth.browser.headless_url", + "oauth.browser.fallback_url", + "oauth.browser.manual_url", + ): + assert "https://x" in translate.t(key, url="https://x") + assert "boom" in translate.t("oauth.browser.open_failed", error="boom") + + +def test_auth_keys_interpolate(): + translate.set_locale("en-US") + assert "5" in translate.t("oauth.auth.discovered_models", count=5, models="a, b") + assert "a, b" in translate.t("oauth.auth.discovered_models", count=5, models="a, b") + + +def test_status_keys_interpolate(): + translate.set_locale("en-US") + assert "2" in translate.t("oauth.cmd.status.expires", hours=2, minutes=30) + assert "30" in translate.t("oauth.cmd.status.expires", hours=2, minutes=30) + assert "claude-3" in translate.t("oauth.cmd.status.models", models="claude-3") + + +def test_fast_keys_interpolate(): + translate.set_locale("en-US") + assert "opus" in translate.t("oauth.cmd.fast.enabled", model="opus") + assert "opus" in translate.t("oauth.cmd.fast.disabled", model="opus") + + +def test_logout_keys_interpolate(): + translate.set_locale("en-US") + assert "7" in translate.t("oauth.cmd.logout.models_removed", count=7) + + +def test_model_no_api_key_interpolates(): + translate.set_locale("en-US") + assert "my-model" in translate.t("oauth.model.no_api_key", model="my-model") + + +def test_no_leftover_placeholders(): + translate.set_locale("en-US") + src = catalog.load_catalog("en-US") + for key in _oauth_keys(): + entry = src[key] + text = entry if isinstance(entry, str) else entry.get("other", "") + if "{{" in text: + continue + params = {name: "X" for name in _PLACEHOLDER.findall(text)} + rendered = translate.t(key, **params) + assert "{" not in rendered.replace("{{", "").replace("}}", ""), ( + f"{key} left an un-substituted placeholder: {rendered!r}" + ) + + +def test_register_callbacks_imports_cleanly(): + import code_puppy.plugins.claude_code_oauth.register_callbacks # noqa: F401 From 11bc76ad9005fd8eaf9d550c49ed56912b926485 Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Mon, 20 Jul 2026 11:58:53 -0700 Subject: [PATCH 2/8] fix(i18n): address review feedback on claude_code_oauth extraction --- .../claude_code_oauth/register_callbacks.py | 12 +++++----- tests/i18n/test_claude_oauth_i18n.py | 22 ++++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/code_puppy/plugins/claude_code_oauth/register_callbacks.py b/code_puppy/plugins/claude_code_oauth/register_callbacks.py index a68efee5b..2b7479dd1 100644 --- a/code_puppy/plugins/claude_code_oauth/register_callbacks.py +++ b/code_puppy/plugins/claude_code_oauth/register_callbacks.py @@ -121,7 +121,7 @@ def run_server(server=server) -> None: except OSError: continue - emit_error("Could not start OAuth callback server; all candidate ports are in use") + emit_error(t("oauth.server.no_ports")) return None @@ -130,7 +130,7 @@ def _assign_manual_redirect_uri(context: OAuthContext) -> bool: try: assign_redirect_uri(context, port_range[0]) except Exception as exc: # noqa: BLE001 - emit_error(f"Failed to assign redirect URI for OAuth flow: {exc}") + emit_error(t("oauth.server.redirect_uri_error", error=exc)) return False return True @@ -139,11 +139,11 @@ def _parse_pasted_callback(context: OAuthContext, raw_input: str) -> Optional[st try: parsed = parse_oauth_callback_input(raw_input) except ValueError as exc: - emit_error(f"Could not parse pasted OAuth input: {exc}") + emit_error(t("oauth.pasteback.parse_error", error=exc)) return None if parsed.error: - emit_error(f"OAuth provider returned an error: {parsed.error_message}") + emit_error(t("oauth.pasteback.provider_error", message=parsed.error_message)) return None if not parsed.code: @@ -234,7 +234,7 @@ def _await_callback(context: OAuthContext) -> Optional[str]: emit_info(t("oauth.browser.fallback_url", url=auth_url)) except Exception as exc: # pragma: no cover if not suppress_browser: - emit_warning(t("oauth.browser.open_failed", error=exc)) + emit_warning(t("oauth.browser.open_failed", error=str(exc))) emit_info(t("oauth.browser.manual_url", url=auth_url)) if server: @@ -450,7 +450,7 @@ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) custom_endpoint["api_key"] = refreshed_token if not api_key: - emit_warning(t("oauth.model.no_api_key", model=model_config.get("name"))) + emit_warning(t("oauth.model.no_api_key", model=model_config.get("name") or "(unknown)")) return None # Check if interleaved thinking is enabled (defaults to True for OAuth models). diff --git a/tests/i18n/test_claude_oauth_i18n.py b/tests/i18n/test_claude_oauth_i18n.py index 2b86d93cb..9661590d8 100644 --- a/tests/i18n/test_claude_oauth_i18n.py +++ b/tests/i18n/test_claude_oauth_i18n.py @@ -41,7 +41,7 @@ def _oauth_keys(): def test_oauth_namespace_is_populated(): - assert len(_oauth_keys()) >= 40 + assert len(_oauth_keys()) >= 48 def test_every_oauth_key_resolves(): @@ -131,3 +131,23 @@ def test_no_leftover_placeholders(): def test_register_callbacks_imports_cleanly(): import code_puppy.plugins.claude_code_oauth.register_callbacks # noqa: F401 + + +def test_no_raw_emit_in_register_callbacks(): + """Fail if any emit_ call bypasses t() in the OAuth plugin.""" + import ast + import pathlib + + src = pathlib.Path( + "code_puppy/plugins/claude_code_oauth/register_callbacks.py" + ).read_text() + tree = ast.parse(src) + offenders = [] + for node in ast.walk(tree): + if isinstance(node, ast.Call): + fn = node.func + if isinstance(fn, ast.Name) and fn.id.startswith("emit_"): + for arg in node.args: + if isinstance(arg, (ast.Constant, ast.JoinedStr)): + offenders.append((node.lineno, fn.id)) + assert not offenders, f"Raw emit_ calls found: {offenders}" From 7d972ae66302ea914f5fd481426dfcf5077d7f6a Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Mon, 20 Jul 2026 13:11:26 -0700 Subject: [PATCH 3/8] fix(i18n): wire remaining oauth raw sites and add structural test --- .../claude_code_oauth/register_callbacks.py | 8 ++++-- tests/i18n/test_claude_oauth_i18n.py | 28 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/code_puppy/plugins/claude_code_oauth/register_callbacks.py b/code_puppy/plugins/claude_code_oauth/register_callbacks.py index 2b7479dd1..da54ea7ce 100644 --- a/code_puppy/plugins/claude_code_oauth/register_callbacks.py +++ b/code_puppy/plugins/claude_code_oauth/register_callbacks.py @@ -130,7 +130,7 @@ def _assign_manual_redirect_uri(context: OAuthContext) -> bool: try: assign_redirect_uri(context, port_range[0]) except Exception as exc: # noqa: BLE001 - emit_error(t("oauth.server.redirect_uri_error", error=exc)) + emit_error(t("oauth.server.redirect_uri_error", error=str(exc))) return False return True @@ -139,7 +139,7 @@ def _parse_pasted_callback(context: OAuthContext, raw_input: str) -> Optional[st try: parsed = parse_oauth_callback_input(raw_input) except ValueError as exc: - emit_error(t("oauth.pasteback.parse_error", error=exc)) + emit_error(t("oauth.pasteback.parse_error", error=str(exc))) return None if parsed.error: @@ -450,7 +450,9 @@ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) custom_endpoint["api_key"] = refreshed_token if not api_key: - emit_warning(t("oauth.model.no_api_key", model=model_config.get("name") or "(unknown)")) + emit_warning( + t("oauth.model.no_api_key", model=model_config.get("name") or "(unknown)") + ) return None # Check if interleaved thinking is enabled (defaults to True for OAuth models). diff --git a/tests/i18n/test_claude_oauth_i18n.py b/tests/i18n/test_claude_oauth_i18n.py index 9661590d8..420b4d196 100644 --- a/tests/i18n/test_claude_oauth_i18n.py +++ b/tests/i18n/test_claude_oauth_i18n.py @@ -134,7 +134,7 @@ def test_register_callbacks_imports_cleanly(): def test_no_raw_emit_in_register_callbacks(): - """Fail if any emit_ call bypasses t() in the OAuth plugin.""" + """Fail fast if any emit_* call bypasses t() in the OAuth plugin.""" import ast import pathlib @@ -146,8 +146,26 @@ def test_no_raw_emit_in_register_callbacks(): for node in ast.walk(tree): if isinstance(node, ast.Call): fn = node.func - if isinstance(fn, ast.Name) and fn.id.startswith("emit_"): + fn_name = ( + fn.id + if isinstance(fn, ast.Name) + else (fn.attr if isinstance(fn, ast.Attribute) else "") + ) + if fn_name.startswith("emit_"): for arg in node.args: - if isinstance(arg, (ast.Constant, ast.JoinedStr)): - offenders.append((node.lineno, fn.id)) - assert not offenders, f"Raw emit_ calls found: {offenders}" + if ( + isinstance(arg, ast.Constant) + and isinstance(arg.value, str) + and arg.value.strip() + ): + offenders.append((node.lineno, fn_name, arg.value[:50])) + elif isinstance(arg, ast.JoinedStr): + for v in arg.values: + if ( + isinstance(v, ast.Constant) + and isinstance(v.value, str) + and v.value.strip() + ): + offenders.append((node.lineno, fn_name, "f-string")) + break + assert not offenders, f"Raw emit_* calls found: {offenders}" From ec1c5d457706b7f63d1a85be56048226f8951a7e Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Thu, 23 Jul 2026 12:25:03 -0700 Subject: [PATCH 4/8] refactor(i18n): move Claude-specific keys to oauth.claude.* namespace --- code_puppy/i18n/locales/en-US.json | 48 +++++++++---------- .../claude_code_oauth/register_callbacks.py | 48 +++++++++---------- tests/i18n/test_claude_oauth_i18n.py | 9 ++-- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/code_puppy/i18n/locales/en-US.json b/code_puppy/i18n/locales/en-US.json index 24cf9a408..490002c56 100644 --- a/code_puppy/i18n/locales/en-US.json +++ b/code_puppy/i18n/locales/en-US.json @@ -126,7 +126,7 @@ "codex.logout.reload_failed": "ChatGPT logout: agent reload failed: {error}", "oauth.server.no_ports": "Could not start OAuth callback server; all candidate ports are in use", "oauth.server.redirect_uri_error": "Failed to assign redirect URI for OAuth flow: {error}", - "oauth.server.pasteback_mode": "Continuing Claude Code OAuth in paste-back mode.", + "oauth.claude.server.pasteback_mode": "Continuing Claude Code OAuth in paste-back mode.", "oauth.server.no_redirect_uri": "Failed to assign redirect URI for OAuth flow", "oauth.server.listening": "Listening for callback on {uri}", "oauth.server.pasteback_uri": "Using redirect URI for paste-back: {uri}", @@ -138,38 +138,38 @@ "oauth.state_mismatch": "State mismatch detected; aborting authentication.", "oauth.callback.error": "OAuth callback error: {error}", "oauth.callback.timeout": "OAuth callback timed out. Please try again.", - "oauth.browser.headless": "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…", + "oauth.claude.browser.headless": "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…", "oauth.browser.headless_url": "In normal mode, would visit: {url}", - "oauth.browser.opening": "Opening browser for Claude Code OAuth…", + "oauth.claude.browser.opening": "Opening browser for Claude Code OAuth…", "oauth.browser.fallback_url": "If it doesn't open automatically, visit: {url}", "oauth.browser.open_failed": "Failed to open browser automatically: {error}", "oauth.browser.manual_url": "Please open the URL manually: {url}", "oauth.auth.exchanging": "Exchanging authorization code for tokens…", "oauth.auth.exchange_failed": "Token exchange failed. Please retry the authentication flow.", "oauth.auth.save_failed": "Tokens retrieved but failed to save locally. Check file permissions.", - "oauth.auth.success": "Claude Code OAuth authentication successful!", + "oauth.claude.auth.success": "Claude Code OAuth authentication successful!", "oauth.auth.no_access_token": "No access token returned; skipping model discovery.", - "oauth.auth.fetching_models": "Fetching available Claude Code models…", - "oauth.auth.no_models": "Claude Code authentication succeeded but no models were returned.", + "oauth.claude.auth.fetching_models": "Fetching available Claude Code models…", + "oauth.claude.auth.no_models": "Claude Code authentication succeeded but no models were returned.", "oauth.auth.discovered_models": "Discovered {count} models: {models}", - "oauth.auth.models_added": "Claude Code models added to your configuration. Use the `claude-code-` prefix!", - "oauth.reauth.refresh_failed": "Claude Code OAuth refresh failed; launching the browser sign-in flow again.", - "oauth.reauth.restored": "Claude Code OAuth restored. Retrying the failed request…", - "oauth.reauth.no_token": "Claude Code OAuth reauthentication did not produce a usable token.", - "oauth.cmd.auth.starting": "Starting Claude Code OAuth authentication…", - "oauth.cmd.auth.overwrite_warning": "Existing Claude Code tokens found. Continuing will overwrite them.", - "oauth.cmd.status.authenticated": "Claude Code OAuth: Authenticated", + "oauth.claude.auth.models_added": "Claude Code models added to your configuration. Use the `claude-code-` prefix!", + "oauth.claude.reauth.refresh_failed": "Claude Code OAuth refresh failed; launching the browser sign-in flow again.", + "oauth.claude.reauth.restored": "Claude Code OAuth restored. Retrying the failed request…", + "oauth.claude.reauth.no_token": "Claude Code OAuth reauthentication did not produce a usable token.", + "oauth.claude.cmd.auth.starting": "Starting Claude Code OAuth authentication…", + "oauth.claude.cmd.auth.overwrite_warning": "Existing Claude Code tokens found. Continuing will overwrite them.", + "oauth.claude.cmd.status.authenticated": "Claude Code OAuth: Authenticated", "oauth.cmd.status.expires": "Token expires in ~{hours}h {minutes}m", - "oauth.cmd.status.models": "Configured Claude Code models: {models}", - "oauth.cmd.status.no_models": "No Claude Code models configured yet.", - "oauth.cmd.status.not_authenticated": "Claude Code OAuth: Not authenticated", - "oauth.cmd.status.hint": "Run /claude-code-auth to begin the browser sign-in flow.", - "oauth.cmd.fast.wrong_model": "Fast mode only applies to Claude Code models. Switch to a claude-code-* model first.", - "oauth.cmd.fast.enabled": "Fast mode ENABLED for {model}", - "oauth.cmd.fast.enabled_detail": "Injecting speed=fast into payloads and fast-mode-2026-02-01 beta header.", - "oauth.cmd.fast.disabled": "Fast mode DISABLED for {model}", - "oauth.cmd.logout.tokens_removed": "Removed Claude Code OAuth tokens", + "oauth.claude.cmd.status.models": "Configured Claude Code models: {models}", + "oauth.claude.cmd.status.no_models": "No Claude Code models configured yet.", + "oauth.claude.cmd.status.not_authenticated": "Claude Code OAuth: Not authenticated", + "oauth.claude.cmd.status.hint": "Run /claude-code-auth to begin the browser sign-in flow.", + "oauth.claude.cmd.fast.wrong_model": "Fast mode only applies to Claude Code models. Switch to a claude-code-* model first.", + "oauth.claude.cmd.fast.enabled": "Fast mode ENABLED for {model}", + "oauth.claude.cmd.fast.enabled_detail": "Injecting speed=fast into payloads and fast-mode-2026-02-01 beta header.", + "oauth.claude.cmd.fast.disabled": "Fast mode DISABLED for {model}", + "oauth.claude.cmd.logout.tokens_removed": "Removed Claude Code OAuth tokens", "oauth.cmd.logout.models_removed": "Removed {count} Claude Code models from configuration", - "oauth.cmd.logout.success": "Claude Code logout complete", - "oauth.model.no_api_key": "API key is not set for Claude Code endpoint; skipping model '{model}'." + "oauth.claude.cmd.logout.success": "Claude Code logout complete", + "oauth.claude.model.no_api_key": "API key is not set for Claude Code endpoint; skipping model '{model}'." } diff --git a/code_puppy/plugins/claude_code_oauth/register_callbacks.py b/code_puppy/plugins/claude_code_oauth/register_callbacks.py index da54ea7ce..88768dcb7 100644 --- a/code_puppy/plugins/claude_code_oauth/register_callbacks.py +++ b/code_puppy/plugins/claude_code_oauth/register_callbacks.py @@ -205,7 +205,7 @@ def _await_callback(context: OAuthContext) -> Optional[str]: if started: server, result, event = started else: - emit_warning(t("oauth.server.pasteback_mode")) + emit_warning(t("oauth.claude.server.pasteback_mode")) if not _assign_manual_redirect_uri(context): return None @@ -226,10 +226,10 @@ def _await_callback(context: OAuthContext) -> Optional[str]: suppress_browser = should_suppress_browser() if suppress_browser: - emit_info(t("oauth.browser.headless")) + emit_info(t("oauth.claude.browser.headless")) emit_info(t("oauth.browser.headless_url", url=auth_url)) else: - emit_info(t("oauth.browser.opening")) + emit_info(t("oauth.claude.browser.opening")) webbrowser.open(auth_url) emit_info(t("oauth.browser.fallback_url", url=auth_url)) except Exception as exc: # pragma: no cover @@ -290,24 +290,24 @@ def _perform_authentication() -> None: emit_error(t("oauth.auth.save_failed")) return - emit_success(t("oauth.auth.success")) + emit_success(t("oauth.claude.auth.success")) access_token = tokens.get("access_token") if not access_token: emit_warning(t("oauth.auth.no_access_token")) return - emit_info(t("oauth.auth.fetching_models")) + emit_info(t("oauth.claude.auth.fetching_models")) models = fetch_claude_code_models(access_token) if not models: - emit_warning(t("oauth.auth.no_models")) + emit_warning(t("oauth.claude.auth.no_models")) return emit_info( t("oauth.auth.discovered_models", count=len(models), models=", ".join(models)) ) if add_models_to_extra_config(models): - emit_success(t("oauth.auth.models_added")) + emit_success(t("oauth.claude.auth.models_added")) def _reauthenticate_after_expired_oauth(model_name: str) -> Optional[str]: @@ -319,15 +319,15 @@ def _reauthenticate_after_expired_oauth(model_name: str) -> Optional[str]: ) return None - emit_warning(t("oauth.reauth.refresh_failed")) + emit_warning(t("oauth.claude.reauth.refresh_failed")) _perform_authentication() access_token = get_valid_access_token() if access_token: - emit_success(t("oauth.reauth.restored")) + emit_success(t("oauth.claude.reauth.restored")) return access_token - emit_error(t("oauth.reauth.no_token")) + emit_error(t("oauth.claude.reauth.no_token")) return None @@ -336,10 +336,10 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: return None if name == "claude-code-auth": - emit_info(t("oauth.cmd.auth.starting")) + emit_info(t("oauth.claude.cmd.auth.starting")) tokens = load_stored_tokens() if tokens and tokens.get("access_token"): - emit_warning(t("oauth.cmd.auth.overwrite_warning")) + emit_warning(t("oauth.claude.cmd.auth.overwrite_warning")) _perform_authentication() set_model_and_reload_agent("claude-code-claude-opus-4-8-long") return True @@ -347,7 +347,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: if name == "claude-code-status": tokens = load_stored_tokens() if tokens and tokens.get("access_token"): - emit_success(t("oauth.cmd.status.authenticated")) + emit_success(t("oauth.claude.cmd.status.authenticated")) expires_at = tokens.get("expires_at") if expires_at: remaining = max(0, int(expires_at - time.time())) @@ -360,12 +360,12 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: if cfg.get("oauth_source") == "claude-code-plugin" ] if claude_models: - emit_info(t("oauth.cmd.status.models", models=", ".join(claude_models))) + emit_info(t("oauth.claude.cmd.status.models", models=", ".join(claude_models))) else: - emit_warning(t("oauth.cmd.status.no_models")) + emit_warning(t("oauth.claude.cmd.status.no_models")) else: - emit_warning(t("oauth.cmd.status.not_authenticated")) - emit_info(t("oauth.cmd.status.hint")) + emit_warning(t("oauth.claude.cmd.status.not_authenticated")) + emit_info(t("oauth.claude.cmd.status.hint")) return True if name == "claude-code-fast": @@ -376,7 +376,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: active_model = get_global_model_name() or "" if not active_model.startswith(CLAUDE_CODE_OAUTH_CONFIG["prefix"]): - emit_warning(t("oauth.cmd.fast.wrong_model")) + emit_warning(t("oauth.claude.cmd.fast.wrong_model")) return True currently_on = is_fast_mode_enabled(active_model) @@ -385,10 +385,10 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: set_model_setting(active_model, FAST_SETTING_KEY, str(new_value).lower()) if new_value: - emit_success(t("oauth.cmd.fast.enabled", model=active_model)) - emit_info(t("oauth.cmd.fast.enabled_detail")) + emit_success(t("oauth.claude.cmd.fast.enabled", model=active_model)) + emit_info(t("oauth.claude.cmd.fast.enabled_detail")) else: - emit_info(t("oauth.cmd.fast.disabled", model=active_model)) + emit_info(t("oauth.claude.cmd.fast.disabled", model=active_model)) # Reload agent so the anthropic-beta header update (set at client # construction time) takes effect. Payload side is live either way. @@ -399,13 +399,13 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: token_path = get_token_storage_path() if token_path.exists(): token_path.unlink() - emit_info(t("oauth.cmd.logout.tokens_removed")) + emit_info(t("oauth.claude.cmd.logout.tokens_removed")) removed = remove_claude_code_models() if removed: emit_info(t("oauth.cmd.logout.models_removed", count=removed)) - emit_success(t("oauth.cmd.logout.success")) + emit_success(t("oauth.claude.cmd.logout.success")) return True return None @@ -451,7 +451,7 @@ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) if not api_key: emit_warning( - t("oauth.model.no_api_key", model=model_config.get("name") or "(unknown)") + t("oauth.claude.model.no_api_key", model=model_config.get("name") or "(unknown)") ) return None diff --git a/tests/i18n/test_claude_oauth_i18n.py b/tests/i18n/test_claude_oauth_i18n.py index 420b4d196..1646c90c8 100644 --- a/tests/i18n/test_claude_oauth_i18n.py +++ b/tests/i18n/test_claude_oauth_i18n.py @@ -23,6 +23,7 @@ "oauth.reauth.", "oauth.cmd.", "oauth.model.", + "oauth.claude.", ) @@ -95,13 +96,13 @@ def test_status_keys_interpolate(): translate.set_locale("en-US") assert "2" in translate.t("oauth.cmd.status.expires", hours=2, minutes=30) assert "30" in translate.t("oauth.cmd.status.expires", hours=2, minutes=30) - assert "claude-3" in translate.t("oauth.cmd.status.models", models="claude-3") + assert "claude-3" in translate.t("oauth.claude.cmd.status.models", models="claude-3") def test_fast_keys_interpolate(): translate.set_locale("en-US") - assert "opus" in translate.t("oauth.cmd.fast.enabled", model="opus") - assert "opus" in translate.t("oauth.cmd.fast.disabled", model="opus") + assert "opus" in translate.t("oauth.claude.cmd.fast.enabled", model="opus") + assert "opus" in translate.t("oauth.claude.cmd.fast.disabled", model="opus") def test_logout_keys_interpolate(): @@ -111,7 +112,7 @@ def test_logout_keys_interpolate(): def test_model_no_api_key_interpolates(): translate.set_locale("en-US") - assert "my-model" in translate.t("oauth.model.no_api_key", model="my-model") + assert "my-model" in translate.t("oauth.claude.model.no_api_key", model="my-model") def test_no_leftover_placeholders(): From 266225dc1f4efb6fe700e8531c738ba5f2daa878 Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Thu, 23 Jul 2026 15:17:50 -0700 Subject: [PATCH 5/8] style(i18n): ruff-format claude_oauth namespace refactor --- .../plugins/claude_code_oauth/register_callbacks.py | 9 +++++++-- tests/i18n/test_claude_oauth_i18n.py | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/code_puppy/plugins/claude_code_oauth/register_callbacks.py b/code_puppy/plugins/claude_code_oauth/register_callbacks.py index 88768dcb7..bc16ef054 100644 --- a/code_puppy/plugins/claude_code_oauth/register_callbacks.py +++ b/code_puppy/plugins/claude_code_oauth/register_callbacks.py @@ -360,7 +360,9 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]: if cfg.get("oauth_source") == "claude-code-plugin" ] if claude_models: - emit_info(t("oauth.claude.cmd.status.models", models=", ".join(claude_models))) + emit_info( + t("oauth.claude.cmd.status.models", models=", ".join(claude_models)) + ) else: emit_warning(t("oauth.claude.cmd.status.no_models")) else: @@ -451,7 +453,10 @@ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) if not api_key: emit_warning( - t("oauth.claude.model.no_api_key", model=model_config.get("name") or "(unknown)") + t( + "oauth.claude.model.no_api_key", + model=model_config.get("name") or "(unknown)", + ) ) return None diff --git a/tests/i18n/test_claude_oauth_i18n.py b/tests/i18n/test_claude_oauth_i18n.py index 1646c90c8..c7501441f 100644 --- a/tests/i18n/test_claude_oauth_i18n.py +++ b/tests/i18n/test_claude_oauth_i18n.py @@ -96,7 +96,9 @@ def test_status_keys_interpolate(): translate.set_locale("en-US") assert "2" in translate.t("oauth.cmd.status.expires", hours=2, minutes=30) assert "30" in translate.t("oauth.cmd.status.expires", hours=2, minutes=30) - assert "claude-3" in translate.t("oauth.claude.cmd.status.models", models="claude-3") + assert "claude-3" in translate.t( + "oauth.claude.cmd.status.models", models="claude-3" + ) def test_fast_keys_interpolate(): From 49f000cafc78336cd86345660a1d178c4df56cde Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Fri, 24 Jul 2026 11:41:20 -0700 Subject: [PATCH 6/8] i18n(es,fr-CA): translate 48 new keys from PR #653 (claude_code_oauth) --- code_puppy/i18n/locales/es.json | 50 ++++++++++++++++++++- code_puppy/i18n/locales/fr-CA.json | 70 +++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 12 deletions(-) diff --git a/code_puppy/i18n/locales/es.json b/code_puppy/i18n/locales/es.json index a21c54a89..79ec90671 100644 --- a/code_puppy/i18n/locales/es.json +++ b/code_puppy/i18n/locales/es.json @@ -19,5 +19,53 @@ "version.fetch_failed": "Error al obtener la versión: {error}", "version.undetected": "No se pudo detectar la versión actual, se usará un valor predeterminado", "confirm.yes": "Sí", - "confirm.no": "No" + "confirm.no": "No", + "oauth.server.no_ports": "No se pudo iniciar el servidor de callback de OAuth; todos los puertos candidatos están en uso", + "oauth.server.redirect_uri_error": "No se pudo asignar el URI de redirección para el flujo de OAuth: {error}", + "oauth.claude.server.pasteback_mode": "Continuando el OAuth de Claude Code en modo paste-back.", + "oauth.server.no_redirect_uri": "No se pudo asignar el URI de redirección para el flujo de OAuth", + "oauth.server.listening": "Escuchando el callback en {uri}", + "oauth.server.pasteback_uri": "Usando URI de redirección para paste-back: {uri}", + "oauth.server.paste_hint": "Si no se puede acceder a localhost, pega aquí la URL completa del callback o el código de autorización y presiona Enter.", + "oauth.pasteback.parse_error": "No se pudo analizar el texto OAuth pegado: {error}", + "oauth.pasteback.provider_error": "El proveedor de OAuth devolvió un error: {message}", + "oauth.pasteback.no_code": "El texto OAuth pegado no contenía un código de autorización.", + "oauth.pasteback.no_state": "El texto OAuth pegado no incluía el estado; se continúa con este intento de inicio de sesión.", + "oauth.state_mismatch": "Se detectó una discrepancia de estado; se cancela la autenticación.", + "oauth.callback.error": "Error de callback de OAuth: {error}", + "oauth.callback.timeout": "El callback de OAuth agotó el tiempo de espera. Vuelve a intentarlo.", + "oauth.claude.browser.headless": "[MODO HEADLESS] Normalmente se abriría el navegador para el OAuth de Claude Code…", + "oauth.browser.headless_url": "En modo normal, se visitaría: {url}", + "oauth.claude.browser.opening": "Abriendo el navegador para el OAuth de Claude Code…", + "oauth.browser.fallback_url": "Si no se abre automáticamente, visita: {url}", + "oauth.browser.open_failed": "No se pudo abrir el navegador automáticamente: {error}", + "oauth.browser.manual_url": "Abre la URL manualmente: {url}", + "oauth.auth.exchanging": "Intercambiando el código de autorización por tokens…", + "oauth.auth.exchange_failed": "El intercambio de tokens falló. Vuelve a intentar el flujo de autenticación.", + "oauth.auth.save_failed": "Se obtuvieron los tokens, pero no se pudieron guardar localmente. Verifica los permisos del archivo.", + "oauth.claude.auth.success": "¡Autenticación de OAuth de Claude Code exitosa!", + "oauth.auth.no_access_token": "No se devolvió ningún token de acceso; se omite el descubrimiento de modelos.", + "oauth.claude.auth.fetching_models": "Obteniendo los modelos de Claude Code disponibles…", + "oauth.claude.auth.no_models": "La autenticación de Claude Code tuvo éxito, pero no se devolvieron modelos.", + "oauth.auth.discovered_models": "Se descubrieron {count} modelos: {models}", + "oauth.claude.auth.models_added": "Modelos de Claude Code agregados a tu configuración. ¡Usa el prefijo `claude-code-`!", + "oauth.claude.reauth.refresh_failed": "La renovación de OAuth de Claude Code falló; se inicia nuevamente el flujo de inicio de sesión en el navegador.", + "oauth.claude.reauth.restored": "OAuth de Claude Code restaurado. Reintentando la solicitud fallida…", + "oauth.claude.reauth.no_token": "La reautenticación de OAuth de Claude Code no generó un token utilizable.", + "oauth.claude.cmd.auth.starting": "Iniciando la autenticación de OAuth de Claude Code…", + "oauth.claude.cmd.auth.overwrite_warning": "Se encontraron tokens existentes de Claude Code. Continuar los sobrescribirá.", + "oauth.claude.cmd.status.authenticated": "OAuth de Claude Code: autenticado", + "oauth.cmd.status.expires": "El token expira en ~{hours}h {minutes}min", + "oauth.claude.cmd.status.models": "Modelos de Claude Code configurados: {models}", + "oauth.claude.cmd.status.no_models": "Aún no hay modelos de Claude Code configurados.", + "oauth.claude.cmd.status.not_authenticated": "OAuth de Claude Code: no autenticado", + "oauth.claude.cmd.status.hint": "Ejecuta /claude-code-auth para iniciar el flujo de inicio de sesión en el navegador.", + "oauth.claude.cmd.fast.wrong_model": "El modo rápido solo se aplica a los modelos de Claude Code. Cambia primero a un modelo claude-code-*.", + "oauth.claude.cmd.fast.enabled": "Modo rápido ACTIVADO para {model}", + "oauth.claude.cmd.fast.enabled_detail": "Se inyectará speed=fast en los payloads y el encabezado beta fast-mode-2026-02-01.", + "oauth.claude.cmd.fast.disabled": "Modo rápido DESACTIVADO para {model}", + "oauth.claude.cmd.logout.tokens_removed": "Se eliminaron los tokens de OAuth de Claude Code", + "oauth.cmd.logout.models_removed": "Se eliminaron {count} modelos de Claude Code de la configuración", + "oauth.claude.cmd.logout.success": "Cierre de sesión de Claude Code completado", + "oauth.claude.model.no_api_key": "La clave de API no está configurada para el endpoint de Claude Code; se omite el modelo '{model}'." } diff --git a/code_puppy/i18n/locales/fr-CA.json b/code_puppy/i18n/locales/fr-CA.json index 1ace31fe1..4ff2a06d8 100644 --- a/code_puppy/i18n/locales/fr-CA.json +++ b/code_puppy/i18n/locales/fr-CA.json @@ -1,23 +1,71 @@ { "startup.welcome": "Bienvenue dans Code Puppy, {name}!", - "startup.ready": "Pr\u00eat \u00e0 aller chercher du code. \ud83d\udc36", - "locale.set": "Langue r\u00e9gl\u00e9e \u00e0 {locale}.", + "startup.ready": "Prêt à aller chercher du code. 🐶", + "locale.set": "Langue réglée à {locale}.", "locale.current": "Langue actuelle : {locale}", "files.deleted": { - "one": "{count} fichier supprim\u00e9.", - "other": "{count} fichiers supprim\u00e9s." + "one": "{count} fichier supprimé.", + "other": "{count} fichiers supprimés." }, "files.written": { - "one": "{count} fichier \u00e9crit.", - "other": "{count} fichiers \u00e9crits." + "one": "{count} fichier écrit.", + "other": "{count} fichiers écrits." }, "error.generic": "Une erreur s'est produite : {detail}", "version.current": "Version actuelle : {version}", - "version.latest": "Derni\u00e8re version : {version}", + "version.latest": "Dernière version : {version}", "version.update_available": "Une nouvelle version de code puppy est disponible : {version}", - "version.please_update": "Pensez \u00e0 faire la mise \u00e0 jour!", - "version.fetch_failed": "Erreur lors de la r\u00e9cup\u00e9ration de la version : {error}", - "version.undetected": "Impossible de d\u00e9tecter la version actuelle; utilisation d'une valeur par d\u00e9faut", + "version.please_update": "Pensez à faire la mise à jour!", + "version.fetch_failed": "Erreur lors de la récupération de la version : {error}", + "version.undetected": "Impossible de détecter la version actuelle; utilisation d'une valeur par défaut", "confirm.yes": "Oui", - "confirm.no": "Non" + "confirm.no": "Non", + "oauth.server.no_ports": "Impossible de démarrer le serveur de rappel OAuth ; tous les ports candidats sont utilisés", + "oauth.server.redirect_uri_error": "Échec de l'attribution de l'URI de redirection pour le flux OAuth : {error}", + "oauth.claude.server.pasteback_mode": "Poursuite de l'OAuth Claude Code en mode paste-back.", + "oauth.server.no_redirect_uri": "Échec de l'attribution de l'URI de redirection pour le flux OAuth", + "oauth.server.listening": "En écoute du rappel sur {uri}", + "oauth.server.pasteback_uri": "URI de redirection utilisé pour le paste-back : {uri}", + "oauth.server.paste_hint": "Si localhost n'est pas accessible, collez ici l'URL complète du rappel ou le code d'autorisation, puis appuyez sur Entrée.", + "oauth.pasteback.parse_error": "Impossible d'analyser l'entrée OAuth collée : {error}", + "oauth.pasteback.provider_error": "Le fournisseur OAuth a retourné une erreur : {message}", + "oauth.pasteback.no_code": "L'entrée OAuth collée ne contient pas de code d'autorisation.", + "oauth.pasteback.no_state": "L'entrée OAuth collée ne contient pas d'état ; poursuite de cette tentative de connexion.", + "oauth.state_mismatch": "Discordance d'état détectée ; abandon de l'authentification.", + "oauth.callback.error": "Erreur de rappel OAuth : {error}", + "oauth.callback.timeout": "Délai d'attente du rappel OAuth dépassé. Veuillez réessayer.", + "oauth.claude.browser.headless": "[MODE HEADLESS] Ouvrirait normalement le navigateur pour l'OAuth Claude Code…", + "oauth.browser.headless_url": "En mode normal, visiterait : {url}", + "oauth.claude.browser.opening": "Ouverture du navigateur pour l'OAuth Claude Code…", + "oauth.browser.fallback_url": "S'il ne s'ouvre pas automatiquement, visitez : {url}", + "oauth.browser.open_failed": "Échec de l'ouverture automatique du navigateur : {error}", + "oauth.browser.manual_url": "Veuillez ouvrir l'URL manuellement : {url}", + "oauth.auth.exchanging": "Échange du code d'autorisation contre des jetons…", + "oauth.auth.exchange_failed": "L'échange de jetons a échoué. Veuillez réessayer le flux d'authentification.", + "oauth.auth.save_failed": "Jetons récupérés, mais impossible de les enregistrer localement. Vérifiez les permissions du fichier.", + "oauth.claude.auth.success": "Authentification OAuth Claude Code réussie!", + "oauth.auth.no_access_token": "Aucun jeton d'accès retourné ; découverte des modèles ignorée.", + "oauth.claude.auth.fetching_models": "Récupération des modèles Claude Code disponibles…", + "oauth.claude.auth.no_models": "L'authentification Claude Code a réussi, mais aucun modèle n'a été retourné.", + "oauth.auth.discovered_models": "{count} modèles découverts : {models}", + "oauth.claude.auth.models_added": "Modèles Claude Code ajoutés à votre configuration. Utilisez le préfixe `claude-code-`!", + "oauth.claude.reauth.refresh_failed": "L'actualisation de l'OAuth Claude Code a échoué ; relance du flux de connexion dans le navigateur.", + "oauth.claude.reauth.restored": "OAuth Claude Code restauré. Nouvelle tentative de la requête ayant échoué…", + "oauth.claude.reauth.no_token": "La réauthentification OAuth Claude Code n'a pas produit de jeton utilisable.", + "oauth.claude.cmd.auth.starting": "Démarrage de l'authentification OAuth Claude Code…", + "oauth.claude.cmd.auth.overwrite_warning": "Des jetons Claude Code existants ont été trouvés. Poursuivre les écrasera.", + "oauth.claude.cmd.status.authenticated": "OAuth Claude Code : authentifié", + "oauth.cmd.status.expires": "Le jeton expire dans ~{hours} h {minutes} min", + "oauth.claude.cmd.status.models": "Modèles Claude Code configurés : {models}", + "oauth.claude.cmd.status.no_models": "Aucun modèle Claude Code configuré pour l'instant.", + "oauth.claude.cmd.status.not_authenticated": "OAuth Claude Code : non authentifié", + "oauth.claude.cmd.status.hint": "Exécutez /claude-code-auth pour démarrer le flux de connexion dans le navigateur.", + "oauth.claude.cmd.fast.wrong_model": "Le mode rapide s'applique uniquement aux modèles Claude Code. Passez d'abord à un modèle claude-code-*.", + "oauth.claude.cmd.fast.enabled": "Mode rapide ACTIVÉ pour {model}", + "oauth.claude.cmd.fast.enabled_detail": "Injection de speed=fast dans les charges utiles et de l'en-tête bêta fast-mode-2026-02-01.", + "oauth.claude.cmd.fast.disabled": "Mode rapide DÉSACTIVÉ pour {model}", + "oauth.claude.cmd.logout.tokens_removed": "Jetons OAuth Claude Code supprimés", + "oauth.cmd.logout.models_removed": "{count} modèles Claude Code supprimés de la configuration", + "oauth.claude.cmd.logout.success": "Déconnexion de Claude Code terminée", + "oauth.claude.model.no_api_key": "Aucune clé d'API n'est configurée pour le point de terminaison Claude Code ; modèle « {model} » ignoré." } From 211bc543bb0b173c32e4aade3ac650cee4ee71ca Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Fri, 24 Jul 2026 11:49:58 -0700 Subject: [PATCH 7/8] i18n(es,fr-CA): apply adversarial review fixes (semantic + QC typography) --- code_puppy/i18n/locales/fr-CA.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code_puppy/i18n/locales/fr-CA.json b/code_puppy/i18n/locales/fr-CA.json index 4ff2a06d8..000365727 100644 --- a/code_puppy/i18n/locales/fr-CA.json +++ b/code_puppy/i18n/locales/fr-CA.json @@ -43,12 +43,12 @@ "oauth.auth.exchanging": "Échange du code d'autorisation contre des jetons…", "oauth.auth.exchange_failed": "L'échange de jetons a échoué. Veuillez réessayer le flux d'authentification.", "oauth.auth.save_failed": "Jetons récupérés, mais impossible de les enregistrer localement. Vérifiez les permissions du fichier.", - "oauth.claude.auth.success": "Authentification OAuth Claude Code réussie!", + "oauth.claude.auth.success": "Authentification OAuth Claude Code réussie !", "oauth.auth.no_access_token": "Aucun jeton d'accès retourné ; découverte des modèles ignorée.", "oauth.claude.auth.fetching_models": "Récupération des modèles Claude Code disponibles…", "oauth.claude.auth.no_models": "L'authentification Claude Code a réussi, mais aucun modèle n'a été retourné.", "oauth.auth.discovered_models": "{count} modèles découverts : {models}", - "oauth.claude.auth.models_added": "Modèles Claude Code ajoutés à votre configuration. Utilisez le préfixe `claude-code-`!", + "oauth.claude.auth.models_added": "Modèles Claude Code ajoutés à votre configuration. Utilisez le préfixe `claude-code-` !", "oauth.claude.reauth.refresh_failed": "L'actualisation de l'OAuth Claude Code a échoué ; relance du flux de connexion dans le navigateur.", "oauth.claude.reauth.restored": "OAuth Claude Code restauré. Nouvelle tentative de la requête ayant échoué…", "oauth.claude.reauth.no_token": "La réauthentification OAuth Claude Code n'a pas produit de jeton utilisable.", From dae0e568924e28d90fd50b42b9095a919f5f7ef8 Mon Sep 17 00:00:00 2001 From: TJ Webb Date: Fri, 24 Jul 2026 11:54:48 -0700 Subject: [PATCH 8/8] i18n(fr-CA): restore pre-existing 15 keys byte-for-byte for in-flight translator work --- code_puppy/i18n/locales/fr-CA.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/code_puppy/i18n/locales/fr-CA.json b/code_puppy/i18n/locales/fr-CA.json index 000365727..dda13a0ad 100644 --- a/code_puppy/i18n/locales/fr-CA.json +++ b/code_puppy/i18n/locales/fr-CA.json @@ -1,23 +1,23 @@ { "startup.welcome": "Bienvenue dans Code Puppy, {name}!", - "startup.ready": "Prêt à aller chercher du code. 🐶", - "locale.set": "Langue réglée à {locale}.", + "startup.ready": "Pr\u00eat \u00e0 aller chercher du code. \ud83d\udc36", + "locale.set": "Langue r\u00e9gl\u00e9e \u00e0 {locale}.", "locale.current": "Langue actuelle : {locale}", "files.deleted": { - "one": "{count} fichier supprimé.", - "other": "{count} fichiers supprimés." + "one": "{count} fichier supprim\u00e9.", + "other": "{count} fichiers supprim\u00e9s." }, "files.written": { - "one": "{count} fichier écrit.", - "other": "{count} fichiers écrits." + "one": "{count} fichier \u00e9crit.", + "other": "{count} fichiers \u00e9crits." }, "error.generic": "Une erreur s'est produite : {detail}", "version.current": "Version actuelle : {version}", - "version.latest": "Dernière version : {version}", + "version.latest": "Derni\u00e8re version : {version}", "version.update_available": "Une nouvelle version de code puppy est disponible : {version}", - "version.please_update": "Pensez à faire la mise à jour!", - "version.fetch_failed": "Erreur lors de la récupération de la version : {error}", - "version.undetected": "Impossible de détecter la version actuelle; utilisation d'une valeur par défaut", + "version.please_update": "Pensez \u00e0 faire la mise \u00e0 jour!", + "version.fetch_failed": "Erreur lors de la r\u00e9cup\u00e9ration de la version : {error}", + "version.undetected": "Impossible de d\u00e9tecter la version actuelle; utilisation d'une valeur par d\u00e9faut", "confirm.yes": "Oui", "confirm.no": "Non", "oauth.server.no_ports": "Impossible de démarrer le serveur de rappel OAuth ; tous les ports candidats sont utilisés",