diff --git a/code_puppy/i18n/locales/en-US.json b/code_puppy/i18n/locales/en-US.json index 485e58bba..490002c56 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.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}", + "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.claude.browser.headless": "[HEADLESS MODE] Would normally open browser for Claude Code OAuth…", + "oauth.browser.headless_url": "In normal mode, would visit: {url}", + "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.claude.auth.success": "Claude Code OAuth authentication successful!", + "oauth.auth.no_access_token": "No access token returned; skipping model discovery.", + "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.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.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.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/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..dda13a0ad 100644 --- a/code_puppy/i18n/locales/fr-CA.json +++ b/code_puppy/i18n/locales/fr-CA.json @@ -19,5 +19,53 @@ "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" + "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é." } diff --git a/code_puppy/plugins/claude_code_oauth/register_callbacks.py b/code_puppy/plugins/claude_code_oauth/register_callbacks.py index d2b1695bc..bc16ef054 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 ( @@ -120,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 @@ -129,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=str(exc))) return False return True @@ -138,25 +139,23 @@ 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=str(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: - 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.claude.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.claude.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.claude.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=str(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.claude.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.claude.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.claude.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.claude.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.claude.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.claude.reauth.restored")) return access_token - emit_error("Claude Code OAuth reauthentication did not produce a usable token.") + emit_error(t("oauth.claude.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.claude.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.claude.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.claude.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,14 @@ 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.claude.cmd.status.models", models=", ".join(claude_models)) + ) else: - emit_warning("No Claude Code models configured yet.") + emit_warning(t("oauth.claude.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.claude.cmd.status.not_authenticated")) + emit_info(t("oauth.claude.cmd.status.hint")) return True if name == "claude-code-fast": @@ -390,10 +378,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.claude.cmd.fast.wrong_model")) return True currently_on = is_fast_mode_enabled(active_model) @@ -402,12 +387,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.claude.cmd.fast.enabled", model=active_model)) + emit_info(t("oauth.claude.cmd.fast.enabled_detail")) else: - emit_info(f"Fast mode DISABLED for {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. @@ -418,13 +401,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.claude.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.claude.cmd.logout.success")) return True return None @@ -470,7 +453,10 @@ def _create_claude_code_model(model_name: str, model_config: Dict, config: Dict) if not api_key: emit_warning( - f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'." + 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 new file mode 100644 index 000000000..c7501441f --- /dev/null +++ b/tests/i18n/test_claude_oauth_i18n.py @@ -0,0 +1,174 @@ +"""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.", + "oauth.claude.", +) + + +@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()) >= 48 + + +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.claude.cmd.status.models", models="claude-3" + ) + + +def test_fast_keys_interpolate(): + translate.set_locale("en-US") + 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(): + 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.claude.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 + + +def test_no_raw_emit_in_register_callbacks(): + """Fail fast 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 + 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) + 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}"