diff --git a/agent/i18n.py b/agent/i18n.py
index 0196439bb4e..034fb747b6b 100644
--- a/agent/i18n.py
+++ b/agent/i18n.py
@@ -39,20 +39,45 @@ from typing import Any
logger = logging.getLogger(__name__)
-SUPPORTED_LANGUAGES: tuple[str, ...] = ("en", "zh", "ja", "de", "es", "fr", "tr", "uk")
+SUPPORTED_LANGUAGES: tuple[str, ...] = (
+ "en", "zh", "zh-hant", "ja", "de", "es", "fr", "tr", "uk",
+ "af", "ko", "it", "ga", "pt", "ru", "hu",
+)
DEFAULT_LANGUAGE = "en"
# Accept a few natural aliases so users who type "chinese" / "zh-CN" / "jp"
# get the right catalog instead of silently falling back to English.
_LANGUAGE_ALIASES: dict[str, str] = {
"english": "en", "en-us": "en", "en-gb": "en",
- "chinese": "zh", "mandarin": "zh", "zh-cn": "zh", "zh-tw": "zh", "zh-hans": "zh", "zh-hant": "zh",
+ # Simplified Chinese — explicit codes route here; bare "chinese" / "mandarin"
+ # also default to Simplified since that's the larger user base.
+ "chinese": "zh", "mandarin": "zh", "zh-cn": "zh", "zh-hans": "zh", "zh-sg": "zh",
+ # Traditional Chinese — distinct catalog. Cover Taiwan / Hong Kong / Macau
+ # locale tags plus the common "traditional" alias.
+ "traditional-chinese": "zh-hant", "traditional_chinese": "zh-hant",
+ "zh-tw": "zh-hant", "zh-hk": "zh-hant", "zh-mo": "zh-hant",
"japanese": "ja", "jp": "ja", "ja-jp": "ja",
- "german": "de", "deutsch": "de", "de-de": "de",
- "spanish": "es", "español": "es", "espanol": "es", "es-es": "es", "es-mx": "es",
+ "german": "de", "deutsch": "de", "de-de": "de", "de-at": "de", "de-ch": "de",
+ "spanish": "es", "español": "es", "espanol": "es", "es-es": "es", "es-mx": "es", "es-ar": "es",
"french": "fr", "français": "fr", "france": "fr", "fr-fr": "fr", "fr-be": "fr", "fr-ca": "fr", "fr-ch": "fr",
"ukrainian": "uk", "ukrainisch": "uk", "українська": "uk", "uk-ua": "uk", "ua": "uk",
"turkish": "tr", "türkçe": "tr", "tr-tr": "tr",
+ # Afrikaans — South African Dutch-derived language; "af-ZA" is the common BCP-47 tag.
+ "afrikaans": "af", "af-za": "af",
+ # Korean
+ "korean": "ko", "한국어": "ko", "ko-kr": "ko",
+ # Italian
+ "italian": "it", "italiano": "it", "it-it": "it", "it-ch": "it",
+ # Irish (Gaeilge) — ga is the BCP-47 code
+ "irish": "ga", "gaeilge": "ga", "ga-ie": "ga",
+ # Portuguese — bare "portuguese" routes to European Portuguese; pt-br
+ # is in the same family but rendered identically here (no separate br catalog).
+ "portuguese": "pt", "português": "pt", "portugues": "pt",
+ "pt-pt": "pt", "pt-br": "pt", "brazilian": "pt", "brasileiro": "pt",
+ # Russian
+ "russian": "ru", "русский": "ru", "ru-ru": "ru",
+ # Hungarian
+ "hungarian": "hu", "magyar": "hu", "hu-hu": "hu",
}
_catalog_cache: dict[str, dict[str, str]] = {}
diff --git a/gateway/run.py b/gateway/run.py
index 1b741b6a81a..6d04ee81f2a 100644
--- a/gateway/run.py
+++ b/gateway/run.py
@@ -5532,7 +5532,7 @@ class GatewayRunner:
invalidation_reason="stop_command",
)
logger.info("STOP for session %s — agent interrupted, session lock released", _quick_key)
- return EphemeralReply("⚡ Stopped. You can continue this session.")
+ return EphemeralReply(t("gateway.stop.stopped"))
# /reset and /new must bypass the running-agent guard so they
# actually dispatch as commands instead of being queued as user
@@ -7684,11 +7684,11 @@ class GatewayRunner:
session_info = ""
if new_entry:
- header = self._telegram_topic_new_header(source) or "✨ Session reset! Starting fresh."
+ header = self._telegram_topic_new_header(source) or t("gateway.reset.header_default")
else:
# No existing session, just create one
new_entry = self.session_store.get_or_create_session(source, force_new=True)
- header = self._telegram_topic_new_header(source) or "✨ New session started!"
+ header = self._telegram_topic_new_header(source) or t("gateway.reset.header_new")
# Set session title if provided with /new
_title_arg = event.get_command_args().strip()
@@ -7699,18 +7699,18 @@ class GatewayRunner:
sanitized = SessionDB.sanitize_title(_title_arg)
except ValueError as e:
sanitized = None
- _title_note = f"\n⚠️ Title rejected: {e}"
+ _title_note = t("gateway.reset.title_rejected", error=str(e))
if sanitized:
try:
self._session_db.set_session_title(new_entry.session_id, sanitized)
- header = f"✨ New session started: {sanitized}"
+ header = t("gateway.reset.header_titled", title=sanitized)
except ValueError as e:
- _title_note = f"\n⚠️ {e} — session started untitled."
+ _title_note = t("gateway.reset.title_error_untitled", error=str(e))
except Exception:
pass
elif not _title_note:
# sanitize_title returned empty (whitespace-only / unprintable)
- _title_note = "\n⚠️ Title is empty after cleanup — session started untitled."
+ _title_note = t("gateway.reset.title_empty_untitled")
header = header + _title_note
# When /new runs inside a Telegram DM topic lane, rewrite the
@@ -7736,7 +7736,7 @@ class GatewayRunner:
# Append a random tip to the reset message
try:
from hermes_cli.tips import get_random_tip
- _tip_line = f"\n✦ Tip: {get_random_tip()}"
+ _tip_line = t("gateway.reset.tip", tip=get_random_tip())
except Exception:
_tip_line = ""
@@ -7753,8 +7753,8 @@ class GatewayRunner:
profile_name = get_active_profile_name()
lines = [
- f"👤 **Profile:** `{profile_name}`",
- f"📂 **Home:** `{display}`",
+ t("gateway.profile.header", profile=profile_name),
+ t("gateway.profile.home", home=display),
]
return "\n".join(lines)
@@ -7790,7 +7790,7 @@ class GatewayRunner:
try:
output = await asyncio.to_thread(run_slash, text)
except Exception as exc: # pragma: no cover - defensive
- return f"⚠ kanban error: {exc}"
+ return t("gateway.kanban.error_prefix", error=exc)
# Auto-subscribe on create. Parse the task id from the CLI's standard
# success line ("Created t_abcd (ready, assignee=...)"). If the user
@@ -7825,8 +7825,8 @@ class GatewayRunner:
await asyncio.to_thread(_sub)
output = (
output.rstrip()
- + f"\n(subscribed — you'll be notified when {task_id} "
- f"completes or blocks)"
+ + "\n"
+ + t("gateway.kanban.subscribed_suffix", task_id=task_id)
)
except Exception as exc:
logger.warning("kanban create auto-subscribe failed: %s", exc)
@@ -7834,8 +7834,8 @@ class GatewayRunner:
# Gateway messages have practical length caps; truncate long
# listings to keep the UX reasonable.
if len(output) > 3800:
- output = output[:3800] + "\n… (truncated; use `hermes kanban …` in your terminal for full output)"
- return output or "(no output)"
+ output = output[:3800] + "\n" + t("gateway.kanban.truncated_suffix")
+ return output or t("gateway.kanban.no_output")
async def _handle_status_command(self, event: MessageEvent) -> str:
"""Handle /status command."""
@@ -7879,23 +7879,23 @@ class GatewayRunner:
db_total_tokens = 0
lines = [
- "📊 **Hermes Gateway Status**",
+ t("gateway.status.header"),
"",
- f"**Session ID:** `{session_entry.session_id}`",
+ t("gateway.status.session_id", session_id=session_entry.session_id),
]
if title:
- lines.append(f"**Title:** {title}")
+ lines.append(t("gateway.status.title", title=title))
lines.extend([
- f"**Created:** {session_entry.created_at.strftime('%Y-%m-%d %H:%M')}",
- f"**Last Activity:** {session_entry.updated_at.strftime('%Y-%m-%d %H:%M')}",
- f"**Tokens:** {db_total_tokens:,}",
- f"**Agent Running:** {'Yes ⚡' if is_running else 'No'}",
+ t("gateway.status.created", timestamp=session_entry.created_at.strftime('%Y-%m-%d %H:%M')),
+ t("gateway.status.last_activity", timestamp=session_entry.updated_at.strftime('%Y-%m-%d %H:%M')),
+ t("gateway.status.tokens", tokens=f"{db_total_tokens:,}"),
+ t("gateway.status.agent_running", state=t("gateway.status.state_yes") if is_running else t("gateway.status.state_no")),
])
if queue_depth:
- lines.append(f"**Queued follow-ups:** {queue_depth}")
+ lines.append(t("gateway.status.queued", count=queue_depth))
lines.extend([
"",
- f"**Connected Platforms:** {', '.join(connected_platforms)}",
+ t("gateway.status.platforms", platforms=', '.join(connected_platforms)),
])
return "\n".join(lines)
@@ -7919,7 +7919,7 @@ class GatewayRunner:
{
"session_key": session_key,
"elapsed": elapsed,
- "state": "starting" if is_pending else "running",
+ "state": t("gateway.agents.state_starting") if is_pending else t("gateway.agents.state_running"),
"session_id": "" if is_pending else str(getattr(agent, "session_id", "") or ""),
"model": "" if is_pending else str(getattr(agent, "model", "") or ""),
}
@@ -7942,14 +7942,14 @@ class GatewayRunner:
]
lines = [
- "🤖 **Active Agents & Tasks**",
+ t("gateway.agents.header"),
"",
- f"**Active agents:** {len(agent_rows)}",
+ t("gateway.agents.active_agents", count=len(agent_rows)),
]
if agent_rows:
for idx, row in enumerate(agent_rows[:12], 1):
- current = " · this chat" if row["session_key"] == current_session_key else ""
+ current = t("gateway.agents.this_chat") if row["session_key"] == current_session_key else ""
sid = f" · `{row['session_id']}`" if row["session_id"] else ""
model = f" · `{row['model']}`" if row["model"] else ""
lines.append(
@@ -7957,12 +7957,12 @@ class GatewayRunner:
f"{format_uptime_short(row['elapsed'])}{sid}{model}{current}"
)
if len(agent_rows) > 12:
- lines.append(f"... and {len(agent_rows) - 12} more")
+ lines.append(t("gateway.agents.more", count=len(agent_rows) - 12))
lines.extend(
[
"",
- f"**Running background processes:** {len(running_processes)}",
+ t("gateway.agents.running_processes", count=len(running_processes)),
]
)
if running_processes:
@@ -7975,18 +7975,18 @@ class GatewayRunner:
f"{format_uptime_short(int(proc.get('uptime_seconds', 0)))} · `{cmd}`"
)
if len(running_processes) > 12:
- lines.append(f"... and {len(running_processes) - 12} more")
+ lines.append(t("gateway.agents.more", count=len(running_processes) - 12))
lines.extend(
[
"",
- f"**Gateway async jobs:** {len(background_tasks)}",
+ t("gateway.agents.async_jobs", count=len(background_tasks)),
]
)
if not agent_rows and not running_processes and not background_tasks:
lines.append("")
- lines.append("No active agents or running tasks.")
+ lines.append(t("gateway.agents.none"))
return "\n".join(lines)
@@ -8015,7 +8015,7 @@ class GatewayRunner:
invalidation_reason="stop_command_pending",
)
logger.info("STOP (pending) for session %s — sentinel cleared", session_key)
- return EphemeralReply("⚡ Stopped. The agent hadn't started yet — you can continue this session.")
+ return EphemeralReply(t("gateway.stop.stopped_pending"))
if agent:
# Force-clean the session lock so a truly hung agent doesn't
# keep it locked forever.
@@ -8025,9 +8025,9 @@ class GatewayRunner:
interrupt_reason=_INTERRUPT_REASON_STOP,
invalidation_reason="stop_command_handler",
)
- return EphemeralReply("⚡ Stopped. You can continue this session.")
+ return EphemeralReply(t("gateway.stop.stopped"))
else:
- return "No active task to stop."
+ return t("gateway.stop.no_active")
async def _handle_restart_command(self, event: MessageEvent) -> Union[str, EphemeralReply]:
"""Handle /restart command - drain active work, then restart the gateway."""
@@ -8055,7 +8055,7 @@ class GatewayRunner:
count = self._running_agent_count()
if count:
return t("gateway.draining", count=count)
- return EphemeralReply("⏳ Gateway restart already in progress...")
+ return EphemeralReply(t("gateway.restart.in_progress"))
# Save the requester's routing info so the new gateway process can
# notify them once it comes back online.
@@ -8107,7 +8107,7 @@ class GatewayRunner:
self.request_restart(detached=True, via_service=False)
if active_agents:
return t("gateway.draining", count=active_agents)
- return EphemeralReply("♻ Restarting gateway. If you aren't notified within 60 seconds, restart from the console with `hermes gateway restart`.")
+ return EphemeralReply(t("gateway.restart.restarting"))
def _is_stale_restart_redelivery(self, event: MessageEvent) -> bool:
"""Return True if this /restart is a Telegram re-delivery we already handled.
@@ -8163,20 +8163,20 @@ class GatewayRunner:
"""Handle /help command - list available commands."""
from hermes_cli.commands import gateway_help_lines
lines = [
- "📖 **Hermes Commands**\n",
+ t("gateway.help.header"),
*gateway_help_lines(),
]
try:
from agent.skill_commands import get_skill_commands
skill_cmds = get_skill_commands()
if skill_cmds:
- lines.append(f"\n⚡ **Skill Commands** ({len(skill_cmds)} active):")
+ lines.append(t("gateway.help.skill_header", count=len(skill_cmds)))
# Show first 10, then point to /commands for the rest
sorted_cmds = sorted(skill_cmds)
for cmd in sorted_cmds[:10]:
lines.append(f"`{cmd}` — {skill_cmds[cmd]['description']}")
if len(sorted_cmds) > 10:
- lines.append(f"\n... and {len(sorted_cmds) - 10} more. Use `/commands` for the full paginated list.")
+ lines.append(t("gateway.help.more_use_commands", count=len(sorted_cmds) - 10))
except Exception:
pass
return _telegramize_command_mentions(
@@ -8193,7 +8193,7 @@ class GatewayRunner:
try:
requested_page = int(raw_args)
except ValueError:
- return "Usage: `/commands [page]`"
+ return t("gateway.commands.usage")
else:
requested_page = 1
@@ -8204,15 +8204,15 @@ class GatewayRunner:
skill_cmds = get_skill_commands()
if skill_cmds:
entries.append("")
- entries.append("⚡ **Skill Commands**:")
+ entries.append(t("gateway.commands.skill_header"))
for cmd in sorted(skill_cmds):
- desc = skill_cmds[cmd].get("description", "").strip() or "Skill command"
+ desc = skill_cmds[cmd].get("description", "").strip() or t("gateway.commands.default_desc")
entries.append(f"`{cmd}` — {desc}")
except Exception:
pass
if not entries:
- return "No commands available."
+ return t("gateway.commands.none")
from gateway.config import Platform
page_size = 15 if event.source.platform == Platform.TELEGRAM else 20
@@ -8222,19 +8222,19 @@ class GatewayRunner:
page_entries = entries[start:start + page_size]
lines = [
- f"📚 **Commands** ({len(entries)} total, page {page}/{total_pages})",
+ t("gateway.commands.header", total=len(entries), page=page, total_pages=total_pages),
"",
*page_entries,
]
if total_pages > 1:
nav_parts = []
if page > 1:
- nav_parts.append(f"`/commands {page - 1}` ← prev")
+ nav_parts.append(t("gateway.commands.nav_prev", page=page - 1))
if page < total_pages:
- nav_parts.append(f"next → `/commands {page + 1}`")
+ nav_parts.append(t("gateway.commands.nav_next", page=page + 1))
lines.extend(["", " | ".join(nav_parts)])
if page != requested_page:
- lines.append(f"_(Requested page {requested_page} was out of range, showing page {page}.)_")
+ lines.append(t("gateway.commands.out_of_range", requested=requested_page, page=page))
return _telegramize_command_mentions(
"\n".join(lines),
getattr(getattr(event, "source", None), "platform", None),
@@ -8346,7 +8346,7 @@ class GatewayRunner:
custom_providers=custom_provs,
)
if not result.success:
- return f"Error: {result.error_message}"
+ return t("gateway.model.error_prefix", error=result.error_message)
# Update cached agent in-place
cached_entry = None
@@ -8390,8 +8390,8 @@ class GatewayRunner:
# Build confirmation text
plabel = result.provider_label or result.target_provider
- lines = [f"Model switched to `{result.new_model}`"]
- lines.append(f"Provider: {plabel}")
+ lines = [t("gateway.model.switched", model=result.new_model)]
+ lines.append(t("gateway.model.provider_label", provider=plabel))
mi = result.model_info
from hermes_cli.model_switch import resolve_display_context_length
_sw_config_ctx = None
@@ -8414,14 +8414,14 @@ class GatewayRunner:
config_context_length=_sw_config_ctx,
)
if ctx:
- lines.append(f"Context: {ctx:,} tokens")
+ lines.append(t("gateway.model.context_label", tokens=f"{ctx:,}"))
if mi:
if mi.max_output:
- lines.append(f"Max output: {mi.max_output:,} tokens")
+ lines.append(t("gateway.model.max_output_label", tokens=f"{mi.max_output:,}"))
if mi.has_cost_data():
- lines.append(f"Cost: {mi.format_cost()}")
- lines.append(f"Capabilities: {mi.format_capabilities()}")
- lines.append("_(session only — use `/model --global` to persist)_")
+ lines.append(t("gateway.model.cost_label", cost=mi.format_cost()))
+ lines.append(t("gateway.model.capabilities_label", capabilities=mi.format_capabilities()))
+ lines.append(t("gateway.model.session_only_hint"))
return "\n".join(lines)
metadata = self._thread_metadata_for_source(source, self._reply_anchor_for_event(event))
@@ -8439,7 +8439,7 @@ class GatewayRunner:
# Fallback: text list (for platforms without picker or if picker failed)
provider_label = get_label(current_provider)
- lines = [f"Current: `{current_model or 'unknown'}` on {provider_label}", ""]
+ lines = [t("gateway.model.current_label", model=current_model or "unknown", provider=provider_label), ""]
try:
providers = list_authenticated_providers(
@@ -8451,11 +8451,11 @@ class GatewayRunner:
max_models=5,
)
for p in providers:
- tag = " (current)" if p["is_current"] else ""
+ tag = t("gateway.model.current_tag") if p["is_current"] else ""
lines.append(f"**{p['name']}** `--provider {p['slug']}`{tag}:")
if p["models"]:
model_strs = ", ".join(f"`{m}`" for m in p["models"])
- extra = f" (+{p['total_models'] - len(p['models'])} more)" if p["total_models"] > len(p["models"]) else ""
+ extra = t("gateway.model.more_models_suffix", count=p["total_models"] - len(p["models"])) if p["total_models"] > len(p["models"]) else ""
lines.append(f" {model_strs}{extra}")
elif p.get("api_url"):
lines.append(f" `{p['api_url']}`")
@@ -8463,9 +8463,9 @@ class GatewayRunner:
except Exception:
pass
- lines.append("`/model ` — switch model")
- lines.append("`/model --provider ` — switch provider")
- lines.append("`/model --global` — persist")
+ lines.append(t("gateway.model.usage_switch_model"))
+ lines.append(t("gateway.model.usage_switch_provider"))
+ lines.append(t("gateway.model.usage_persist"))
return "\n".join(lines)
# Perform the switch
@@ -8482,7 +8482,7 @@ class GatewayRunner:
)
if not result.success:
- return f"Error: {result.error_message}"
+ return t("gateway.model.error_prefix", error=result.error_message)
# If there's a cached agent, update it in-place
cached_entry = None
@@ -8547,8 +8547,8 @@ class GatewayRunner:
# Build confirmation message with full metadata
provider_label = result.provider_label or result.target_provider
- lines = [f"Model switched to `{result.new_model}`"]
- lines.append(f"Provider: {provider_label}")
+ lines = [t("gateway.model.switched", model=result.new_model)]
+ lines.append(t("gateway.model.provider_label", provider=provider_label))
# Context: always resolve via the provider-aware chain so Codex OAuth,
# Copilot, and Nous-enforced caps win over the raw models.dev entry.
@@ -8574,13 +8574,13 @@ class GatewayRunner:
config_context_length=_sw2_config_ctx,
)
if ctx:
- lines.append(f"Context: {ctx:,} tokens")
+ lines.append(t("gateway.model.context_label", tokens=f"{ctx:,}"))
if mi:
if mi.max_output:
- lines.append(f"Max output: {mi.max_output:,} tokens")
+ lines.append(t("gateway.model.max_output_label", tokens=f"{mi.max_output:,}"))
if mi.has_cost_data():
- lines.append(f"Cost: {mi.format_cost()}")
- lines.append(f"Capabilities: {mi.format_capabilities()}")
+ lines.append(t("gateway.model.cost_label", cost=mi.format_cost()))
+ lines.append(t("gateway.model.capabilities_label", capabilities=mi.format_capabilities()))
# Cache notice
cache_enabled = (
@@ -8588,15 +8588,15 @@ class GatewayRunner:
or result.api_mode == "anthropic_messages"
)
if cache_enabled:
- lines.append("Prompt caching: enabled")
+ lines.append(t("gateway.model.prompt_caching_enabled"))
if result.warning_message:
- lines.append(f"Warning: {result.warning_message}")
+ lines.append(t("gateway.model.warning_prefix", warning=result.warning_message))
if persist_global:
- lines.append("Saved to config.yaml (`--global`)")
+ lines.append(t("gateway.model.saved_global"))
else:
- lines.append("_(session only -- add `--global` to persist)_")
+ lines.append(t("gateway.model.session_only_hint"))
return "\n".join(lines)
@@ -8615,18 +8615,18 @@ class GatewayRunner:
personalities = {}
if not personalities:
- return f"No personalities configured in `{display_hermes_home()}/config.yaml`"
+ return t("gateway.personality.none_configured", path=display_hermes_home())
if not args:
- lines = ["🎭 **Available Personalities**\n"]
- lines.append("• `none` — (no personality overlay)")
+ lines = [t("gateway.personality.header")]
+ lines.append(t("gateway.personality.none_option"))
for name, prompt in personalities.items():
if isinstance(prompt, dict):
preview = prompt.get("description") or prompt.get("system_prompt", "")[:50]
else:
preview = prompt[:50] + "..." if len(prompt) > 50 else prompt
- lines.append(f"• `{name}` — {preview}")
- lines.append("\nUsage: `/personality `")
+ lines.append(t("gateway.personality.item", name=name, preview=preview))
+ lines.append(t("gateway.personality.usage"))
return "\n".join(lines)
def _resolve_prompt(value):
@@ -8646,9 +8646,9 @@ class GatewayRunner:
config["agent"]["system_prompt"] = ""
atomic_yaml_write(config_path, config)
except Exception as e:
- return f"⚠️ Failed to save personality change: {e}"
+ return t("gateway.personality.save_failed", error=str(e))
self._ephemeral_system_prompt = ""
- return "🎭 Personality cleared — using base agent behavior.\n_(takes effect on next message)_"
+ return t("gateway.personality.cleared")
elif args in personalities:
new_prompt = _resolve_prompt(personalities[args])
@@ -8659,15 +8659,15 @@ class GatewayRunner:
config["agent"]["system_prompt"] = new_prompt
atomic_yaml_write(config_path, config)
except Exception as e:
- return f"⚠️ Failed to save personality change: {e}"
+ return t("gateway.personality.save_failed", error=str(e))
# Update in-memory so it takes effect on the very next message.
self._ephemeral_system_prompt = new_prompt
- return f"🎭 Personality set to **{args}**\n_(takes effect on next message)_"
+ return t("gateway.personality.set_to", name=args)
available = "`none`, " + ", ".join(f"`{n}`" for n in personalities)
- return f"Unknown personality: `{args}`\n\nAvailable: {available}"
+ return t("gateway.personality.unknown", name=args, available=available)
async def _handle_retry_command(self, event: MessageEvent) -> str:
"""Handle /retry command - re-send the last user message."""
@@ -8685,7 +8685,7 @@ class GatewayRunner:
break
if not last_user_msg:
- return "No previous message to retry."
+ return t("gateway.retry.no_previous")
# Truncate history to before the last user message and persist
truncated = history[:last_user_idx]
@@ -8767,7 +8767,7 @@ class GatewayRunner:
mgr, session_entry = self._get_goal_manager_for_event(event)
if mgr is None:
- return "Goals unavailable on this session."
+ return t("gateway.goal.unavailable")
if not args or lower == "status":
return mgr.status_line()
@@ -8775,7 +8775,7 @@ class GatewayRunner:
if lower == "pause":
state = mgr.pause(reason="user-paused")
if state is None:
- return "No goal set."
+ return t("gateway.goal.no_goal_set")
try:
adapter = self.adapters.get(event.source.platform) if event.source else None
_quick_key = self._session_key_for_source(event.source) if event.source else None
@@ -8783,16 +8783,13 @@ class GatewayRunner:
self._clear_goal_pending_continuations(_quick_key, adapter)
except Exception as exc:
logger.debug("goal pause: pending continuation cleanup failed: %s", exc)
- return f"⏸ Goal paused: {state.goal}"
+ return t("gateway.goal.paused", goal=state.goal)
if lower == "resume":
state = mgr.resume()
if state is None:
- return "No goal to resume."
- return (
- f"▶ Goal resumed: {state.goal}\n"
- "Send any message to continue, or wait — I'll take the next step on the next turn."
- )
+ return t("gateway.goal.no_resume")
+ return t("gateway.goal.resumed", goal=state.goal)
if lower in ("clear", "stop", "done"):
had = mgr.has_goal()
@@ -8810,7 +8807,7 @@ class GatewayRunner:
try:
state = mgr.set(args)
except ValueError as exc:
- return f"Invalid goal: {exc}"
+ return t("gateway.goal.invalid", error=str(exc))
# Queue the goal text as an immediate first turn so the agent
# starts making progress. The post-turn hook takes over after.
@@ -8829,11 +8826,7 @@ class GatewayRunner:
except Exception as exc:
logger.debug("goal kickoff enqueue failed: %s", exc)
- return (
- f"⊙ Goal set ({state.max_turns}-turn budget): {state.goal}\n"
- "I'll keep working until the goal is done, you pause/clear it, or the budget is exhausted.\n"
- "Controls: /goal status · /goal pause · /goal resume · /goal clear"
- )
+ return t("gateway.goal.set", budget=state.max_turns, goal=state.goal)
async def _send_goal_status_notice(self, source: Any, message: str) -> None:
"""Send a /goal judge status line back to the originating chat/thread."""
@@ -8980,7 +8973,7 @@ class GatewayRunner:
break
if last_user_idx is None:
- return "Nothing to undo."
+ return t("gateway.undo.nothing")
removed_msg = history[last_user_idx].get("content", "")
removed_count = len(history) - last_user_idx
@@ -8989,7 +8982,7 @@ class GatewayRunner:
session_entry.last_prompt_tokens = 0
preview = removed_msg[:40] + "..." if len(removed_msg) > 40 else removed_msg
- return f"↩️ Undid {removed_count} message(s).\nRemoved: \"{preview}\""
+ return t("gateway.undo.removed", count=removed_count, preview=preview)
async def _handle_set_home_command(self, event: MessageEvent) -> str:
"""Handle /sethome command -- set the current chat as the platform's home channel."""
@@ -9010,7 +9003,7 @@ class GatewayRunner:
# /sethome is run from the parent chat instead of a thread.
save_env_value(thread_env_key, str(thread_id or ""))
except Exception as e:
- return f"Failed to save home channel: {e}"
+ return t("gateway.set_home.save_failed", error=e)
# Keep the running gateway config in sync too. The pre-restart
# notification path reads self.config before the process reloads env.
@@ -9026,10 +9019,7 @@ class GatewayRunner:
thread_id=str(thread_id) if thread_id else None,
)
- return (
- f"✅ Home channel set to **{chat_name}** (ID: {chat_id}).\n"
- f"Cron jobs and cross-platform messages will be delivered here."
- )
+ return t("gateway.set_home.success", name=chat_name, chat_id=chat_id)
@staticmethod
def _get_guild_id(event: MessageEvent) -> Optional[int]:
@@ -9059,26 +9049,19 @@ class GatewayRunner:
self._save_voice_modes()
if adapter:
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
- return (
- "Voice mode enabled.\n"
- "I'll reply with voice when you send voice messages.\n"
- "Use /voice tts to get voice replies for all messages."
- )
+ return t("gateway.voice.enabled_voice_only")
elif args in ("off", "disable"):
self._voice_mode[voice_key] = "off"
self._save_voice_modes()
if adapter:
self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=True)
- return "Voice mode disabled. Text-only replies."
+ return t("gateway.voice.disabled_text")
elif args == "tts":
self._voice_mode[voice_key] = "all"
self._save_voice_modes()
if adapter:
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
- return (
- "Auto-TTS enabled.\n"
- "All replies will include a voice message."
- )
+ return t("gateway.voice.tts_enabled")
elif args in ("channel", "join"):
return await self._handle_voice_channel_join(event)
elif args == "leave":
@@ -9086,9 +9069,9 @@ class GatewayRunner:
elif args == "status":
mode = self._voice_mode.get(voice_key, "off")
labels = {
- "off": "Off (text only)",
- "voice_only": "On (voice reply to voice messages)",
- "all": "TTS (voice reply to all messages)",
+ "off": t("gateway.voice.label_off"),
+ "voice_only": t("gateway.voice.label_voice_only"),
+ "all": t("gateway.voice.label_all"),
}
# Append voice channel info if connected
adapter = self.adapters.get(event.source.platform)
@@ -9097,15 +9080,15 @@ class GatewayRunner:
info = adapter.get_voice_channel_info(guild_id)
if info:
lines = [
- f"Voice mode: {labels.get(mode, mode)}",
- f"Voice channel: #{info['channel_name']}",
- f"Participants: {info['member_count']}",
+ t("gateway.voice.status_mode", label=labels.get(mode, mode)),
+ t("gateway.voice.status_channel", channel=info['channel_name']),
+ t("gateway.voice.status_participants", count=info['member_count']),
]
for m in info["members"]:
- status = " (speaking)" if m.get("is_speaking") else ""
- lines.append(f" - {m['display_name']}{status}")
+ status = t("gateway.voice.speaking") if m.get("is_speaking") else ""
+ lines.append(t("gateway.voice.status_member", name=m['display_name'], status=status))
return "\n".join(lines)
- return f"Voice mode: {labels.get(mode, mode)}"
+ return t("gateway.voice.status_mode", label=labels.get(mode, mode))
else:
# Toggle: off → on, on/all → off
current = self._voice_mode.get(voice_key, "off")
@@ -9114,13 +9097,13 @@ class GatewayRunner:
self._save_voice_modes()
if adapter:
self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True)
- return "Voice mode enabled."
+ return t("gateway.voice.enabled_short")
else:
self._voice_mode[voice_key] = "off"
self._save_voice_modes()
if adapter:
self._set_adapter_auto_tts_disabled(adapter, chat_id, disabled=True)
- return "Voice mode disabled."
+ return t("gateway.voice.disabled_short")
async def _handle_voice_channel_join(self, event: MessageEvent) -> str:
"""Join the user's current Discord voice channel."""
@@ -9560,10 +9543,7 @@ class GatewayRunner:
pass
if not cp_cfg.get("enabled", False):
- return (
- "Checkpoints are not enabled.\n"
- "Enable in config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
- )
+ return t("gateway.rollback.not_enabled")
mgr = CheckpointManager(
enabled=True,
@@ -9582,7 +9562,7 @@ class GatewayRunner:
# Restore by number or hash
checkpoints = mgr.list_checkpoints(cwd)
if not checkpoints:
- return f"No checkpoints found for {cwd}"
+ return t("gateway.rollback.none_found", cwd=cwd)
target_hash = None
try:
@@ -9590,17 +9570,18 @@ class GatewayRunner:
if 0 <= idx < len(checkpoints):
target_hash = checkpoints[idx]["hash"]
else:
- return f"Invalid checkpoint number. Use 1-{len(checkpoints)}."
+ return t("gateway.rollback.invalid_number", max=len(checkpoints))
except ValueError:
target_hash = arg
result = mgr.restore(cwd, target_hash)
if result["success"]:
- return (
- f"✅ Restored to checkpoint {result['restored_to']}: {result['reason']}\n"
- f"A pre-rollback snapshot was saved automatically."
+ return t(
+ "gateway.rollback.restored",
+ hash=result["restored_to"],
+ reason=result["reason"],
)
- return f"❌ {result['error']}"
+ return t("gateway.rollback.restore_failed", error=result["error"])
async def _handle_background_command(self, event: MessageEvent) -> str:
"""Handle /background — run a prompt in a separate background session.
@@ -9611,12 +9592,7 @@ class GatewayRunner:
"""
prompt = event.get_command_args().strip()
if not prompt:
- return (
- "Usage: /background \n"
- "Example: /background Summarize the top HN stories today\n\n"
- "Runs the prompt in a separate session. "
- "You can keep chatting — the result will appear here when done."
- )
+ return t("gateway.background.usage")
source = event.source
task_id = f"bg_{datetime.now().strftime('%H%M%S')}_{os.urandom(3).hex()}"
@@ -9636,7 +9612,7 @@ class GatewayRunner:
_task.add_done_callback(self._background_tasks.discard)
preview = prompt[:60] + ("..." if len(prompt) > 60 else "")
- return f'🔄 Background task started: "{preview}"\nTask ID: {task_id}\nYou can keep chatting — results will appear when done.'
+ return t("gateway.background.started", preview=preview, task_id=task_id)
async def _run_background_task(
self,
@@ -9835,20 +9811,27 @@ class GatewayRunner:
# Show current state
rc = self._reasoning_config
if rc is None:
- level = "medium (default)"
+ level = t("gateway.reasoning.level_default")
elif rc.get("enabled") is False:
- level = "none (disabled)"
+ level = t("gateway.reasoning.level_disabled")
else:
level = rc.get("effort", "medium")
- display_state = "on ✓" if self._show_reasoning else "off"
+ display_state = (
+ t("gateway.reasoning.display_on")
+ if self._show_reasoning
+ else t("gateway.reasoning.display_off")
+ )
has_session_override = session_key in (getattr(self, "_session_reasoning_overrides", {}) or {})
- scope = "session override" if has_session_override else "global config"
- return (
- "🧠 **Reasoning Settings**\n\n"
- f"**Effort:** `{level}`\n"
- f"**Scope:** {scope}\n"
- f"**Display:** {display_state}\n\n"
- "_Usage:_ `/reasoning [--global]`"
+ scope = (
+ t("gateway.reasoning.scope_session")
+ if has_session_override
+ else t("gateway.reasoning.scope_global")
+ )
+ return t(
+ "gateway.reasoning.status",
+ level=level,
+ scope=scope,
+ display=display_state,
)
# Display toggle (per-platform)
@@ -9856,35 +9839,30 @@ class GatewayRunner:
if args in ("show", "on"):
self._show_reasoning = True
_save_config_key(f"display.platforms.{platform_key}.show_reasoning", True)
- return (
- "🧠 ✓ Reasoning display: **ON**\n"
- f"Model thinking will be shown before each response on **{platform_key}**."
- )
+ return t("gateway.reasoning.display_set_on", platform=platform_key)
if args in ("hide", "off"):
self._show_reasoning = False
_save_config_key(f"display.platforms.{platform_key}.show_reasoning", False)
- return f"🧠 ✓ Reasoning display: **OFF** for **{platform_key}**"
+ return t("gateway.reasoning.display_set_off", platform=platform_key)
# Effort level change
effort = args.strip()
if effort == "reset":
if persist_global:
- return "⚠️ `/reasoning reset --global` is not supported. Use `/reasoning --global` to change the global default."
+ return t("gateway.reasoning.reset_global_unsupported")
self._set_session_reasoning_override(session_key, None)
self._reasoning_config = self._load_reasoning_config()
self._evict_cached_agent(session_key)
- return "🧠 ✓ Session reasoning override cleared; falling back to global config."
+ return t("gateway.reasoning.reset_done")
if effort == "none":
parsed = {"enabled": False}
elif effort in ("minimal", "low", "medium", "high", "xhigh"):
parsed = {"enabled": True, "effort": effort}
else:
- return (
- f"⚠️ Unknown argument: `{effort or raw_args.lower()}`\n\n"
- "**Valid levels:** none, minimal, low, medium, high, xhigh\n"
- "**Display:** show, hide\n"
- "**Persist:** add `--global` to save beyond this session"
+ return t(
+ "gateway.reasoning.unknown_arg",
+ arg=effort or raw_args.lower(),
)
self._reasoning_config = parsed
@@ -9892,14 +9870,14 @@ class GatewayRunner:
if _save_config_key("agent.reasoning_effort", effort):
self._set_session_reasoning_override(session_key, None)
self._evict_cached_agent(session_key)
- return f"🧠 ✓ Reasoning effort set to `{effort}` (saved to config)\n_(takes effect on next message)_"
+ return t("gateway.reasoning.set_global", effort=effort)
self._set_session_reasoning_override(session_key, parsed)
self._evict_cached_agent(session_key)
- return f"🧠 ✓ Reasoning effort set to `{effort}` (session only — config save failed)\n_(takes effect on next message)_"
+ return t("gateway.reasoning.set_global_save_failed", effort=effort)
self._set_session_reasoning_override(session_key, parsed)
self._evict_cached_agent(session_key)
- return f"🧠 ✓ Reasoning effort set to `{effort}` (session only — add `--global` to persist)\n_(takes effect on next message)_"
+ return t("gateway.reasoning.set_session", effort=effort)
async def _handle_fast_command(self, event: MessageEvent) -> str:
"""Handle /fast — mirror the CLI Priority Processing toggle in gateway chats."""
@@ -9913,7 +9891,7 @@ class GatewayRunner:
user_config = _load_gateway_config()
model = _resolve_gateway_model(user_config)
if not model_supports_fast_mode(model):
- return "⚡ /fast is only available for OpenAI models that support Priority Processing."
+ return t("gateway.fast.not_supported")
def _save_config_key(key_path: str, value):
"""Save a dot-separated key to config.yaml."""
@@ -9936,30 +9914,23 @@ class GatewayRunner:
return False
if not args or args == "status":
- status = "fast" if self._service_tier == "priority" else "normal"
- return (
- "⚡ Priority Processing\n\n"
- f"Current mode: `{status}`\n\n"
- "_Usage:_ `/fast `"
- )
+ status = t("gateway.fast.status_fast") if self._service_tier == "priority" else t("gateway.fast.status_normal")
+ return t("gateway.fast.status", mode=status)
if args in {"fast", "on"}:
self._service_tier = "priority"
saved_value = "fast"
- label = "FAST"
+ label = t("gateway.fast.label_fast")
elif args in {"normal", "off"}:
self._service_tier = None
saved_value = "normal"
- label = "NORMAL"
+ label = t("gateway.fast.label_normal")
else:
- return (
- f"⚠️ Unknown argument: `{args}`\n\n"
- "**Valid options:** normal, fast, status"
- )
+ return t("gateway.fast.unknown_arg", arg=args)
if _save_config_key("agent.service_tier", saved_value):
- return f"⚡ ✓ Priority Processing: **{label}** (saved to config)\n_(takes effect on next message)_"
- return f"⚡ ✓ Priority Processing: **{label}** (this session only)"
+ return t("gateway.fast.saved", label=label)
+ return t("gateway.fast.session_only", label=label)
async def _handle_yolo_command(self, event: MessageEvent) -> Union[str, EphemeralReply]:
"""Handle /yolo — toggle dangerous command approval bypass for this session only."""
@@ -9973,10 +9944,10 @@ class GatewayRunner:
current = is_session_yolo_enabled(session_key)
if current:
disable_session_yolo(session_key)
- return EphemeralReply("⚠️ YOLO mode **OFF** for this session — dangerous commands will require approval.")
+ return EphemeralReply(t("gateway.yolo.disabled"))
else:
enable_session_yolo(session_key)
- return EphemeralReply("⚡ YOLO mode **ON** for this session — all commands auto-approved. Use with caution.")
+ return EphemeralReply(t("gateway.yolo.enabled"))
async def _handle_verbose_command(self, event: MessageEvent) -> str:
"""Handle /verbose command — cycle tool progress display mode.
@@ -10002,19 +9973,15 @@ class GatewayRunner:
gate_enabled = False
if not gate_enabled:
- return (
- "The `/verbose` command is not enabled for messaging platforms.\n\n"
- "Enable it in `config.yaml`:\n```yaml\n"
- "display:\n tool_progress_command: true\n```"
- )
+ return t("gateway.verbose.not_enabled")
# --- cycle mode (per-platform) ----------------------------------------
cycle = ["off", "new", "all", "verbose"]
descriptions = {
- "off": "⚙️ Tool progress: **OFF** — no tool activity shown.",
- "new": "⚙️ Tool progress: **NEW** — shown when tool changes (preview length: `display.tool_preview_length`, default 40).",
- "all": "⚙️ Tool progress: **ALL** — every tool call shown (preview length: `display.tool_preview_length`, default 40).",
- "verbose": "⚙️ Tool progress: **VERBOSE** — every tool call with full arguments.",
+ "off": t("gateway.verbose.mode_off"),
+ "new": t("gateway.verbose.mode_new"),
+ "all": t("gateway.verbose.mode_all"),
+ "verbose": t("gateway.verbose.mode_verbose"),
}
# Read current effective mode for this platform via the resolver
@@ -10038,11 +10005,11 @@ class GatewayRunner:
atomic_yaml_write(config_path, user_config)
return (
f"{descriptions[new_mode]}\n"
- f"_(saved for **{platform_key}** — takes effect on next message)_"
+ + t("gateway.verbose.saved_suffix", platform=platform_key)
)
except Exception as e:
logger.warning("Failed to save tool_progress mode: %s", e)
- return f"{descriptions[new_mode]}\n_(could not save to config: {e})_"
+ return f"{descriptions[new_mode]}\n" + t("gateway.verbose.save_failed", error=e)
async def _handle_footer_command(self, event: MessageEvent) -> str:
"""Handle /footer command — toggle the runtime-metadata footer.
@@ -10083,12 +10050,13 @@ class GatewayRunner:
effective = resolve_footer_config(user_config, platform_key)
if arg in ("status", "?"):
- state = "ON" if effective["enabled"] else "OFF"
+ state = t("gateway.footer.state_on") if effective["enabled"] else t("gateway.footer.state_off")
fields = ", ".join(effective.get("fields") or [])
- return (
- f"📎 Runtime footer: **{state}**\n"
- f"Fields: `{fields}`\n"
- f"Platform: `{platform_key}`"
+ return t(
+ "gateway.footer.status",
+ state=state,
+ fields=fields,
+ platform=platform_key,
)
if arg in ("on", "enable", "true", "1"):
@@ -10098,7 +10066,7 @@ class GatewayRunner:
elif arg == "":
new_state = not effective["enabled"]
else:
- return "Usage: `/footer [on|off|status]`"
+ return t("gateway.footer.usage")
# --- write global flag ---------------------------------------------
try:
@@ -10113,7 +10081,7 @@ class GatewayRunner:
logger.warning("Failed to save runtime_footer.enabled: %s", e)
return t("gateway.config_save_failed", error=e)
- state = "ON" if new_state else "OFF"
+ state = t("gateway.footer.state_on") if new_state else t("gateway.footer.state_off")
example = ""
if new_state:
# Show a preview using current agent state if available.
@@ -10125,12 +10093,8 @@ class GatewayRunner:
fields=effective.get("fields") or ["model", "context_pct", "cwd"],
)
if preview:
- example = f"\nExample: `{preview}`"
- return (
- f"📎 Runtime footer: **{state}**"
- f"{example}\n"
- f"_(saved globally — takes effect on next message)_"
- )
+ example = t("gateway.footer.example_line", preview=preview)
+ return t("gateway.footer.saved", state=state, example=example)
async def _handle_compress_command(self, event: MessageEvent) -> str:
"""Handle /compress command -- manually compress conversation context.
@@ -10144,7 +10108,7 @@ class GatewayRunner:
history = self.session_store.load_transcript(session_entry.session_id)
if not history or len(history) < 4:
- return "Not enough conversation to compress (need at least 4 messages)."
+ return t("gateway.compress.not_enough")
# Extract optional focus topic from command args
focus_topic = (event.get_command_args() or "").strip() or None
@@ -10160,7 +10124,7 @@ class GatewayRunner:
session_key=session_key,
)
if not runtime_kwargs.get("api_key"):
- return "No provider configured -- cannot compress."
+ return t("gateway.compress.no_provider")
msgs = [
{"role": m.get("role"), "content": m.get("content")}
@@ -10192,7 +10156,7 @@ class GatewayRunner:
compressor = tmp_agent.context_compressor
if not compressor.has_content_to_compress(msgs):
- return "Nothing to compress yet (the transcript is still all protected context)."
+ return t("gateway.compress.nothing_to_do")
loop = asyncio.get_running_loop()
compressed, _ = await loop.run_in_executor(
@@ -10241,28 +10205,30 @@ class GatewayRunner:
self._cleanup_agent_resources(tmp_agent)
lines = [f"🗜️ {summary['headline']}"]
if focus_topic:
- lines.append(f"Focus: \"{focus_topic}\"")
+ lines.append(t("gateway.compress.focus_line", topic=focus_topic))
lines.append(summary["token_line"])
if summary["note"]:
lines.append(summary["note"])
if _summary_failed:
lines.append(
- f"⚠️ Summary generation failed ({_summary_err or 'unknown error'}). "
- f"{_dropped_count} historical message(s) were removed and replaced "
- "with a placeholder; earlier context is no longer recoverable. "
- "Consider checking your auxiliary.compression model configuration."
+ t(
+ "gateway.compress.summary_failed",
+ error=(_summary_err or "unknown error"),
+ count=_dropped_count,
+ )
)
elif _aux_fail_model:
lines.append(
- f"ℹ️ Configured compression model `{_aux_fail_model}` failed "
- f"({_aux_fail_err or 'unknown error'}). Recovered using your main "
- "model — context is intact — but you may want to check "
- "`auxiliary.compression.model` in config.yaml."
+ t(
+ "gateway.compress.aux_failed",
+ model=_aux_fail_model,
+ error=(_aux_fail_err or "unknown error"),
+ )
)
return "\n".join(lines)
except Exception as e:
logger.warning("Manual compress failed: %s", e)
- return f"Compression failed: {e}"
+ return t("gateway.compress.failed", error=e)
async def _get_telegram_topic_capabilities(self, source: SessionSource) -> dict:
"""Read Telegram private-topic capability flags via Bot API getMe."""
@@ -10518,7 +10484,7 @@ class GatewayRunner:
"""Cleanly disable topic mode for a chat via /topic off."""
if not self._session_db:
from hermes_state import format_session_db_unavailable
- return format_session_db_unavailable()
+ return format_session_db_unavailable(prefix=t("gateway.shared.session_db_unavailable_prefix"))
chat_id = str(source.chat_id or "")
if not chat_id:
return "Could not determine chat ID."
@@ -10554,10 +10520,10 @@ class GatewayRunner:
"""Handle /topic for Telegram DM user-managed topic sessions."""
source = event.source
if source.platform != Platform.TELEGRAM or source.chat_type != "dm":
- return "The /topic command is only available in Telegram private chats."
+ return t("gateway.topic.not_telegram_dm")
if not self._session_db:
from hermes_state import format_session_db_unavailable
- return format_session_db_unavailable()
+ return format_session_db_unavailable(prefix=t("gateway.shared.session_db_unavailable_prefix"))
# Authorization: /topic activates multi-session mode and mutates
# SQLite side tables. Unauthorized senders (not in allowlist) must
@@ -10567,7 +10533,7 @@ class GatewayRunner:
if callable(auth_fn):
try:
if not auth_fn(source):
- return "You are not authorized to use /topic on this bot."
+ return t("gateway.topic.unauthorized")
except Exception:
logger.debug("Topic auth check failed", exc_info=True)
@@ -10583,11 +10549,7 @@ class GatewayRunner:
if args:
if not source.thread_id:
- return (
- "To restore a session, first create or open a Telegram topic, "
- "then send /topic inside that topic. To create a "
- "new topic, open All Messages and send any message there."
- )
+ return t("gateway.topic.restore_needs_topic")
return await self._restore_telegram_topic_session(event, args)
capabilities = await self._get_telegram_topic_capabilities(source)
@@ -10597,24 +10559,11 @@ class GatewayRunner:
# /topic while threads are still disabled.
if self._should_send_telegram_capability_hint(source):
await self._send_telegram_topic_setup_image(source)
- return (
- "Telegram topics are not enabled for this bot yet.\n\n"
- "How to enable them:\n"
- "1. Open @BotFather.\n"
- "2. Choose your bot.\n"
- "3. Open Bot Settings → Threads Settings.\n"
- "4. Turn on Threaded Mode and make sure users are allowed to create new threads.\n\n"
- "Then send /topic again."
- )
+ return t("gateway.topic.topics_disabled")
if capabilities.get("allows_users_to_create_topics") is False:
if self._should_send_telegram_capability_hint(source):
await self._send_telegram_topic_setup_image(source)
- return (
- "Telegram topics are enabled, but users are not allowed to create topics.\n\n"
- "Open @BotFather → choose your bot → Bot Settings → Threads Settings, "
- "then turn off 'Disallow users to create new threads'.\n\n"
- "Then send /topic again."
- )
+ return t("gateway.topic.topics_user_disallowed")
try:
self._session_db.enable_telegram_topic_mode(
@@ -10625,7 +10574,7 @@ class GatewayRunner:
)
except Exception as exc:
logger.exception("Failed to enable Telegram topic mode")
- return f"Failed to enable Telegram topic mode: {exc}"
+ return t("gateway.topic.enable_failed", error=exc)
if not source.thread_id:
await self._ensure_telegram_system_topic(source)
@@ -10646,21 +10595,13 @@ class GatewayRunner:
title = self._session_db.get_session_title(session_id)
except Exception:
title = None
- session_label = title or "Untitled session"
- return (
- "This topic is linked to:\n"
- f"Session: {session_label}\n"
- f"ID: {session_id}\n\n"
- "Use /new to replace this topic with a fresh session.\n"
- "For parallel work, open All Messages and send a message there "
- "to create another topic."
+ session_label = title or t("gateway.topic.untitled_session")
+ return t(
+ "gateway.topic.bound_status",
+ label=session_label,
+ session_id=session_id,
)
- return (
- "Telegram multi-session topics are enabled.\n\n"
- "This topic will be used as an independent Hermes session. "
- "Use /new to replace this topic's current session. For parallel "
- "work, open All Messages and send a message there to create another topic."
- )
+ return t("gateway.topic.thread_ready")
return self._telegram_topic_root_status_message(source)
@@ -10772,7 +10713,7 @@ class GatewayRunner:
if not self._session_db:
from hermes_state import format_session_db_unavailable
- return format_session_db_unavailable()
+ return format_session_db_unavailable(prefix=t("gateway.shared.session_db_unavailable_prefix"))
# Ensure session exists in SQLite DB (it may only exist in session_store
# if this is the first command in a new session)
@@ -10794,30 +10735,30 @@ class GatewayRunner:
try:
sanitized = self._session_db.sanitize_title(title_arg)
except ValueError as e:
- return f"⚠️ {e}"
+ return t("gateway.shared.warn_passthrough", error=e)
if not sanitized:
- return "⚠️ Title is empty after cleanup. Please use printable characters."
+ return t("gateway.title.empty_after_clean")
# Set the title
try:
if self._session_db.set_session_title(session_id, sanitized):
- return f"✏️ Session title set: **{sanitized}**"
+ return t("gateway.title.set_to", title=sanitized)
else:
- return "Session not found in database."
+ return t("gateway.title.not_found")
except ValueError as e:
- return f"⚠️ {e}"
+ return t("gateway.shared.warn_passthrough", error=e)
else:
# Show the current title and session ID
title = self._session_db.get_session_title(session_id)
if title:
- return f"📌 Session: `{session_id}`\nTitle: **{title}**"
+ return t("gateway.title.current_with_title", session_id=session_id, title=title)
else:
- return f"📌 Session: `{session_id}`\nNo title set. Usage: `/title My Session Name`"
+ return t("gateway.title.current_no_title", session_id=session_id)
async def _handle_resume_command(self, event: MessageEvent) -> str:
"""Handle /resume command — switch to a previously-named session."""
if not self._session_db:
from hermes_state import format_session_db_unavailable
- return format_session_db_unavailable()
+ return format_session_db_unavailable(prefix=t("gateway.shared.session_db_unavailable_prefix"))
source = event.source
session_key = self._session_key_for_source(source)
@@ -10832,30 +10773,23 @@ class GatewayRunner:
)
titled = [s for s in sessions if s.get("title")]
if not titled:
- return (
- "No named sessions found.\n"
- "Use `/title My Session` to name your current session, "
- "then `/resume My Session` to return to it later."
- )
- lines = ["📋 **Named Sessions**\n"]
+ return t("gateway.resume.no_named_sessions")
+ lines = [t("gateway.resume.list_header")]
for s in titled[:10]:
title = s["title"]
preview = s.get("preview", "")[:40]
- preview_part = f" — _{preview}_" if preview else ""
- lines.append(f"• **{title}**{preview_part}")
- lines.append("\nUsage: `/resume `")
+ preview_part = t("gateway.resume.list_preview_suffix", preview=preview) if preview else ""
+ lines.append(t("gateway.resume.list_item", title=title, preview_part=preview_part))
+ lines.append(t("gateway.resume.list_footer"))
return "\n".join(lines)
except Exception as e:
logger.debug("Failed to list titled sessions: %s", e)
- return f"Could not list sessions: {e}"
+ return t("gateway.resume.list_failed", error=e)
# Resolve the name to a session ID.
target_id = self._session_db.resolve_session_by_title(name)
if not target_id:
- return (
- f"No session found matching '**{name}**'.\n"
- "Use `/resume` with no arguments to see available sessions."
- )
+ return t("gateway.resume.not_found", name=name)
# Compression creates child continuations that hold the live transcript.
# Follow that chain so gateway /resume matches CLI behavior (#15000).
try:
@@ -10866,7 +10800,7 @@ class GatewayRunner:
# Check if already on that session
current_entry = self.session_store.get_or_create_session(source)
if current_entry.session_id == target_id:
- return f"📌 Already on session **{name}**."
+ return t("gateway.resume.already_on", name=name)
# Clear any running agent for this session key
self._release_running_agent_state(session_key)
@@ -10874,7 +10808,7 @@ class GatewayRunner:
# Switch the session entry to point at the old session
new_entry = self.session_store.switch_session(session_key, target_id)
if not new_entry:
- return "Failed to switch session."
+ return t("gateway.resume.switch_failed")
self._clear_session_boundary_security_state(session_key)
# Evict any cached agent for this session so the next message
@@ -10890,9 +10824,11 @@ class GatewayRunner:
# Count messages for context
history = self.session_store.load_transcript(target_id)
msg_count = len([m for m in history if m.get("role") == "user"]) if history else 0
- msg_part = f" ({msg_count} message{'s' if msg_count != 1 else ''})" if msg_count else ""
-
- return f"↻ Resumed session **{title}**{msg_part}. Conversation restored."
+ if not msg_count:
+ return t("gateway.resume.resumed_no_count", title=title)
+ if msg_count == 1:
+ return t("gateway.resume.resumed_one", title=title, count=msg_count)
+ return t("gateway.resume.resumed_many", title=title, count=msg_count)
async def _handle_branch_command(self, event: MessageEvent) -> str:
"""Handle /branch [name] — fork the current session into a new independent copy.
@@ -10905,7 +10841,7 @@ class GatewayRunner:
if not self._session_db:
from hermes_state import format_session_db_unavailable
- return format_session_db_unavailable()
+ return format_session_db_unavailable(prefix=t("gateway.shared.session_db_unavailable_prefix"))
source = event.source
session_key = self._session_key_for_source(source)
@@ -10914,7 +10850,7 @@ class GatewayRunner:
current_entry = self.session_store.get_or_create_session(source)
history = self.session_store.load_transcript(current_entry.session_id)
if not history:
- return "No conversation to branch — send a message first."
+ return t("gateway.branch.no_conversation")
branch_name = event.get_command_args().strip()
@@ -10945,7 +10881,7 @@ class GatewayRunner:
)
except Exception as e:
logger.error("Failed to create branch session: %s", e)
- return f"Failed to create branch: {e}"
+ return t("gateway.branch.create_failed", error=e)
# Copy conversation history to the new session
for msg in history:
@@ -10976,20 +10912,15 @@ class GatewayRunner:
# Switch the session store entry to the new session
new_entry = self.session_store.switch_session(session_key, new_session_id)
if not new_entry:
- return "Branch created but failed to switch to it."
+ return t("gateway.branch.switch_failed")
self._clear_session_boundary_security_state(session_key)
# Evict any cached agent for this session
self._evict_cached_agent(session_key)
msg_count = len([m for m in history if m.get("role") == "user"])
- return (
- f"⑂ Branched to **{branch_title}**"
- f" ({msg_count} message{'s' if msg_count != 1 else ''} copied)\n"
- f"Original: `{parent_session_id}`\n"
- f"Branch: `{new_session_id}`\n"
- f"Use `/resume` to switch back to the original."
- )
+ key = "gateway.branch.branched_one" if msg_count == 1 else "gateway.branch.branched_many"
+ return t(key, title=branch_title, count=msg_count, parent=parent_session_id, new=new_session_id)
async def _handle_usage_command(self, event: MessageEvent) -> str:
"""Handle /usage command -- show token usage for the current session.
@@ -11051,7 +10982,7 @@ class GatewayRunner:
rl_state = agent.get_rate_limit_state()
if rl_state and rl_state.has_data:
from agent.rate_limit_tracker import format_rate_limit_compact
- lines.append(f"⏱️ **Rate Limits:** {format_rate_limit_compact(rl_state)}")
+ lines.append(t("gateway.usage.rate_limits", state=format_rate_limit_compact(rl_state)))
lines.append("")
# Session token usage — detailed breakdown matching CLI
@@ -11060,16 +10991,16 @@ class GatewayRunner:
cache_read = getattr(agent, "session_cache_read_tokens", 0) or 0
cache_write = getattr(agent, "session_cache_write_tokens", 0) or 0
- lines.append("📊 **Session Token Usage**")
- lines.append(f"Model: `{agent.model}`")
- lines.append(f"Input tokens: {input_tokens:,}")
+ lines.append(t("gateway.usage.header_session"))
+ lines.append(t("gateway.usage.label_model", model=agent.model))
+ lines.append(t("gateway.usage.label_input_tokens", count=f"{input_tokens:,}"))
if cache_read:
- lines.append(f"Cache read tokens: {cache_read:,}")
+ lines.append(t("gateway.usage.label_cache_read", count=f"{cache_read:,}"))
if cache_write:
- lines.append(f"Cache write tokens: {cache_write:,}")
- lines.append(f"Output tokens: {output_tokens:,}")
- lines.append(f"Total: {agent.session_total_tokens:,}")
- lines.append(f"API calls: {agent.session_api_calls}")
+ lines.append(t("gateway.usage.label_cache_write", count=f"{cache_write:,}"))
+ lines.append(t("gateway.usage.label_output_tokens", count=f"{output_tokens:,}"))
+ lines.append(t("gateway.usage.label_total", count=f"{agent.session_total_tokens:,}"))
+ lines.append(t("gateway.usage.label_api_calls", count=agent.session_api_calls))
# Cost estimation
try:
@@ -11087,9 +11018,9 @@ class GatewayRunner:
)
if cost_result.amount_usd is not None:
prefix = "~" if cost_result.status == "estimated" else ""
- lines.append(f"Cost: {prefix}${float(cost_result.amount_usd):.4f}")
+ lines.append(t("gateway.usage.label_cost", prefix=prefix, amount=f"{float(cost_result.amount_usd):.4f}"))
elif cost_result.status == "included":
- lines.append("Cost: included")
+ lines.append(t("gateway.usage.label_cost_included"))
except Exception:
pass
@@ -11097,9 +11028,9 @@ class GatewayRunner:
ctx = agent.context_compressor
if ctx.last_prompt_tokens:
pct = min(100, ctx.last_prompt_tokens / ctx.context_length * 100) if ctx.context_length else 0
- lines.append(f"Context: {ctx.last_prompt_tokens:,} / {ctx.context_length:,} ({pct:.0f}%)")
+ lines.append(t("gateway.usage.label_context", used=f"{ctx.last_prompt_tokens:,}", total=f"{ctx.context_length:,}", pct=f"{pct:.0f}"))
if ctx.compression_count:
- lines.append(f"Compressions: {ctx.compression_count}")
+ lines.append(t("gateway.usage.label_compressions", count=ctx.compression_count))
if account_lines:
lines.append("")
@@ -11115,10 +11046,10 @@ class GatewayRunner:
msgs = [m for m in history if m.get("role") in ("user", "assistant") and m.get("content")]
approx = estimate_messages_tokens_rough(msgs)
lines = [
- "📊 **Session Info**",
- f"Messages: {len(msgs)}",
- f"Estimated context: ~{approx:,} tokens",
- "_(Detailed usage available after the first agent response)_",
+ t("gateway.usage.header_session_info"),
+ t("gateway.usage.label_messages", count=len(msgs)),
+ t("gateway.usage.label_estimated_context", count=f"{approx:,}"),
+ t("gateway.usage.detailed_after_first"),
]
if account_lines:
lines.append("")
@@ -11126,7 +11057,7 @@ class GatewayRunner:
return "\n".join(lines)
if account_lines:
return "\n".join(account_lines)
- return "No usage data available for this session."
+ return t("gateway.usage.no_data")
async def _handle_insights_command(self, event: MessageEvent) -> str:
"""Handle /insights command -- show usage insights and analytics."""
@@ -11147,7 +11078,7 @@ class GatewayRunner:
try:
days = int(parts[i + 1])
except ValueError:
- return f"Invalid --days value: {parts[i + 1]}"
+ return t("gateway.insights.invalid_days", value=parts[i + 1])
i += 2
elif parts[i] == "--source" and i + 1 < len(parts):
source = parts[i + 1]
@@ -11175,7 +11106,7 @@ class GatewayRunner:
return await loop.run_in_executor(None, _run_insights)
except Exception as e:
logger.error("Insights command error: %s", e, exc_info=True)
- return f"Error generating insights: {e}"
+ return t("gateway.insights.error", error=e)
async def _handle_reload_mcp_command(self, event: MessageEvent) -> Optional[str]:
"""Handle /reload-mcp — reconnect MCP servers and rebuild the cached agent.
@@ -11213,7 +11144,7 @@ class GatewayRunner:
# chosen outcome.
async def _on_confirm(choice: str) -> Optional[str]:
if choice == "cancel":
- return "🟡 /reload-mcp cancelled. MCP tools unchanged."
+ return t("gateway.reload_mcp.cancelled")
if choice == "always":
# Persist the opt-out and run the reload.
try:
@@ -11228,25 +11159,10 @@ class GatewayRunner:
# once / always → run the reload
result = await self._execute_mcp_reload(event)
if choice == "always":
- return (
- f"{result}\n\n"
- "ℹ️ Future `/reload-mcp` calls will run without confirmation. "
- "Re-enable via `approvals.mcp_reload_confirm: true` in config.yaml."
- )
+ return f"{result}\n\n" + t("gateway.reload_mcp.always_followup")
return result
- prompt_message = (
- "⚠️ **Confirm /reload-mcp**\n\n"
- "Reloading MCP servers rebuilds the tool set for this session "
- "and **invalidates the provider prompt cache** — the next "
- "message will re-send full input tokens. On long-context or "
- "high-reasoning models this can be expensive.\n\n"
- "Choose:\n"
- "• **Approve Once** — reload now\n"
- "• **Always Approve** — reload now and silence this prompt permanently\n"
- "• **Cancel** — leave MCP tools unchanged\n\n"
- "_Text fallback: reply `/approve`, `/always`, or `/cancel`._"
- )
+ prompt_message = t("gateway.reload_mcp.confirm_prompt")
return await self._request_slash_confirm(
event=event,
command="reload-mcp",
@@ -11285,17 +11201,17 @@ class GatewayRunner:
removed = old_servers - connected_servers
reconnected = connected_servers & old_servers
- lines = ["🔄 **MCP Servers Reloaded**\n"]
+ lines = [t("gateway.reload_mcp.header")]
if reconnected:
- lines.append(f"♻️ Reconnected: {', '.join(sorted(reconnected))}")
+ lines.append(t("gateway.reload_mcp.reconnected", names=", ".join(sorted(reconnected))))
if added:
- lines.append(f"➕ Added: {', '.join(sorted(added))}")
+ lines.append(t("gateway.reload_mcp.added", names=", ".join(sorted(added))))
if removed:
- lines.append(f"➖ Removed: {', '.join(sorted(removed))}")
+ lines.append(t("gateway.reload_mcp.removed", names=", ".join(sorted(removed))))
if not connected_servers:
- lines.append("No MCP servers connected.")
+ lines.append(t("gateway.reload_mcp.none_connected"))
else:
- lines.append(f"\n🔧 {len(new_tools)} tool(s) available from {len(connected_servers)} server(s)")
+ lines.append(t("gateway.reload_mcp.tools_available", tools=len(new_tools), servers=len(connected_servers)))
# Inject a message at the END of the session history so the
# model knows tools changed on its next turn. Appended after
@@ -11325,7 +11241,7 @@ class GatewayRunner:
except Exception as e:
logger.warning("MCP reload failed: %s", e)
- return f"❌ MCP reload failed: {e}"
+ return t("gateway.reload_mcp.failed", error=e)
async def _handle_reload_skills_command(self, event: MessageEvent) -> str:
"""Handle /reload-skills — rescan skills dir, queue a note for next turn.
@@ -11373,26 +11289,28 @@ class GatewayRunner:
getattr(adapter, "name", adapter), exc,
)
- lines = ["🔄 **Skills Reloaded**\n"]
+ lines = [t("gateway.reload_skills.header")]
if not added and not removed:
- lines.append("No new skills detected.")
- lines.append(f"\n📚 {total} skill(s) available")
+ lines.append(t("gateway.reload_skills.no_new"))
+ lines.append(t("gateway.reload_skills.total", count=total))
return "\n".join(lines)
def _fmt_line(item: dict) -> str:
nm = item.get("name", "")
desc = item.get("description", "")
- return f" - {nm}: {desc}" if desc else f" - {nm}"
+ if desc:
+ return t("gateway.reload_skills.item_with_desc", name=nm, desc=desc)
+ return t("gateway.reload_skills.item_no_desc", name=nm)
if added:
- lines.append("➕ **Added Skills:**")
+ lines.append(t("gateway.reload_skills.added_header"))
for item in added:
lines.append(_fmt_line(item))
if removed:
- lines.append("➖ **Removed Skills:**")
+ lines.append(t("gateway.reload_skills.removed_header"))
for item in removed:
lines.append(_fmt_line(item))
- lines.append(f"\n📚 {total} skill(s) available")
+ lines.append(t("gateway.reload_skills.total", count=total))
# Queue the one-shot note for the next user turn in this session.
# Format matches how the system prompt renders pre-existing
@@ -11423,7 +11341,7 @@ class GatewayRunner:
except Exception as e:
logger.warning("Skills reload failed: %s", e)
- return f"❌ Skills reload failed: {e}"
+ return t("gateway.reload_skills.failed", error=e)
# ------------------------------------------------------------------
# Slash-command confirmation primitive (generic)
@@ -11672,7 +11590,7 @@ class GatewayRunner:
if session_key in self._pending_approvals:
self._pending_approvals.pop(session_key)
return t("gateway.approval_expired")
- return "No pending command to approve."
+ return t("gateway.approve.no_pending")
# Parse args: support "all", "all session", "all always", "session", "always"
args = event.get_command_args().strip().lower().split()
@@ -11681,26 +11599,23 @@ class GatewayRunner:
if any(a in ("always", "permanent", "permanently") for a in remaining):
choice = "always"
- scope_msg = " (pattern approved permanently)"
elif any(a in ("session", "ses") for a in remaining):
choice = "session"
- scope_msg = " (pattern approved for this session)"
else:
choice = "once"
- scope_msg = ""
count = resolve_gateway_approval(session_key, choice, resolve_all=resolve_all)
if not count:
- return "No pending command to approve."
+ return t("gateway.approve.no_pending")
# Resume typing indicator — agent is about to continue processing.
_adapter = self.adapters.get(source.platform)
if _adapter:
_adapter.resume_typing_for_chat(source.chat_id)
- count_msg = f" ({count} commands)" if count > 1 else ""
- logger.info("User approved %d dangerous command(s) via /approve%s", count, scope_msg)
- return f"✅ Command{'s' if count > 1 else ''} approved{scope_msg}{count_msg}. The agent is resuming..."
+ logger.info("User approved %d dangerous command(s) via /approve (%s)", count, choice)
+ plural = "plural" if count > 1 else "singular"
+ return t(f"gateway.approve.{choice}_{plural}", count=count)
async def _handle_deny_command(self, event: MessageEvent) -> str:
"""Handle /deny command — reject pending dangerous command(s).
@@ -11720,24 +11635,25 @@ class GatewayRunner:
if not has_blocking_approval(session_key):
if session_key in self._pending_approvals:
self._pending_approvals.pop(session_key)
- return "❌ Command denied (approval was stale)."
- return "No pending command to deny."
+ return t("gateway.deny.stale")
+ return t("gateway.deny.no_pending")
args = event.get_command_args().strip().lower()
resolve_all = "all" in args
count = resolve_gateway_approval(session_key, "deny", resolve_all=resolve_all)
if not count:
- return "No pending command to deny."
+ return t("gateway.deny.no_pending")
# Resume typing indicator — agent continues (with BLOCKED result).
_adapter = self.adapters.get(source.platform)
if _adapter:
_adapter.resume_typing_for_chat(source.chat_id)
- count_msg = f" ({count} commands)" if count > 1 else ""
logger.info("User denied %d dangerous command(s) via /deny", count)
- return f"❌ Command{'s' if count > 1 else ''} denied{count_msg}."
+ if count > 1:
+ return t("gateway.deny.denied_plural", count=count)
+ return t("gateway.deny.denied_singular")
# Platforms where /update is allowed. ACP, API server, and webhooks are
# programmatic interfaces that should not trigger system updates.
@@ -11774,20 +11690,20 @@ class GatewayRunner:
try:
urls["Report"] = upload_to_pastebin(report)
except Exception as exc:
- return f"✗ Failed to upload debug report: {exc}"
+ return t("gateway.debug.upload_failed", error=exc)
# Schedule auto-deletion after 6 hours
_schedule_auto_delete(list(urls.values()))
- lines = [_GATEWAY_PRIVACY_NOTICE, "", "**Debug report uploaded:**", ""]
+ lines = [_GATEWAY_PRIVACY_NOTICE, "", t("gateway.debug.header"), ""]
label_width = max(len(k) for k in urls)
for label, url in urls.items():
lines.append(f"`{label:<{label_width}}` {url}")
lines.append("")
- lines.append("⏱ Pastes will auto-delete in 6 hours.")
- lines.append("For full log uploads, use `hermes debug share` from the CLI.")
- lines.append("Share these links with the Hermes team for support.")
+ lines.append(t("gateway.debug.auto_delete"))
+ lines.append(t("gateway.debug.full_logs_hint"))
+ lines.append(t("gateway.debug.share_hint"))
return "\n".join(lines)
return await loop.run_in_executor(None, _collect_and_upload)
@@ -11815,9 +11731,9 @@ class GatewayRunner:
from gateway.platform_registry import platform_registry
entry = platform_registry.get(platform.value)
if not entry or not entry.allow_update_command:
- return "✗ /update is only available from messaging platforms. Run `hermes update` from the terminal."
+ return t("gateway.update.platform_not_messaging")
except Exception:
- return "✗ /update is only available from messaging platforms. Run `hermes update` from the terminal."
+ return t("gateway.update.platform_not_messaging")
if is_managed():
return f"✗ {format_managed_message('update Hermes Agent')}"
@@ -11826,16 +11742,11 @@ class GatewayRunner:
git_dir = project_root / '.git'
if not git_dir.exists():
- return "✗ Not a git repository — cannot update."
+ return t("gateway.update.not_git_repo")
hermes_cmd = _resolve_hermes_bin()
if not hermes_cmd:
- return (
- "✗ Could not locate the `hermes` command. "
- "Hermes is running, but the update command could not find the "
- "executable on PATH or via the current Python interpreter. "
- "Try running `hermes update` manually in your terminal."
- )
+ return t("gateway.update.hermes_cmd_not_found")
pending_path = _hermes_home / ".update_pending.json"
output_path = _hermes_home / ".update_output.txt"
@@ -11938,10 +11849,10 @@ class GatewayRunner:
except Exception as e:
pending_path.unlink(missing_ok=True)
exit_code_path.unlink(missing_ok=True)
- return f"✗ Failed to start update: {e}"
+ return t("gateway.update.start_failed", error=e)
self._schedule_update_notification_watch()
- return "⚕ Starting Hermes update… I'll stream progress here."
+ return t("gateway.update.starting")
def _schedule_update_notification_watch(self) -> None:
"""Ensure a background task is watching for update completion."""
diff --git a/locales/af.yaml b/locales/af.yaml
new file mode 100644
index 00000000000..264b4b321a5
--- /dev/null
+++ b/locales/af.yaml
@@ -0,0 +1,350 @@
+# Hermes statiese boodskap-katalogus -- Afrikaans
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ GEVAARLIKE OPDRAG: {description}"
+ choose_long: " [o]eenmalig | [s]sessie | [a]altyd | [d]weier"
+ choose_short: " [o]eenmalig | [s]sessie | [d]weier"
+ prompt_long: " Keuse [o/s/a/D]: "
+ prompt_short: " Keuse [o/s/D]: "
+ timeout: " ⏱ Tyd verstreke - opdrag word geweier"
+ allowed_once: " ✓ Eenmalig toegelaat"
+ allowed_session: " ✓ Vir hierdie sessie toegelaat"
+ allowed_always: " ✓ By permanente toelaatlys gevoeg"
+ denied: " ✗ Geweier"
+ cancelled: " ✗ Gekanselleer"
+ blocklist_message: "Hierdie opdrag is op die onvoorwaardelike blokkeerlys en kan nie goedgekeur word nie."
+
+gateway:
+ approval_expired: "⚠️ Goedkeuring het verval (die agent wag nie meer nie). Vra die agent om weer te probeer."
+ draining: "⏳ Wag vir {count} aktiewe agent(e) voor herbegin..."
+ goal_cleared: "✓ Doelwit verwyder."
+ no_active_goal: "Geen aktiewe doelwit nie."
+ config_read_failed: "⚠️ Kon nie config.yaml lees nie: {error}"
+ config_save_failed: "⚠️ Kon nie konfigurasie stoor nie: {error}"
+
+ model:
+ error_prefix: "Fout: {error}"
+ switched: "Model verander na `{model}`"
+ provider_label: "Verskaffer: {provider}"
+ context_label: "Konteks: {tokens} tokens"
+ max_output_label: "Maks. uitvoer: {tokens} tokens"
+ cost_label: "Koste: {cost}"
+ capabilities_label: "Vermoëns: {capabilities}"
+ prompt_caching_enabled: "Prompt-kasing: geaktiveer"
+ warning_prefix: "Waarskuwing: {warning}"
+ saved_global: "Gestoor in config.yaml (`--global`)"
+ session_only_hint: "_(slegs sessie — voeg `--global` by om permanent te stoor)_"
+ current_label: "Huidig: `{model}` op {provider}"
+ current_tag: " (huidig)"
+ more_models_suffix: " (+{count} meer)"
+ usage_switch_model: "`/model ` — verander model"
+ usage_switch_provider: "`/model --provider ` — verander verskaffer"
+ usage_persist: "`/model --global` — stoor permanent"
+
+ agents:
+ header: "🤖 **Aktiewe Agente & Take**"
+ active_agents: "**Aktiewe agente:** {count}"
+ this_chat: " · hierdie geselsie"
+ more: "... en nog {count}"
+ running_processes: "**Lopende agtergrondprosesse:** {count}"
+ async_jobs: "**Asinchrone werke van die gateway:** {count}"
+ none: "Geen aktiewe agente of lopende take nie."
+ state_starting: "begin"
+ state_running: "loop"
+
+ approve:
+ no_pending: "Geen hangende opdrag om goed te keur nie."
+ once_singular: "✅ Opdrag goedgekeur. Die agent gaan voort..."
+ once_plural: "✅ Opdragte goedgekeur ({count} opdragte). Die agent gaan voort..."
+ session_singular: "✅ Opdrag goedgekeur (patroon goedgekeur vir hierdie sessie). Die agent gaan voort..."
+ session_plural: "✅ Opdragte goedgekeur (patroon goedgekeur vir hierdie sessie) ({count} opdragte). Die agent gaan voort..."
+ always_singular: "✅ Opdrag goedgekeur (patroon permanent goedgekeur). Die agent gaan voort..."
+ always_plural: "✅ Opdragte goedgekeur (patroon permanent goedgekeur) ({count} opdragte). Die agent gaan voort..."
+
+ background:
+ usage: "Gebruik: /background \nVoorbeeld: /background Som vandag se top HN-stories op\n\nVoer die prompt in 'n aparte sessie uit. Jy kan aanhou gesels — die resultaat verskyn hier wanneer dit klaar is."
+ started: "🔄 Agtergrondtaak begin: \"{preview}\"\nTaak-ID: {task_id}\nJy kan aanhou gesels — resultate verskyn hier wanneer dit klaar is."
+
+ branch:
+ db_unavailable: "Sessie-databasis is nie beskikbaar nie."
+ no_conversation: "Geen gesprek om te vertak nie — stuur eers 'n boodskap."
+ create_failed: "Kon nie tak skep nie: {error}"
+ switch_failed: "Tak is geskep, maar oorskakeling het misluk."
+ branched_one: "⑂ Vertak na **{title}** ({count} boodskap gekopieer)\nOorspronklik: `{parent}`\nTak: `{new}`\nGebruik `/resume` om terug te gaan na die oorspronklike."
+ branched_many: "⑂ Vertak na **{title}** ({count} boodskappe gekopieer)\nOorspronklik: `{parent}`\nTak: `{new}`\nGebruik `/resume` om terug te gaan na die oorspronklike."
+
+ commands:
+ usage: "Gebruik: `/commands [page]`"
+ skill_header: "⚡ **Vaardigheidsopdragte**:"
+ default_desc: "Vaardigheidsopdrag"
+ none: "Geen opdragte beskikbaar nie."
+ header: "📚 **Opdragte** ({total} altesaam, bladsy {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← vorige"
+ nav_next: "volgende → `/commands {page}`"
+ out_of_range: "_(Versoekte bladsy {requested} was buite reikwydte; bladsy {page} word vertoon.)_"
+
+ compress:
+ not_enough: "Nie genoeg gesprek om saam te pers nie (ten minste 4 boodskappe nodig)."
+ no_provider: "Geen verskaffer opgestel nie -- kan nie saampers nie."
+ nothing_to_do: "Niks om saam te pers nie (die transkripsie is steeds heeltemal beskermde konteks)."
+ focus_line: "Fokus: \"{topic}\""
+ summary_failed: "⚠️ Opsomming kon nie gegenereer word nie ({error}). {count} historiese boodskap(pe) is verwyder en met 'n plekhouer vervang; vroeëre konteks kan nie meer herstel word nie. Oorweeg om jou auxiliary.compression-modelopstelling na te gaan."
+ aux_failed: "ℹ️ Opgestelde saamperseringsmodel `{model}` het misluk ({error}). Herstel met jou hoofmodel — konteks is intakt — maar jy mag dalk `auxiliary.compression.model` in config.yaml wil nagaan."
+ failed: "Saampersing het misluk: {error}"
+
+ debug:
+ upload_failed: "✗ Kon nie ontfoutverslag oplaai nie: {error}"
+ header: "**Ontfoutverslag opgelaai:**"
+ auto_delete: "⏱ Plakke sal outomaties oor 6 uur uitgevee word."
+ full_logs_hint: "Vir volledige loglae, gebruik `hermes debug share` vanaf die CLI."
+ share_hint: "Deel hierdie skakels met die Hermes-span vir ondersteuning."
+
+ deny:
+ stale: "❌ Opdrag geweier (goedkeuring was verouderd)."
+ no_pending: "Geen hangende opdrag om te weier nie."
+ denied_singular: "❌ Opdrag geweier."
+ denied_plural: "❌ Opdragte geweier ({count} opdragte)."
+
+ fast:
+ not_supported: "⚡ /fast is slegs beskikbaar vir OpenAI-modelle wat Priority Processing ondersteun."
+ status: "⚡ Priority Processing\n\nHuidige modus: `{mode}`\n\n_Gebruik:_ `/fast `"
+ unknown_arg: "⚠️ Onbekende argument: `{arg}`\n\n**Geldige opsies:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (gestoor in konfigurasie)\n_(neem effek by die volgende boodskap)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (slegs hierdie sessie)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Looptyd-voetstuk: **{state}**\nVelde: `{fields}`\nPlatform: `{platform}`"
+ usage: "Gebruik: `/footer [on|off|status]`"
+ saved: "📎 Looptyd-voetstuk: **{state}**{example}\n_(globaal gestoor — neem effek by die volgende boodskap)_"
+ example_line: "\nVoorbeeld: `{preview}`"
+ state_on: "AAN"
+ state_off: "AF"
+
+ goal:
+ unavailable: "Doelwitte is nie beskikbaar in hierdie sessie nie."
+ no_goal_set: "Geen doelwit gestel nie."
+ paused: "⏸ Doelwit gepouse: {goal}"
+ no_resume: "Geen doelwit om voort te sit nie."
+ resumed: "▶ Doelwit hervat: {goal}\nStuur enige boodskap om voort te gaan, of wag — ek sal die volgende stap met die volgende beurt neem."
+ invalid: "Ongeldige doelwit: {error}"
+ set: "⊙ Doelwit gestel ({budget}-beurt-begroting): {goal}\nEk sal aanhou werk totdat die doelwit klaar is, jy dit pouseer/verwyder, of die begroting opgebruik is.\nBeheer: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes-opdragte**\n"
+ skill_header: "\n⚡ **Vaardigheidsopdragte** ({count} aktief):"
+ more_use_commands: "\n... en nog {count}. Gebruik `/commands` vir die volledige bladsy-lys."
+
+ insights:
+ invalid_days: "Ongeldige --days waarde: {value}"
+ error: "Fout met genereer van insigte: {error}"
+
+ kanban:
+ error_prefix: "⚠ kanban-fout: {error}"
+ subscribed_suffix: "(ingeteken — jy sal in kennis gestel word wanneer {task_id} voltooi of vasval)"
+ truncated_suffix: "… (afgekap; gebruik `hermes kanban …` in jou terminale vir volle uitvoer)"
+ no_output: "(geen uitvoer)"
+
+ personality:
+ none_configured: "Geen persoonlikhede opgestel in `{path}/config.yaml` nie"
+ header: "🎭 **Beskikbare Persoonlikhede**\n"
+ none_option: "• `none` — (geen persoonlikheidslaag)"
+ item: "• `{name}` — {preview}"
+ usage: "\nGebruik: `/personality `"
+ save_failed: "⚠️ Kon nie persoonlikheidsverandering stoor nie: {error}"
+ cleared: "🎭 Persoonlikheid verwyder — basis-agentgedrag word gebruik.\n_(neem effek by die volgende boodskap)_"
+ set_to: "🎭 Persoonlikheid gestel op **{name}**\n_(neem effek by die volgende boodskap)_"
+ unknown: "Onbekende persoonlikheid: `{name}`\n\nBeskikbaar: {available}"
+
+ profile:
+ header: "👤 **Profiel:** `{profile}`"
+ home: "📂 **Tuiste:** `{home}`"
+
+ reasoning:
+ level_default: "medium (verstek)"
+ level_disabled: "none (gedeaktiveer)"
+ scope_session: "sessie-oorskryf"
+ scope_global: "globale konfigurasie"
+ status: "🧠 **Redenering-instellings**\n\n**Inspanning:** `{level}`\n**Bereik:** {scope}\n**Vertoon:** {display}\n\n_Gebruik:_ `/reasoning [--global]`"
+ display_on: "aan ✓"
+ display_off: "af"
+ display_set_on: "🧠 ✓ Redenering-vertoon: **AAN**\nDie model se denke sal voor elke antwoord op **{platform}** vertoon word."
+ display_set_off: "🧠 ✓ Redenering-vertoon: **AF** vir **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` word nie ondersteun nie. Gebruik `/reasoning --global` om die globale verstek te verander."
+ reset_done: "🧠 ✓ Sessie-redenering-oorskryf verwyder; val terug op globale konfigurasie."
+ unknown_arg: "⚠️ Onbekende argument: `{arg}`\n\n**Geldige vlakke:** none, minimal, low, medium, high, xhigh\n**Vertoon:** show, hide\n**Permanent:** voeg `--global` by om verby hierdie sessie te stoor"
+ set_global: "🧠 ✓ Redenering-inspanning gestel op `{effort}` (gestoor in konfigurasie)\n_(neem effek by die volgende boodskap)_"
+ set_global_save_failed: "🧠 ✓ Redenering-inspanning gestel op `{effort}` (slegs sessie — konfigurasie-stoor het misluk)\n_(neem effek by die volgende boodskap)_"
+ set_session: "🧠 ✓ Redenering-inspanning gestel op `{effort}` (slegs sessie — voeg `--global` by om permanent te stoor)\n_(neem effek by die volgende boodskap)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp gekanselleer. MCP-gereedskap onveranderd."
+ always_followup: "ℹ️ Toekomstige `/reload-mcp`-oproepe sal sonder bevestiging loop. Heraktiveer via `approvals.mcp_reload_confirm: true` in config.yaml."
+ confirm_prompt: "⚠️ **Bevestig /reload-mcp**\n\nOm MCP-bedieners te herlaai, herbou die gereedskapsstel vir hierdie sessie en **maak die verskaffer se prompt-kasie ongeldig** — die volgende boodskap sal alle invoertokens herstuur. Op modelle met lang konteks of hoë redenering kan dit duur wees.\n\nKies:\n• **Eenmaal Goedkeur** — herlaai nou\n• **Altyd Goedkeur** — herlaai nou en stop hierdie prompt permanent\n• **Kanselleer** — laat MCP-gereedskap onveranderd\n\n_Teks-alternatief: antwoord `/approve`, `/always`, of `/cancel`._"
+ header: "🔄 **MCP-bedieners herlaai**\n"
+ reconnected: "♻️ Herverbind: {names}"
+ added: "➕ Bygevoeg: {names}"
+ removed: "➖ Verwyder: {names}"
+ none_connected: "Geen MCP-bedieners verbind nie."
+ tools_available: "\n🔧 {tools} gereedskap beskikbaar van {servers} bediener(s)"
+ failed: "❌ MCP-herlaai het misluk: {error}"
+
+ reload_skills:
+ header: "🔄 **Vaardighede herlaai**\n"
+ no_new: "Geen nuwe vaardighede opgespoor nie."
+ total: "\n📚 {count} vaardigheid(e) beskikbaar"
+ added_header: "➕ **Bygevoegde Vaardighede:**"
+ removed_header: "➖ **Verwyderde Vaardighede:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Vaardigheids-herlaai het misluk: {error}"
+
+ reset:
+ header_default: "✨ Sessie herstel! Begin van voor."
+ header_new: "✨ Nuwe sessie begin!"
+ header_titled: "✨ Nuwe sessie begin: {title}"
+ title_rejected: "\n⚠️ Titel verwerp: {error}"
+ title_error_untitled: "\n⚠️ {error} — sessie sonder titel begin."
+ title_empty_untitled: "\n⚠️ Titel is leeg na opruiming — sessie sonder titel begin."
+ tip: "\n✦ Wenk: {tip}"
+
+ restart:
+ in_progress: "⏳ Gateway-herbegin reeds aan die gang..."
+ restarting: "♻ Herbegin van gateway. As jy nie binne 60 sekondes in kennis gestel word nie, herbegin vanaf die konsole met `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Sessie-databasis is nie beskikbaar nie."
+ no_named_sessions: "Geen benoemde sessies gevind nie.\nGebruik `/title My Sessie` om jou huidige sessie 'n naam te gee, en dan `/resume My Sessie` om later daarheen terug te keer."
+ list_header: "📋 **Benoemde Sessies**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nGebruik: `/resume `"
+ list_failed: "Kon nie sessies lys nie: {error}"
+ not_found: "Geen sessie gevind wat by '**{name}**' pas nie.\nGebruik `/resume` sonder argumente om beskikbare sessies te sien."
+ already_on: "📌 Reeds op sessie **{name}**."
+ switch_failed: "Kon nie sessie verander nie."
+ resumed_one: "↻ Sessie **{title}** hervat ({count} boodskap). Gesprek herstel."
+ resumed_many: "↻ Sessie **{title}** hervat ({count} boodskappe). Gesprek herstel."
+ resumed_no_count: "↻ Sessie **{title}** hervat. Gesprek herstel."
+
+ retry:
+ no_previous: "Geen vorige boodskap om te herhaal nie."
+
+ rollback:
+ not_enabled: "Kontrolepunte is nie geaktiveer nie.\nAktiveer in config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Geen kontrolepunte vir {cwd} gevind nie"
+ invalid_number: "Ongeldige kontrolepunt-nommer. Gebruik 1-{max}."
+ restored: "✅ Herstel na kontrolepunt {hash}: {reason}\n'n Voor-terugrol-momentopname is outomaties gestoor."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Kon nie tuiste-kanaal stoor nie: {error}"
+ success: "✅ Tuiste-kanaal gestel op **{name}** (ID: {chat_id}).\nKron-take en kruisplatform-boodskappe sal hier afgelewer word."
+
+ status:
+ header: "📊 **Hermes Gateway Status**"
+ session_id: "**Sessie-ID:** `{session_id}`"
+ title: "**Titel:** {title}"
+ created: "**Geskep:** {timestamp}"
+ last_activity: "**Laaste aktiwiteit:** {timestamp}"
+ tokens: "**Tokens:** {tokens}"
+ agent_running: "**Agent loop:** {state}"
+ state_yes: "Ja ⚡"
+ state_no: "Nee"
+ queued: "**Opgehoopte opvolge:** {count}"
+ platforms: "**Verbinde Platforms:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Gestop. Die agent het nog nie begin nie — jy kan met hierdie sessie voortgaan."
+ stopped: "⚡ Gestop. Jy kan met hierdie sessie voortgaan."
+ no_active: "Geen aktiewe taak om te stop nie."
+
+ title:
+ db_unavailable: "Sessie-databasis is nie beskikbaar nie."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Titel is leeg na opruiming. Gebruik asseblief drukbare karakters."
+ set_to: "✏️ Sessie-titel gestel: **{title}**"
+ not_found: "Sessie nie in databasis gevind nie."
+ current_with_title: "📌 Sessie: `{session_id}`\nTitel: **{title}**"
+ current_no_title: "📌 Sessie: `{session_id}`\nGeen titel gestel nie. Gebruik: `/title My Sessie Naam`"
+
+ topic:
+ not_telegram_dm: "Die /topic-opdrag is slegs beskikbaar in Telegram-privaatgesprekke."
+ no_session_db: "Sessie-databasis is nie beskikbaar nie."
+ unauthorized: "Jy het nie toestemming om /topic op hierdie bot te gebruik nie."
+ restore_needs_topic: "Om 'n sessie te herstel, skep of open eers 'n Telegram-onderwerp en stuur dan /topic binne daardie onderwerp. Om 'n nuwe onderwerp te skep, open All Messages en stuur enige boodskap daar."
+ topics_disabled: "Telegram-onderwerpe is nog nie vir hierdie bot geaktiveer nie.\n\nHoe om dit te aktiveer:\n1. Open @BotFather.\n2. Kies jou bot.\n3. Open Bot Settings → Threads Settings.\n4. Skakel Threaded Mode aan en maak seker gebruikers mag nuwe drade skep.\n\nStuur dan weer /topic."
+ topics_user_disallowed: "Telegram-onderwerpe is geaktiveer, maar gebruikers mag nie onderwerpe skep nie.\n\nOpen @BotFather → kies jou bot → Bot Settings → Threads Settings, en skakel dan 'Disallow users to create new threads' af.\n\nStuur dan weer /topic."
+ enable_failed: "Kon nie Telegram-onderwerpmodus aktiveer nie: {error}"
+ bound_status: "Hierdie onderwerp is gekoppel aan:\nSessie: {label}\nID: {session_id}\n\nGebruik /new om hierdie onderwerp met 'n vars sessie te vervang.\nVir parallelle werk, open All Messages en stuur 'n boodskap daar om 'n ander onderwerp te skep."
+ thread_ready: "Telegram multi-sessie-onderwerpe is geaktiveer.\n\nHierdie onderwerp sal as 'n onafhanklike Hermes-sessie gebruik word. Gebruik /new om hierdie onderwerp se huidige sessie te vervang. Vir parallelle werk, open All Messages en stuur 'n boodskap daar om 'n ander onderwerp te skep."
+ untitled_session: "Sessie sonder titel"
+
+ undo:
+ nothing: "Niks om ongedaan te maak nie."
+ removed: "↩️ {count} boodskap(pe) ongedaan gemaak.\nVerwyder: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update is slegs beskikbaar vanaf boodskapplatforms. Voer `hermes update` vanaf die terminale uit."
+ not_git_repo: "✗ Nie 'n git-bewaarplek nie — kan nie opdateer nie."
+ hermes_cmd_not_found: "✗ Kon nie die `hermes`-opdrag vind nie. Hermes loop, maar die opdateeropdrag kon nie die uitvoerbare lêer op PATH of via die huidige Python-vertolker vind nie. Probeer `hermes update` met die hand in jou terminale uitvoer."
+ start_failed: "✗ Kon nie opdatering begin nie: {error}"
+ starting: "⚕ Begin Hermes-opdatering… Ek sal vordering hier stroom."
+
+ usage:
+ rate_limits: "⏱️ **Tariefperke:** {state}"
+ header_session: "📊 **Sessie-tokengebruik**"
+ label_model: "Model: `{model}`"
+ label_input_tokens: "Invoertokens: {count}"
+ label_cache_read: "Kasie-leestokens: {count}"
+ label_cache_write: "Kasie-skryftokens: {count}"
+ label_output_tokens: "Uitvoertokens: {count}"
+ label_total: "Totaal: {count}"
+ label_api_calls: "API-oproepe: {count}"
+ label_cost: "Koste: {prefix}${amount}"
+ label_cost_included: "Koste: ingesluit"
+ label_context: "Konteks: {used} / {total} ({pct}%)"
+ label_compressions: "Saamperserings: {count}"
+ header_session_info: "📊 **Sessie-inligting**"
+ label_messages: "Boodskappe: {count}"
+ label_estimated_context: "Geskatte konteks: ~{count} tokens"
+ detailed_after_first: "_(Gedetailleerde gebruik beskikbaar na die eerste agent-antwoord)_"
+ no_data: "Geen gebruiksdata beskikbaar vir hierdie sessie nie."
+
+ verbose:
+ not_enabled: "Die `/verbose`-opdrag is nie vir boodskapplatforms geaktiveer nie.\n\nAktiveer dit in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Gereedskap-vordering: **AF** — geen gereedskap-aktiwiteit word vertoon nie."
+ mode_new: "⚙️ Gereedskap-vordering: **NUUT** — vertoon wanneer gereedskap verander (voorskoulengte: `display.tool_preview_length`, verstek 40)."
+ mode_all: "⚙️ Gereedskap-vordering: **ALMAL** — elke gereedskaps-oproep vertoon (voorskoulengte: `display.tool_preview_length`, verstek 40)."
+ mode_verbose: "⚙️ Gereedskap-vordering: **OMSLAGTIG** — elke gereedskaps-oproep met volle argumente."
+ saved_suffix: "_(gestoor vir **{platform}** — neem effek by die volgende boodskap)_"
+ save_failed: "_(kon nie in konfigurasie stoor nie: {error})_"
+
+ voice:
+ enabled_voice_only: "Stemmodus geaktiveer.\nEk sal met stem antwoord wanneer jy stemboodskappe stuur.\nGebruik /voice tts om stemantwoorde vir alle boodskappe te kry."
+ disabled_text: "Stemmodus gedeaktiveer. Slegs teks-antwoorde."
+ tts_enabled: "Outo-TTS geaktiveer.\nAlle antwoorde sal 'n stemboodskap insluit."
+ status_mode: "Stemmodus: {label}"
+ status_channel: "Stemkanaal: #{channel}"
+ status_participants: "Deelnemers: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (praat)"
+ enabled_short: "Stemmodus geaktiveer."
+ disabled_short: "Stemmodus gedeaktiveer."
+ label_off: "Af (slegs teks)"
+ label_voice_only: "Aan (stemantwoord op stemboodskappe)"
+ label_all: "TTS (stemantwoord op alle boodskappe)"
+
+ yolo:
+ disabled: "⚠️ YOLO-modus **AF** vir hierdie sessie — gevaarlike opdragte sal goedkeuring vereis."
+ enabled: "⚡ YOLO-modus **AAN** vir hierdie sessie — alle opdragte word outomaties goedgekeur. Gebruik versigtig."
+
+ shared:
+ session_db_unavailable: "Sessie-databasis is nie beskikbaar nie."
+ session_db_unavailable_prefix: "Sessie-databasis is nie beskikbaar"
+ session_not_found: "Sessie nie in databasis gevind nie."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/de.yaml b/locales/de.yaml
index e0087c651f7..86aa0fae9ac 100644
--- a/locales/de.yaml
+++ b/locales/de.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "Kein aktives Ziel."
config_read_failed: "⚠️ config.yaml konnte nicht gelesen werden: {error}"
config_save_failed: "⚠️ Konfiguration konnte nicht gespeichert werden: {error}"
+
+ model:
+ error_prefix: "Fehler: {error}"
+ switched: "Modell gewechselt zu `{model}`"
+ provider_label: "Anbieter: {provider}"
+ context_label: "Kontext: {tokens} Tokens"
+ max_output_label: "Max. Ausgabe: {tokens} Tokens"
+ cost_label: "Kosten: {cost}"
+ capabilities_label: "Fähigkeiten: {capabilities}"
+ prompt_caching_enabled: "Prompt-Caching: aktiviert"
+ warning_prefix: "Warnung: {warning}"
+ saved_global: "In config.yaml gespeichert (`--global`)"
+ session_only_hint: "_(nur für diese Sitzung — `--global` ergänzen, um zu speichern)_"
+ current_label: "Aktuell: `{model}` bei {provider}"
+ current_tag: " (aktuell)"
+ more_models_suffix: " (+{count} weitere)"
+ usage_switch_model: "`/model ` — Modell wechseln"
+ usage_switch_provider: "`/model --provider ` — Anbieter wechseln"
+ usage_persist: "`/model --global` — dauerhaft speichern"
+
+ agents:
+ header: "🤖 **Aktive Agenten & Aufgaben**"
+ active_agents: "**Aktive Agenten:** {count}"
+ this_chat: " · dieser Chat"
+ more: "... und {count} weitere"
+ running_processes: "**Laufende Hintergrundprozesse:** {count}"
+ async_jobs: "**Gateway-Async-Jobs:** {count}"
+ none: "Keine aktiven Agenten oder laufenden Aufgaben."
+ state_starting: "startet"
+ state_running: "läuft"
+
+ approve:
+ no_pending: "Kein ausstehender Befehl zum Genehmigen."
+ once_singular: "✅ Befehl genehmigt. Der Agent wird fortgesetzt..."
+ once_plural: "✅ Befehle genehmigt ({count} Befehle). Der Agent wird fortgesetzt..."
+ session_singular: "✅ Befehl genehmigt (Muster für diese Sitzung genehmigt). Der Agent wird fortgesetzt..."
+ session_plural: "✅ Befehle genehmigt (Muster für diese Sitzung genehmigt) ({count} Befehle). Der Agent wird fortgesetzt..."
+ always_singular: "✅ Befehl genehmigt (Muster dauerhaft genehmigt). Der Agent wird fortgesetzt..."
+ always_plural: "✅ Befehle genehmigt (Muster dauerhaft genehmigt) ({count} Befehle). Der Agent wird fortgesetzt..."
+
+ background:
+ usage: "Verwendung: /background \nBeispiel: /background Fasse die Top-HN-Storys von heute zusammen\n\nFührt den Prompt in einer separaten Sitzung aus. Sie können weiter chatten — das Ergebnis erscheint hier, wenn es fertig ist."
+ started: "🔄 Hintergrund-Aufgabe gestartet: \"{preview}\"\nAufgaben-ID: {task_id}\nSie können weiter chatten — die Ergebnisse erscheinen hier, wenn sie fertig sind."
+
+ branch:
+ db_unavailable: "Sitzungsdatenbank nicht verfügbar."
+ no_conversation: "Keine Konversation zum Verzweigen — senden Sie zuerst eine Nachricht."
+ create_failed: "Verzweigung fehlgeschlagen: {error}"
+ switch_failed: "Verzweigung erstellt, aber Wechsel fehlgeschlagen."
+ branched_one: "⑂ Verzweigt zu **{title}** ({count} Nachricht kopiert)\nOriginal: `{parent}`\nZweig: `{new}`\nVerwenden Sie `/resume`, um zum Original zurückzukehren."
+ branched_many: "⑂ Verzweigt zu **{title}** ({count} Nachrichten kopiert)\nOriginal: `{parent}`\nZweig: `{new}`\nVerwenden Sie `/resume`, um zum Original zurückzukehren."
+
+ commands:
+ usage: "Verwendung: `/commands [page]`"
+ skill_header: "⚡ **Skill-Befehle**:"
+ default_desc: "Skill-Befehl"
+ none: "Keine Befehle verfügbar."
+ header: "📚 **Befehle** ({total} insgesamt, Seite {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← zurück"
+ nav_next: "weiter → `/commands {page}`"
+ out_of_range: "_(Angeforderte Seite {requested} liegt außerhalb des Bereichs, Seite {page} wird angezeigt.)_"
+
+ compress:
+ not_enough: "Nicht genug Konversation zum Komprimieren (mindestens 4 Nachrichten erforderlich)."
+ no_provider: "Kein Anbieter konfiguriert — Komprimierung nicht möglich."
+ nothing_to_do: "Noch nichts zu komprimieren (das Transkript ist weiterhin vollständig geschützter Kontext)."
+ focus_line: "Fokus: \"{topic}\""
+ summary_failed: "⚠️ Zusammenfassungsgenerierung fehlgeschlagen ({error}). {count} historische Nachricht(en) wurden entfernt und durch einen Platzhalter ersetzt; früherer Kontext ist nicht mehr wiederherstellbar. Überprüfen Sie die Konfiguration des auxiliary.compression-Modells."
+ aux_failed: "ℹ️ Das konfigurierte Komprimierungsmodell `{model}` ist fehlgeschlagen ({error}). Wiederherstellung mit Ihrem Hauptmodell — Kontext ist intakt — Sie sollten jedoch `auxiliary.compression.model` in config.yaml überprüfen."
+ failed: "Komprimierung fehlgeschlagen: {error}"
+
+ debug:
+ upload_failed: "✗ Debug-Bericht konnte nicht hochgeladen werden: {error}"
+ header: "**Debug-Bericht hochgeladen:**"
+ auto_delete: "⏱ Pastes werden in 6 Stunden automatisch gelöscht."
+ full_logs_hint: "Für vollständige Log-Uploads verwenden Sie `hermes debug share` aus der CLI."
+ share_hint: "Teilen Sie diese Links mit dem Hermes-Team, um Unterstützung zu erhalten."
+
+ deny:
+ stale: "❌ Befehl abgelehnt (Genehmigung war veraltet)."
+ no_pending: "Kein ausstehender Befehl zum Ablehnen."
+ denied_singular: "❌ Befehl abgelehnt."
+ denied_plural: "❌ Befehle abgelehnt ({count} Befehle)."
+
+ fast:
+ not_supported: "⚡ /fast ist nur für OpenAI-Modelle mit Priority Processing verfügbar."
+ status: "⚡ Priority Processing\n\nAktueller Modus: `{mode}`\n\n_Verwendung:_ `/fast `"
+ unknown_arg: "⚠️ Unbekanntes Argument: `{arg}`\n\n**Gültige Optionen:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (in Konfiguration gespeichert)\n_(wird ab nächster Nachricht wirksam)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (nur diese Sitzung)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Laufzeit-Fußzeile: **{state}**\nFelder: `{fields}`\nPlattform: `{platform}`"
+ usage: "Verwendung: `/footer [on|off|status]`"
+ saved: "📎 Laufzeit-Fußzeile: **{state}**{example}\n_(global gespeichert — wird ab nächster Nachricht wirksam)_"
+ example_line: "\nBeispiel: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Ziele sind in dieser Sitzung nicht verfügbar."
+ no_goal_set: "Kein Ziel gesetzt."
+ paused: "⏸ Ziel pausiert: {goal}"
+ no_resume: "Kein Ziel zum Fortsetzen."
+ resumed: "▶ Ziel fortgesetzt: {goal}\nSenden Sie eine Nachricht zum Fortfahren oder warten Sie — ich übernehme den nächsten Schritt im nächsten Zug."
+ invalid: "Ungültiges Ziel: {error}"
+ set: "⊙ Ziel gesetzt ({budget}-Zug-Budget): {goal}\nIch arbeite weiter, bis das Ziel erreicht ist, Sie es pausieren/löschen oder das Budget aufgebraucht ist.\nSteuerung: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes-Befehle**\n"
+ skill_header: "\n⚡ **Skill-Befehle** ({count} aktiv):"
+ more_use_commands: "\n... und {count} weitere. Verwenden Sie `/commands` für die vollständige paginierte Liste."
+
+ insights:
+ invalid_days: "Ungültiger --days-Wert: {value}"
+ error: "Fehler beim Erstellen der Auswertung: {error}"
+
+ kanban:
+ error_prefix: "⚠ Kanban-Fehler: {error}"
+ subscribed_suffix: "(abonniert — Sie werden benachrichtigt, wenn {task_id} abgeschlossen oder blockiert wird)"
+ truncated_suffix: "… (gekürzt; verwenden Sie `hermes kanban …` im Terminal für die vollständige Ausgabe)"
+ no_output: "(keine Ausgabe)"
+
+ personality:
+ none_configured: "Keine Persönlichkeiten in `{path}/config.yaml` konfiguriert"
+ header: "🎭 **Verfügbare Persönlichkeiten**\n"
+ none_option: "• `none` — (kein Persönlichkeits-Overlay)"
+ item: "• `{name}` — {preview}"
+ usage: "\nVerwendung: `/personality `"
+ save_failed: "⚠️ Speichern der Persönlichkeitsänderung fehlgeschlagen: {error}"
+ cleared: "🎭 Persönlichkeit gelöscht — Basisverhalten des Agenten wird verwendet.\n_(wird mit der nächsten Nachricht wirksam)_"
+ set_to: "🎭 Persönlichkeit auf **{name}** gesetzt\n_(wird mit der nächsten Nachricht wirksam)_"
+ unknown: "Unbekannte Persönlichkeit: `{name}`\n\nVerfügbar: {available}"
+
+ profile:
+ header: "👤 **Profil:** `{profile}`"
+ home: "📂 **Stammverzeichnis:** `{home}`"
+
+ reasoning:
+ level_default: "medium (Standard)"
+ level_disabled: "none (deaktiviert)"
+ scope_session: "Sitzungs-Override"
+ scope_global: "Globale Konfiguration"
+ status: "🧠 **Reasoning-Einstellungen**\n\n**Stärke:** `{level}`\n**Geltungsbereich:** {scope}\n**Anzeige:** {display}\n\n_Verwendung:_ `/reasoning [--global]`"
+ display_on: "an ✓"
+ display_off: "aus"
+ display_set_on: "🧠 ✓ Reasoning-Anzeige: **AN**\nDas Modelldenken wird vor jeder Antwort auf **{platform}** angezeigt."
+ display_set_off: "🧠 ✓ Reasoning-Anzeige: **AUS** für **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` wird nicht unterstützt. Verwenden Sie `/reasoning --global`, um den globalen Standard zu ändern."
+ reset_done: "🧠 ✓ Sitzungs-Reasoning-Override gelöscht; Rückfall auf globale Konfiguration."
+ unknown_arg: "⚠️ Unbekanntes Argument: `{arg}`\n\n**Gültige Stärken:** none, minimal, low, medium, high, xhigh\n**Anzeige:** show, hide\n**Speichern:** `--global` hinzufügen, um über die Sitzung hinaus zu speichern"
+ set_global: "🧠 ✓ Reasoning-Stärke auf `{effort}` gesetzt (in Konfiguration gespeichert)\n_(wird mit der nächsten Nachricht wirksam)_"
+ set_global_save_failed: "🧠 ✓ Reasoning-Stärke auf `{effort}` gesetzt (nur Sitzung — Konfiguration konnte nicht gespeichert werden)\n_(wird mit der nächsten Nachricht wirksam)_"
+ set_session: "🧠 ✓ Reasoning-Stärke auf `{effort}` gesetzt (nur Sitzung — `--global` hinzufügen, um zu speichern)\n_(wird mit der nächsten Nachricht wirksam)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp abgebrochen. MCP-Tools unverändert."
+ always_followup: "ℹ️ Künftige `/reload-mcp`-Aufrufe laufen ohne Bestätigung. Wieder aktivieren über `approvals.mcp_reload_confirm: true` in `config.yaml`."
+ confirm_prompt: "⚠️ **/reload-mcp bestätigen**\n\nDas Neuladen der MCP-Server baut das Toolset für diese Sitzung neu auf und **invalidiert den Prompt-Cache des Anbieters** — die nächste Nachricht sendet die vollständigen Eingabetokens erneut. Bei langem Kontext oder Modellen mit hohem Reasoning-Aufwand kann das teuer sein.\n\nWählen Sie:\n• **Einmal genehmigen** — jetzt neu laden\n• **Immer genehmigen** — jetzt neu laden und diese Bestätigung dauerhaft unterdrücken\n• **Abbrechen** — MCP-Tools unverändert lassen\n\n_Text-Alternative: Antworten Sie mit `/approve`, `/always` oder `/cancel`._"
+ header: "🔄 **MCP-Server neu geladen**\n"
+ reconnected: "♻️ Wiederverbunden: {names}"
+ added: "➕ Hinzugefügt: {names}"
+ removed: "➖ Entfernt: {names}"
+ none_connected: "Keine MCP-Server verbunden."
+ tools_available: "\n🔧 {tools} Tool(s) von {servers} Server(n) verfügbar"
+ failed: "❌ MCP-Neuladen fehlgeschlagen: {error}"
+
+ reload_skills:
+ header: "🔄 **Skills neu geladen**\n"
+ no_new: "Keine neuen Skills erkannt."
+ total: "\n📚 {count} Skill(s) verfügbar"
+ added_header: "➕ **Hinzugefügte Skills:**"
+ removed_header: "➖ **Entfernte Skills:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Skill-Neuladen fehlgeschlagen: {error}"
+
+ reset:
+ header_default: "✨ Sitzung zurückgesetzt! Neuanfang."
+ header_new: "✨ Neue Sitzung gestartet!"
+ header_titled: "✨ Neue Sitzung gestartet: {title}"
+ title_rejected: "\n⚠️ Titel abgelehnt: {error}"
+ title_error_untitled: "\n⚠️ {error} — Sitzung ohne Titel gestartet."
+ title_empty_untitled: "\n⚠️ Titel ist nach Bereinigung leer — Sitzung ohne Titel gestartet."
+ tip: "\n✦ Tipp: {tip}"
+
+ restart:
+ in_progress: "⏳ Gateway-Neustart läuft bereits..."
+ restarting: "♻ Gateway wird neu gestartet. Falls Sie nicht innerhalb von 60 Sekunden benachrichtigt werden, starten Sie über die Konsole mit `hermes gateway restart` neu."
+
+ resume:
+ db_unavailable: "Sitzungsdatenbank nicht verfügbar."
+ no_named_sessions: "Keine benannten Sitzungen gefunden.\nVerwenden Sie `/title Meine Sitzung`, um die aktuelle Sitzung zu benennen, dann `/resume Meine Sitzung`, um später dorthin zurückzukehren."
+ list_header: "📋 **Benannte Sitzungen**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nVerwendung: `/resume `"
+ list_failed: "Sitzungen konnten nicht aufgelistet werden: {error}"
+ not_found: "Keine Sitzung passend zu '**{name}**' gefunden.\nVerwenden Sie `/resume` ohne Argumente, um verfügbare Sitzungen zu sehen."
+ already_on: "📌 Bereits in Sitzung **{name}**."
+ switch_failed: "Sitzungswechsel fehlgeschlagen."
+ resumed_one: "↻ Sitzung **{title}** fortgesetzt ({count} Nachricht). Konversation wiederhergestellt."
+ resumed_many: "↻ Sitzung **{title}** fortgesetzt ({count} Nachrichten). Konversation wiederhergestellt."
+ resumed_no_count: "↻ Sitzung **{title}** fortgesetzt. Konversation wiederhergestellt."
+
+ retry:
+ no_previous: "Keine vorherige Nachricht zum Wiederholen."
+
+ rollback:
+ not_enabled: "Checkpoints sind nicht aktiviert.\nIn config.yaml aktivieren:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Keine Checkpoints für {cwd} gefunden"
+ invalid_number: "Ungültige Checkpoint-Nummer. Verwenden Sie 1-{max}."
+ restored: "✅ Auf Checkpoint {hash} wiederhergestellt: {reason}\nEin Pre-Rollback-Snapshot wurde automatisch gespeichert."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Home-Kanal konnte nicht gespeichert werden: {error}"
+ success: "✅ Home-Kanal auf **{name}** (ID: {chat_id}) gesetzt.\nCron-Jobs und plattformübergreifende Nachrichten werden hierher geliefert."
+
+ status:
+ header: "📊 **Hermes-Gateway-Status**"
+ session_id: "**Sitzungs-ID:** `{session_id}`"
+ title: "**Titel:** {title}"
+ created: "**Erstellt:** {timestamp}"
+ last_activity: "**Letzte Aktivität:** {timestamp}"
+ tokens: "**Tokens:** {tokens}"
+ agent_running: "**Agent läuft:** {state}"
+ state_yes: "Ja ⚡"
+ state_no: "Nein"
+ queued: "**Wartende Folgenachrichten:** {count}"
+ platforms: "**Verbundene Plattformen:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Gestoppt. Der Agent hatte noch nicht begonnen — Sie können diese Sitzung fortsetzen."
+ stopped: "⚡ Gestoppt. Sie können diese Sitzung fortsetzen."
+ no_active: "Keine aktive Aufgabe zum Stoppen."
+
+ title:
+ db_unavailable: "Sitzungsdatenbank nicht verfügbar."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Titel ist nach der Bereinigung leer. Bitte druckbare Zeichen verwenden."
+ set_to: "✏️ Sitzungstitel gesetzt: **{title}**"
+ not_found: "Sitzung nicht in der Datenbank gefunden."
+ current_with_title: "📌 Sitzung: `{session_id}`\nTitel: **{title}**"
+ current_no_title: "📌 Sitzung: `{session_id}`\nKein Titel gesetzt. Verwendung: `/title Mein Sitzungsname`"
+
+ topic:
+ not_telegram_dm: "Der /topic-Befehl ist nur in Telegram-Privatchats verfügbar."
+ no_session_db: "Sitzungsdatenbank nicht verfügbar."
+ unauthorized: "Sie sind nicht berechtigt, /topic auf diesem Bot zu verwenden."
+ restore_needs_topic: "Um eine Sitzung wiederherzustellen, erstellen oder öffnen Sie zuerst ein Telegram-Topic und senden Sie dann /topic innerhalb dieses Topics. Um ein neues Topic zu erstellen, öffnen Sie All Messages und senden Sie dort eine beliebige Nachricht."
+ topics_disabled: "Telegram-Topics sind für diesen Bot noch nicht aktiviert.\n\nSo aktivieren Sie sie:\n1. Öffnen Sie @BotFather.\n2. Wählen Sie Ihren Bot.\n3. Öffnen Sie Bot Settings → Threads Settings.\n4. Aktivieren Sie Threaded Mode und stellen Sie sicher, dass Benutzer neue Threads erstellen dürfen.\n\nDann senden Sie /topic erneut."
+ topics_user_disallowed: "Telegram-Topics sind aktiviert, aber Benutzer dürfen keine Topics erstellen.\n\nÖffnen Sie @BotFather → wählen Sie Ihren Bot → Bot Settings → Threads Settings, und deaktivieren Sie dann 'Disallow users to create new threads'.\n\nDann senden Sie /topic erneut."
+ enable_failed: "Telegram-Topic-Modus konnte nicht aktiviert werden: {error}"
+ bound_status: "Dieses Topic ist verknüpft mit:\nSitzung: {label}\nID: {session_id}\n\nVerwenden Sie /new, um dieses Topic durch eine neue Sitzung zu ersetzen.\nFür parallele Arbeit öffnen Sie All Messages und senden Sie dort eine Nachricht, um ein weiteres Topic zu erstellen."
+ thread_ready: "Telegram-Multi-Session-Topics sind aktiviert.\n\nDieses Topic wird als unabhängige Hermes-Sitzung verwendet. Verwenden Sie /new, um die aktuelle Sitzung dieses Topics zu ersetzen. Für parallele Arbeit öffnen Sie All Messages und senden Sie dort eine Nachricht, um ein weiteres Topic zu erstellen."
+ untitled_session: "Unbenannte Sitzung"
+
+ undo:
+ nothing: "Nichts zum Rückgängigmachen."
+ removed: "↩️ {count} Nachricht(en) rückgängig gemacht.\nEntfernt: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update ist nur auf Messaging-Plattformen verfügbar. Führen Sie `hermes update` im Terminal aus."
+ not_git_repo: "✗ Kein Git-Repository — Update nicht möglich."
+ hermes_cmd_not_found: "✗ Der Befehl `hermes` konnte nicht gefunden werden. Hermes läuft, aber der Update-Befehl konnte das ausführbare Programm weder im PATH noch über den aktuellen Python-Interpreter finden. Versuchen Sie, `hermes update` manuell im Terminal auszuführen."
+ start_failed: "✗ Update konnte nicht gestartet werden: {error}"
+ starting: "⚕ Hermes-Update wird gestartet… Ich streame den Fortschritt hier."
+
+ usage:
+ rate_limits: "⏱️ **Ratenlimits:** {state}"
+ header_session: "📊 **Sitzungs-Token-Nutzung**"
+ label_model: "Modell: `{model}`"
+ label_input_tokens: "Eingabetokens: {count}"
+ label_cache_read: "Cache-Lesetokens: {count}"
+ label_cache_write: "Cache-Schreibtokens: {count}"
+ label_output_tokens: "Ausgabetokens: {count}"
+ label_total: "Gesamt: {count}"
+ label_api_calls: "API-Aufrufe: {count}"
+ label_cost: "Kosten: {prefix}${amount}"
+ label_cost_included: "Kosten: inbegriffen"
+ label_context: "Kontext: {used} / {total} ({pct}%)"
+ label_compressions: "Kompressionen: {count}"
+ header_session_info: "📊 **Sitzungsinfo**"
+ label_messages: "Nachrichten: {count}"
+ label_estimated_context: "Geschätzter Kontext: ~{count} Tokens"
+ detailed_after_first: "_(Detaillierte Nutzung nach der ersten Agentenantwort verfügbar)_"
+ no_data: "Keine Nutzungsdaten für diese Sitzung verfügbar."
+
+ verbose:
+ not_enabled: "Der Befehl `/verbose` ist für Messaging-Plattformen nicht aktiviert.\n\nIn `config.yaml` aktivieren:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Tool-Fortschritt: **OFF** — keine Tool-Aktivität angezeigt."
+ mode_new: "⚙️ Tool-Fortschritt: **NEW** — angezeigt bei Tool-Wechsel (Vorschaulänge: `display.tool_preview_length`, Standard 40)."
+ mode_all: "⚙️ Tool-Fortschritt: **ALL** — jeder Tool-Aufruf wird angezeigt (Vorschaulänge: `display.tool_preview_length`, Standard 40)."
+ mode_verbose: "⚙️ Tool-Fortschritt: **VERBOSE** — jeder Tool-Aufruf mit vollständigen Argumenten."
+ saved_suffix: "_(für **{platform}** gespeichert — wird ab nächster Nachricht wirksam)_"
+ save_failed: "_(konnte nicht in der Konfiguration gespeichert werden: {error})_"
+
+ voice:
+ enabled_voice_only: "Sprachmodus aktiviert.\nIch antworte mit Sprache, wenn Sie Sprachnachrichten senden.\nVerwenden Sie /voice tts für Sprachantworten auf alle Nachrichten."
+ disabled_text: "Sprachmodus deaktiviert. Nur Textantworten."
+ tts_enabled: "Auto-TTS aktiviert.\nAlle Antworten enthalten eine Sprachnachricht."
+ status_mode: "Sprachmodus: {label}"
+ status_channel: "Sprachkanal: #{channel}"
+ status_participants: "Teilnehmer: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (spricht)"
+ enabled_short: "Sprachmodus aktiviert."
+ disabled_short: "Sprachmodus deaktiviert."
+ label_off: "Aus (nur Text)"
+ label_voice_only: "An (Sprachantwort auf Sprachnachrichten)"
+ label_all: "TTS (Sprachantwort auf alle Nachrichten)"
+
+ yolo:
+ disabled: "⚠️ YOLO-Modus für diese Sitzung **AUS** — gefährliche Befehle benötigen eine Genehmigung."
+ enabled: "⚡ YOLO-Modus für diese Sitzung **AN** — alle Befehle werden automatisch genehmigt. Mit Vorsicht verwenden."
+
+ shared:
+ session_db_unavailable: "Session-Datenbank nicht verfügbar."
+ session_db_unavailable_prefix: "Session-Datenbank nicht verfügbar"
+ session_not_found: "Session nicht in der Datenbank gefunden."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/en.yaml b/locales/en.yaml
index 017c73c75e6..d485efe7561 100644
--- a/locales/en.yaml
+++ b/locales/en.yaml
@@ -33,3 +33,333 @@ gateway:
no_active_goal: "No active goal."
config_read_failed: "⚠️ Could not read config.yaml: {error}"
config_save_failed: "⚠️ Could not save config: {error}"
+
+ # /model command output -- shown after a model switch or when listing models.
+ # Provider names, model IDs, capability strings, and cost figures are NOT
+ # translated -- they're identifiers/values, not prose. Only the labels
+ # ("Provider:", "Context:", etc.) and the help/footer lines are localized.
+ model:
+ error_prefix: "Error: {error}"
+ switched: "Model switched to `{model}`"
+ provider_label: "Provider: {provider}"
+ context_label: "Context: {tokens} tokens"
+ max_output_label: "Max output: {tokens} tokens"
+ cost_label: "Cost: {cost}"
+ capabilities_label: "Capabilities: {capabilities}"
+ prompt_caching_enabled: "Prompt caching: enabled"
+ warning_prefix: "Warning: {warning}"
+ saved_global: "Saved to config.yaml (`--global`)"
+ session_only_hint: "_(session only — add `--global` to persist)_"
+ current_label: "Current: `{model}` on {provider}"
+ current_tag: " (current)"
+ more_models_suffix: " (+{count} more)"
+ usage_switch_model: "`/model ` — switch model"
+ usage_switch_provider: "`/model --provider ` — switch provider"
+ usage_persist: "`/model --global` — persist"
+
+ agents:
+ header: "🤖 **Active Agents & Tasks**"
+ active_agents: "**Active agents:** {count}"
+ this_chat: " · this chat"
+ more: "... and {count} more"
+ running_processes: "**Running background processes:** {count}"
+ async_jobs: "**Gateway async jobs:** {count}"
+ none: "No active agents or running tasks."
+ state_starting: "starting"
+ state_running: "running"
+
+ approve:
+ no_pending: "No pending command to approve."
+ once_singular: "✅ Command approved. The agent is resuming..."
+ once_plural: "✅ Commands approved ({count} commands). The agent is resuming..."
+ session_singular: "✅ Command approved (pattern approved for this session). The agent is resuming..."
+ session_plural: "✅ Commands approved (pattern approved for this session) ({count} commands). The agent is resuming..."
+ always_singular: "✅ Command approved (pattern approved permanently). The agent is resuming..."
+ always_plural: "✅ Commands approved (pattern approved permanently) ({count} commands). The agent is resuming..."
+
+ background:
+ usage: "Usage: /background \nExample: /background Summarize the top HN stories today\n\nRuns the prompt in a separate session. You can keep chatting — the result will appear here when done."
+ started: "🔄 Background task started: \"{preview}\"\nTask ID: {task_id}\nYou can keep chatting — results will appear when done."
+
+ branch:
+ db_unavailable: "Session database not available."
+ no_conversation: "No conversation to branch — send a message first."
+ create_failed: "Failed to create branch: {error}"
+ switch_failed: "Branch created but failed to switch to it."
+ branched_one: "⑂ Branched to **{title}** ({count} message copied)\nOriginal: `{parent}`\nBranch: `{new}`\nUse `/resume` to switch back to the original."
+ branched_many: "⑂ Branched to **{title}** ({count} messages copied)\nOriginal: `{parent}`\nBranch: `{new}`\nUse `/resume` to switch back to the original."
+
+ commands:
+ usage: "Usage: `/commands [page]`"
+ skill_header: "⚡ **Skill Commands**:"
+ default_desc: "Skill command"
+ none: "No commands available."
+ header: "📚 **Commands** ({total} total, page {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← prev"
+ nav_next: "next → `/commands {page}`"
+ out_of_range: "_(Requested page {requested} was out of range, showing page {page}.)_"
+
+ compress:
+ not_enough: "Not enough conversation to compress (need at least 4 messages)."
+ no_provider: "No provider configured -- cannot compress."
+ nothing_to_do: "Nothing to compress yet (the transcript is still all protected context)."
+ focus_line: "Focus: \"{topic}\""
+ summary_failed: "⚠️ Summary generation failed ({error}). {count} historical message(s) were removed and replaced with a placeholder; earlier context is no longer recoverable. Consider checking your auxiliary.compression model configuration."
+ aux_failed: "ℹ️ Configured compression model `{model}` failed ({error}). Recovered using your main model — context is intact — but you may want to check `auxiliary.compression.model` in config.yaml."
+ failed: "Compression failed: {error}"
+
+ debug:
+ upload_failed: "✗ Failed to upload debug report: {error}"
+ header: "**Debug report uploaded:**"
+ auto_delete: "⏱ Pastes will auto-delete in 6 hours."
+ full_logs_hint: "For full log uploads, use `hermes debug share` from the CLI."
+ share_hint: "Share these links with the Hermes team for support."
+
+ deny:
+ stale: "❌ Command denied (approval was stale)."
+ no_pending: "No pending command to deny."
+ denied_singular: "❌ Command denied."
+ denied_plural: "❌ Commands denied ({count} commands)."
+
+ fast:
+ not_supported: "⚡ /fast is only available for OpenAI models that support Priority Processing."
+ status: "⚡ Priority Processing\n\nCurrent mode: `{mode}`\n\n_Usage:_ `/fast `"
+ unknown_arg: "⚠️ Unknown argument: `{arg}`\n\n**Valid options:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (saved to config)\n_(takes effect on next message)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (this session only)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Runtime footer: **{state}**\nFields: `{fields}`\nPlatform: `{platform}`"
+ usage: "Usage: `/footer [on|off|status]`"
+ saved: "📎 Runtime footer: **{state}**{example}\n_(saved globally — takes effect on next message)_"
+ example_line: "\nExample: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Goals unavailable on this session."
+ no_goal_set: "No goal set."
+ paused: "⏸ Goal paused: {goal}"
+ no_resume: "No goal to resume."
+ resumed: "▶ Goal resumed: {goal}\nSend any message to continue, or wait — I'll take the next step on the next turn."
+ invalid: "Invalid goal: {error}"
+ set: "⊙ Goal set ({budget}-turn budget): {goal}\nI'll keep working until the goal is done, you pause/clear it, or the budget is exhausted.\nControls: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes Commands**\n"
+ skill_header: "\n⚡ **Skill Commands** ({count} active):"
+ more_use_commands: "\n... and {count} more. Use `/commands` for the full paginated list."
+
+ insights:
+ invalid_days: "Invalid --days value: {value}"
+ error: "Error generating insights: {error}"
+
+ kanban:
+ error_prefix: "⚠ kanban error: {error}"
+ subscribed_suffix: "(subscribed — you'll be notified when {task_id} completes or blocks)"
+ truncated_suffix: "… (truncated; use `hermes kanban …` in your terminal for full output)"
+ no_output: "(no output)"
+
+ personality:
+ none_configured: "No personalities configured in `{path}/config.yaml`"
+ header: "🎭 **Available Personalities**\n"
+ none_option: "• `none` — (no personality overlay)"
+ item: "• `{name}` — {preview}"
+ usage: "\nUsage: `/personality `"
+ save_failed: "⚠️ Failed to save personality change: {error}"
+ cleared: "🎭 Personality cleared — using base agent behavior.\n_(takes effect on next message)_"
+ set_to: "🎭 Personality set to **{name}**\n_(takes effect on next message)_"
+ unknown: "Unknown personality: `{name}`\n\nAvailable: {available}"
+
+ profile:
+ header: "👤 **Profile:** `{profile}`"
+ home: "📂 **Home:** `{home}`"
+
+ reasoning:
+ level_default: "medium (default)"
+ level_disabled: "none (disabled)"
+ scope_session: "session override"
+ scope_global: "global config"
+ status: "🧠 **Reasoning Settings**\n\n**Effort:** `{level}`\n**Scope:** {scope}\n**Display:** {display}\n\n_Usage:_ `/reasoning [--global]`"
+ display_on: "on ✓"
+ display_off: "off"
+ display_set_on: "🧠 ✓ Reasoning display: **ON**\nModel thinking will be shown before each response on **{platform}**."
+ display_set_off: "🧠 ✓ Reasoning display: **OFF** for **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` is not supported. Use `/reasoning --global` to change the global default."
+ reset_done: "🧠 ✓ Session reasoning override cleared; falling back to global config."
+ unknown_arg: "⚠️ Unknown argument: `{arg}`\n\n**Valid levels:** none, minimal, low, medium, high, xhigh\n**Display:** show, hide\n**Persist:** add `--global` to save beyond this session"
+ set_global: "🧠 ✓ Reasoning effort set to `{effort}` (saved to config)\n_(takes effect on next message)_"
+ set_global_save_failed: "🧠 ✓ Reasoning effort set to `{effort}` (session only — config save failed)\n_(takes effect on next message)_"
+ set_session: "🧠 ✓ Reasoning effort set to `{effort}` (session only — add `--global` to persist)\n_(takes effect on next message)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp cancelled. MCP tools unchanged."
+ always_followup: "ℹ️ Future `/reload-mcp` calls will run without confirmation. Re-enable via `approvals.mcp_reload_confirm: true` in config.yaml."
+ confirm_prompt: "⚠️ **Confirm /reload-mcp**\n\nReloading MCP servers rebuilds the tool set for this session and **invalidates the provider prompt cache** — the next message will re-send full input tokens. On long-context or high-reasoning models this can be expensive.\n\nChoose:\n• **Approve Once** — reload now\n• **Always Approve** — reload now and silence this prompt permanently\n• **Cancel** — leave MCP tools unchanged\n\n_Text fallback: reply `/approve`, `/always`, or `/cancel`._"
+ header: "🔄 **MCP Servers Reloaded**\n"
+ reconnected: "♻️ Reconnected: {names}"
+ added: "➕ Added: {names}"
+ removed: "➖ Removed: {names}"
+ none_connected: "No MCP servers connected."
+ tools_available: "\n🔧 {tools} tool(s) available from {servers} server(s)"
+ failed: "❌ MCP reload failed: {error}"
+
+ reload_skills:
+ header: "🔄 **Skills Reloaded**\n"
+ no_new: "No new skills detected."
+ total: "\n📚 {count} skill(s) available"
+ added_header: "➕ **Added Skills:**"
+ removed_header: "➖ **Removed Skills:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Skills reload failed: {error}"
+
+ reset:
+ header_default: "✨ Session reset! Starting fresh."
+ header_new: "✨ New session started!"
+ header_titled: "✨ New session started: {title}"
+ title_rejected: "\n⚠️ Title rejected: {error}"
+ title_error_untitled: "\n⚠️ {error} — session started untitled."
+ title_empty_untitled: "\n⚠️ Title is empty after cleanup — session started untitled."
+ tip: "\n✦ Tip: {tip}"
+
+ restart:
+ in_progress: "⏳ Gateway restart already in progress..."
+ restarting: "♻ Restarting gateway. If you aren't notified within 60 seconds, restart from the console with `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Session database not available."
+ no_named_sessions: "No named sessions found.\nUse `/title My Session` to name your current session, then `/resume My Session` to return to it later."
+ list_header: "📋 **Named Sessions**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nUsage: `/resume `"
+ list_failed: "Could not list sessions: {error}"
+ not_found: "No session found matching '**{name}**'.\nUse `/resume` with no arguments to see available sessions."
+ already_on: "📌 Already on session **{name}**."
+ switch_failed: "Failed to switch session."
+ resumed_one: "↻ Resumed session **{title}** ({count} message). Conversation restored."
+ resumed_many: "↻ Resumed session **{title}** ({count} messages). Conversation restored."
+ resumed_no_count: "↻ Resumed session **{title}**. Conversation restored."
+
+ retry:
+ no_previous: "No previous message to retry."
+
+ rollback:
+ not_enabled: "Checkpoints are not enabled.\nEnable in config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "No checkpoints found for {cwd}"
+ invalid_number: "Invalid checkpoint number. Use 1-{max}."
+ restored: "✅ Restored to checkpoint {hash}: {reason}\nA pre-rollback snapshot was saved automatically."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Failed to save home channel: {error}"
+ success: "✅ Home channel set to **{name}** (ID: {chat_id}).\nCron jobs and cross-platform messages will be delivered here."
+
+ status:
+ header: "📊 **Hermes Gateway Status**"
+ session_id: "**Session ID:** `{session_id}`"
+ title: "**Title:** {title}"
+ created: "**Created:** {timestamp}"
+ last_activity: "**Last Activity:** {timestamp}"
+ tokens: "**Tokens:** {tokens}"
+ agent_running: "**Agent Running:** {state}"
+ state_yes: "Yes ⚡"
+ state_no: "No"
+ queued: "**Queued follow-ups:** {count}"
+ platforms: "**Connected Platforms:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Stopped. The agent hadn't started yet — you can continue this session."
+ stopped: "⚡ Stopped. You can continue this session."
+ no_active: "No active task to stop."
+
+ title:
+ db_unavailable: "Session database not available."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Title is empty after cleanup. Please use printable characters."
+ set_to: "✏️ Session title set: **{title}**"
+ not_found: "Session not found in database."
+ current_with_title: "📌 Session: `{session_id}`\nTitle: **{title}**"
+ current_no_title: "📌 Session: `{session_id}`\nNo title set. Usage: `/title My Session Name`"
+
+ topic:
+ not_telegram_dm: "The /topic command is only available in Telegram private chats."
+ no_session_db: "Session database not available."
+ unauthorized: "You are not authorized to use /topic on this bot."
+ restore_needs_topic: "To restore a session, first create or open a Telegram topic, then send /topic inside that topic. To create a new topic, open All Messages and send any message there."
+ topics_disabled: "Telegram topics are not enabled for this bot yet.\n\nHow to enable them:\n1. Open @BotFather.\n2. Choose your bot.\n3. Open Bot Settings → Threads Settings.\n4. Turn on Threaded Mode and make sure users are allowed to create new threads.\n\nThen send /topic again."
+ topics_user_disallowed: "Telegram topics are enabled, but users are not allowed to create topics.\n\nOpen @BotFather → choose your bot → Bot Settings → Threads Settings, then turn off 'Disallow users to create new threads'.\n\nThen send /topic again."
+ enable_failed: "Failed to enable Telegram topic mode: {error}"
+ bound_status: "This topic is linked to:\nSession: {label}\nID: {session_id}\n\nUse /new to replace this topic with a fresh session.\nFor parallel work, open All Messages and send a message there to create another topic."
+ thread_ready: "Telegram multi-session topics are enabled.\n\nThis topic will be used as an independent Hermes session. Use /new to replace this topic's current session. For parallel work, open All Messages and send a message there to create another topic."
+ untitled_session: "Untitled session"
+
+ undo:
+ nothing: "Nothing to undo."
+ removed: "↩️ Undid {count} message(s).\nRemoved: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update is only available from messaging platforms. Run `hermes update` from the terminal."
+ not_git_repo: "✗ Not a git repository — cannot update."
+ hermes_cmd_not_found: "✗ Could not locate the `hermes` command. Hermes is running, but the update command could not find the executable on PATH or via the current Python interpreter. Try running `hermes update` manually in your terminal."
+ start_failed: "✗ Failed to start update: {error}"
+ starting: "⚕ Starting Hermes update… I'll stream progress here."
+
+ usage:
+ rate_limits: "⏱️ **Rate Limits:** {state}"
+ header_session: "📊 **Session Token Usage**"
+ label_model: "Model: `{model}`"
+ label_input_tokens: "Input tokens: {count}"
+ label_cache_read: "Cache read tokens: {count}"
+ label_cache_write: "Cache write tokens: {count}"
+ label_output_tokens: "Output tokens: {count}"
+ label_total: "Total: {count}"
+ label_api_calls: "API calls: {count}"
+ label_cost: "Cost: {prefix}${amount}"
+ label_cost_included: "Cost: included"
+ label_context: "Context: {used} / {total} ({pct}%)"
+ label_compressions: "Compressions: {count}"
+ header_session_info: "📊 **Session Info**"
+ label_messages: "Messages: {count}"
+ label_estimated_context: "Estimated context: ~{count} tokens"
+ detailed_after_first: "_(Detailed usage available after the first agent response)_"
+ no_data: "No usage data available for this session."
+
+ verbose:
+ not_enabled: "The `/verbose` command is not enabled for messaging platforms.\n\nEnable it in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Tool progress: **OFF** — no tool activity shown."
+ mode_new: "⚙️ Tool progress: **NEW** — shown when tool changes (preview length: `display.tool_preview_length`, default 40)."
+ mode_all: "⚙️ Tool progress: **ALL** — every tool call shown (preview length: `display.tool_preview_length`, default 40)."
+ mode_verbose: "⚙️ Tool progress: **VERBOSE** — every tool call with full arguments."
+ saved_suffix: "_(saved for **{platform}** — takes effect on next message)_"
+ save_failed: "_(could not save to config: {error})_"
+
+ voice:
+ enabled_voice_only: "Voice mode enabled.\nI'll reply with voice when you send voice messages.\nUse /voice tts to get voice replies for all messages."
+ disabled_text: "Voice mode disabled. Text-only replies."
+ tts_enabled: "Auto-TTS enabled.\nAll replies will include a voice message."
+ status_mode: "Voice mode: {label}"
+ status_channel: "Voice channel: #{channel}"
+ status_participants: "Participants: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (speaking)"
+ enabled_short: "Voice mode enabled."
+ disabled_short: "Voice mode disabled."
+ label_off: "Off (text only)"
+ label_voice_only: "On (voice reply to voice messages)"
+ label_all: "TTS (voice reply to all messages)"
+
+ yolo:
+ disabled: "⚠️ YOLO mode **OFF** for this session — dangerous commands will require approval."
+ enabled: "⚡ YOLO mode **ON** for this session — all commands auto-approved. Use with caution."
+
+ shared:
+ session_db_unavailable: "Session database not available."
+ session_db_unavailable_prefix: "Session database not available"
+ session_not_found: "Session not found in database."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/es.yaml b/locales/es.yaml
index aa7c2c60941..6e7a8a34cda 100644
--- a/locales/es.yaml
+++ b/locales/es.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "No hay objetivo activo."
config_read_failed: "⚠️ No se pudo leer config.yaml: {error}"
config_save_failed: "⚠️ No se pudo guardar la configuración: {error}"
+
+ model:
+ error_prefix: "Error: {error}"
+ switched: "Modelo cambiado a `{model}`"
+ provider_label: "Proveedor: {provider}"
+ context_label: "Contexto: {tokens} tokens"
+ max_output_label: "Salida máxima: {tokens} tokens"
+ cost_label: "Coste: {cost}"
+ capabilities_label: "Capacidades: {capabilities}"
+ prompt_caching_enabled: "Caché de prompts: activado"
+ warning_prefix: "Advertencia: {warning}"
+ saved_global: "Guardado en config.yaml (`--global`)"
+ session_only_hint: "_(solo para esta sesión — añade `--global` para guardarlo)_"
+ current_label: "Actual: `{model}` en {provider}"
+ current_tag: " (actual)"
+ more_models_suffix: " (+{count} más)"
+ usage_switch_model: "`/model ` — cambiar modelo"
+ usage_switch_provider: "`/model --provider ` — cambiar proveedor"
+ usage_persist: "`/model --global` — guardar de forma permanente"
+
+ agents:
+ header: "🤖 **Agentes y tareas activos**"
+ active_agents: "**Agentes activos:** {count}"
+ this_chat: " · este chat"
+ more: "... y {count} más"
+ running_processes: "**Procesos en segundo plano en ejecución:** {count}"
+ async_jobs: "**Tareas asíncronas del gateway:** {count}"
+ none: "No hay agentes activos ni tareas en ejecución."
+ state_starting: "iniciando"
+ state_running: "en ejecución"
+
+ approve:
+ no_pending: "No hay ningún comando pendiente que aprobar."
+ once_singular: "✅ Comando aprobado. El agente se está reanudando..."
+ once_plural: "✅ Comandos aprobados ({count} comandos). El agente se está reanudando..."
+ session_singular: "✅ Comando aprobado (patrón aprobado para esta sesión). El agente se está reanudando..."
+ session_plural: "✅ Comandos aprobados (patrón aprobado para esta sesión) ({count} comandos). El agente se está reanudando..."
+ always_singular: "✅ Comando aprobado (patrón aprobado permanentemente). El agente se está reanudando..."
+ always_plural: "✅ Comandos aprobados (patrón aprobado permanentemente) ({count} comandos). El agente se está reanudando..."
+
+ background:
+ usage: "Uso: /background \nEjemplo: /background Resume las principales historias de HN de hoy\n\nEjecuta el prompt en una sesión separada. Puedes seguir chateando — el resultado aparecerá aquí cuando termine."
+ started: "🔄 Tarea en segundo plano iniciada: \"{preview}\"\nID de tarea: {task_id}\nPuedes seguir chateando — los resultados aparecerán aquí cuando terminen."
+
+ branch:
+ db_unavailable: "Base de datos de sesiones no disponible."
+ no_conversation: "No hay conversación para ramificar — envía un mensaje primero."
+ create_failed: "No se pudo crear la rama: {error}"
+ switch_failed: "Rama creada pero no se pudo cambiar a ella."
+ branched_one: "⑂ Ramificado a **{title}** ({count} mensaje copiado)\nOriginal: `{parent}`\nRama: `{new}`\nUsa `/resume` para volver al original."
+ branched_many: "⑂ Ramificado a **{title}** ({count} mensajes copiados)\nOriginal: `{parent}`\nRama: `{new}`\nUsa `/resume` para volver al original."
+
+ commands:
+ usage: "Uso: `/commands [page]`"
+ skill_header: "⚡ **Comandos de skill**:"
+ default_desc: "Comando de skill"
+ none: "No hay comandos disponibles."
+ header: "📚 **Comandos** ({total} en total, página {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← anterior"
+ nav_next: "siguiente → `/commands {page}`"
+ out_of_range: "_(La página solicitada {requested} estaba fuera de rango, mostrando la página {page}.)_"
+
+ compress:
+ not_enough: "No hay suficiente conversación para comprimir (se necesitan al menos 4 mensajes)."
+ no_provider: "No hay proveedor configurado — no se puede comprimir."
+ nothing_to_do: "Aún no hay nada que comprimir (la transcripción sigue siendo todo contexto protegido)."
+ focus_line: "Enfoque: \"{topic}\""
+ summary_failed: "⚠️ Falló la generación del resumen ({error}). Se eliminaron {count} mensaje(s) históricos y se reemplazaron por un marcador; el contexto anterior ya no se puede recuperar. Considera revisar la configuración del modelo auxiliary.compression."
+ aux_failed: "ℹ️ El modelo de compresión configurado `{model}` falló ({error}). Recuperado con tu modelo principal — el contexto está intacto — pero quizá quieras revisar `auxiliary.compression.model` en config.yaml."
+ failed: "Compresión fallida: {error}"
+
+ debug:
+ upload_failed: "✗ No se pudo subir el informe de depuración: {error}"
+ header: "**Informe de depuración subido:**"
+ auto_delete: "⏱ Los pastes se eliminarán automáticamente en 6 horas."
+ full_logs_hint: "Para subir registros completos, usa `hermes debug share` desde la CLI."
+ share_hint: "Comparte estos enlaces con el equipo de Hermes para obtener soporte."
+
+ deny:
+ stale: "❌ Comando denegado (la aprobación había caducado)."
+ no_pending: "No hay ningún comando pendiente que denegar."
+ denied_singular: "❌ Comando denegado."
+ denied_plural: "❌ Comandos denegados ({count} comandos)."
+
+ fast:
+ not_supported: "⚡ /fast solo está disponible para modelos de OpenAI que admiten Priority Processing."
+ status: "⚡ Priority Processing\n\nModo actual: `{mode}`\n\n_Uso:_ `/fast `"
+ unknown_arg: "⚠️ Argumento desconocido: `{arg}`\n\n**Opciones válidas:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (guardado en la configuración)\n_(se aplica en el próximo mensaje)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (solo esta sesión)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Pie de ejecución: **{state}**\nCampos: `{fields}`\nPlataforma: `{platform}`"
+ usage: "Uso: `/footer [on|off|status]`"
+ saved: "📎 Pie de ejecución: **{state}**{example}\n_(guardado globalmente — se aplica en el próximo mensaje)_"
+ example_line: "\nEjemplo: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Los objetivos no están disponibles en esta sesión."
+ no_goal_set: "No hay objetivo establecido."
+ paused: "⏸ Objetivo pausado: {goal}"
+ no_resume: "No hay objetivo para reanudar."
+ resumed: "▶ Objetivo reanudado: {goal}\nEnvía cualquier mensaje para continuar, o espera — daré el siguiente paso en el próximo turno."
+ invalid: "Objetivo no válido: {error}"
+ set: "⊙ Objetivo establecido (presupuesto de {budget} turnos): {goal}\nSeguiré trabajando hasta que el objetivo se complete, lo pauses/elimines o se agote el presupuesto.\nControles: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Comandos de Hermes**\n"
+ skill_header: "\n⚡ **Comandos de skill** ({count} activos):"
+ more_use_commands: "\n... y {count} más. Usa `/commands` para la lista paginada completa."
+
+ insights:
+ invalid_days: "Valor --days no válido: {value}"
+ error: "Error al generar el análisis: {error}"
+
+ kanban:
+ error_prefix: "⚠ error de kanban: {error}"
+ subscribed_suffix: "(suscrito — recibirás una notificación cuando {task_id} termine o se bloquee)"
+ truncated_suffix: "… (truncado; usa `hermes kanban …` en tu terminal para la salida completa)"
+ no_output: "(sin salida)"
+
+ personality:
+ none_configured: "No hay personalidades configuradas en `{path}/config.yaml`"
+ header: "🎭 **Personalidades disponibles**\n"
+ none_option: "• `none` — (sin superposición de personalidad)"
+ item: "• `{name}` — {preview}"
+ usage: "\nUso: `/personality `"
+ save_failed: "⚠️ No se pudo guardar el cambio de personalidad: {error}"
+ cleared: "🎭 Personalidad eliminada — usando el comportamiento base del agente.\n_(surte efecto en el siguiente mensaje)_"
+ set_to: "🎭 Personalidad establecida en **{name}**\n_(surte efecto en el siguiente mensaje)_"
+ unknown: "Personalidad desconocida: `{name}`\n\nDisponibles: {available}"
+
+ profile:
+ header: "👤 **Perfil:** `{profile}`"
+ home: "📂 **Inicio:** `{home}`"
+
+ reasoning:
+ level_default: "medium (predeterminado)"
+ level_disabled: "none (deshabilitado)"
+ scope_session: "anulación de sesión"
+ scope_global: "configuración global"
+ status: "🧠 **Ajustes de razonamiento**\n\n**Esfuerzo:** `{level}`\n**Alcance:** {scope}\n**Visualización:** {display}\n\n_Uso:_ `/reasoning [--global]`"
+ display_on: "activada ✓"
+ display_off: "desactivada"
+ display_set_on: "🧠 ✓ Visualización de razonamiento: **ACTIVADA**\nEl pensamiento del modelo se mostrará antes de cada respuesta en **{platform}**."
+ display_set_off: "🧠 ✓ Visualización de razonamiento: **DESACTIVADA** para **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` no es compatible. Usa `/reasoning --global` para cambiar el valor global por defecto."
+ reset_done: "🧠 ✓ Anulación de razonamiento de la sesión borrada; volviendo a la configuración global."
+ unknown_arg: "⚠️ Argumento desconocido: `{arg}`\n\n**Niveles válidos:** none, minimal, low, medium, high, xhigh\n**Visualización:** show, hide\n**Persistir:** añade `--global` para guardar más allá de esta sesión"
+ set_global: "🧠 ✓ Esfuerzo de razonamiento ajustado a `{effort}` (guardado en la configuración)\n_(se aplica en el próximo mensaje)_"
+ set_global_save_failed: "🧠 ✓ Esfuerzo de razonamiento ajustado a `{effort}` (solo en la sesión — error al guardar la configuración)\n_(se aplica en el próximo mensaje)_"
+ set_session: "🧠 ✓ Esfuerzo de razonamiento ajustado a `{effort}` (solo en la sesión — añade `--global` para persistir)\n_(se aplica en el próximo mensaje)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp cancelado. Las herramientas MCP no han cambiado."
+ always_followup: "ℹ️ Las próximas llamadas a `/reload-mcp` se ejecutarán sin confirmación. Reactiva mediante `approvals.mcp_reload_confirm: true` en `config.yaml`."
+ confirm_prompt: "⚠️ **Confirmar /reload-mcp**\n\nRecargar los servidores MCP reconstruye el conjunto de herramientas de esta sesión e **invalida la caché de prompt del proveedor** — el siguiente mensaje reenviará los tokens de entrada completos. En modelos de contexto largo o de razonamiento alto esto puede resultar costoso.\n\nElige:\n• **Aprobar una vez** — recargar ahora\n• **Aprobar siempre** — recargar ahora y silenciar esta confirmación permanentemente\n• **Cancelar** — dejar las herramientas MCP sin cambios\n\n_Alternativa de texto: responde `/approve`, `/always` o `/cancel`._"
+ header: "🔄 **Servidores MCP recargados**\n"
+ reconnected: "♻️ Reconectados: {names}"
+ added: "➕ Añadidos: {names}"
+ removed: "➖ Eliminados: {names}"
+ none_connected: "No hay servidores MCP conectados."
+ tools_available: "\n🔧 {tools} herramienta(s) disponibles de {servers} servidor(es)"
+ failed: "❌ Falló la recarga de MCP: {error}"
+
+ reload_skills:
+ header: "🔄 **Skills recargadas**\n"
+ no_new: "No se detectaron nuevas skills."
+ total: "\n📚 {count} skill(s) disponibles"
+ added_header: "➕ **Skills añadidas:**"
+ removed_header: "➖ **Skills eliminadas:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Falló la recarga de skills: {error}"
+
+ reset:
+ header_default: "✨ ¡Sesión reiniciada! Empezando de nuevo."
+ header_new: "✨ ¡Nueva sesión iniciada!"
+ header_titled: "✨ Nueva sesión iniciada: {title}"
+ title_rejected: "\n⚠️ Título rechazado: {error}"
+ title_error_untitled: "\n⚠️ {error} — sesión iniciada sin título."
+ title_empty_untitled: "\n⚠️ El título queda vacío tras la limpieza — sesión iniciada sin título."
+ tip: "\n✦ Consejo: {tip}"
+
+ restart:
+ in_progress: "⏳ El reinicio del gateway ya está en curso..."
+ restarting: "♻ Reiniciando el gateway. Si no recibes notificación en 60 segundos, reinicia desde la consola con `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Base de datos de sesiones no disponible."
+ no_named_sessions: "No se encontraron sesiones con nombre.\nUsa `/title Mi sesión` para nombrar la sesión actual y luego `/resume Mi sesión` para volver a ella."
+ list_header: "📋 **Sesiones con nombre**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nUso: `/resume `"
+ list_failed: "No se pudieron listar las sesiones: {error}"
+ not_found: "No se encontró ninguna sesión que coincida con '**{name}**'.\nUsa `/resume` sin argumentos para ver las sesiones disponibles."
+ already_on: "📌 Ya estás en la sesión **{name}**."
+ switch_failed: "No se pudo cambiar de sesión."
+ resumed_one: "↻ Sesión **{title}** reanudada ({count} mensaje). Conversación restaurada."
+ resumed_many: "↻ Sesión **{title}** reanudada ({count} mensajes). Conversación restaurada."
+ resumed_no_count: "↻ Sesión **{title}** reanudada. Conversación restaurada."
+
+ retry:
+ no_previous: "No hay un mensaje anterior para reintentar."
+
+ rollback:
+ not_enabled: "Los checkpoints no están habilitados.\nHabilítalos en config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "No se encontraron checkpoints para {cwd}"
+ invalid_number: "Número de checkpoint inválido. Usa 1-{max}."
+ restored: "✅ Restaurado al checkpoint {hash}: {reason}\nSe guardó automáticamente un snapshot previo al rollback."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "No se pudo guardar el canal principal: {error}"
+ success: "✅ Canal principal establecido en **{name}** (ID: {chat_id}).\nLas tareas cron y los mensajes entre plataformas se entregarán aquí."
+
+ status:
+ header: "📊 **Estado de Hermes Gateway**"
+ session_id: "**ID de sesión:** `{session_id}`"
+ title: "**Título:** {title}"
+ created: "**Creado:** {timestamp}"
+ last_activity: "**Última actividad:** {timestamp}"
+ tokens: "**Tokens:** {tokens}"
+ agent_running: "**Agente activo:** {state}"
+ state_yes: "Sí ⚡"
+ state_no: "No"
+ queued: "**Seguimientos en cola:** {count}"
+ platforms: "**Plataformas conectadas:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Detenido. El agente aún no había comenzado — puedes continuar esta sesión."
+ stopped: "⚡ Detenido. Puedes continuar esta sesión."
+ no_active: "No hay ninguna tarea activa que detener."
+
+ title:
+ db_unavailable: "Base de datos de sesiones no disponible."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ El título está vacío tras la limpieza. Usa caracteres imprimibles."
+ set_to: "✏️ Título de sesión establecido: **{title}**"
+ not_found: "Sesión no encontrada en la base de datos."
+ current_with_title: "📌 Sesión: `{session_id}`\nTítulo: **{title}**"
+ current_no_title: "📌 Sesión: `{session_id}`\nSin título. Uso: `/title Mi nombre de sesión`"
+
+ topic:
+ not_telegram_dm: "El comando /topic solo está disponible en chats privados de Telegram."
+ no_session_db: "Base de datos de sesiones no disponible."
+ unauthorized: "No tienes autorización para usar /topic en este bot."
+ restore_needs_topic: "Para restaurar una sesión, primero crea o abre un topic de Telegram, luego envía /topic dentro de ese topic. Para crear un topic nuevo, abre All Messages y envía cualquier mensaje allí."
+ topics_disabled: "Los topics de Telegram aún no están habilitados para este bot.\n\nCómo habilitarlos:\n1. Abre @BotFather.\n2. Elige tu bot.\n3. Abre Bot Settings → Threads Settings.\n4. Activa Threaded Mode y asegúrate de permitir que los usuarios creen nuevos threads.\n\nLuego envía /topic de nuevo."
+ topics_user_disallowed: "Los topics de Telegram están habilitados, pero los usuarios no pueden crearlos.\n\nAbre @BotFather → elige tu bot → Bot Settings → Threads Settings, luego desactiva 'Disallow users to create new threads'.\n\nLuego envía /topic de nuevo."
+ enable_failed: "No se pudo habilitar el modo topic de Telegram: {error}"
+ bound_status: "Este topic está vinculado a:\nSesión: {label}\nID: {session_id}\n\nUsa /new para reemplazar este topic con una sesión nueva.\nPara trabajo paralelo, abre All Messages y envía un mensaje allí para crear otro topic."
+ thread_ready: "Los topics multisesión de Telegram están habilitados.\n\nEste topic se usará como una sesión independiente de Hermes. Usa /new para reemplazar la sesión actual de este topic. Para trabajo paralelo, abre All Messages y envía un mensaje allí para crear otro topic."
+ untitled_session: "Sesión sin título"
+
+ undo:
+ nothing: "Nada que deshacer."
+ removed: "↩️ {count} mensaje(s) deshecho(s).\nEliminado: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update solo está disponible en plataformas de mensajería. Ejecuta `hermes update` desde la terminal."
+ not_git_repo: "✗ No es un repositorio git — no se puede actualizar."
+ hermes_cmd_not_found: "✗ No se pudo localizar el comando `hermes`. Hermes está en ejecución, pero el comando de actualización no encontró el ejecutable en PATH ni a través del intérprete de Python actual. Intenta ejecutar `hermes update` manualmente en tu terminal."
+ start_failed: "✗ No se pudo iniciar la actualización: {error}"
+ starting: "⚕ Iniciando la actualización de Hermes… Transmitiré el progreso aquí."
+
+ usage:
+ rate_limits: "⏱️ **Límites de tasa:** {state}"
+ header_session: "📊 **Uso de tokens de la sesión**"
+ label_model: "Modelo: `{model}`"
+ label_input_tokens: "Tokens de entrada: {count}"
+ label_cache_read: "Tokens de lectura de caché: {count}"
+ label_cache_write: "Tokens de escritura de caché: {count}"
+ label_output_tokens: "Tokens de salida: {count}"
+ label_total: "Total: {count}"
+ label_api_calls: "Llamadas API: {count}"
+ label_cost: "Costo: {prefix}${amount}"
+ label_cost_included: "Costo: incluido"
+ label_context: "Contexto: {used} / {total} ({pct}%)"
+ label_compressions: "Compresiones: {count}"
+ header_session_info: "📊 **Información de la sesión**"
+ label_messages: "Mensajes: {count}"
+ label_estimated_context: "Contexto estimado: ~{count} tokens"
+ detailed_after_first: "_(Uso detallado disponible tras la primera respuesta del agente)_"
+ no_data: "No hay datos de uso disponibles para esta sesión."
+
+ verbose:
+ not_enabled: "El comando `/verbose` no está habilitado para plataformas de mensajería.\n\nHabilítalo en `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Progreso de herramientas: **OFF** — no se muestra actividad de herramientas."
+ mode_new: "⚙️ Progreso de herramientas: **NEW** — se muestra al cambiar de herramienta (longitud de vista previa: `display.tool_preview_length`, por defecto 40)."
+ mode_all: "⚙️ Progreso de herramientas: **ALL** — se muestra cada llamada a herramienta (longitud de vista previa: `display.tool_preview_length`, por defecto 40)."
+ mode_verbose: "⚙️ Progreso de herramientas: **VERBOSE** — cada llamada a herramienta con sus argumentos completos."
+ saved_suffix: "_(guardado para **{platform}** — se aplica en el próximo mensaje)_"
+ save_failed: "_(no se pudo guardar en la configuración: {error})_"
+
+ voice:
+ enabled_voice_only: "Modo de voz activado.\nResponderé con voz cuando envíes mensajes de voz.\nUsa /voice tts para recibir respuestas de voz en todos los mensajes."
+ disabled_text: "Modo de voz desactivado. Respuestas solo de texto."
+ tts_enabled: "Auto-TTS activado.\nTodas las respuestas incluirán un mensaje de voz."
+ status_mode: "Modo de voz: {label}"
+ status_channel: "Canal de voz: #{channel}"
+ status_participants: "Participantes: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (hablando)"
+ enabled_short: "Modo de voz activado."
+ disabled_short: "Modo de voz desactivado."
+ label_off: "Desactivado (solo texto)"
+ label_voice_only: "Activado (responder con voz a mensajes de voz)"
+ label_all: "TTS (responder con voz a todos los mensajes)"
+
+ yolo:
+ disabled: "⚠️ Modo YOLO **DESACTIVADO** en esta sesión — los comandos peligrosos requerirán aprobación."
+ enabled: "⚡ Modo YOLO **ACTIVADO** en esta sesión — todos los comandos se aprueban automáticamente. Úsalo con precaución."
+
+ shared:
+ session_db_unavailable: "Base de datos de sesiones no disponible."
+ session_db_unavailable_prefix: "Base de datos de sesiones no disponible"
+ session_not_found: "Sesión no encontrada en la base de datos."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/fr.yaml b/locales/fr.yaml
index 2127f7396bb..0a8399f2748 100644
--- a/locales/fr.yaml
+++ b/locales/fr.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "Aucun objectif actif."
config_read_failed: "⚠️ Impossible de lire config.yaml : {error}"
config_save_failed: "⚠️ Impossible de sauvegarder la configuration : {error}"
+
+ model:
+ error_prefix: "Erreur : {error}"
+ switched: "Modèle changé pour `{model}`"
+ provider_label: "Fournisseur : {provider}"
+ context_label: "Contexte : {tokens} tokens"
+ max_output_label: "Sortie max. : {tokens} tokens"
+ cost_label: "Coût : {cost}"
+ capabilities_label: "Capacités : {capabilities}"
+ prompt_caching_enabled: "Cache de prompts : activé"
+ warning_prefix: "Avertissement : {warning}"
+ saved_global: "Enregistré dans config.yaml (`--global`)"
+ session_only_hint: "_(session uniquement — ajoutez `--global` pour conserver)_"
+ current_label: "Actuel : `{model}` chez {provider}"
+ current_tag: " (actuel)"
+ more_models_suffix: " (+{count} autres)"
+ usage_switch_model: "`/model ` — changer de modèle"
+ usage_switch_provider: "`/model --provider ` — changer de fournisseur"
+ usage_persist: "`/model --global` — conserver"
+
+ agents:
+ header: "🤖 **Agents et tâches actifs**"
+ active_agents: "**Agents actifs :** {count}"
+ this_chat: " · ce chat"
+ more: "... et {count} de plus"
+ running_processes: "**Processus d'arrière-plan en cours :** {count}"
+ async_jobs: "**Tâches asynchrones du gateway :** {count}"
+ none: "Aucun agent actif ni tâche en cours."
+ state_starting: "démarrage"
+ state_running: "en cours"
+
+ approve:
+ no_pending: "Aucune commande en attente d'approbation."
+ once_singular: "✅ Commande approuvée. L'agent reprend..."
+ once_plural: "✅ Commandes approuvées ({count} commandes). L'agent reprend..."
+ session_singular: "✅ Commande approuvée (modèle approuvé pour cette session). L'agent reprend..."
+ session_plural: "✅ Commandes approuvées (modèle approuvé pour cette session) ({count} commandes). L'agent reprend..."
+ always_singular: "✅ Commande approuvée (modèle approuvé de manière permanente). L'agent reprend..."
+ always_plural: "✅ Commandes approuvées (modèle approuvé de manière permanente) ({count} commandes). L'agent reprend..."
+
+ background:
+ usage: "Usage : /background \nExemple : /background Résume les meilleures histoires HN d'aujourd'hui\n\nExécute le prompt dans une session séparée. Vous pouvez continuer à discuter — le résultat apparaîtra ici une fois terminé."
+ started: "🔄 Tâche d'arrière-plan démarrée : « {preview} »\nID de tâche : {task_id}\nVous pouvez continuer à discuter — les résultats apparaîtront ici une fois terminés."
+
+ branch:
+ db_unavailable: "Base de données des sessions indisponible."
+ no_conversation: "Aucune conversation à brancher — envoyez d'abord un message."
+ create_failed: "Échec de la création de la branche : {error}"
+ switch_failed: "Branche créée mais impossible de basculer dessus."
+ branched_one: "⑂ Branche **{title}** créée ({count} message copié)\nOriginal : `{parent}`\nBranche : `{new}`\nUtilisez `/resume` pour revenir à l'original."
+ branched_many: "⑂ Branche **{title}** créée ({count} messages copiés)\nOriginal : `{parent}`\nBranche : `{new}`\nUtilisez `/resume` pour revenir à l'original."
+
+ commands:
+ usage: "Utilisation : `/commands [page]`"
+ skill_header: "⚡ **Commandes de skill** :"
+ default_desc: "Commande de skill"
+ none: "Aucune commande disponible."
+ header: "📚 **Commandes** ({total} au total, page {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← précédent"
+ nav_next: "suivant → `/commands {page}`"
+ out_of_range: "_(La page demandée {requested} était hors limites, affichage de la page {page}.)_"
+
+ compress:
+ not_enough: "Conversation insuffisante pour la compression (au moins 4 messages nécessaires)."
+ no_provider: "Aucun fournisseur configuré — compression impossible."
+ nothing_to_do: "Rien à compresser pour l'instant (la transcription est encore entièrement du contexte protégé)."
+ focus_line: "Focus : \"{topic}\""
+ summary_failed: "⚠️ Échec de la génération du résumé ({error}). {count} message(s) historique(s) ont été supprimés et remplacés par un espace réservé ; le contexte antérieur n'est plus récupérable. Vérifiez la configuration du modèle auxiliary.compression."
+ aux_failed: "ℹ️ Le modèle de compression configuré `{model}` a échoué ({error}). Récupéré avec votre modèle principal — le contexte est intact — mais vous pouvez vérifier `auxiliary.compression.model` dans config.yaml."
+ failed: "Échec de la compression : {error}"
+
+ debug:
+ upload_failed: "✗ Échec de l'envoi du rapport de débogage : {error}"
+ header: "**Rapport de débogage envoyé :**"
+ auto_delete: "⏱ Les pastes s'effaceront automatiquement dans 6 heures."
+ full_logs_hint: "Pour envoyer les journaux complets, utilisez `hermes debug share` depuis la CLI."
+ share_hint: "Partagez ces liens avec l'équipe Hermes pour obtenir de l'aide."
+
+ deny:
+ stale: "❌ Commande refusée (l'approbation était périmée)."
+ no_pending: "Aucune commande en attente de refus."
+ denied_singular: "❌ Commande refusée."
+ denied_plural: "❌ Commandes refusées ({count} commandes)."
+
+ fast:
+ not_supported: "⚡ /fast n'est disponible que pour les modèles OpenAI qui prennent en charge Priority Processing."
+ status: "⚡ Priority Processing\n\nMode actuel : `{mode}`\n\n_Usage :_ `/fast `"
+ unknown_arg: "⚠️ Argument inconnu : `{arg}`\n\n**Options valides :** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing : **{label}** (enregistré dans la configuration)\n_(prend effet au prochain message)_"
+ session_only: "⚡ ✓ Priority Processing : **{label}** (cette session uniquement)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Pied de page d'exécution : **{state}**\nChamps : `{fields}`\nPlateforme : `{platform}`"
+ usage: "Usage : `/footer [on|off|status]`"
+ saved: "📎 Pied de page d'exécution : **{state}**{example}\n_(enregistré globalement — prend effet au prochain message)_"
+ example_line: "\nExemple : `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Les objectifs ne sont pas disponibles dans cette session."
+ no_goal_set: "Aucun objectif défini."
+ paused: "⏸ Objectif en pause : {goal}"
+ no_resume: "Aucun objectif à reprendre."
+ resumed: "▶ Objectif repris : {goal}\nEnvoyez un message pour continuer, ou attendez — je passerai à l'étape suivante au prochain tour."
+ invalid: "Objectif invalide : {error}"
+ set: "⊙ Objectif défini (budget de {budget} tours) : {goal}\nJe continuerai jusqu'à ce que l'objectif soit terminé, que vous le mettiez en pause/effaciez, ou que le budget soit épuisé.\nContrôles : /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Commandes Hermes**\n"
+ skill_header: "\n⚡ **Commandes de skill** ({count} actives) :"
+ more_use_commands: "\n... et {count} de plus. Utilisez `/commands` pour la liste paginée complète."
+
+ insights:
+ invalid_days: "Valeur --days invalide : {value}"
+ error: "Erreur lors de la génération des analyses : {error}"
+
+ kanban:
+ error_prefix: "⚠ erreur kanban : {error}"
+ subscribed_suffix: "(abonné — vous serez notifié lorsque {task_id} se terminera ou sera bloqué)"
+ truncated_suffix: "… (tronqué ; utilisez `hermes kanban …` dans votre terminal pour la sortie complète)"
+ no_output: "(aucune sortie)"
+
+ personality:
+ none_configured: "Aucune personnalité configurée dans `{path}/config.yaml`"
+ header: "🎭 **Personnalités disponibles**\n"
+ none_option: "• `none` — (aucune superposition de personnalité)"
+ item: "• `{name}` — {preview}"
+ usage: "\nUtilisation : `/personality `"
+ save_failed: "⚠️ Échec de l'enregistrement du changement de personnalité : {error}"
+ cleared: "🎭 Personnalité effacée — comportement de base de l'agent utilisé.\n_(prend effet au prochain message)_"
+ set_to: "🎭 Personnalité définie sur **{name}**\n_(prend effet au prochain message)_"
+ unknown: "Personnalité inconnue : `{name}`\n\nDisponibles : {available}"
+
+ profile:
+ header: "👤 **Profil :** `{profile}`"
+ home: "📂 **Dossier personnel :** `{home}`"
+
+ reasoning:
+ level_default: "medium (par défaut)"
+ level_disabled: "none (désactivé)"
+ scope_session: "remplacement de session"
+ scope_global: "configuration globale"
+ status: "🧠 **Paramètres de raisonnement**\n\n**Effort :** `{level}`\n**Portée :** {scope}\n**Affichage :** {display}\n\n_Usage :_ `/reasoning [--global]`"
+ display_on: "activé ✓"
+ display_off: "désactivé"
+ display_set_on: "🧠 ✓ Affichage du raisonnement : **ACTIVÉ**\nLa réflexion du modèle sera affichée avant chaque réponse sur **{platform}**."
+ display_set_off: "🧠 ✓ Affichage du raisonnement : **DÉSACTIVÉ** pour **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` n'est pas pris en charge. Utilisez `/reasoning --global` pour modifier la valeur globale par défaut."
+ reset_done: "🧠 ✓ Remplacement de raisonnement de la session effacé ; retour à la configuration globale."
+ unknown_arg: "⚠️ Argument inconnu : `{arg}`\n\n**Niveaux valides :** none, minimal, low, medium, high, xhigh\n**Affichage :** show, hide\n**Persister :** ajoutez `--global` pour enregistrer au-delà de cette session"
+ set_global: "🧠 ✓ Effort de raisonnement défini sur `{effort}` (enregistré dans la configuration)\n_(prend effet au prochain message)_"
+ set_global_save_failed: "🧠 ✓ Effort de raisonnement défini sur `{effort}` (session uniquement — échec de l'enregistrement de la configuration)\n_(prend effet au prochain message)_"
+ set_session: "🧠 ✓ Effort de raisonnement défini sur `{effort}` (session uniquement — ajoutez `--global` pour persister)\n_(prend effet au prochain message)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp annulé. Outils MCP inchangés."
+ always_followup: "ℹ️ Les prochains appels `/reload-mcp` s'exécuteront sans confirmation. Réactivez via `approvals.mcp_reload_confirm: true` dans `config.yaml`."
+ confirm_prompt: "⚠️ **Confirmer /reload-mcp**\n\nRecharger les serveurs MCP reconstruit l'ensemble d'outils de cette session et **invalide le cache de prompt du fournisseur** — le prochain message renverra l'intégralité des jetons d'entrée. Sur les modèles à long contexte ou à raisonnement élevé, cela peut être coûteux.\n\nChoisissez :\n• **Approuver une fois** — recharger maintenant\n• **Toujours approuver** — recharger maintenant et masquer cette confirmation définitivement\n• **Annuler** — laisser les outils MCP inchangés\n\n_Alternative texte : répondez `/approve`, `/always` ou `/cancel`._"
+ header: "🔄 **Serveurs MCP rechargés**\n"
+ reconnected: "♻️ Reconnectés : {names}"
+ added: "➕ Ajoutés : {names}"
+ removed: "➖ Supprimés : {names}"
+ none_connected: "Aucun serveur MCP connecté."
+ tools_available: "\n🔧 {tools} outil(s) disponible(s) sur {servers} serveur(s)"
+ failed: "❌ Échec du rechargement MCP : {error}"
+
+ reload_skills:
+ header: "🔄 **Skills rechargées**\n"
+ no_new: "Aucune nouvelle skill détectée."
+ total: "\n📚 {count} skill(s) disponible(s)"
+ added_header: "➕ **Skills ajoutées :**"
+ removed_header: "➖ **Skills supprimées :**"
+ item_with_desc: " - {name} : {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Échec du rechargement des skills : {error}"
+
+ reset:
+ header_default: "✨ Session réinitialisée ! Nouveau départ."
+ header_new: "✨ Nouvelle session démarrée !"
+ header_titled: "✨ Nouvelle session démarrée : {title}"
+ title_rejected: "\n⚠️ Titre refusé : {error}"
+ title_error_untitled: "\n⚠️ {error} — session démarrée sans titre."
+ title_empty_untitled: "\n⚠️ Le titre est vide après nettoyage — session démarrée sans titre."
+ tip: "\n✦ Astuce : {tip}"
+
+ restart:
+ in_progress: "⏳ Redémarrage du gateway déjà en cours..."
+ restarting: "♻ Redémarrage du gateway. Si vous n'êtes pas notifié dans les 60 secondes, redémarrez depuis la console avec `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Base de données des sessions indisponible."
+ no_named_sessions: "Aucune session nommée trouvée.\nUtilisez `/title Ma session` pour nommer la session actuelle, puis `/resume Ma session` pour y revenir plus tard."
+ list_header: "📋 **Sessions nommées**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nUsage : `/resume `"
+ list_failed: "Impossible de lister les sessions : {error}"
+ not_found: "Aucune session correspondant à '**{name}**' trouvée.\nUtilisez `/resume` sans argument pour voir les sessions disponibles."
+ already_on: "📌 Déjà sur la session **{name}**."
+ switch_failed: "Échec du changement de session."
+ resumed_one: "↻ Session **{title}** reprise ({count} message). Conversation restaurée."
+ resumed_many: "↻ Session **{title}** reprise ({count} messages). Conversation restaurée."
+ resumed_no_count: "↻ Session **{title}** reprise. Conversation restaurée."
+
+ retry:
+ no_previous: "Aucun message précédent à réessayer."
+
+ rollback:
+ not_enabled: "Les points de contrôle ne sont pas activés.\nActivez-les dans config.yaml :\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Aucun point de contrôle trouvé pour {cwd}"
+ invalid_number: "Numéro de point de contrôle invalide. Utilisez 1-{max}."
+ restored: "✅ Restauré au point de contrôle {hash} : {reason}\nUn instantané pré-rollback a été enregistré automatiquement."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Impossible d'enregistrer le canal principal : {error}"
+ success: "✅ Canal principal défini sur **{name}** (ID : {chat_id}).\nLes tâches cron et les messages multi-plateformes seront livrés ici."
+
+ status:
+ header: "📊 **État de Hermes Gateway**"
+ session_id: "**ID de session :** `{session_id}`"
+ title: "**Titre :** {title}"
+ created: "**Créé :** {timestamp}"
+ last_activity: "**Dernière activité :** {timestamp}"
+ tokens: "**Jetons :** {tokens}"
+ agent_running: "**Agent en cours :** {state}"
+ state_yes: "Oui ⚡"
+ state_no: "Non"
+ queued: "**Suivis en file :** {count}"
+ platforms: "**Plateformes connectées :** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Arrêté. L'agent n'avait pas encore commencé — vous pouvez continuer cette session."
+ stopped: "⚡ Arrêté. Vous pouvez continuer cette session."
+ no_active: "Aucune tâche active à arrêter."
+
+ title:
+ db_unavailable: "Base de données des sessions indisponible."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Le titre est vide après nettoyage. Utilisez des caractères imprimables."
+ set_to: "✏️ Titre de session défini : **{title}**"
+ not_found: "Session introuvable dans la base de données."
+ current_with_title: "📌 Session : `{session_id}`\nTitre : **{title}**"
+ current_no_title: "📌 Session : `{session_id}`\nAucun titre défini. Usage : `/title Mon nom de session`"
+
+ topic:
+ not_telegram_dm: "La commande /topic n'est disponible que dans les chats privés Telegram."
+ no_session_db: "Base de données de sessions non disponible."
+ unauthorized: "Vous n'êtes pas autorisé à utiliser /topic sur ce bot."
+ restore_needs_topic: "Pour restaurer une session, créez ou ouvrez d'abord un topic Telegram, puis envoyez /topic dans ce topic. Pour créer un nouveau topic, ouvrez All Messages et envoyez-y n'importe quel message."
+ topics_disabled: "Les topics Telegram ne sont pas encore activés pour ce bot.\n\nComment les activer :\n1. Ouvrez @BotFather.\n2. Choisissez votre bot.\n3. Ouvrez Bot Settings → Threads Settings.\n4. Activez Threaded Mode et assurez-vous que les utilisateurs sont autorisés à créer de nouveaux threads.\n\nPuis envoyez /topic à nouveau."
+ topics_user_disallowed: "Les topics Telegram sont activés, mais les utilisateurs ne peuvent pas en créer.\n\nOuvrez @BotFather → choisissez votre bot → Bot Settings → Threads Settings, puis désactivez 'Disallow users to create new threads'.\n\nPuis envoyez /topic à nouveau."
+ enable_failed: "Échec de l'activation du mode topic Telegram : {error}"
+ bound_status: "Ce topic est lié à :\nSession : {label}\nID : {session_id}\n\nUtilisez /new pour remplacer ce topic par une nouvelle session.\nPour un travail parallèle, ouvrez All Messages et envoyez-y un message pour créer un autre topic."
+ thread_ready: "Les topics multi-sessions Telegram sont activés.\n\nCe topic sera utilisé comme session Hermes indépendante. Utilisez /new pour remplacer la session actuelle de ce topic. Pour un travail parallèle, ouvrez All Messages et envoyez-y un message pour créer un autre topic."
+ untitled_session: "Session sans titre"
+
+ undo:
+ nothing: "Rien à annuler."
+ removed: "↩️ {count} message(s) annulé(s).\nSupprimé : « {preview} »"
+
+ update:
+ platform_not_messaging: "✗ /update n'est disponible que depuis les plateformes de messagerie. Exécutez `hermes update` depuis le terminal."
+ not_git_repo: "✗ Pas un dépôt git — impossible de mettre à jour."
+ hermes_cmd_not_found: "✗ Impossible de localiser la commande `hermes`. Hermes est en cours d'exécution, mais la commande de mise à jour n'a pas pu trouver l'exécutable dans le PATH ni via l'interpréteur Python actuel. Essayez d'exécuter `hermes update` manuellement dans votre terminal."
+ start_failed: "✗ Échec du démarrage de la mise à jour : {error}"
+ starting: "⚕ Démarrage de la mise à jour Hermes… Je diffuserai la progression ici."
+
+ usage:
+ rate_limits: "⏱️ **Limites de débit :** {state}"
+ header_session: "📊 **Utilisation des jetons de session**"
+ label_model: "Modèle : `{model}`"
+ label_input_tokens: "Jetons d'entrée : {count}"
+ label_cache_read: "Jetons de lecture du cache : {count}"
+ label_cache_write: "Jetons d'écriture du cache : {count}"
+ label_output_tokens: "Jetons de sortie : {count}"
+ label_total: "Total : {count}"
+ label_api_calls: "Appels API : {count}"
+ label_cost: "Coût : {prefix}${amount}"
+ label_cost_included: "Coût : inclus"
+ label_context: "Contexte : {used} / {total} ({pct}%)"
+ label_compressions: "Compressions : {count}"
+ header_session_info: "📊 **Infos de session**"
+ label_messages: "Messages : {count}"
+ label_estimated_context: "Contexte estimé : ~{count} jetons"
+ detailed_after_first: "_(Utilisation détaillée disponible après la première réponse de l'agent)_"
+ no_data: "Aucune donnée d'utilisation disponible pour cette session."
+
+ verbose:
+ not_enabled: "La commande `/verbose` n'est pas activée pour les plateformes de messagerie.\n\nActivez-la dans `config.yaml` :\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Progression des outils : **OFF** — aucune activité d'outil affichée."
+ mode_new: "⚙️ Progression des outils : **NEW** — affichée lors d'un changement d'outil (longueur d'aperçu : `display.tool_preview_length`, par défaut 40)."
+ mode_all: "⚙️ Progression des outils : **ALL** — chaque appel d'outil est affiché (longueur d'aperçu : `display.tool_preview_length`, par défaut 40)."
+ mode_verbose: "⚙️ Progression des outils : **VERBOSE** — chaque appel d'outil avec ses arguments complets."
+ saved_suffix: "_(enregistré pour **{platform}** — prend effet au prochain message)_"
+ save_failed: "_(impossible d'enregistrer dans la configuration : {error})_"
+
+ voice:
+ enabled_voice_only: "Mode vocal activé.\nJe répondrai en vocal quand vous envoyez des messages vocaux.\nUtilisez /voice tts pour obtenir des réponses vocales à tous les messages."
+ disabled_text: "Mode vocal désactivé. Réponses uniquement textuelles."
+ tts_enabled: "TTS automatique activé.\nToutes les réponses incluront un message vocal."
+ status_mode: "Mode vocal : {label}"
+ status_channel: "Canal vocal : #{channel}"
+ status_participants: "Participants : {count}"
+ status_member: " - {name}{status}"
+ speaking: " (parle)"
+ enabled_short: "Mode vocal activé."
+ disabled_short: "Mode vocal désactivé."
+ label_off: "Désactivé (texte seulement)"
+ label_voice_only: "Activé (réponse vocale aux messages vocaux)"
+ label_all: "TTS (réponse vocale à tous les messages)"
+
+ yolo:
+ disabled: "⚠️ Mode YOLO **DÉSACTIVÉ** pour cette session — les commandes dangereuses nécessiteront une approbation."
+ enabled: "⚡ Mode YOLO **ACTIVÉ** pour cette session — toutes les commandes sont auto-approuvées. À utiliser avec prudence."
+
+ shared:
+ session_db_unavailable: "Base de données de sessions indisponible."
+ session_db_unavailable_prefix: "Base de données de sessions indisponible"
+ session_not_found: "Session introuvable dans la base de données."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/ga.yaml b/locales/ga.yaml
new file mode 100644
index 00000000000..551d8d3362d
--- /dev/null
+++ b/locales/ga.yaml
@@ -0,0 +1,354 @@
+# Hermes static-message catalog -- Gaeilge (Irish)
+# See locales/en.yaml for the source of truth; keep keys in sync.
+#
+# Modern Irish technical writing freely uses English loanwords for terms
+# without good native equivalents (e.g. "session", "tokens", "API").
+# Where Irish has a settled term we use it; otherwise we keep the English.
+
+approval:
+ dangerous_header: "⚠️ ORDÚ CONTÚIRTEACH: {description}"
+ choose_long: " [o]uair amháin | [s]eisiún | [a]i gcónaí | [d]iúltaigh"
+ choose_short: " [o]uair amháin | [s]eisiún | [d]iúltaigh"
+ prompt_long: " Rogha [o/s/a/D]: "
+ prompt_short: " Rogha [o/s/D]: "
+ timeout: " ⏱ Am istigh — ag diúltú don ordú"
+ allowed_once: " ✓ Ceadaithe uair amháin"
+ allowed_session: " ✓ Ceadaithe don seisiún seo"
+ allowed_always: " ✓ Curtha leis an liosta ceadaithe buan"
+ denied: " ✗ Diúltaithe"
+ cancelled: " ✗ Cealaithe"
+ blocklist_message: "Tá an t-ordú seo ar an liosta cosc gan choinníoll agus ní féidir é a cheadú."
+
+gateway:
+ approval_expired: "⚠️ Tá an cead imithe in éag (níl an gníomhaire ag fanacht níos mó). Iarr ar an ngníomhaire iarracht eile a dhéanamh."
+ draining: "⏳ Ag fanacht le {count} gníomhaire(í) gníomhach roimh atosú..."
+ goal_cleared: "✓ Sprioc glanta."
+ no_active_goal: "Níl aon sprioc ghníomhach ann."
+ config_read_failed: "⚠️ Níorbh fhéidir config.yaml a léamh: {error}"
+ config_save_failed: "⚠️ Níorbh fhéidir an chumraíocht a shábháil: {error}"
+
+ model:
+ error_prefix: "Earráid: {error}"
+ switched: "Athraíodh an tsamhail go `{model}`"
+ provider_label: "Soláthraí: {provider}"
+ context_label: "Comhthéacs: {tokens} comhartha"
+ max_output_label: "Aschur uasta: {tokens} comhartha"
+ cost_label: "Costas: {cost}"
+ capabilities_label: "Cumais: {capabilities}"
+ prompt_caching_enabled: "Taisceadh leid: cumasaithe"
+ warning_prefix: "Rabhadh: {warning}"
+ saved_global: "Sábháilte i config.yaml (`--global`)"
+ session_only_hint: "_(seisiún amháin — cuir `--global` leis chun é a choinneáil)_"
+ current_label: "Reatha: `{model}` ar {provider}"
+ current_tag: " (reatha)"
+ more_models_suffix: " (+{count} eile)"
+ usage_switch_model: "`/model ` — athraigh an tsamhail"
+ usage_switch_provider: "`/model --provider ` — athraigh an soláthraí"
+ usage_persist: "`/model --global` — coinnigh"
+
+ agents:
+ header: "🤖 **Gníomhairí & Tascanna Gníomhacha**"
+ active_agents: "**Gníomhairí gníomhacha:** {count}"
+ this_chat: " · an comhrá seo"
+ more: "... agus {count} eile"
+ running_processes: "**Próisis chúlra ag rith:** {count}"
+ async_jobs: "**Tascanna asincrónacha gateway:** {count}"
+ none: "Níl aon ghníomhairí gníomhacha ná tascanna ag rith."
+ state_starting: "ag tosú"
+ state_running: "ag rith"
+
+ approve:
+ no_pending: "Níl aon ordú ag fanacht le ceadú."
+ once_singular: "✅ Ordú ceadaithe. Tá an gníomhaire ag atosú..."
+ once_plural: "✅ Orduithe ceadaithe ({count} ordú). Tá an gníomhaire ag atosú..."
+ session_singular: "✅ Ordú ceadaithe (patrún ceadaithe don seisiún seo). Tá an gníomhaire ag atosú..."
+ session_plural: "✅ Orduithe ceadaithe (patrún ceadaithe don seisiún seo) ({count} ordú). Tá an gníomhaire ag atosú..."
+ always_singular: "✅ Ordú ceadaithe (patrún ceadaithe go buan). Tá an gníomhaire ag atosú..."
+ always_plural: "✅ Orduithe ceadaithe (patrún ceadaithe go buan) ({count} ordú). Tá an gníomhaire ag atosú..."
+
+ background:
+ usage: "Úsáid: /background \nSampla: /background Déan achoimre ar phríomhscéalta HN inniu\n\nRitheann an leid i seisiún ar leith. Is féidir leat leanúint leis an gcomhrá — taispeánfar an toradh anseo nuair a bheidh sé críochnaithe."
+ started: "🔄 Tasc cúlra tosaithe: \"{preview}\"\nAitheantas an tasc: {task_id}\nIs féidir leat leanúint leis an gcomhrá — taispeánfar na torthaí nuair a bheidh sé críochnaithe."
+
+ branch:
+ db_unavailable: "Níl bunachar sonraí na seisiún ar fáil."
+ no_conversation: "Níl aon chomhrá le brainseáil — seol teachtaireacht ar dtús."
+ create_failed: "Theip ar an mbrainse a chruthú: {error}"
+ switch_failed: "Cruthaíodh an brainse ach theip ar athrú chuige."
+ branched_one: "⑂ Brainseáilte go **{title}** ({count} teachtaireacht cóipeáilte)\nBunaidh: `{parent}`\nBrainse: `{new}`\nÚsáid `/resume` chun filleadh ar an mbunaidh."
+ branched_many: "⑂ Brainseáilte go **{title}** ({count} teachtaireacht cóipeáilte)\nBunaidh: `{parent}`\nBrainse: `{new}`\nÚsáid `/resume` chun filleadh ar an mbunaidh."
+
+ commands:
+ usage: "Úsáid: `/commands [page]`"
+ skill_header: "⚡ **Orduithe Scileanna**:"
+ default_desc: "Ordú scile"
+ none: "Níl aon ordú ar fáil."
+ header: "📚 **Orduithe** ({total} san iomlán, leathanach {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← roimhe seo"
+ nav_next: "ar aghaidh → `/commands {page}`"
+ out_of_range: "_(Bhí leathanach {requested} a iarradh as raon, ag taispeáint leathanach {page}.)_"
+
+ compress:
+ not_enough: "Níl go leor comhrá le dlúthú (teastaíonn 4 theachtaireacht ar a laghad)."
+ no_provider: "Níl aon soláthraí cumraithe — ní féidir dlúthú."
+ nothing_to_do: "Níl aon rud le dlúthú fós (tá an traschríbhinn fós uile mar chomhthéacs cosanta)."
+ focus_line: "Fócas: \"{topic}\""
+ summary_failed: "⚠️ Theip ar ghiniúint achoimre ({error}). Baineadh {count} teachtaireacht stairiúil agus cuireadh ionadaí ina n-áit; níl an comhthéacs roimhe seo in-aisghabhála a thuilleadh. Smaoinigh ar an gcumraíocht auxiliary.compression a sheiceáil."
+ aux_failed: "ℹ️ Theip ar an tsamhail dlúthúcháin chumraithe `{model}` ({error}). Aisghafa ag baint úsáide as do phríomhshamhail — tá an comhthéacs slán — ach b'fhéidir gur mhaith leat `auxiliary.compression.model` i config.yaml a sheiceáil."
+ failed: "Theip ar dhlúthú: {error}"
+
+ debug:
+ upload_failed: "✗ Theip ar uaslódáil tuairisce dífhabhtaithe: {error}"
+ header: "**Tuairisc dhífhabhtaithe uaslódáilte:**"
+ auto_delete: "⏱ Scriosfar na pastes go huathoibríoch i 6 huaire."
+ full_logs_hint: "Le haghaidh uaslódálacha logála iomlána, úsáid `hermes debug share` ón CLI."
+ share_hint: "Roinn na naisc seo le foireann Hermes le haghaidh tacaíochta."
+
+ deny:
+ stale: "❌ Ordú diúltaithe (bhí an cead imithe i léig)."
+ no_pending: "Níl aon ordú ag fanacht le diúltú."
+ denied_singular: "❌ Ordú diúltaithe."
+ denied_plural: "❌ Orduithe diúltaithe ({count} ordú)."
+
+ fast:
+ not_supported: "⚡ Tá /fast ar fáil amháin do shamhlacha OpenAI a thacaíonn le Priority Processing."
+ status: "⚡ Priority Processing\n\nMód reatha: `{mode}`\n\n_Úsáid:_ `/fast `"
+ unknown_arg: "⚠️ Argóint anaithnid: `{arg}`\n\n**Roghanna bailí:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (sábháilte sa chumraíocht)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (an seisiún seo amháin)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Buntásc rite: **{state}**\nRéimsí: `{fields}`\nArdán: `{platform}`"
+ usage: "Úsáid: `/footer [on|off|status]`"
+ saved: "📎 Buntásc rite: **{state}**{example}\n_(sábháilte go domhanda — éifeachtach ón gcéad teachtaireacht eile)_"
+ example_line: "\nSampla: `{preview}`"
+ state_on: "AR"
+ state_off: "AS"
+
+ goal:
+ unavailable: "Níl spriocanna ar fáil sa seisiún seo."
+ no_goal_set: "Níl aon sprioc socraithe."
+ paused: "⏸ Sprioc curtha ar sos: {goal}"
+ no_resume: "Níl aon sprioc le hatosú."
+ resumed: "▶ Sprioc atosaithe: {goal}\nSeol teachtaireacht ar bith chun leanúint, nó fan — déanfaidh mé an chéad chéim eile sa chéad seal eile."
+ invalid: "Sprioc neamhbhailí: {error}"
+ set: "⊙ Sprioc socraithe (buiséad {budget} seal): {goal}\nLeanfaidh mé ag obair go dtí go bhfuil an sprioc críochnaithe, go gcuirfidh tú ar sos / go nglanfaidh tú í, nó go n-úsáidfear an buiséad.\nSmacht: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Orduithe Hermes**\n"
+ skill_header: "\n⚡ **Orduithe Scileanna** ({count} gníomhach):"
+ more_use_commands: "\n... agus {count} eile. Úsáid `/commands` don liosta iomlán uimhrithe."
+
+ insights:
+ invalid_days: "Luach --days neamhbhailí: {value}"
+ error: "Earráid agus léargais á gcruthú: {error}"
+
+ kanban:
+ error_prefix: "⚠ earráid kanban: {error}"
+ subscribed_suffix: "(síntiúsaithe — cuirfear in iúl duit nuair a chríochnóidh nó a stopfaidh {task_id})"
+ truncated_suffix: "… (giorraithe; úsáid `hermes kanban …` i do theirminéal le haghaidh aschur iomláin)"
+ no_output: "(gan aschur)"
+
+ personality:
+ none_configured: "Níl aon phearsantachtaí cumraithe in `{path}/config.yaml`"
+ header: "🎭 **Pearsantachtaí ar fáil**\n"
+ none_option: "• `none` — (gan forleagan pearsantachta)"
+ item: "• `{name}` — {preview}"
+ usage: "\nÚsáid: `/personality `"
+ save_failed: "⚠️ Theip ar shábháil athraithe pearsantachta: {error}"
+ cleared: "🎭 Pearsantacht glanta — ag úsáid iompair bunúsaigh an ghníomhaire.\n_(éifeachtach ón gcéad teachtaireacht eile)_"
+ set_to: "🎭 Pearsantacht socraithe go **{name}**\n_(éifeachtach ón gcéad teachtaireacht eile)_"
+ unknown: "Pearsantacht anaithnid: `{name}`\n\nAr fáil: {available}"
+
+ profile:
+ header: "👤 **Próifíl:** `{profile}`"
+ home: "📂 **Baile:** `{home}`"
+
+ reasoning:
+ level_default: "medium (réamhshocraithe)"
+ level_disabled: "none (díchumasaithe)"
+ scope_session: "sárú seisiúin"
+ scope_global: "cumraíocht dhomhanda"
+ status: "🧠 **Socruithe Réasúnaíochta**\n\n**Iarracht:** `{level}`\n**Scóip:** {scope}\n**Taispeáint:** {display}\n\n_Úsáid:_ `/reasoning [--global]`"
+ display_on: "ar ✓"
+ display_off: "as"
+ display_set_on: "🧠 ✓ Taispeáint réasúnaíochta: **AR**\nTaispeánfar smaointeoireacht na samhla roimh gach freagra ar **{platform}**."
+ display_set_off: "🧠 ✓ Taispeáint réasúnaíochta: **AS** do **{platform}**"
+ reset_global_unsupported: "⚠️ Ní thacaítear le `/reasoning reset --global`. Úsáid `/reasoning --global` chun an réamhshocrú domhanda a athrú."
+ reset_done: "🧠 ✓ Sárú réasúnaíochta seisiúin glanta; ag titim siar ar an gcumraíocht dhomhanda."
+ unknown_arg: "⚠️ Argóint anaithnid: `{arg}`\n\n**Leibhéil bhailí:** none, minimal, low, medium, high, xhigh\n**Taispeáint:** show, hide\n**Coinnigh:** cuir `--global` leis chun sábháil thar an seisiún seo"
+ set_global: "🧠 ✓ Iarracht réasúnaíochta socraithe go `{effort}` (sábháilte sa chumraíocht)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
+ set_global_save_failed: "🧠 ✓ Iarracht réasúnaíochta socraithe go `{effort}` (seisiún amháin — theip ar shábháil cumraíochta)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
+ set_session: "🧠 ✓ Iarracht réasúnaíochta socraithe go `{effort}` (seisiún amháin — cuir `--global` leis chun é a choinneáil)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp cealaithe. Tá uirlisí MCP gan athrú."
+ always_followup: "ℹ️ Rithfear glaonna `/reload-mcp` amach anseo gan dearbhú. Athchumasaigh trí `approvals.mcp_reload_confirm: true` a shocrú in config.yaml."
+ confirm_prompt: "⚠️ **Dearbhaigh /reload-mcp**\n\nAthlódáil freastalaithe MCP a athchruthaíonn an tacar uirlisí don seisiún seo agus **cuireann sé taisce leid an tsoláthraí ar neamhní** — seolfaidh an chéad teachtaireacht eile na comharthaí ionchuir iomlána arís. Ar shamhlacha le comhthéacs fada nó réasúnaíocht ard, is féidir leis seo a bheith costasach.\n\nRoghnaigh:\n• **Approve Once** — athlódáil anois\n• **Always Approve** — athlódáil anois agus an leid seo a chiúnú go buan\n• **Cancel** — fág uirlisí MCP gan athrú\n\n_Cúltaca téacs: freagair `/approve`, `/always`, nó `/cancel`._"
+ header: "🔄 **Freastalaithe MCP Athlódáilte**\n"
+ reconnected: "♻️ Athcheanglaithe: {names}"
+ added: "➕ Curtha leis: {names}"
+ removed: "➖ Bainte: {names}"
+ none_connected: "Níl aon fhreastalaí MCP ceangailte."
+ tools_available: "\n🔧 {tools} uirlis(í) ar fáil ó {servers} freastalaí(thí)"
+ failed: "❌ Theip ar athlódáil MCP: {error}"
+
+ reload_skills:
+ header: "🔄 **Scileanna Athlódáilte**\n"
+ no_new: "Níor braitheadh aon scil nua."
+ total: "\n📚 {count} scil(eanna) ar fáil"
+ added_header: "➕ **Scileanna Curtha leis:**"
+ removed_header: "➖ **Scileanna Bainte:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Theip ar athlódáil scileanna: {error}"
+
+ reset:
+ header_default: "✨ Seisiún athshocraithe! Ag tosú as an nua."
+ header_new: "✨ Seisiún nua tosaithe!"
+ header_titled: "✨ Seisiún nua tosaithe: {title}"
+ title_rejected: "\n⚠️ Teideal diúltaithe: {error}"
+ title_error_untitled: "\n⚠️ {error} — seisiún tosaithe gan teideal."
+ title_empty_untitled: "\n⚠️ Tá an teideal folamh tar éis glanta — seisiún tosaithe gan teideal."
+ tip: "\n✦ Leid: {tip}"
+
+ restart:
+ in_progress: "⏳ Tá atosú gateway ar siúl cheana féin..."
+ restarting: "♻ Ag atosú gateway. Mura gcuirfear in iúl duit laistigh de 60 soicind, atosaigh ón gconsól le `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Níl bunachar sonraí na seisiún ar fáil."
+ no_named_sessions: "Níor aimsíodh aon seisiún ainmnithe.\nÚsáid `/title M'Ainm Seisiúin` chun do sheisiún reatha a ainmniú, ansin `/resume M'Ainm Seisiúin` chun filleadh air níos déanaí."
+ list_header: "📋 **Seisiúin Ainmnithe**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nÚsáid: `/resume `"
+ list_failed: "Níorbh fhéidir seisiúin a liostáil: {error}"
+ not_found: "Níor aimsíodh aon seisiún ag teacht le '**{name}**'.\nÚsáid `/resume` gan argóintí chun seisiúin atá ar fáil a fheiceáil."
+ already_on: "📌 Cheana ar an seisiún **{name}**."
+ switch_failed: "Theip ar athrú seisiúin."
+ resumed_one: "↻ Seisiún **{title}** atosaithe ({count} teachtaireacht). Comhrá aischurtha."
+ resumed_many: "↻ Seisiún **{title}** atosaithe ({count} teachtaireacht). Comhrá aischurtha."
+ resumed_no_count: "↻ Seisiún **{title}** atosaithe. Comhrá aischurtha."
+
+ retry:
+ no_previous: "Níl aon teachtaireacht roimhe seo le hath-iarraidh."
+
+ rollback:
+ not_enabled: "Níl seicphointí cumasaithe.\nCumasaigh in config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Níor aimsíodh aon seicphointe do {cwd}"
+ invalid_number: "Uimhir seicphointe neamhbhailí. Úsáid 1-{max}."
+ restored: "✅ Aischurtha go seicphointe {hash}: {reason}\nSábháladh roghchóip réamh-rollback go huathoibríoch."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Theip ar shábháil chainéil bhaile: {error}"
+ success: "✅ Cainéal baile socraithe go **{name}** (ID: {chat_id}).\nSeachadfar tascanna cron agus teachtaireachtaí trasardáin anseo."
+
+ status:
+ header: "📊 **Stádas Hermes Gateway**"
+ session_id: "**ID Seisiúin:** `{session_id}`"
+ title: "**Teideal:** {title}"
+ created: "**Cruthaithe:** {timestamp}"
+ last_activity: "**Gníomhaíocht is déanaí:** {timestamp}"
+ tokens: "**Comharthaí:** {tokens}"
+ agent_running: "**Gníomhaire ag rith:** {state}"
+ state_yes: "Tá ⚡"
+ state_no: "Níl"
+ queued: "**Tascanna i scuaine:** {count}"
+ platforms: "**Ardáin Cheangailte:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Stoptha. Ní raibh an gníomhaire tosaithe fós — is féidir leat leanúint leis an seisiún seo."
+ stopped: "⚡ Stoptha. Is féidir leat leanúint leis an seisiún seo."
+ no_active: "Níl aon tasc gníomhach le stopadh."
+
+ title:
+ db_unavailable: "Níl bunachar sonraí na seisiún ar fáil."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Tá an teideal folamh tar éis glanta. Bain úsáid as carachtair inphriontáilte le do thoil."
+ set_to: "✏️ Teideal seisiúin socraithe: **{title}**"
+ not_found: "Seisiún gan a aimsiú sa bhunachar sonraí."
+ current_with_title: "📌 Seisiún: `{session_id}`\nTeideal: **{title}**"
+ current_no_title: "📌 Seisiún: `{session_id}`\nGan teideal socraithe. Úsáid: `/title M'Ainm Seisiúin`"
+
+ topic:
+ not_telegram_dm: "Tá an t-ordú /topic ar fáil amháin i gcomhráite príobháideacha Telegram."
+ no_session_db: "Níl bunachar sonraí na seisiún ar fáil."
+ unauthorized: "Níl tú údaraithe chun /topic a úsáid ar an mbot seo."
+ restore_needs_topic: "Chun seisiún a athchóiriú, cruthaigh nó oscail topaic Telegram ar dtús, ansin seol /topic taobh istigh den topaic sin. Chun topaic nua a chruthú, oscail All Messages agus seol teachtaireacht ar bith ann."
+ topics_disabled: "Níl topaicí Telegram cumasaithe don bhot seo fós.\n\nConas iad a chumasú:\n1. Oscail @BotFather.\n2. Roghnaigh do bhot.\n3. Oscail Bot Settings → Threads Settings.\n4. Casadh ar Threaded Mode agus déan cinnte go bhfuil cead ag úsáideoirí snáitheanna nua a chruthú.\n\nAnsin seol /topic arís."
+ topics_user_disallowed: "Tá topaicí Telegram cumasaithe, ach níl cead ag úsáideoirí topaicí a chruthú.\n\nOscail @BotFather → roghnaigh do bhot → Bot Settings → Threads Settings, ansin múchadh 'Disallow users to create new threads'.\n\nAnsin seol /topic arís."
+ enable_failed: "Theip ar mhodh topaice Telegram a chumasú: {error}"
+ bound_status: "Tá an topaic seo nasctha le:\nSeisiún: {label}\nID: {session_id}\n\nÚsáid /new chun an topaic seo a athsholáthar le seisiún úr.\nLe haghaidh oibre comhthreomhaire, oscail All Messages agus seol teachtaireacht ann chun topaic eile a chruthú."
+ thread_ready: "Tá topaicí il-seisiúin Telegram cumasaithe.\n\nÚsáidfear an topaic seo mar sheisiún Hermes neamhspleách. Úsáid /new chun seisiún reatha na topaice seo a athsholáthar. Le haghaidh oibre comhthreomhaire, oscail All Messages agus seol teachtaireacht ann chun topaic eile a chruthú."
+ untitled_session: "Seisiún gan teideal"
+
+ undo:
+ nothing: "Níl aon rud le cealú."
+ removed: "↩️ Cealaíodh {count} teachtaireacht.\nBaineadh: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ Tá /update ar fáil amháin ó ardáin teachtaireachtaí. Rith `hermes update` ón teirminéal."
+ not_git_repo: "✗ Ní stór git é seo — ní féidir nuashonrú."
+ hermes_cmd_not_found: "✗ Níorbh fhéidir an t-ordú `hermes` a aimsiú. Tá Hermes ag rith, ach níorbh fhéidir leis an ordú nuashonraithe an inrite a aimsiú ar PATH ná tríd an léirmhínitheoir Python reatha. Bain triail as `hermes update` a rith de láimh i do theirminéal."
+ start_failed: "✗ Theip ar nuashonrú a thosú: {error}"
+ starting: "⚕ Ag tosú nuashonrú Hermes… Cuirfidh mé an dul chun cinn ar shruth anseo."
+
+ usage:
+ rate_limits: "⏱️ **Teorainneacha Ráta:** {state}"
+ header_session: "📊 **Úsáid Comharthaí Seisiúin**"
+ label_model: "Samhail: `{model}`"
+ label_input_tokens: "Comharthaí ionchuir: {count}"
+ label_cache_read: "Comharthaí léite ón taisce: {count}"
+ label_cache_write: "Comharthaí scríofa sa taisce: {count}"
+ label_output_tokens: "Comharthaí aschuir: {count}"
+ label_total: "Iomlán: {count}"
+ label_api_calls: "Glaonna API: {count}"
+ label_cost: "Costas: {prefix}${amount}"
+ label_cost_included: "Costas: san áireamh"
+ label_context: "Comhthéacs: {used} / {total} ({pct}%)"
+ label_compressions: "Dlúthuithe: {count}"
+ header_session_info: "📊 **Eolas Seisiúin**"
+ label_messages: "Teachtaireachtaí: {count}"
+ label_estimated_context: "Comhthéacs measta: ~{count} comhartha"
+ detailed_after_first: "_(Úsáid mhionsonraithe ar fáil tar éis chéad fhreagra an ghníomhaire)_"
+ no_data: "Níl aon sonraí úsáide ar fáil don seisiún seo."
+
+ verbose:
+ not_enabled: "Níl an t-ordú `/verbose` cumasaithe d'ardáin teachtaireachtaí.\n\nCumasaigh in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Dul chun cinn uirlise: **AS** — gan aon ghníomhaíocht uirlise á thaispeáint."
+ mode_new: "⚙️ Dul chun cinn uirlise: **NUA** — taispeánta nuair a athraíonn an uirlis (fad réamhamhairc: `display.tool_preview_length`, réamhshocrú 40)."
+ mode_all: "⚙️ Dul chun cinn uirlise: **GACH CEANN** — taispeántar gach glao uirlise (fad réamhamhairc: `display.tool_preview_length`, réamhshocrú 40)."
+ mode_verbose: "⚙️ Dul chun cinn uirlise: **BÉALSCAOILTE** — gach glao uirlise le hargóintí iomlána."
+ saved_suffix: "_(sábháilte do **{platform}** — éifeachtach ón gcéad teachtaireacht eile)_"
+ save_failed: "_(níorbh fhéidir sábháil sa chumraíocht: {error})_"
+
+ voice:
+ enabled_voice_only: "Mód gutha cumasaithe.\nFreagróidh mé le guth nuair a sheolann tú teachtaireachtaí gutha.\nÚsáid /voice tts chun freagraí gutha a fháil do gach teachtaireacht."
+ disabled_text: "Mód gutha díchumasaithe. Freagraí téacs amháin."
+ tts_enabled: "Auto-TTS cumasaithe.\nBeidh teachtaireacht gutha mar chuid de gach freagra."
+ status_mode: "Mód gutha: {label}"
+ status_channel: "Cainéal gutha: #{channel}"
+ status_participants: "Rannpháirtithe: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (ag labhairt)"
+ enabled_short: "Mód gutha cumasaithe."
+ disabled_short: "Mód gutha díchumasaithe."
+ label_off: "As (téacs amháin)"
+ label_voice_only: "Ar (freagra gutha do theachtaireachtaí gutha)"
+ label_all: "TTS (freagra gutha do gach teachtaireacht)"
+
+ yolo:
+ disabled: "⚠️ Mód YOLO **AS** don seisiún seo — beidh cead de dhíth d'orduithe contúirteacha."
+ enabled: "⚡ Mód YOLO **AR** don seisiún seo — gach ordú ceadaithe go huathoibríoch. Úsáid go cúramach."
+
+ shared:
+ session_db_unavailable: "Níl bunachar sonraí na seisiún ar fáil."
+ session_db_unavailable_prefix: "Níl bunachar sonraí na seisiún ar fáil"
+ session_not_found: "Seisiún gan a aimsiú sa bhunachar sonraí."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/hu.yaml b/locales/hu.yaml
new file mode 100644
index 00000000000..21fb4c81324
--- /dev/null
+++ b/locales/hu.yaml
@@ -0,0 +1,350 @@
+# Hermes statikus üzenetkatalógus -- Magyar
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ VESZÉLYES PARANCS: {description}"
+ choose_long: " [o]egyszer | [s]munkamenet | [a]mindig | [d]elutasít"
+ choose_short: " [o]egyszer | [s]munkamenet | [d]elutasít"
+ prompt_long: " Választás [o/s/a/D]: "
+ prompt_short: " Választás [o/s/D]: "
+ timeout: " ⏱ Időtúllépés - parancs elutasítva"
+ allowed_once: " ✓ Egyszer engedélyezve"
+ allowed_session: " ✓ Engedélyezve ehhez a munkamenethez"
+ allowed_always: " ✓ Hozzáadva az állandó engedélylistához"
+ denied: " ✗ Elutasítva"
+ cancelled: " ✗ Megszakítva"
+ blocklist_message: "Ez a parancs a feltétel nélküli tiltólistán van, és nem hagyható jóvá."
+
+gateway:
+ approval_expired: "⚠️ A jóváhagyás lejárt (az ügynök már nem vár). Kérd meg az ügynököt, hogy próbálja újra."
+ draining: "⏳ {count} aktív ügynök befejezésére várunk az újraindítás előtt..."
+ goal_cleared: "✓ A cél törölve."
+ no_active_goal: "Nincs aktív cél."
+ config_read_failed: "⚠️ Nem sikerült olvasni a config.yaml fájlt: {error}"
+ config_save_failed: "⚠️ Nem sikerült menteni a konfigurációt: {error}"
+
+ model:
+ error_prefix: "Hiba: {error}"
+ switched: "Modell átváltva: `{model}`"
+ provider_label: "Szolgáltató: {provider}"
+ context_label: "Kontextus: {tokens} token"
+ max_output_label: "Max. kimenet: {tokens} token"
+ cost_label: "Költség: {cost}"
+ capabilities_label: "Képességek: {capabilities}"
+ prompt_caching_enabled: "Prompt-gyorsítótárazás: bekapcsolva"
+ warning_prefix: "Figyelmeztetés: {warning}"
+ saved_global: "Mentve a config.yaml fájlba (`--global`)"
+ session_only_hint: "_(csak ehhez a munkamenethez — add hozzá a `--global` opciót a megőrzéshez)_"
+ current_label: "Aktuális: `{model}` ezen: {provider}"
+ current_tag: " (aktuális)"
+ more_models_suffix: " (+{count} további)"
+ usage_switch_model: "`/model ` — modell váltása"
+ usage_switch_provider: "`/model --provider ` — szolgáltató váltása"
+ usage_persist: "`/model --global` — megőrzés"
+
+ agents:
+ header: "🤖 **Aktív ügynökök és feladatok**"
+ active_agents: "**Aktív ügynökök:** {count}"
+ this_chat: " · ez a csevegés"
+ more: "... és még {count}"
+ running_processes: "**Futó háttérfolyamatok:** {count}"
+ async_jobs: "**Átjáró aszinkron feladatai:** {count}"
+ none: "Nincsenek aktív ügynökök vagy futó feladatok."
+ state_starting: "indul"
+ state_running: "fut"
+
+ approve:
+ no_pending: "Nincs jóváhagyásra váró parancs."
+ once_singular: "✅ Parancs jóváhagyva. Az ügynök folytatja..."
+ once_plural: "✅ Parancsok jóváhagyva ({count} parancs). Az ügynök folytatja..."
+ session_singular: "✅ Parancs jóváhagyva (minta jóváhagyva ehhez a munkamenethez). Az ügynök folytatja..."
+ session_plural: "✅ Parancsok jóváhagyva (minta jóváhagyva ehhez a munkamenethez) ({count} parancs). Az ügynök folytatja..."
+ always_singular: "✅ Parancs jóváhagyva (minta véglegesen jóváhagyva). Az ügynök folytatja..."
+ always_plural: "✅ Parancsok jóváhagyva (minta véglegesen jóváhagyva) ({count} parancs). Az ügynök folytatja..."
+
+ background:
+ usage: "Használat: /background \nPélda: /background Foglald össze a mai legjobb HN sztorikat\n\nKülön munkamenetben futtatja a promptot. Folytathatod a beszélgetést — az eredmény itt jelenik meg, amint elkészül."
+ started: "🔄 Háttérfeladat elindítva: \"{preview}\"\nFeladatazonosító: {task_id}\nFolytathatod a beszélgetést — az eredmények itt jelennek meg, amint elkészülnek."
+
+ branch:
+ db_unavailable: "A munkamenet-adatbázis nem érhető el."
+ no_conversation: "Nincs elágaztatható beszélgetés — küldj előbb egy üzenetet."
+ create_failed: "Nem sikerült létrehozni az ágat: {error}"
+ switch_failed: "Az ág létrejött, de nem sikerült rá váltani."
+ branched_one: "⑂ Új ág: **{title}** ({count} üzenet másolva)\nEredeti: `{parent}`\nÁg: `{new}`\nHasználd a `/resume` parancsot az eredetihez való visszatéréshez."
+ branched_many: "⑂ Új ág: **{title}** ({count} üzenet másolva)\nEredeti: `{parent}`\nÁg: `{new}`\nHasználd a `/resume` parancsot az eredetihez való visszatéréshez."
+
+ commands:
+ usage: "Használat: `/commands [page]`"
+ skill_header: "⚡ **Készségparancsok**:"
+ default_desc: "Készségparancs"
+ none: "Nincsenek elérhető parancsok."
+ header: "📚 **Parancsok** (összesen {total}, {page}/{total_pages}. oldal)"
+ nav_prev: "`/commands {page}` ← előző"
+ nav_next: "következő → `/commands {page}`"
+ out_of_range: "_(A kért {requested}. oldal a tartományon kívül esik, a(z) {page}. oldal jelenik meg.)_"
+
+ compress:
+ not_enough: "Nincs elég beszélgetés a tömörítéshez (legalább 4 üzenet kell)."
+ no_provider: "Nincs konfigurált szolgáltató — nem lehet tömöríteni."
+ nothing_to_do: "Még nincs mit tömöríteni (a teljes átirat még védett kontextus)."
+ focus_line: "Fókusz: \"{topic}\""
+ summary_failed: "⚠️ Az összefoglaló generálása sikertelen ({error}). {count} korábbi üzenet eltávolítva és helykitöltővel helyettesítve; a korábbi kontextus már nem helyreállítható. Érdemes ellenőrizni az auxiliary.compression modell konfigurációját."
+ aux_failed: "ℹ️ A beállított tömörítőmodell (`{model}`) hibát adott ({error}). A főmodellel helyreállítva — a kontextus érintetlen — de érdemes ellenőrizni az `auxiliary.compression.model` beállítást a config.yaml fájlban."
+ failed: "Tömörítés sikertelen: {error}"
+
+ debug:
+ upload_failed: "✗ Nem sikerült feltölteni a hibakeresési jelentést: {error}"
+ header: "**Hibakeresési jelentés feltöltve:**"
+ auto_delete: "⏱ A beillesztések 6 óra múlva automatikusan törlődnek."
+ full_logs_hint: "Teljes naplók feltöltéséhez használd a `hermes debug share` parancsot a CLI-ből."
+ share_hint: "Oszd meg ezeket a hivatkozásokat a Hermes csapattal támogatásért."
+
+ deny:
+ stale: "❌ Parancs elutasítva (a jóváhagyás elavult)."
+ no_pending: "Nincs elutasítható függőben lévő parancs."
+ denied_singular: "❌ Parancs elutasítva."
+ denied_plural: "❌ Parancsok elutasítva ({count} parancs)."
+
+ fast:
+ not_supported: "⚡ A /fast csak olyan OpenAI modelleknél érhető el, amelyek támogatják a Priority Processinget."
+ status: "⚡ Priority Processing\n\nJelenlegi mód: `{mode}`\n\n_Használat:_ `/fast `"
+ unknown_arg: "⚠️ Ismeretlen argumentum: `{arg}`\n\n**Érvényes lehetőségek:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (mentve a konfigurációba)\n_(a következő üzenettől lép életbe)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (csak ebben a munkamenetben)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Futási idejű lábléc: **{state}**\nMezők: `{fields}`\nPlatform: `{platform}`"
+ usage: "Használat: `/footer [on|off|status]`"
+ saved: "📎 Futási idejű lábléc: **{state}**{example}\n_(globálisan elmentve — a következő üzenettől lép életbe)_"
+ example_line: "\nPélda: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "A célok nem érhetők el ebben a munkamenetben."
+ no_goal_set: "Nincs cél beállítva."
+ paused: "⏸ Cél szüneteltetve: {goal}"
+ no_resume: "Nincs folytatható cél."
+ resumed: "▶ Cél folytatva: {goal}\nKüldj bármilyen üzenetet a folytatáshoz, vagy várj — a következő körben megteszem a következő lépést."
+ invalid: "Érvénytelen cél: {error}"
+ set: "⊙ Cél beállítva ({budget} körös keret): {goal}\nDolgozni fogok rajta, amíg a cél el nem készül, te nem szünetelteted/törlöd, vagy a keret ki nem merül.\nVezérlés: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes parancsok**\n"
+ skill_header: "\n⚡ **Készségparancsok** ({count} aktív):"
+ more_use_commands: "\n... és még {count}. Használd a `/commands` parancsot a teljes, lapozható listához."
+
+ insights:
+ invalid_days: "Érvénytelen --days érték: {value}"
+ error: "Hiba a betekintések generálásakor: {error}"
+
+ kanban:
+ error_prefix: "⚠ kanban hiba: {error}"
+ subscribed_suffix: "(feliratkozva — értesítést kapsz, ha a {task_id} befejeződik vagy elakad)"
+ truncated_suffix: "… (csonkítva; használd a `hermes kanban …` parancsot a terminálban a teljes kimenethez)"
+ no_output: "(nincs kimenet)"
+
+ personality:
+ none_configured: "Nincs személyiség beállítva itt: `{path}/config.yaml`"
+ header: "🎭 **Elérhető személyiségek**\n"
+ none_option: "• `none` — (nincs személyiségréteg)"
+ item: "• `{name}` — {preview}"
+ usage: "\nHasználat: `/personality `"
+ save_failed: "⚠️ Nem sikerült menteni a személyiség módosítását: {error}"
+ cleared: "🎭 Személyiség törölve — alap ügynöki viselkedés használatban.\n_(a következő üzenettől lép életbe)_"
+ set_to: "🎭 Személyiség beállítva: **{name}**\n_(a következő üzenettől lép életbe)_"
+ unknown: "Ismeretlen személyiség: `{name}`\n\nElérhetők: {available}"
+
+ profile:
+ header: "👤 **Profil:** `{profile}`"
+ home: "📂 **Kezdőkönyvtár:** `{home}`"
+
+ reasoning:
+ level_default: "medium (alapértelmezett)"
+ level_disabled: "none (kikapcsolva)"
+ scope_session: "munkamenet-felülbírálás"
+ scope_global: "globális konfiguráció"
+ status: "🧠 **Gondolkodási beállítások**\n\n**Erőfeszítés:** `{level}`\n**Hatókör:** {scope}\n**Megjelenítés:** {display}\n\n_Használat:_ `/reasoning [--global]`"
+ display_on: "be ✓"
+ display_off: "ki"
+ display_set_on: "🧠 ✓ Gondolkodás megjelenítése: **BE**\nA modell gondolatai minden válasz előtt megjelennek itt: **{platform}**."
+ display_set_off: "🧠 ✓ Gondolkodás megjelenítése: **KI** itt: **{platform}**"
+ reset_global_unsupported: "⚠️ A `/reasoning reset --global` nem támogatott. Használd a `/reasoning --global` parancsot a globális alapérték módosításához."
+ reset_done: "🧠 ✓ A munkamenet gondolkodási felülbírálása törölve; visszaállás a globális konfigurációra."
+ unknown_arg: "⚠️ Ismeretlen argumentum: `{arg}`\n\n**Érvényes szintek:** none, minimal, low, medium, high, xhigh\n**Megjelenítés:** show, hide\n**Megőrzés:** add hozzá a `--global` opciót a munkameneten túli mentéshez"
+ set_global: "🧠 ✓ Gondolkodási erőfeszítés beállítva: `{effort}` (mentve a konfigurációba)\n_(a következő üzenettől lép életbe)_"
+ set_global_save_failed: "🧠 ✓ Gondolkodási erőfeszítés beállítva: `{effort}` (csak ebben a munkamenetben — a konfiguráció mentése sikertelen)\n_(a következő üzenettől lép életbe)_"
+ set_session: "🧠 ✓ Gondolkodási erőfeszítés beállítva: `{effort}` (csak ebben a munkamenetben — add hozzá a `--global` opciót a megőrzéshez)\n_(a következő üzenettől lép életbe)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp megszakítva. Az MCP-eszközök változatlanok."
+ always_followup: "ℹ️ A jövőbeli `/reload-mcp` hívások megerősítés nélkül futnak. Újra engedélyezhető az `approvals.mcp_reload_confirm: true` beállítással a config.yaml fájlban."
+ confirm_prompt: "⚠️ **A /reload-mcp megerősítése**\n\nAz MCP-szerverek újratöltése újraépíti az eszközkészletet ehhez a munkamenethez, és **érvényteleníti a szolgáltató prompt-gyorsítótárát** — a következő üzenet újraküldi a teljes bemeneti tokent. Hosszú kontextusú vagy magas gondolkodási szintű modelleknél ez költséges lehet.\n\nVálassz:\n• **Egyszeri jóváhagyás** — újratöltés most\n• **Mindig jóváhagy** — újratöltés most, és ennek a kérdésnek a végleges elnémítása\n• **Megszakítás** — az MCP-eszközök változatlanok maradnak\n\n_Szöveges alternatíva: válaszolj `/approve`, `/always` vagy `/cancel` paranccsal._"
+ header: "🔄 **MCP-szerverek újratöltve**\n"
+ reconnected: "♻️ Újracsatlakozva: {names}"
+ added: "➕ Hozzáadva: {names}"
+ removed: "➖ Eltávolítva: {names}"
+ none_connected: "Nincsenek csatlakoztatott MCP-szerverek."
+ tools_available: "\n🔧 {tools} eszköz érhető el {servers} szerverről"
+ failed: "❌ MCP újratöltés sikertelen: {error}"
+
+ reload_skills:
+ header: "🔄 **Készségek újratöltve**\n"
+ no_new: "Nem észleltünk új készséget."
+ total: "\n📚 {count} készség érhető el"
+ added_header: "➕ **Hozzáadott készségek:**"
+ removed_header: "➖ **Eltávolított készségek:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Készségek újratöltése sikertelen: {error}"
+
+ reset:
+ header_default: "✨ Munkamenet visszaállítva! Kezdjük tiszta lappal."
+ header_new: "✨ Új munkamenet elindítva!"
+ header_titled: "✨ Új munkamenet elindítva: {title}"
+ title_rejected: "\n⚠️ Cím elutasítva: {error}"
+ title_error_untitled: "\n⚠️ {error} — a munkamenet cím nélkül indult."
+ title_empty_untitled: "\n⚠️ Tisztítás után a cím üres — a munkamenet cím nélkül indult."
+ tip: "\n✦ Tipp: {tip}"
+
+ restart:
+ in_progress: "⏳ Az átjáró újraindítása már folyamatban van..."
+ restarting: "♻ Átjáró újraindítása. Ha 60 másodpercen belül nem kapsz értesítést, indítsd újra a konzolból a `hermes gateway restart` paranccsal."
+
+ resume:
+ db_unavailable: "A munkamenet-adatbázis nem érhető el."
+ no_named_sessions: "Nem található elnevezett munkamenet.\nHasználd a `/title Saját munkamenet` parancsot a jelenlegi munkamenet elnevezéséhez, majd a `/resume Saját munkamenet` paranccsal térhetsz vissza hozzá."
+ list_header: "📋 **Elnevezett munkamenetek**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nHasználat: `/resume `"
+ list_failed: "Nem sikerült listázni a munkameneteket: {error}"
+ not_found: "Nem található '**{name}**' nevű munkamenet.\nArgumentumok nélkül használd a `/resume` parancsot az elérhető munkamenetek megtekintéséhez."
+ already_on: "📌 Már a **{name}** munkamenetben vagy."
+ switch_failed: "Nem sikerült munkamenetet váltani."
+ resumed_one: "↻ **{title}** munkamenet folytatva ({count} üzenet). Beszélgetés visszaállítva."
+ resumed_many: "↻ **{title}** munkamenet folytatva ({count} üzenet). Beszélgetés visszaállítva."
+ resumed_no_count: "↻ **{title}** munkamenet folytatva. Beszélgetés visszaállítva."
+
+ retry:
+ no_previous: "Nincs előző üzenet az újrapróbáláshoz."
+
+ rollback:
+ not_enabled: "Az ellenőrzőpontok nincsenek bekapcsolva.\nKapcsold be a config.yaml fájlban:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Nem található ellenőrzőpont ehhez: {cwd}"
+ invalid_number: "Érvénytelen ellenőrzőpont-szám. Használj 1-{max} közötti értéket."
+ restored: "✅ Visszaállítva a(z) {hash} ellenőrzőpontra: {reason}\nA visszaállítás előtti pillanatkép automatikusan elmentve."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Nem sikerült menteni a kezdőcsatornát: {error}"
+ success: "✅ Kezdőcsatorna beállítva: **{name}** (ID: {chat_id}).\nA cron-feladatok és a platformok közötti üzenetek ide érkeznek."
+
+ status:
+ header: "📊 **Hermes Gateway állapot**"
+ session_id: "**Munkamenet-azonosító:** `{session_id}`"
+ title: "**Cím:** {title}"
+ created: "**Létrehozva:** {timestamp}"
+ last_activity: "**Utolsó tevékenység:** {timestamp}"
+ tokens: "**Tokenek:** {tokens}"
+ agent_running: "**Ügynök fut:** {state}"
+ state_yes: "Igen ⚡"
+ state_no: "Nem"
+ queued: "**Sorban álló folytatások:** {count}"
+ platforms: "**Csatlakoztatott platformok:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Leállítva. Az ügynök még el sem kezdte — folytathatod ezt a munkamenetet."
+ stopped: "⚡ Leállítva. Folytathatod ezt a munkamenetet."
+ no_active: "Nincs leállítható aktív feladat."
+
+ title:
+ db_unavailable: "A munkamenet-adatbázis nem érhető el."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Tisztítás után a cím üres. Használj nyomtatható karaktereket."
+ set_to: "✏️ Munkamenet címe beállítva: **{title}**"
+ not_found: "A munkamenet nem található az adatbázisban."
+ current_with_title: "📌 Munkamenet: `{session_id}`\nCím: **{title}**"
+ current_no_title: "📌 Munkamenet: `{session_id}`\nNincs cím beállítva. Használat: `/title Saját munkamenet neve`"
+
+ topic:
+ not_telegram_dm: "A /topic parancs csak Telegram privát csevegésekben érhető el."
+ no_session_db: "A munkamenet-adatbázis nem érhető el."
+ unauthorized: "Nincs jogosultságod a /topic használatához ezen a boton."
+ restore_needs_topic: "Egy munkamenet visszaállításához először hozz létre vagy nyiss meg egy Telegram topicot, majd küldd a /topic parancsot abban a topicban. Új topic létrehozásához nyisd meg az All Messagest, és küldj oda bármilyen üzenetet."
+ topics_disabled: "A Telegram topicok még nincsenek engedélyezve ehhez a bothoz.\n\nHogyan engedélyezd:\n1. Nyisd meg a @BotFathert.\n2. Válaszd ki a botod.\n3. Nyisd meg a Bot Settings → Threads Settings menüt.\n4. Kapcsold be a Threaded Mode-ot, és győződj meg róla, hogy a felhasználók új threadeket hozhatnak létre.\n\nEzután küldd újra a /topic parancsot."
+ topics_user_disallowed: "A Telegram topicok engedélyezve vannak, de a felhasználók nem hozhatnak létre topicokat.\n\nNyisd meg a @BotFather → válaszd ki a botod → Bot Settings → Threads Settings menüt, majd kapcsold ki a 'Disallow users to create new threads' opciót.\n\nEzután küldd újra a /topic parancsot."
+ enable_failed: "Nem sikerült engedélyezni a Telegram topic módot: {error}"
+ bound_status: "Ez a topic ehhez van kapcsolva:\nMunkamenet: {label}\nID: {session_id}\n\nHasználd a /new parancsot, hogy lecseréld ezt a topicot új munkamenetre.\nPárhuzamos munkához nyisd meg az All Messagest, és küldj oda egy üzenetet egy másik topic létrehozásához."
+ thread_ready: "A többmunkamenetes Telegram topicok engedélyezve vannak.\n\nEz a topic független Hermes-munkamenetként szolgál. Használd a /new parancsot, hogy lecseréld a topic jelenlegi munkamenetét. Párhuzamos munkához nyisd meg az All Messagest, és küldj oda egy üzenetet egy másik topic létrehozásához."
+ untitled_session: "Cím nélküli munkamenet"
+
+ undo:
+ nothing: "Nincs mit visszavonni."
+ removed: "↩️ {count} üzenet visszavonva.\nEltávolítva: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ A /update csak üzenetküldő platformokról érhető el. Futtasd a `hermes update` parancsot a terminálból."
+ not_git_repo: "✗ Nem git-tárhely — frissítés nem lehetséges."
+ hermes_cmd_not_found: "✗ Nem sikerült megtalálni a `hermes` parancsot. A Hermes fut, de a frissítőparancs nem találta a futtatható fájlt a PATH-on vagy a jelenlegi Python interpreteren keresztül. Próbáld futtatni a `hermes update` parancsot manuálisan a terminálban."
+ start_failed: "✗ Nem sikerült elindítani a frissítést: {error}"
+ starting: "⚕ Hermes frissítés indítása… A folyamatot itt fogom közvetíteni."
+
+ usage:
+ rate_limits: "⏱️ **Sebességkorlátok:** {state}"
+ header_session: "📊 **Munkamenet tokenhasználat**"
+ label_model: "Modell: `{model}`"
+ label_input_tokens: "Bemeneti tokenek: {count}"
+ label_cache_read: "Gyorsítótár-olvasási tokenek: {count}"
+ label_cache_write: "Gyorsítótár-írási tokenek: {count}"
+ label_output_tokens: "Kimeneti tokenek: {count}"
+ label_total: "Összesen: {count}"
+ label_api_calls: "API-hívások: {count}"
+ label_cost: "Költség: {prefix}${amount}"
+ label_cost_included: "Költség: belefoglalva"
+ label_context: "Kontextus: {used} / {total} ({pct}%)"
+ label_compressions: "Tömörítések: {count}"
+ header_session_info: "📊 **Munkamenet-információ**"
+ label_messages: "Üzenetek: {count}"
+ label_estimated_context: "Becsült kontextus: ~{count} token"
+ detailed_after_first: "_(A részletes használat az első ügynökválasz után érhető el)_"
+ no_data: "Ehhez a munkamenethez nincsenek elérhető használati adatok."
+
+ verbose:
+ not_enabled: "A `/verbose` parancs nincs engedélyezve az üzenetküldő platformokon.\n\nEngedélyezd a `config.yaml` fájlban:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Eszközfolyamat: **OFF** — nem jelenik meg eszközaktivitás."
+ mode_new: "⚙️ Eszközfolyamat: **NEW** — eszközváltáskor jelenik meg (előnézet hossza: `display.tool_preview_length`, alapértelmezetten 40)."
+ mode_all: "⚙️ Eszközfolyamat: **ALL** — minden eszközhívás megjelenik (előnézet hossza: `display.tool_preview_length`, alapértelmezetten 40)."
+ mode_verbose: "⚙️ Eszközfolyamat: **VERBOSE** — minden eszközhívás teljes argumentumokkal."
+ saved_suffix: "_(elmentve ehhez: **{platform}** — a következő üzenettől lép életbe)_"
+ save_failed: "_(nem sikerült menteni a konfigurációba: {error})_"
+
+ voice:
+ enabled_voice_only: "Hangmód bekapcsolva.\nHanggal válaszolok, ha hangüzenetet küldesz.\nHasználd a /voice tts parancsot, hogy minden üzenetre hangválaszt kapj."
+ disabled_text: "Hangmód kikapcsolva. Csak szöveges válaszok."
+ tts_enabled: "Auto-TTS bekapcsolva.\nMinden válasz tartalmaz egy hangüzenetet."
+ status_mode: "Hangmód: {label}"
+ status_channel: "Hangcsatorna: #{channel}"
+ status_participants: "Résztvevők: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (beszél)"
+ enabled_short: "Hangmód bekapcsolva."
+ disabled_short: "Hangmód kikapcsolva."
+ label_off: "Ki (csak szöveg)"
+ label_voice_only: "Be (hangválasz hangüzenetekre)"
+ label_all: "TTS (hangválasz minden üzenetre)"
+
+ yolo:
+ disabled: "⚠️ YOLO mód **KI** ebben a munkamenetben — a veszélyes parancsok jóváhagyást igényelnek."
+ enabled: "⚡ YOLO mód **BE** ebben a munkamenetben — minden parancs automatikusan jóváhagyva. Óvatosan használd."
+
+ shared:
+ session_db_unavailable: "A munkamenet-adatbázis nem érhető el."
+ session_db_unavailable_prefix: "A munkamenet-adatbázis nem érhető el"
+ session_not_found: "A munkamenet nem található az adatbázisban."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/it.yaml b/locales/it.yaml
new file mode 100644
index 00000000000..2e4d9940194
--- /dev/null
+++ b/locales/it.yaml
@@ -0,0 +1,350 @@
+# Catalogo dei messaggi statici di Hermes -- Italiano
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ COMANDO PERICOLOSO: {description}"
+ choose_long: " [o]una volta | [s]essione | [a]sempre | [d]nega"
+ choose_short: " [o]una volta | [s]essione | [d]nega"
+ prompt_long: " Scelta [o/s/a/D]: "
+ prompt_short: " Scelta [o/s/D]: "
+ timeout: " ⏱ Tempo scaduto — comando negato"
+ allowed_once: " ✓ Consentito una volta"
+ allowed_session: " ✓ Consentito per questa sessione"
+ allowed_always: " ✓ Aggiunto alla lista permessi permanente"
+ denied: " ✗ Negato"
+ cancelled: " ✗ Annullato"
+ blocklist_message: "Questo comando è nella lista di blocco incondizionata e non può essere approvato."
+
+gateway:
+ approval_expired: "⚠️ Approvazione scaduta (l'agente non è più in attesa). Chiedi all'agente di riprovare."
+ draining: "⏳ Attendo il completamento di {count} agente/i attivo/i prima di riavviare..."
+ goal_cleared: "✓ Obiettivo cancellato."
+ no_active_goal: "Nessun obiettivo attivo."
+ config_read_failed: "⚠️ Impossibile leggere config.yaml: {error}"
+ config_save_failed: "⚠️ Impossibile salvare la configurazione: {error}"
+
+ model:
+ error_prefix: "Errore: {error}"
+ switched: "Modello cambiato a `{model}`"
+ provider_label: "Provider: {provider}"
+ context_label: "Contesto: {tokens} token"
+ max_output_label: "Output massimo: {tokens} token"
+ cost_label: "Costo: {cost}"
+ capabilities_label: "Capacità: {capabilities}"
+ prompt_caching_enabled: "Caching dei prompt: attivo"
+ warning_prefix: "Avviso: {warning}"
+ saved_global: "Salvato in config.yaml (`--global`)"
+ session_only_hint: "_(solo per questa sessione — aggiungi `--global` per renderlo permanente)_"
+ current_label: "Attuale: `{model}` su {provider}"
+ current_tag: " (attuale)"
+ more_models_suffix: " (+{count} altri)"
+ usage_switch_model: "`/model ` — cambia modello"
+ usage_switch_provider: "`/model --provider ` — cambia provider"
+ usage_persist: "`/model --global` — rendi permanente"
+
+ agents:
+ header: "🤖 **Agenti e attività attivi**"
+ active_agents: "**Agenti attivi:** {count}"
+ this_chat: " · questa chat"
+ more: "... e {count} altri"
+ running_processes: "**Processi in background in esecuzione:** {count}"
+ async_jobs: "**Job asincroni del gateway:** {count}"
+ none: "Nessun agente attivo o attività in esecuzione."
+ state_starting: "in avvio"
+ state_running: "in esecuzione"
+
+ approve:
+ no_pending: "Nessun comando in attesa di approvazione."
+ once_singular: "✅ Comando approvato. L'agente sta riprendendo..."
+ once_plural: "✅ Comandi approvati ({count} comandi). L'agente sta riprendendo..."
+ session_singular: "✅ Comando approvato (modello approvato per questa sessione). L'agente sta riprendendo..."
+ session_plural: "✅ Comandi approvati (modello approvato per questa sessione) ({count} comandi). L'agente sta riprendendo..."
+ always_singular: "✅ Comando approvato (modello approvato in modo permanente). L'agente sta riprendendo..."
+ always_plural: "✅ Comandi approvati (modello approvato in modo permanente) ({count} comandi). L'agente sta riprendendo..."
+
+ background:
+ usage: "Uso: /background \nEsempio: /background Riassumi le principali notizie di HN di oggi\n\nEsegue il prompt in una sessione separata. Puoi continuare a chattare — il risultato apparirà qui al termine."
+ started: "🔄 Attività in background avviata: \"{preview}\"\nID attività: {task_id}\nPuoi continuare a chattare — i risultati appariranno al termine."
+
+ branch:
+ db_unavailable: "Database delle sessioni non disponibile."
+ no_conversation: "Nessuna conversazione da diramare — invia prima un messaggio."
+ create_failed: "Creazione del ramo non riuscita: {error}"
+ switch_failed: "Ramo creato ma il passaggio ad esso non è riuscito."
+ branched_one: "⑂ Diramato in **{title}** ({count} messaggio copiato)\nOriginale: `{parent}`\nRamo: `{new}`\nUsa `/resume` per tornare all'originale."
+ branched_many: "⑂ Diramato in **{title}** ({count} messaggi copiati)\nOriginale: `{parent}`\nRamo: `{new}`\nUsa `/resume` per tornare all'originale."
+
+ commands:
+ usage: "Uso: `/commands [page]`"
+ skill_header: "⚡ **Comandi skill**:"
+ default_desc: "Comando skill"
+ none: "Nessun comando disponibile."
+ header: "📚 **Comandi** ({total} totali, pagina {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← prec"
+ nav_next: "succ → `/commands {page}`"
+ out_of_range: "_(La pagina richiesta {requested} è fuori intervallo, mostrando la pagina {page}.)_"
+
+ compress:
+ not_enough: "Conversazione insufficiente da comprimere (servono almeno 4 messaggi)."
+ no_provider: "Nessun provider configurato — impossibile comprimere."
+ nothing_to_do: "Niente da comprimere per ora (la trascrizione è ancora tutta contesto protetto)."
+ focus_line: "Focus: \"{topic}\""
+ summary_failed: "⚠️ Generazione del riepilogo non riuscita ({error}). {count} messaggio/i storico/i sono stati rimossi e sostituiti con un segnaposto; il contesto precedente non è più recuperabile. Considera di controllare la configurazione del modello auxiliary.compression."
+ aux_failed: "ℹ️ Il modello di compressione configurato `{model}` non è riuscito ({error}). Recupero effettuato usando il modello principale — il contesto è intatto — ma potresti voler controllare `auxiliary.compression.model` in config.yaml."
+ failed: "Compressione non riuscita: {error}"
+
+ debug:
+ upload_failed: "✗ Caricamento del report di debug non riuscito: {error}"
+ header: "**Report di debug caricato:**"
+ auto_delete: "⏱ I paste verranno eliminati automaticamente tra 6 ore."
+ full_logs_hint: "Per il caricamento dei log completi, usa `hermes debug share` dalla CLI."
+ share_hint: "Condividi questi link con il team Hermes per ricevere supporto."
+
+ deny:
+ stale: "❌ Comando negato (l'approvazione era obsoleta)."
+ no_pending: "Nessun comando in attesa da negare."
+ denied_singular: "❌ Comando negato."
+ denied_plural: "❌ Comandi negati ({count} comandi)."
+
+ fast:
+ not_supported: "⚡ /fast è disponibile solo per i modelli OpenAI che supportano Priority Processing."
+ status: "⚡ Priority Processing\n\nModalità attuale: `{mode}`\n\n_Uso:_ `/fast `"
+ unknown_arg: "⚠️ Argomento sconosciuto: `{arg}`\n\n**Opzioni valide:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (salvato nella configurazione)\n_(verrà applicato al prossimo messaggio)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (solo per questa sessione)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Footer di runtime: **{state}**\nCampi: `{fields}`\nPiattaforma: `{platform}`"
+ usage: "Uso: `/footer [on|off|status]`"
+ saved: "📎 Footer di runtime: **{state}**{example}\n_(salvato globalmente — verrà applicato al prossimo messaggio)_"
+ example_line: "\nEsempio: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Gli obiettivi non sono disponibili in questa sessione."
+ no_goal_set: "Nessun obiettivo impostato."
+ paused: "⏸ Obiettivo in pausa: {goal}"
+ no_resume: "Nessun obiettivo da riprendere."
+ resumed: "▶ Obiettivo ripreso: {goal}\nInvia un messaggio per continuare, oppure aspetta — farò il prossimo passo al turno successivo."
+ invalid: "Obiettivo non valido: {error}"
+ set: "⊙ Obiettivo impostato (budget di {budget} turni): {goal}\nContinuerò a lavorare finché l'obiettivo non sarà completato, lo metterai in pausa/lo cancellerai, oppure il budget sarà esaurito.\nControlli: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Comandi Hermes**\n"
+ skill_header: "\n⚡ **Comandi skill** ({count} attivi):"
+ more_use_commands: "\n... e altri {count}. Usa `/commands` per la lista paginata completa."
+
+ insights:
+ invalid_days: "Valore --days non valido: {value}"
+ error: "Errore nella generazione degli insight: {error}"
+
+ kanban:
+ error_prefix: "⚠ errore kanban: {error}"
+ subscribed_suffix: "(iscritto — riceverai notifica quando {task_id} verrà completato o si bloccherà)"
+ truncated_suffix: "… (troncato; usa `hermes kanban …` nel terminale per l'output completo)"
+ no_output: "(nessun output)"
+
+ personality:
+ none_configured: "Nessuna personalità configurata in `{path}/config.yaml`"
+ header: "🎭 **Personalità disponibili**\n"
+ none_option: "• `none` — (nessun overlay di personalità)"
+ item: "• `{name}` — {preview}"
+ usage: "\nUso: `/personality `"
+ save_failed: "⚠️ Salvataggio del cambio di personalità non riuscito: {error}"
+ cleared: "🎭 Personalità cancellata — uso il comportamento base dell'agente.\n_(verrà applicato al prossimo messaggio)_"
+ set_to: "🎭 Personalità impostata su **{name}**\n_(verrà applicato al prossimo messaggio)_"
+ unknown: "Personalità sconosciuta: `{name}`\n\nDisponibili: {available}"
+
+ profile:
+ header: "👤 **Profilo:** `{profile}`"
+ home: "📂 **Home:** `{home}`"
+
+ reasoning:
+ level_default: "medio (predefinito)"
+ level_disabled: "nessuno (disattivato)"
+ scope_session: "override di sessione"
+ scope_global: "configurazione globale"
+ status: "🧠 **Impostazioni di reasoning**\n\n**Sforzo:** `{level}`\n**Ambito:** {scope}\n**Visualizzazione:** {display}\n\n_Uso:_ `/reasoning [--global]`"
+ display_on: "attivo ✓"
+ display_off: "disattivato"
+ display_set_on: "🧠 ✓ Visualizzazione del reasoning: **ATTIVA**\nIl pensiero del modello verrà mostrato prima di ogni risposta su **{platform}**."
+ display_set_off: "🧠 ✓ Visualizzazione del reasoning: **DISATTIVATA** per **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` non è supportato. Usa `/reasoning --global` per cambiare il valore predefinito globale."
+ reset_done: "🧠 ✓ Override di reasoning della sessione cancellato; ripristino della configurazione globale."
+ unknown_arg: "⚠️ Argomento sconosciuto: `{arg}`\n\n**Livelli validi:** none, minimal, low, medium, high, xhigh\n**Visualizzazione:** show, hide\n**Persistenza:** aggiungi `--global` per salvare oltre questa sessione"
+ set_global: "🧠 ✓ Sforzo di reasoning impostato su `{effort}` (salvato nella configurazione)\n_(verrà applicato al prossimo messaggio)_"
+ set_global_save_failed: "🧠 ✓ Sforzo di reasoning impostato su `{effort}` (solo per questa sessione — salvataggio della configurazione non riuscito)\n_(verrà applicato al prossimo messaggio)_"
+ set_session: "🧠 ✓ Sforzo di reasoning impostato su `{effort}` (solo per questa sessione — aggiungi `--global` per renderlo permanente)\n_(verrà applicato al prossimo messaggio)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp annullato. Strumenti MCP invariati."
+ always_followup: "ℹ️ Le future chiamate a `/reload-mcp` verranno eseguite senza conferma. Riattiva tramite `approvals.mcp_reload_confirm: true` in config.yaml."
+ confirm_prompt: "⚠️ **Conferma /reload-mcp**\n\nIl ricaricamento dei server MCP ricostruisce il set di strumenti per questa sessione e **invalida la cache dei prompt del provider** — il prossimo messaggio invierà nuovamente tutti i token di input. Sui modelli a contesto lungo o ad alto reasoning questo può essere costoso.\n\nScegli:\n• **Approva una volta** — ricarica ora\n• **Approva sempre** — ricarica ora e silenzia questa richiesta in modo permanente\n• **Annulla** — lascia gli strumenti MCP invariati\n\n_Alternativa testuale: rispondi `/approve`, `/always`, oppure `/cancel`._"
+ header: "🔄 **Server MCP ricaricati**\n"
+ reconnected: "♻️ Riconnessi: {names}"
+ added: "➕ Aggiunti: {names}"
+ removed: "➖ Rimossi: {names}"
+ none_connected: "Nessun server MCP connesso."
+ tools_available: "\n🔧 {tools} strumento/i disponibile/i da {servers} server"
+ failed: "❌ Ricaricamento MCP non riuscito: {error}"
+
+ reload_skills:
+ header: "🔄 **Skill ricaricate**\n"
+ no_new: "Nessuna nuova skill rilevata."
+ total: "\n📚 {count} skill disponibili"
+ added_header: "➕ **Skill aggiunte:**"
+ removed_header: "➖ **Skill rimosse:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Ricaricamento delle skill non riuscito: {error}"
+
+ reset:
+ header_default: "✨ Sessione reimpostata! Si ricomincia da zero."
+ header_new: "✨ Nuova sessione avviata!"
+ header_titled: "✨ Nuova sessione avviata: {title}"
+ title_rejected: "\n⚠️ Titolo rifiutato: {error}"
+ title_error_untitled: "\n⚠️ {error} — sessione avviata senza titolo."
+ title_empty_untitled: "\n⚠️ Il titolo è vuoto dopo la pulizia — sessione avviata senza titolo."
+ tip: "\n✦ Suggerimento: {tip}"
+
+ restart:
+ in_progress: "⏳ Riavvio del gateway già in corso..."
+ restarting: "♻ Riavvio del gateway. Se non ricevi una notifica entro 60 secondi, riavvia dalla console con `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Database delle sessioni non disponibile."
+ no_named_sessions: "Nessuna sessione con nome trovata.\nUsa `/title My Session` per dare un nome alla sessione attuale, poi `/resume My Session` per tornare a essa in seguito."
+ list_header: "📋 **Sessioni con nome**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nUso: `/resume `"
+ list_failed: "Impossibile elencare le sessioni: {error}"
+ not_found: "Nessuna sessione trovata corrispondente a '**{name}**'.\nUsa `/resume` senza argomenti per vedere le sessioni disponibili."
+ already_on: "📌 Già nella sessione **{name}**."
+ switch_failed: "Cambio di sessione non riuscito."
+ resumed_one: "↻ Sessione **{title}** ripresa ({count} messaggio). Conversazione ripristinata."
+ resumed_many: "↻ Sessione **{title}** ripresa ({count} messaggi). Conversazione ripristinata."
+ resumed_no_count: "↻ Sessione **{title}** ripresa. Conversazione ripristinata."
+
+ retry:
+ no_previous: "Nessun messaggio precedente da ripetere."
+
+ rollback:
+ not_enabled: "I checkpoint non sono abilitati.\nAbilitali in config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Nessun checkpoint trovato per {cwd}"
+ invalid_number: "Numero di checkpoint non valido. Usa 1-{max}."
+ restored: "✅ Ripristinato al checkpoint {hash}: {reason}\nUno snapshot pre-rollback è stato salvato automaticamente."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Salvataggio del canale home non riuscito: {error}"
+ success: "✅ Canale home impostato su **{name}** (ID: {chat_id}).\nI cron job e i messaggi cross-platform verranno consegnati qui."
+
+ status:
+ header: "📊 **Stato del Gateway Hermes**"
+ session_id: "**ID sessione:** `{session_id}`"
+ title: "**Titolo:** {title}"
+ created: "**Creata:** {timestamp}"
+ last_activity: "**Ultima attività:** {timestamp}"
+ tokens: "**Token:** {tokens}"
+ agent_running: "**Agente in esecuzione:** {state}"
+ state_yes: "Sì ⚡"
+ state_no: "No"
+ queued: "**Follow-up in coda:** {count}"
+ platforms: "**Piattaforme connesse:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Fermato. L'agente non era ancora partito — puoi continuare questa sessione."
+ stopped: "⚡ Fermato. Puoi continuare questa sessione."
+ no_active: "Nessuna attività attiva da fermare."
+
+ title:
+ db_unavailable: "Database delle sessioni non disponibile."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Il titolo è vuoto dopo la pulizia. Usa caratteri stampabili."
+ set_to: "✏️ Titolo della sessione impostato: **{title}**"
+ not_found: "Sessione non trovata nel database."
+ current_with_title: "📌 Sessione: `{session_id}`\nTitolo: **{title}**"
+ current_no_title: "📌 Sessione: `{session_id}`\nNessun titolo impostato. Uso: `/title My Session Name`"
+
+ topic:
+ not_telegram_dm: "Il comando /topic è disponibile solo nelle chat private di Telegram."
+ no_session_db: "Database delle sessioni non disponibile."
+ unauthorized: "Non sei autorizzato a usare /topic su questo bot."
+ restore_needs_topic: "Per ripristinare una sessione, crea o apri prima un topic Telegram, poi invia /topic all'interno di quel topic. Per creare un nuovo topic, apri All Messages e invia un messaggio qualsiasi lì."
+ topics_disabled: "I topic Telegram non sono ancora abilitati per questo bot.\n\nCome abilitarli:\n1. Apri @BotFather.\n2. Scegli il tuo bot.\n3. Apri Bot Settings → Threads Settings.\n4. Attiva la modalità Threaded e assicurati che gli utenti possano creare nuovi thread.\n\nPoi invia di nuovo /topic."
+ topics_user_disallowed: "I topic Telegram sono abilitati, ma agli utenti non è permesso crearne.\n\nApri @BotFather → scegli il tuo bot → Bot Settings → Threads Settings, poi disattiva 'Disallow users to create new threads'.\n\nPoi invia di nuovo /topic."
+ enable_failed: "Abilitazione della modalità topic Telegram non riuscita: {error}"
+ bound_status: "Questo topic è collegato a:\nSessione: {label}\nID: {session_id}\n\nUsa /new per sostituire questo topic con una nuova sessione.\nPer lavorare in parallelo, apri All Messages e invia un messaggio lì per creare un altro topic."
+ thread_ready: "I topic multi-sessione di Telegram sono abilitati.\n\nQuesto topic verrà usato come una sessione Hermes indipendente. Usa /new per sostituire la sessione corrente di questo topic. Per lavorare in parallelo, apri All Messages e invia un messaggio lì per creare un altro topic."
+ untitled_session: "Sessione senza titolo"
+
+ undo:
+ nothing: "Niente da annullare."
+ removed: "↩️ Annullati {count} messaggio/i.\nRimosso: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update è disponibile solo dalle piattaforme di messaggistica. Esegui `hermes update` dal terminale."
+ not_git_repo: "✗ Non è un repository git — impossibile aggiornare."
+ hermes_cmd_not_found: "✗ Impossibile localizzare il comando `hermes`. Hermes è in esecuzione, ma il comando di aggiornamento non ha trovato l'eseguibile nel PATH o tramite l'interprete Python attuale. Prova a eseguire `hermes update` manualmente nel terminale."
+ start_failed: "✗ Avvio dell'aggiornamento non riuscito: {error}"
+ starting: "⚕ Avvio dell'aggiornamento di Hermes… mostrerò qui i progressi in streaming."
+
+ usage:
+ rate_limits: "⏱️ **Limiti di frequenza:** {state}"
+ header_session: "📊 **Uso dei token della sessione**"
+ label_model: "Modello: `{model}`"
+ label_input_tokens: "Token di input: {count}"
+ label_cache_read: "Token di lettura cache: {count}"
+ label_cache_write: "Token di scrittura cache: {count}"
+ label_output_tokens: "Token di output: {count}"
+ label_total: "Totale: {count}"
+ label_api_calls: "Chiamate API: {count}"
+ label_cost: "Costo: {prefix}${amount}"
+ label_cost_included: "Costo: incluso"
+ label_context: "Contesto: {used} / {total} ({pct}%)"
+ label_compressions: "Compressioni: {count}"
+ header_session_info: "📊 **Info sessione**"
+ label_messages: "Messaggi: {count}"
+ label_estimated_context: "Contesto stimato: ~{count} token"
+ detailed_after_first: "_(L'uso dettagliato sarà disponibile dopo la prima risposta dell'agente)_"
+ no_data: "Nessun dato di utilizzo disponibile per questa sessione."
+
+ verbose:
+ not_enabled: "Il comando `/verbose` non è abilitato per le piattaforme di messaggistica.\n\nAbilitalo in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Progresso strumenti: **OFF** — nessuna attività degli strumenti mostrata."
+ mode_new: "⚙️ Progresso strumenti: **NEW** — mostrato quando lo strumento cambia (lunghezza anteprima: `display.tool_preview_length`, predefinito 40)."
+ mode_all: "⚙️ Progresso strumenti: **ALL** — ogni chiamata a uno strumento viene mostrata (lunghezza anteprima: `display.tool_preview_length`, predefinito 40)."
+ mode_verbose: "⚙️ Progresso strumenti: **VERBOSE** — ogni chiamata a uno strumento con argomenti completi."
+ saved_suffix: "_(salvato per **{platform}** — verrà applicato al prossimo messaggio)_"
+ save_failed: "_(impossibile salvare nella configurazione: {error})_"
+
+ voice:
+ enabled_voice_only: "Modalità vocale attivata.\nRisponderò con la voce quando invii messaggi vocali.\nUsa /voice tts per ricevere risposte vocali per tutti i messaggi."
+ disabled_text: "Modalità vocale disattivata. Risposte solo testuali."
+ tts_enabled: "Auto-TTS attivato.\nTutte le risposte includeranno un messaggio vocale."
+ status_mode: "Modalità vocale: {label}"
+ status_channel: "Canale vocale: #{channel}"
+ status_participants: "Partecipanti: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (sta parlando)"
+ enabled_short: "Modalità vocale attivata."
+ disabled_short: "Modalità vocale disattivata."
+ label_off: "Off (solo testo)"
+ label_voice_only: "On (risposta vocale ai messaggi vocali)"
+ label_all: "TTS (risposta vocale a tutti i messaggi)"
+
+ yolo:
+ disabled: "⚠️ Modalità YOLO **OFF** per questa sessione — i comandi pericolosi richiederanno approvazione."
+ enabled: "⚡ Modalità YOLO **ON** per questa sessione — tutti i comandi auto-approvati. Usa con cautela."
+
+ shared:
+ session_db_unavailable: "Database delle sessioni non disponibile."
+ session_db_unavailable_prefix: "Database delle sessioni non disponibile"
+ session_not_found: "Sessione non trovata nel database."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/ja.yaml b/locales/ja.yaml
index 5cf229a5206..55c42915e65 100644
--- a/locales/ja.yaml
+++ b/locales/ja.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "アクティブな目標はありません。"
config_read_failed: "⚠️ config.yaml を読み込めませんでした: {error}"
config_save_failed: "⚠️ 設定を保存できませんでした: {error}"
+
+ model:
+ error_prefix: "エラー: {error}"
+ switched: "モデルを `{model}` に切り替えました"
+ provider_label: "プロバイダー: {provider}"
+ context_label: "コンテキスト: {tokens} トークン"
+ max_output_label: "最大出力: {tokens} トークン"
+ cost_label: "コスト: {cost}"
+ capabilities_label: "機能: {capabilities}"
+ prompt_caching_enabled: "プロンプトキャッシュ: 有効"
+ warning_prefix: "警告: {warning}"
+ saved_global: "config.yaml に保存しました (`--global`)"
+ session_only_hint: "_(このセッションのみ — 永続化するには `--global` を追加)_"
+ current_label: "現在: `{model}` ({provider})"
+ current_tag: " (現在)"
+ more_models_suffix: " (他 {count} 件)"
+ usage_switch_model: "`/model ` — モデルを切り替え"
+ usage_switch_provider: "`/model --provider ` — プロバイダーを切り替え"
+ usage_persist: "`/model --global` — 永続化"
+
+ agents:
+ header: "🤖 **アクティブなエージェントとタスク**"
+ active_agents: "**アクティブなエージェント:** {count}"
+ this_chat: " · このチャット"
+ more: "... 他に {count} 件"
+ running_processes: "**実行中のバックグラウンドプロセス:** {count}"
+ async_jobs: "**ゲートウェイ非同期ジョブ:** {count}"
+ none: "アクティブなエージェントや実行中のタスクはありません。"
+ state_starting: "起動中"
+ state_running: "実行中"
+
+ approve:
+ no_pending: "承認待ちのコマンドはありません。"
+ once_singular: "✅ コマンドを承認しました。エージェントを再開しています..."
+ once_plural: "✅ コマンドを承認しました ({count} 件)。エージェントを再開しています..."
+ session_singular: "✅ コマンドを承認しました (このセッション中はパターンを許可)。エージェントを再開しています..."
+ session_plural: "✅ コマンドを承認しました (このセッション中はパターンを許可) ({count} 件)。エージェントを再開しています..."
+ always_singular: "✅ コマンドを承認しました (パターンを永続的に許可)。エージェントを再開しています..."
+ always_plural: "✅ コマンドを承認しました (パターンを永続的に許可) ({count} 件)。エージェントを再開しています..."
+
+ background:
+ usage: "使い方: /background <プロンプト>\n例: /background 今日の HN トップ記事を要約して\n\nプロンプトを別のセッションで実行します。チャットを続けられます — 完了したらここに結果が表示されます。"
+ started: "🔄 バックグラウンドタスクを開始しました: 「{preview}」\nタスク ID: {task_id}\nチャットを続けられます — 完了したらここに結果が表示されます。"
+
+ branch:
+ db_unavailable: "セッションデータベースは利用できません。"
+ no_conversation: "分岐する会話がありません — まずメッセージを送信してください。"
+ create_failed: "ブランチの作成に失敗しました: {error}"
+ switch_failed: "ブランチは作成されましたが、切り替えに失敗しました。"
+ branched_one: "⑂ **{title}** に分岐しました ({count} メッセージをコピー)\n元: `{parent}`\nブランチ: `{new}`\n元のセッションに戻るには `/resume` を使用してください。"
+ branched_many: "⑂ **{title}** に分岐しました ({count} メッセージをコピー)\n元: `{parent}`\nブランチ: `{new}`\n元のセッションに戻るには `/resume` を使用してください。"
+
+ commands:
+ usage: "使い方: `/commands [page]`"
+ skill_header: "⚡ **スキルコマンド**:"
+ default_desc: "スキルコマンド"
+ none: "利用可能なコマンドはありません。"
+ header: "📚 **コマンド** (合計 {total}、{page}/{total_pages} ページ)"
+ nav_prev: "`/commands {page}` ← 前へ"
+ nav_next: "次へ → `/commands {page}`"
+ out_of_range: "_(要求されたページ {requested} は範囲外のため、{page} ページを表示しています。)_"
+
+ compress:
+ not_enough: "圧縮するための会話が不十分です (少なくとも 4 件のメッセージが必要)。"
+ no_provider: "プロバイダーが構成されていません — 圧縮できません。"
+ nothing_to_do: "まだ圧縮するものがありません (トランスクリプトはすべて保護されたコンテキストのままです)。"
+ focus_line: "フォーカス: \"{topic}\""
+ summary_failed: "⚠️ 要約の生成に失敗しました ({error})。{count} 件の履歴メッセージが削除され、プレースホルダーに置き換えられました。以前のコンテキストは復元できません。auxiliary.compression モデルの設定を確認してください。"
+ aux_failed: "ℹ️ 構成された圧縮モデル `{model}` が失敗しました ({error})。メインモデルで復旧しました — コンテキストは無傷です — config.yaml の `auxiliary.compression.model` を確認するとよいでしょう。"
+ failed: "圧縮に失敗しました: {error}"
+
+ debug:
+ upload_failed: "✗ デバッグレポートのアップロードに失敗しました: {error}"
+ header: "**デバッグレポートをアップロードしました:**"
+ auto_delete: "⏱ ペーストは 6 時間後に自動削除されます。"
+ full_logs_hint: "完全なログのアップロードには、CLI から `hermes debug share` を使用してください。"
+ share_hint: "サポートを受けるには、このリンクを Hermes チームに共有してください。"
+
+ deny:
+ stale: "❌ コマンドを拒否しました (承認は期限切れでした)。"
+ no_pending: "拒否待ちのコマンドはありません。"
+ denied_singular: "❌ コマンドを拒否しました。"
+ denied_plural: "❌ コマンドを拒否しました ({count} 件)。"
+
+ fast:
+ not_supported: "⚡ /fast は Priority Processing をサポートする OpenAI モデルでのみ利用できます。"
+ status: "⚡ Priority Processing\n\n現在のモード: `{mode}`\n\n_使い方:_ `/fast `"
+ unknown_arg: "⚠️ 不明な引数: `{arg}`\n\n**有効なオプション:** normal、fast、status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (設定に保存しました)\n_(次のメッセージから有効)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (このセッションのみ)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 ランタイムフッター: **{state}**\nフィールド: `{fields}`\nプラットフォーム: `{platform}`"
+ usage: "使い方: `/footer [on|off|status]`"
+ saved: "📎 ランタイムフッター: **{state}**{example}\n_(グローバルに保存しました — 次のメッセージから有効)_"
+ example_line: "\n例: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "このセッションでは目標機能を利用できません。"
+ no_goal_set: "目標が設定されていません。"
+ paused: "⏸ 目標を一時停止しました: {goal}"
+ no_resume: "再開する目標がありません。"
+ resumed: "▶ 目標を再開しました: {goal}\nメッセージを送って続行するか、お待ちください — 次のターンで続きを進めます。"
+ invalid: "無効な目標: {error}"
+ set: "⊙ 目標を設定しました ({budget} ターンの予算): {goal}\n目標が完了するか、一時停止/解除されるか、予算が尽きるまで作業を続けます。\nコントロール: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes コマンド**\n"
+ skill_header: "\n⚡ **スキルコマンド** ({count} 件アクティブ):"
+ more_use_commands: "\n... 他に {count} 件。完全なページ分けリストは `/commands` で確認してください。"
+
+ insights:
+ invalid_days: "--days の値が無効です: {value}"
+ error: "インサイトの生成中にエラーが発生しました: {error}"
+
+ kanban:
+ error_prefix: "⚠ kanban エラー: {error}"
+ subscribed_suffix: "(購読しました — {task_id} が完了またはブロックされたときに通知されます)"
+ truncated_suffix: "… (切り詰めました; 完全な出力にはターミナルで `hermes kanban …` を使用してください)"
+ no_output: "(出力なし)"
+
+ personality:
+ none_configured: "`{path}/config.yaml` に人格が設定されていません"
+ header: "🎭 **利用可能な人格**\n"
+ none_option: "• `none` — (人格オーバーレイなし)"
+ item: "• `{name}` — {preview}"
+ usage: "\n使い方: `/personality `"
+ save_failed: "⚠️ 人格変更の保存に失敗しました: {error}"
+ cleared: "🎭 人格をクリアしました — 基本のエージェント動作を使用します。\n_(次のメッセージから有効)_"
+ set_to: "🎭 人格を **{name}** に設定しました\n_(次のメッセージから有効)_"
+ unknown: "不明な人格: `{name}`\n\n利用可能: {available}"
+
+ profile:
+ header: "👤 **プロファイル:** `{profile}`"
+ home: "📂 **ホーム:** `{home}`"
+
+ reasoning:
+ level_default: "medium (デフォルト)"
+ level_disabled: "none (無効)"
+ scope_session: "セッションのオーバーライド"
+ scope_global: "グローバル設定"
+ status: "🧠 **推論設定**\n\n**強度:** `{level}`\n**スコープ:** {scope}\n**表示:** {display}\n\n_使い方:_ `/reasoning [--global]`"
+ display_on: "オン ✓"
+ display_off: "オフ"
+ display_set_on: "🧠 ✓ 推論表示: **オン**\n**{platform}** 上で各応答の前にモデルの思考が表示されます。"
+ display_set_off: "🧠 ✓ **{platform}** での推論表示: **オフ**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` はサポートされていません。グローバルのデフォルトを変更するには `/reasoning --global` を使用してください。"
+ reset_done: "🧠 ✓ セッションの推論オーバーライドをクリアしました。グローバル設定にフォールバックします。"
+ unknown_arg: "⚠️ 不明な引数: `{arg}`\n\n**有効なレベル:** none, minimal, low, medium, high, xhigh\n**表示:** show, hide\n**永続化:** セッションを越えて保存するには `--global` を追加"
+ set_global: "🧠 ✓ 推論強度を `{effort}` に設定しました (設定に保存)\n_(次のメッセージから有効)_"
+ set_global_save_failed: "🧠 ✓ 推論強度を `{effort}` に設定しました (セッションのみ — 設定の保存に失敗)\n_(次のメッセージから有効)_"
+ set_session: "🧠 ✓ 推論強度を `{effort}` に設定しました (セッションのみ — 永続化するには `--global` を追加)\n_(次のメッセージから有効)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp をキャンセルしました。MCP ツールは変更されていません。"
+ always_followup: "ℹ️ 今後の `/reload-mcp` は確認なしで実行されます。`config.yaml` で `approvals.mcp_reload_confirm: true` を設定すると再有効化できます。"
+ confirm_prompt: "⚠️ **/reload-mcp の確認**\n\nMCP サーバーを再読み込みすると、このセッションのツールセットが再構築され、**プロバイダーのプロンプトキャッシュが無効化されます** — 次のメッセージで完全な入力トークンが再送信されます。長コンテキストや高推論モデルではコストが高くなる可能性があります。\n\n選択してください:\n• **一度だけ承認** — 今すぐ再読み込み\n• **常に承認** — 今すぐ再読み込みし、このプロンプトを永続的に非表示\n• **キャンセル** — MCP ツールを変更しない\n\n_テキスト代替: `/approve`、`/always`、または `/cancel` と返信してください。_"
+ header: "🔄 **MCP サーバーを再読み込みしました**\n"
+ reconnected: "♻️ 再接続: {names}"
+ added: "➕ 追加: {names}"
+ removed: "➖ 削除: {names}"
+ none_connected: "接続中の MCP サーバーはありません。"
+ tools_available: "\n🔧 {servers} 台のサーバーから {tools} 個のツールが利用可能"
+ failed: "❌ MCP の再読み込みに失敗しました: {error}"
+
+ reload_skills:
+ header: "🔄 **スキルを再読み込みしました**\n"
+ no_new: "新しいスキルは検出されませんでした。"
+ total: "\n📚 {count} 個のスキルが利用可能"
+ added_header: "➕ **追加されたスキル:**"
+ removed_header: "➖ **削除されたスキル:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ スキルの再読み込みに失敗しました: {error}"
+
+ reset:
+ header_default: "✨ セッションをリセットしました。新たに開始します。"
+ header_new: "✨ 新しいセッションを開始しました。"
+ header_titled: "✨ 新しいセッションを開始しました: {title}"
+ title_rejected: "\n⚠️ タイトルが拒否されました: {error}"
+ title_error_untitled: "\n⚠️ {error} — タイトルなしでセッションを開始しました。"
+ title_empty_untitled: "\n⚠️ クリーンアップ後にタイトルが空になりました — タイトルなしでセッションを開始しました。"
+ tip: "\n✦ ヒント: {tip}"
+
+ restart:
+ in_progress: "⏳ ゲートウェイの再起動はすでに進行中です..."
+ restarting: "♻ ゲートウェイを再起動しています。60 秒以内に通知が届かない場合は、コンソールで `hermes gateway restart` を実行してください。"
+
+ resume:
+ db_unavailable: "セッションデータベースは利用できません。"
+ no_named_sessions: "名前付きセッションが見つかりません。\n`/title セッション名` で現在のセッションに名前を付けると、後で `/resume セッション名` で戻れます。"
+ list_header: "📋 **名前付きセッション**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\n使い方: `/resume <セッション名>`"
+ list_failed: "セッションを一覧表示できませんでした: {error}"
+ not_found: "'**{name}**' に一致するセッションが見つかりません。\n引数なしで `/resume` を実行すると利用可能なセッションを表示します。"
+ already_on: "📌 既にセッション **{name}** にいます。"
+ switch_failed: "セッションの切り替えに失敗しました。"
+ resumed_one: "↻ セッション **{title}** を再開しました ({count} メッセージ)。会話を復元しました。"
+ resumed_many: "↻ セッション **{title}** を再開しました ({count} メッセージ)。会話を復元しました。"
+ resumed_no_count: "↻ セッション **{title}** を再開しました。会話を復元しました。"
+
+ retry:
+ no_previous: "再試行する前のメッセージがありません。"
+
+ rollback:
+ not_enabled: "チェックポイントは有効になっていません。\nconfig.yaml で有効にしてください:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "{cwd} のチェックポイントが見つかりません"
+ invalid_number: "無効なチェックポイント番号です。1-{max} を使用してください。"
+ restored: "✅ チェックポイント {hash} に復元しました: {reason}\nロールバック前のスナップショットが自動的に保存されました。"
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "ホームチャンネルを保存できませんでした: {error}"
+ success: "✅ ホームチャンネルを **{name}** (ID: {chat_id}) に設定しました。\nCron ジョブとプラットフォーム間メッセージはここに配信されます。"
+
+ status:
+ header: "📊 **Hermes ゲートウェイ状態**"
+ session_id: "**セッション ID:** `{session_id}`"
+ title: "**タイトル:** {title}"
+ created: "**作成日時:** {timestamp}"
+ last_activity: "**最終アクティビティ:** {timestamp}"
+ tokens: "**トークン:** {tokens}"
+ agent_running: "**エージェント実行中:** {state}"
+ state_yes: "はい ⚡"
+ state_no: "いいえ"
+ queued: "**キュー内の後続:** {count}"
+ platforms: "**接続プラットフォーム:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ 停止しました。エージェントはまだ開始していません — このセッションを続行できます。"
+ stopped: "⚡ 停止しました。このセッションを続行できます。"
+ no_active: "停止できるアクティブなタスクはありません。"
+
+ title:
+ db_unavailable: "セッションデータベースは利用できません。"
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ クリーンアップ後にタイトルが空になりました。印字可能な文字を使用してください。"
+ set_to: "✏️ セッションタイトルを設定しました: **{title}**"
+ not_found: "データベースにセッションが見つかりません。"
+ current_with_title: "📌 セッション: `{session_id}`\nタイトル: **{title}**"
+ current_no_title: "📌 セッション: `{session_id}`\nタイトル未設定。使い方: `/title セッション名`"
+
+ topic:
+ not_telegram_dm: "/topic コマンドは Telegram のプライベートチャットでのみ利用できます。"
+ no_session_db: "セッションデータベースを利用できません。"
+ unauthorized: "この bot で /topic を使用する権限がありません。"
+ restore_needs_topic: "セッションを復元するには、まず Telegram topic を作成または開いてから、その topic 内で /topic を送信してください。新しい topic を作成するには、All Messages を開いて任意のメッセージを送信してください。"
+ topics_disabled: "この bot ではまだ Telegram topics が有効になっていません。\n\n有効にする方法:\n1. @BotFather を開きます。\n2. 自分の bot を選びます。\n3. Bot Settings → Threads Settings を開きます。\n4. Threaded Mode をオンにし、ユーザーが新しいスレッドを作成できるように設定します。\n\nそして /topic をもう一度送信してください。"
+ topics_user_disallowed: "Telegram topics は有効ですが、ユーザーは topic を作成できません。\n\n@BotFather → 自分の bot → Bot Settings → Threads Settings を開き、'Disallow users to create new threads' をオフにしてください。\n\nそして /topic をもう一度送信してください。"
+ enable_failed: "Telegram topic モードの有効化に失敗しました: {error}"
+ bound_status: "この topic は次にリンクされています:\nセッション: {label}\nID: {session_id}\n\nこの topic を新しいセッションに置き換えるには /new を使用してください。\n並行作業には、All Messages を開いてメッセージを送信し、別の topic を作成してください。"
+ thread_ready: "Telegram のマルチセッション topics が有効です。\n\nこの topic は独立した Hermes セッションとして使用されます。この topic の現在のセッションを置き換えるには /new を使用してください。並行作業には、All Messages を開いてメッセージを送信し、別の topic を作成してください。"
+ untitled_session: "無題のセッション"
+
+ undo:
+ nothing: "元に戻せる操作がありません。"
+ removed: "↩️ {count} 件のメッセージを取り消しました。\n削除: 「{preview}」"
+
+ update:
+ platform_not_messaging: "✗ /update はメッセージングプラットフォームでのみ利用可能です。ターミナルで `hermes update` を実行してください。"
+ not_git_repo: "✗ Git リポジトリではありません — 更新できません。"
+ hermes_cmd_not_found: "✗ `hermes` コマンドが見つかりません。Hermes は実行中ですが、更新コマンドは PATH 上にも現在の Python インタープリタ経由でも実行可能ファイルを見つけられませんでした。ターミナルで `hermes update` を手動で実行してみてください。"
+ start_failed: "✗ 更新の開始に失敗しました: {error}"
+ starting: "⚕ Hermes の更新を開始しています… 進捗をここにストリーミングします。"
+
+ usage:
+ rate_limits: "⏱️ **レート制限:** {state}"
+ header_session: "📊 **セッショントークン使用状況**"
+ label_model: "モデル: `{model}`"
+ label_input_tokens: "入力トークン: {count}"
+ label_cache_read: "キャッシュ読み取りトークン: {count}"
+ label_cache_write: "キャッシュ書き込みトークン: {count}"
+ label_output_tokens: "出力トークン: {count}"
+ label_total: "合計: {count}"
+ label_api_calls: "API 呼び出し: {count}"
+ label_cost: "コスト: {prefix}${amount}"
+ label_cost_included: "コスト: 含まれています"
+ label_context: "コンテキスト: {used} / {total} ({pct}%)"
+ label_compressions: "圧縮回数: {count}"
+ header_session_info: "📊 **セッション情報**"
+ label_messages: "メッセージ数: {count}"
+ label_estimated_context: "推定コンテキスト: ~{count} トークン"
+ detailed_after_first: "_(詳細な使用状況は最初のエージェント応答後に利用可能)_"
+ no_data: "このセッションの使用データはありません。"
+
+ verbose:
+ not_enabled: "`/verbose` コマンドはメッセージングプラットフォームで有効になっていません。\n\n`config.yaml` で有効にしてください:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ ツール進捗: **OFF** — ツールの動作は表示されません。"
+ mode_new: "⚙️ ツール進捗: **NEW** — ツールが変わったときに表示 (プレビュー長: `display.tool_preview_length`、デフォルト 40)。"
+ mode_all: "⚙️ ツール進捗: **ALL** — すべてのツール呼び出しを表示 (プレビュー長: `display.tool_preview_length`、デフォルト 40)。"
+ mode_verbose: "⚙️ ツール進捗: **VERBOSE** — すべてのツール呼び出しを完全な引数とともに表示。"
+ saved_suffix: "_(**{platform}** に保存しました — 次のメッセージから有効)_"
+ save_failed: "_(設定に保存できませんでした: {error})_"
+
+ voice:
+ enabled_voice_only: "音声モードを有効にしました。\n音声メッセージを送ると音声で返信します。\nすべてのメッセージへの音声返信は /voice tts を使ってください。"
+ disabled_text: "音声モードを無効にしました。テキストのみで返信します。"
+ tts_enabled: "自動 TTS を有効にしました。\nすべての返信に音声メッセージが含まれます。"
+ status_mode: "音声モード: {label}"
+ status_channel: "音声チャンネル: #{channel}"
+ status_participants: "参加者: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (発話中)"
+ enabled_short: "音声モードを有効にしました。"
+ disabled_short: "音声モードを無効にしました。"
+ label_off: "オフ (テキストのみ)"
+ label_voice_only: "オン (音声メッセージにのみ音声で返信)"
+ label_all: "TTS (すべてのメッセージに音声で返信)"
+
+ yolo:
+ disabled: "⚠️ このセッションの YOLO モードは **OFF** — 危険なコマンドには承認が必要です。"
+ enabled: "⚡ このセッションの YOLO モードは **ON** — すべてのコマンドが自動承認されます。注意して使用してください。"
+
+ shared:
+ session_db_unavailable: "セッションデータベースが利用できません。"
+ session_db_unavailable_prefix: "セッションデータベースが利用できません"
+ session_not_found: "データベースにセッションが見つかりません。"
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/ko.yaml b/locales/ko.yaml
new file mode 100644
index 00000000000..11f5380e319
--- /dev/null
+++ b/locales/ko.yaml
@@ -0,0 +1,350 @@
+# Hermes 정적 메시지 카탈로그 -- 한국어
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ 위험한 명령: {description}"
+ choose_long: " [o]한 번 | [s]세션 | [a]항상 | [d]거부"
+ choose_short: " [o]한 번 | [s]세션 | [d]거부"
+ prompt_long: " 선택 [o/s/a/D]: "
+ prompt_short: " 선택 [o/s/D]: "
+ timeout: " ⏱ 시간 초과 - 명령을 거부합니다"
+ allowed_once: " ✓ 한 번 허용됨"
+ allowed_session: " ✓ 이 세션에서 허용됨"
+ allowed_always: " ✓ 영구 허용 목록에 추가됨"
+ denied: " ✗ 거부됨"
+ cancelled: " ✗ 취소됨"
+ blocklist_message: "이 명령은 무조건 차단 목록에 있으며 승인할 수 없습니다."
+
+gateway:
+ approval_expired: "⚠️ 승인이 만료되었습니다 (에이전트가 더 이상 대기하지 않습니다). 에이전트에게 다시 시도하도록 요청하세요."
+ draining: "⏳ 재시작 전에 활성 에이전트 {count}명을 정리하는 중..."
+ goal_cleared: "✓ 목표가 삭제되었습니다."
+ no_active_goal: "활성 목표가 없습니다."
+ config_read_failed: "⚠️ config.yaml을 읽을 수 없습니다: {error}"
+ config_save_failed: "⚠️ 설정을 저장할 수 없습니다: {error}"
+
+ model:
+ error_prefix: "오류: {error}"
+ switched: "모델이 `{model}`(으)로 전환되었습니다"
+ provider_label: "제공자: {provider}"
+ context_label: "컨텍스트: {tokens} 토큰"
+ max_output_label: "최대 출력: {tokens} 토큰"
+ cost_label: "비용: {cost}"
+ capabilities_label: "기능: {capabilities}"
+ prompt_caching_enabled: "프롬프트 캐싱: 활성화됨"
+ warning_prefix: "경고: {warning}"
+ saved_global: "config.yaml에 저장됨 (`--global`)"
+ session_only_hint: "_(세션 한정 — 영구 저장하려면 `--global`을 추가하세요)_"
+ current_label: "현재: `{model}` ({provider})"
+ current_tag: " (현재)"
+ more_models_suffix: " (+{count}개 더 있음)"
+ usage_switch_model: "`/model ` — 모델 전환"
+ usage_switch_provider: "`/model --provider ` — 제공자 전환"
+ usage_persist: "`/model --global` — 영구 저장"
+
+ agents:
+ header: "🤖 **활성 에이전트 및 작업**"
+ active_agents: "**활성 에이전트:** {count}"
+ this_chat: " · 이 채팅"
+ more: "... 외 {count}개 더"
+ running_processes: "**실행 중인 백그라운드 프로세스:** {count}"
+ async_jobs: "**게이트웨이 비동기 작업:** {count}"
+ none: "활성 에이전트나 실행 중인 작업이 없습니다."
+ state_starting: "시작 중"
+ state_running: "실행 중"
+
+ approve:
+ no_pending: "승인 대기 중인 명령이 없습니다."
+ once_singular: "✅ 명령이 승인되었습니다. 에이전트가 재개됩니다..."
+ once_plural: "✅ 명령이 승인되었습니다 ({count}개). 에이전트가 재개됩니다..."
+ session_singular: "✅ 명령이 승인되었습니다 (이 세션 동안 패턴 승인됨). 에이전트가 재개됩니다..."
+ session_plural: "✅ 명령이 승인되었습니다 (이 세션 동안 패턴 승인됨) ({count}개). 에이전트가 재개됩니다..."
+ always_singular: "✅ 명령이 승인되었습니다 (패턴 영구 승인됨). 에이전트가 재개됩니다..."
+ always_plural: "✅ 명령이 승인되었습니다 (패턴 영구 승인됨) ({count}개). 에이전트가 재개됩니다..."
+
+ background:
+ usage: "사용법: /background \n예시: /background 오늘 HN 인기 글을 요약해줘\n\n프롬프트를 별도 세션에서 실행합니다. 계속 대화할 수 있으며, 완료되면 결과가 여기에 표시됩니다."
+ started: "🔄 백그라운드 작업이 시작되었습니다: \"{preview}\"\n작업 ID: {task_id}\n계속 대화하실 수 있습니다 — 완료되면 결과가 여기에 표시됩니다."
+
+ branch:
+ db_unavailable: "세션 데이터베이스를 사용할 수 없습니다."
+ no_conversation: "분기할 대화가 없습니다 — 먼저 메시지를 보내주세요."
+ create_failed: "분기 생성에 실패했습니다: {error}"
+ switch_failed: "분기는 생성되었으나 전환에 실패했습니다."
+ branched_one: "⑂ **{title}**(으)로 분기했습니다 (메시지 {count}개 복사됨)\n원본: `{parent}`\n분기: `{new}`\n원본으로 돌아가려면 `/resume`을 사용하세요."
+ branched_many: "⑂ **{title}**(으)로 분기했습니다 (메시지 {count}개 복사됨)\n원본: `{parent}`\n분기: `{new}`\n원본으로 돌아가려면 `/resume`을 사용하세요."
+
+ commands:
+ usage: "사용법: `/commands [page]`"
+ skill_header: "⚡ **스킬 명령**:"
+ default_desc: "스킬 명령"
+ none: "사용 가능한 명령이 없습니다."
+ header: "📚 **명령 목록** (총 {total}개, {page}/{total_pages} 페이지)"
+ nav_prev: "`/commands {page}` ← 이전"
+ nav_next: "다음 → `/commands {page}`"
+ out_of_range: "_(요청한 페이지 {requested}이(가) 범위를 벗어났습니다. {page} 페이지를 표시합니다.)_"
+
+ compress:
+ not_enough: "압축할 대화가 충분하지 않습니다 (최소 4개의 메시지가 필요합니다)."
+ no_provider: "구성된 제공자가 없습니다 -- 압축할 수 없습니다."
+ nothing_to_do: "아직 압축할 내용이 없습니다 (대화 내용이 모두 보호된 컨텍스트입니다)."
+ focus_line: "초점: \"{topic}\""
+ summary_failed: "⚠️ 요약 생성에 실패했습니다 ({error}). 과거 메시지 {count}개가 제거되어 자리표시자로 대체되었으며, 이전 컨텍스트는 더 이상 복구할 수 없습니다. auxiliary.compression 모델 설정을 확인해 보세요."
+ aux_failed: "ℹ️ 구성된 압축 모델 `{model}`이(가) 실패했습니다 ({error}). 메인 모델로 복구되어 컨텍스트는 보존되었지만, config.yaml의 `auxiliary.compression.model` 설정을 확인하는 것이 좋습니다."
+ failed: "압축 실패: {error}"
+
+ debug:
+ upload_failed: "✗ 디버그 보고서 업로드 실패: {error}"
+ header: "**디버그 보고서가 업로드되었습니다:**"
+ auto_delete: "⏱ 페이스트는 6시간 후 자동 삭제됩니다."
+ full_logs_hint: "전체 로그 업로드는 CLI에서 `hermes debug share`를 사용하세요."
+ share_hint: "지원을 받으려면 이 링크를 Hermes 팀과 공유하세요."
+
+ deny:
+ stale: "❌ 명령이 거부되었습니다 (승인이 만료됨)."
+ no_pending: "거부 대기 중인 명령이 없습니다."
+ denied_singular: "❌ 명령이 거부되었습니다."
+ denied_plural: "❌ 명령이 거부되었습니다 ({count}개)."
+
+ fast:
+ not_supported: "⚡ /fast는 Priority Processing을 지원하는 OpenAI 모델에서만 사용할 수 있습니다."
+ status: "⚡ Priority Processing\n\n현재 모드: `{mode}`\n\n_사용법:_ `/fast `"
+ unknown_arg: "⚠️ 알 수 없는 인수: `{arg}`\n\n**유효한 옵션:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (설정에 저장됨)\n_(다음 메시지부터 적용됩니다)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (이 세션에만 적용)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 런타임 푸터: **{state}**\n필드: `{fields}`\n플랫폼: `{platform}`"
+ usage: "사용법: `/footer [on|off|status]`"
+ saved: "📎 런타임 푸터: **{state}**{example}\n_(전역 저장됨 — 다음 메시지부터 적용됩니다)_"
+ example_line: "\n예시: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "이 세션에서는 목표 기능을 사용할 수 없습니다."
+ no_goal_set: "설정된 목표가 없습니다."
+ paused: "⏸ 목표 일시정지: {goal}"
+ no_resume: "재개할 목표가 없습니다."
+ resumed: "▶ 목표 재개: {goal}\n메시지를 보내 계속하거나 기다려 주세요 — 다음 차례에 다음 단계를 진행하겠습니다."
+ invalid: "잘못된 목표: {error}"
+ set: "⊙ 목표 설정됨 ({budget}회 예산): {goal}\n목표가 완료되거나, 일시정지/삭제하거나, 예산이 소진될 때까지 계속 작업하겠습니다.\n제어: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes 명령**\n"
+ skill_header: "\n⚡ **스킬 명령** ({count}개 활성):"
+ more_use_commands: "\n... 외 {count}개 더. 전체 목록은 `/commands`로 확인하세요."
+
+ insights:
+ invalid_days: "잘못된 --days 값: {value}"
+ error: "인사이트 생성 중 오류: {error}"
+
+ kanban:
+ error_prefix: "⚠ kanban 오류: {error}"
+ subscribed_suffix: "(구독 중 — {task_id}이(가) 완료되거나 차단되면 알림을 받습니다)"
+ truncated_suffix: "… (잘림; 전체 출력을 보려면 터미널에서 `hermes kanban …`을 사용하세요)"
+ no_output: "(출력 없음)"
+
+ personality:
+ none_configured: "`{path}/config.yaml`에 구성된 성격이 없습니다"
+ header: "🎭 **사용 가능한 성격**\n"
+ none_option: "• `none` — (성격 오버레이 없음)"
+ item: "• `{name}` — {preview}"
+ usage: "\n사용법: `/personality `"
+ save_failed: "⚠️ 성격 변경 저장에 실패했습니다: {error}"
+ cleared: "🎭 성격이 해제되었습니다 — 기본 에이전트 동작을 사용합니다.\n_(다음 메시지부터 적용됩니다)_"
+ set_to: "🎭 성격이 **{name}**(으)로 설정되었습니다\n_(다음 메시지부터 적용됩니다)_"
+ unknown: "알 수 없는 성격: `{name}`\n\n사용 가능: {available}"
+
+ profile:
+ header: "👤 **프로필:** `{profile}`"
+ home: "📂 **홈:** `{home}`"
+
+ reasoning:
+ level_default: "medium (기본값)"
+ level_disabled: "none (비활성화됨)"
+ scope_session: "세션 재정의"
+ scope_global: "전역 설정"
+ status: "🧠 **추론 설정**\n\n**노력:** `{level}`\n**범위:** {scope}\n**표시:** {display}\n\n_사용법:_ `/reasoning [--global]`"
+ display_on: "켜짐 ✓"
+ display_off: "꺼짐"
+ display_set_on: "🧠 ✓ 추론 표시: **켜짐**\n**{platform}**에서 응답 전에 모델의 사고 과정이 표시됩니다."
+ display_set_off: "🧠 ✓ 추론 표시: **꺼짐** (**{platform}**에서)"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global`은 지원되지 않습니다. 전역 기본값을 변경하려면 `/reasoning --global`을 사용하세요."
+ reset_done: "🧠 ✓ 세션 추론 재정의가 해제되었습니다. 전역 설정으로 돌아갑니다."
+ unknown_arg: "⚠️ 알 수 없는 인수: `{arg}`\n\n**유효한 수준:** none, minimal, low, medium, high, xhigh\n**표시:** show, hide\n**영구화:** 이 세션을 넘어 저장하려면 `--global`을 추가하세요"
+ set_global: "🧠 ✓ 추론 노력이 `{effort}`(으)로 설정되었습니다 (설정에 저장됨)\n_(다음 메시지부터 적용됩니다)_"
+ set_global_save_failed: "🧠 ✓ 추론 노력이 `{effort}`(으)로 설정되었습니다 (세션 한정 — 설정 저장 실패)\n_(다음 메시지부터 적용됩니다)_"
+ set_session: "🧠 ✓ 추론 노력이 `{effort}`(으)로 설정되었습니다 (세션 한정 — 영구 저장하려면 `--global` 추가)\n_(다음 메시지부터 적용됩니다)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp가 취소되었습니다. MCP 도구는 변경되지 않았습니다."
+ always_followup: "ℹ️ 이후 `/reload-mcp` 호출은 확인 없이 실행됩니다. config.yaml의 `approvals.mcp_reload_confirm: true`로 다시 활성화할 수 있습니다."
+ confirm_prompt: "⚠️ **/reload-mcp 확인**\n\nMCP 서버를 재로드하면 이 세션의 도구 세트가 재구성되며 **제공자 프롬프트 캐시가 무효화됩니다** — 다음 메시지에서 전체 입력 토큰이 다시 전송됩니다. 긴 컨텍스트 또는 고도 추론 모델에서는 비용이 클 수 있습니다.\n\n선택하세요:\n• **한 번 승인** — 지금 재로드\n• **항상 승인** — 지금 재로드하고 이 프롬프트를 영구 비활성화\n• **취소** — MCP 도구를 변경하지 않음\n\n_텍스트 대체: `/approve`, `/always`, `/cancel`로 응답하세요._"
+ header: "🔄 **MCP 서버가 재로드되었습니다**\n"
+ reconnected: "♻️ 재연결됨: {names}"
+ added: "➕ 추가됨: {names}"
+ removed: "➖ 제거됨: {names}"
+ none_connected: "연결된 MCP 서버가 없습니다."
+ tools_available: "\n🔧 {servers}개 서버에서 {tools}개 도구 사용 가능"
+ failed: "❌ MCP 재로드 실패: {error}"
+
+ reload_skills:
+ header: "🔄 **스킬이 재로드되었습니다**\n"
+ no_new: "새로운 스킬이 감지되지 않았습니다."
+ total: "\n📚 {count}개 스킬 사용 가능"
+ added_header: "➕ **추가된 스킬:**"
+ removed_header: "➖ **제거된 스킬:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ 스킬 재로드 실패: {error}"
+
+ reset:
+ header_default: "✨ 세션이 초기화되었습니다! 새로 시작합니다."
+ header_new: "✨ 새 세션이 시작되었습니다!"
+ header_titled: "✨ 새 세션이 시작되었습니다: {title}"
+ title_rejected: "\n⚠️ 제목이 거부되었습니다: {error}"
+ title_error_untitled: "\n⚠️ {error} — 제목 없이 세션을 시작했습니다."
+ title_empty_untitled: "\n⚠️ 정리 후 제목이 비어 있습니다 — 제목 없이 세션을 시작했습니다."
+ tip: "\n✦ 팁: {tip}"
+
+ restart:
+ in_progress: "⏳ 게이트웨이 재시작이 이미 진행 중입니다..."
+ restarting: "♻ 게이트웨이를 재시작 중입니다. 60초 이내에 알림이 오지 않으면 콘솔에서 `hermes gateway restart`로 재시작하세요."
+
+ resume:
+ db_unavailable: "세션 데이터베이스를 사용할 수 없습니다."
+ no_named_sessions: "이름이 지정된 세션이 없습니다.\n현재 세션에 이름을 지정하려면 `/title 내 세션`을 사용하고, 나중에 `/resume 내 세션`으로 돌아오세요."
+ list_header: "📋 **이름이 지정된 세션**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\n사용법: `/resume `"
+ list_failed: "세션 목록을 가져올 수 없습니다: {error}"
+ not_found: "'**{name}**'와 일치하는 세션이 없습니다.\n사용 가능한 세션을 보려면 인수 없이 `/resume`을 사용하세요."
+ already_on: "📌 이미 **{name}** 세션에 있습니다."
+ switch_failed: "세션 전환에 실패했습니다."
+ resumed_one: "↻ **{title}** 세션 재개됨 (메시지 {count}개). 대화가 복원되었습니다."
+ resumed_many: "↻ **{title}** 세션 재개됨 (메시지 {count}개). 대화가 복원되었습니다."
+ resumed_no_count: "↻ **{title}** 세션 재개됨. 대화가 복원되었습니다."
+
+ retry:
+ no_previous: "재시도할 이전 메시지가 없습니다."
+
+ rollback:
+ not_enabled: "체크포인트가 활성화되어 있지 않습니다.\nconfig.yaml에서 활성화하세요:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "{cwd}에 체크포인트를 찾을 수 없습니다"
+ invalid_number: "잘못된 체크포인트 번호입니다. 1-{max}을 사용하세요."
+ restored: "✅ 체크포인트 {hash}(으)로 복원됨: {reason}\n롤백 전 스냅샷이 자동으로 저장되었습니다."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "홈 채널 저장에 실패했습니다: {error}"
+ success: "✅ 홈 채널이 **{name}**(ID: {chat_id})(으)로 설정되었습니다.\n크론 작업과 플랫폼 간 메시지가 여기로 전달됩니다."
+
+ status:
+ header: "📊 **Hermes 게이트웨이 상태**"
+ session_id: "**세션 ID:** `{session_id}`"
+ title: "**제목:** {title}"
+ created: "**생성됨:** {timestamp}"
+ last_activity: "**최종 활동:** {timestamp}"
+ tokens: "**토큰:** {tokens}"
+ agent_running: "**에이전트 실행 중:** {state}"
+ state_yes: "예 ⚡"
+ state_no: "아니오"
+ queued: "**대기 중인 후속 작업:** {count}"
+ platforms: "**연결된 플랫폼:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ 중지되었습니다. 에이전트가 아직 시작되지 않았습니다 — 이 세션을 계속할 수 있습니다."
+ stopped: "⚡ 중지되었습니다. 이 세션을 계속할 수 있습니다."
+ no_active: "중지할 활성 작업이 없습니다."
+
+ title:
+ db_unavailable: "세션 데이터베이스를 사용할 수 없습니다."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ 정리 후 제목이 비어 있습니다. 인쇄 가능한 문자를 사용해 주세요."
+ set_to: "✏️ 세션 제목 설정됨: **{title}**"
+ not_found: "데이터베이스에서 세션을 찾을 수 없습니다."
+ current_with_title: "📌 세션: `{session_id}`\n제목: **{title}**"
+ current_no_title: "📌 세션: `{session_id}`\n제목이 설정되지 않았습니다. 사용법: `/title 내 세션 이름`"
+
+ topic:
+ not_telegram_dm: "/topic 명령은 Telegram 비공개 채팅에서만 사용할 수 있습니다."
+ no_session_db: "세션 데이터베이스를 사용할 수 없습니다."
+ unauthorized: "이 봇에서 /topic을 사용할 권한이 없습니다."
+ restore_needs_topic: "세션을 복원하려면 먼저 Telegram 토픽을 만들거나 열고, 해당 토픽 안에서 /topic 를 보내세요. 새 토픽을 만들려면 All Messages를 열고 그곳으로 메시지를 보내세요."
+ topics_disabled: "이 봇에는 아직 Telegram 토픽이 활성화되어 있지 않습니다.\n\n활성화 방법:\n1. @BotFather를 엽니다.\n2. 봇을 선택합니다.\n3. Bot Settings → Threads Settings를 엽니다.\n4. Threaded Mode를 켜고 사용자가 새 스레드를 만들 수 있도록 허용합니다.\n\n그런 다음 다시 /topic을 보내세요."
+ topics_user_disallowed: "Telegram 토픽이 활성화되어 있지만, 사용자가 토픽을 만들 수 없습니다.\n\n@BotFather → 봇 선택 → Bot Settings → Threads Settings를 열고 'Disallow users to create new threads'를 끄세요.\n\n그런 다음 다시 /topic을 보내세요."
+ enable_failed: "Telegram 토픽 모드 활성화에 실패했습니다: {error}"
+ bound_status: "이 토픽은 다음에 연결되어 있습니다:\n세션: {label}\nID: {session_id}\n\n이 토픽을 새 세션으로 교체하려면 /new를 사용하세요.\n병렬 작업을 위해서는 All Messages를 열고 메시지를 보내 다른 토픽을 만드세요."
+ thread_ready: "Telegram 다중 세션 토픽이 활성화되었습니다.\n\n이 토픽은 독립된 Hermes 세션으로 사용됩니다. 이 토픽의 현재 세션을 교체하려면 /new를 사용하세요. 병렬 작업을 위해서는 All Messages를 열고 메시지를 보내 다른 토픽을 만드세요."
+ untitled_session: "제목 없는 세션"
+
+ undo:
+ nothing: "되돌릴 내용이 없습니다."
+ removed: "↩️ 메시지 {count}개를 되돌렸습니다.\n제거됨: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update는 메시징 플랫폼에서만 사용할 수 있습니다. 터미널에서 `hermes update`를 실행하세요."
+ not_git_repo: "✗ git 저장소가 아닙니다 — 업데이트할 수 없습니다."
+ hermes_cmd_not_found: "✗ `hermes` 명령을 찾을 수 없습니다. Hermes는 실행 중이지만 PATH나 현재 Python 인터프리터를 통해 실행 파일을 찾을 수 없습니다. 터미널에서 `hermes update`를 직접 실행해 보세요."
+ start_failed: "✗ 업데이트 시작 실패: {error}"
+ starting: "⚕ Hermes 업데이트를 시작합니다… 진행 상황을 여기에 스트리밍하겠습니다."
+
+ usage:
+ rate_limits: "⏱️ **요청 제한:** {state}"
+ header_session: "📊 **세션 토큰 사용량**"
+ label_model: "모델: `{model}`"
+ label_input_tokens: "입력 토큰: {count}"
+ label_cache_read: "캐시 읽기 토큰: {count}"
+ label_cache_write: "캐시 쓰기 토큰: {count}"
+ label_output_tokens: "출력 토큰: {count}"
+ label_total: "합계: {count}"
+ label_api_calls: "API 호출: {count}"
+ label_cost: "비용: {prefix}${amount}"
+ label_cost_included: "비용: 포함됨"
+ label_context: "컨텍스트: {used} / {total} ({pct}%)"
+ label_compressions: "압축: {count}"
+ header_session_info: "📊 **세션 정보**"
+ label_messages: "메시지: {count}"
+ label_estimated_context: "예상 컨텍스트: 약 {count} 토큰"
+ detailed_after_first: "_(자세한 사용량은 첫 에이전트 응답 이후 확인할 수 있습니다)_"
+ no_data: "이 세션에 사용 가능한 사용량 데이터가 없습니다."
+
+ verbose:
+ not_enabled: "`/verbose` 명령은 메시징 플랫폼에서 활성화되어 있지 않습니다.\n\n`config.yaml`에서 활성화하세요:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ 도구 진행 상황: **OFF** — 도구 활동이 표시되지 않습니다."
+ mode_new: "⚙️ 도구 진행 상황: **NEW** — 도구가 변경될 때 표시됩니다 (미리보기 길이: `display.tool_preview_length`, 기본 40)."
+ mode_all: "⚙️ 도구 진행 상황: **ALL** — 모든 도구 호출이 표시됩니다 (미리보기 길이: `display.tool_preview_length`, 기본 40)."
+ mode_verbose: "⚙️ 도구 진행 상황: **VERBOSE** — 모든 도구 호출이 전체 인수와 함께 표시됩니다."
+ saved_suffix: "_(**{platform}**에 저장됨 — 다음 메시지부터 적용됩니다)_"
+ save_failed: "_(설정에 저장할 수 없습니다: {error})_"
+
+ voice:
+ enabled_voice_only: "음성 모드가 활성화되었습니다.\n음성 메시지를 보내시면 음성으로 답변하겠습니다.\n모든 메시지에 대해 음성으로 응답받으려면 /voice tts를 사용하세요."
+ disabled_text: "음성 모드가 비활성화되었습니다. 텍스트로만 응답합니다."
+ tts_enabled: "자동 TTS가 활성화되었습니다.\n모든 응답에 음성 메시지가 포함됩니다."
+ status_mode: "음성 모드: {label}"
+ status_channel: "음성 채널: #{channel}"
+ status_participants: "참가자: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (말하는 중)"
+ enabled_short: "음성 모드가 활성화되었습니다."
+ disabled_short: "음성 모드가 비활성화되었습니다."
+ label_off: "꺼짐 (텍스트 전용)"
+ label_voice_only: "켜짐 (음성 메시지에 음성으로 응답)"
+ label_all: "TTS (모든 메시지에 음성으로 응답)"
+
+ yolo:
+ disabled: "⚠️ 이 세션에서 YOLO 모드 **꺼짐** — 위험한 명령은 승인이 필요합니다."
+ enabled: "⚡ 이 세션에서 YOLO 모드 **켜짐** — 모든 명령이 자동 승인됩니다. 주의해서 사용하세요."
+
+ shared:
+ session_db_unavailable: "세션 데이터베이스를 사용할 수 없습니다."
+ session_db_unavailable_prefix: "세션 데이터베이스를 사용할 수 없습니다"
+ session_not_found: "데이터베이스에서 세션을 찾을 수 없습니다."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/pt.yaml b/locales/pt.yaml
new file mode 100644
index 00000000000..e74c218d6ba
--- /dev/null
+++ b/locales/pt.yaml
@@ -0,0 +1,350 @@
+# Catálogo de mensagens estáticas do Hermes -- Português
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ COMANDO PERIGOSO: {description}"
+ choose_long: " [o]uma vez | [s]sessão | [a]sempre | [d]negar"
+ choose_short: " [o]uma vez | [s]sessão | [d]negar"
+ prompt_long: " Escolha [o/s/a/D]: "
+ prompt_short: " Escolha [o/s/D]: "
+ timeout: " ⏱ Tempo esgotado — comando negado"
+ allowed_once: " ✓ Permitido uma vez"
+ allowed_session: " ✓ Permitido nesta sessão"
+ allowed_always: " ✓ Adicionado à lista de permissões permanente"
+ denied: " ✗ Negado"
+ cancelled: " ✗ Cancelado"
+ blocklist_message: "Este comando está na lista de bloqueio incondicional e não pode ser aprovado."
+
+gateway:
+ approval_expired: "⚠️ A aprovação expirou (o agente já não está à espera). Peça ao agente para tentar novamente."
+ draining: "⏳ A aguardar que {count} agente(s) ativo(s) terminem antes de reiniciar..."
+ goal_cleared: "✓ Objetivo removido."
+ no_active_goal: "Não há objetivo ativo."
+ config_read_failed: "⚠️ Não foi possível ler config.yaml: {error}"
+ config_save_failed: "⚠️ Não foi possível guardar a configuração: {error}"
+
+ model:
+ error_prefix: "Erro: {error}"
+ switched: "Modelo alterado para `{model}`"
+ provider_label: "Fornecedor: {provider}"
+ context_label: "Contexto: {tokens} tokens"
+ max_output_label: "Saída máxima: {tokens} tokens"
+ cost_label: "Custo: {cost}"
+ capabilities_label: "Capacidades: {capabilities}"
+ prompt_caching_enabled: "Cache de prompts: ativado"
+ warning_prefix: "Aviso: {warning}"
+ saved_global: "Guardado em config.yaml (`--global`)"
+ session_only_hint: "_(apenas para esta sessão — adiciona `--global` para tornar permanente)_"
+ current_label: "Atual: `{model}` em {provider}"
+ current_tag: " (atual)"
+ more_models_suffix: " (+{count} mais)"
+ usage_switch_model: "`/model ` — mudar de modelo"
+ usage_switch_provider: "`/model --provider ` — mudar de fornecedor"
+ usage_persist: "`/model --global` — guardar permanentemente"
+
+ agents:
+ header: "🤖 **Agentes e tarefas ativos**"
+ active_agents: "**Agentes ativos:** {count}"
+ this_chat: " · este chat"
+ more: "... e mais {count}"
+ running_processes: "**Processos em segundo plano em execução:** {count}"
+ async_jobs: "**Tarefas assíncronas do gateway:** {count}"
+ none: "Não há agentes ativos nem tarefas em execução."
+ state_starting: "a iniciar"
+ state_running: "em execução"
+
+ approve:
+ no_pending: "Não há nenhum comando pendente para aprovar."
+ once_singular: "✅ Comando aprovado. O agente está a retomar..."
+ once_plural: "✅ Comandos aprovados ({count} comandos). O agente está a retomar..."
+ session_singular: "✅ Comando aprovado (padrão aprovado para esta sessão). O agente está a retomar..."
+ session_plural: "✅ Comandos aprovados (padrão aprovado para esta sessão) ({count} comandos). O agente está a retomar..."
+ always_singular: "✅ Comando aprovado (padrão aprovado permanentemente). O agente está a retomar..."
+ always_plural: "✅ Comandos aprovados (padrão aprovado permanentemente) ({count} comandos). O agente está a retomar..."
+
+ background:
+ usage: "Uso: /background \nExemplo: /background Resume as principais histórias do HN de hoje\n\nExecuta o prompt numa sessão separada. Podes continuar a conversar — o resultado aparecerá aqui quando estiver concluído."
+ started: "🔄 Tarefa em segundo plano iniciada: \"{preview}\"\nID da tarefa: {task_id}\nPodes continuar a conversar — os resultados aparecerão aqui quando estiverem prontos."
+
+ branch:
+ db_unavailable: "Base de dados de sessões indisponível."
+ no_conversation: "Não há conversa para ramificar — envia uma mensagem primeiro."
+ create_failed: "Falha ao criar ramo: {error}"
+ switch_failed: "Ramo criado, mas não foi possível mudar para ele."
+ branched_one: "⑂ Ramificado para **{title}** ({count} mensagem copiada)\nOriginal: `{parent}`\nRamo: `{new}`\nUsa `/resume` para voltar ao original."
+ branched_many: "⑂ Ramificado para **{title}** ({count} mensagens copiadas)\nOriginal: `{parent}`\nRamo: `{new}`\nUsa `/resume` para voltar ao original."
+
+ commands:
+ usage: "Uso: `/commands [page]`"
+ skill_header: "⚡ **Comandos de skill**:"
+ default_desc: "Comando de skill"
+ none: "Não há comandos disponíveis."
+ header: "📚 **Comandos** ({total} no total, página {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← anterior"
+ nav_next: "seguinte → `/commands {page}`"
+ out_of_range: "_(A página solicitada {requested} estava fora do intervalo, a mostrar a página {page}.)_"
+
+ compress:
+ not_enough: "Não há conversa suficiente para comprimir (são necessárias pelo menos 4 mensagens)."
+ no_provider: "Nenhum fornecedor configurado — não é possível comprimir."
+ nothing_to_do: "Ainda não há nada para comprimir (a transcrição continua a ser todo o contexto protegido)."
+ focus_line: "Foco: \"{topic}\""
+ summary_failed: "⚠️ Falha ao gerar o resumo ({error}). {count} mensagem(ns) histórica(s) foram removidas e substituídas por um marcador; o contexto anterior já não pode ser recuperado. Considera verificar a configuração do modelo auxiliary.compression."
+ aux_failed: "ℹ️ O modelo de compressão configurado `{model}` falhou ({error}). Recuperado com o teu modelo principal — o contexto está intacto — mas talvez queiras verificar `auxiliary.compression.model` em config.yaml."
+ failed: "Compressão falhou: {error}"
+
+ debug:
+ upload_failed: "✗ Falha ao carregar relatório de depuração: {error}"
+ header: "**Relatório de depuração carregado:**"
+ auto_delete: "⏱ Os pastes serão eliminados automaticamente em 6 horas."
+ full_logs_hint: "Para enviar logs completos, usa `hermes debug share` a partir da CLI."
+ share_hint: "Partilha estes links com a equipa do Hermes para obter suporte."
+
+ deny:
+ stale: "❌ Comando negado (a aprovação tinha expirado)."
+ no_pending: "Não há nenhum comando pendente para negar."
+ denied_singular: "❌ Comando negado."
+ denied_plural: "❌ Comandos negados ({count} comandos)."
+
+ fast:
+ not_supported: "⚡ /fast só está disponível para modelos da OpenAI que suportam Priority Processing."
+ status: "⚡ Priority Processing\n\nModo atual: `{mode}`\n\n_Uso:_ `/fast `"
+ unknown_arg: "⚠️ Argumento desconhecido: `{arg}`\n\n**Opções válidas:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (guardado na configuração)\n_(produz efeito na próxima mensagem)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (apenas esta sessão)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Rodapé de execução: **{state}**\nCampos: `{fields}`\nPlataforma: `{platform}`"
+ usage: "Uso: `/footer [on|off|status]`"
+ saved: "📎 Rodapé de execução: **{state}**{example}\n_(guardado globalmente — produz efeito na próxima mensagem)_"
+ example_line: "\nExemplo: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Os objetivos não estão disponíveis nesta sessão."
+ no_goal_set: "Nenhum objetivo definido."
+ paused: "⏸ Objetivo pausado: {goal}"
+ no_resume: "Nenhum objetivo para retomar."
+ resumed: "▶ Objetivo retomado: {goal}\nEnvia qualquer mensagem para continuar, ou aguarda — darei o próximo passo no próximo turno."
+ invalid: "Objetivo inválido: {error}"
+ set: "⊙ Objetivo definido (orçamento de {budget} turnos): {goal}\nVou continuar a trabalhar até o objetivo estar concluído, pausares/limpares ou o orçamento esgotar.\nControlos: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Comandos do Hermes**\n"
+ skill_header: "\n⚡ **Comandos de skill** ({count} ativos):"
+ more_use_commands: "\n... e mais {count}. Usa `/commands` para a lista paginada completa."
+
+ insights:
+ invalid_days: "Valor --days inválido: {value}"
+ error: "Erro ao gerar análise: {error}"
+
+ kanban:
+ error_prefix: "⚠ erro do kanban: {error}"
+ subscribed_suffix: "(subscrito — receberás uma notificação quando {task_id} terminar ou bloquear)"
+ truncated_suffix: "… (truncado; usa `hermes kanban …` no teu terminal para a saída completa)"
+ no_output: "(sem saída)"
+
+ personality:
+ none_configured: "Nenhuma personalidade configurada em `{path}/config.yaml`"
+ header: "🎭 **Personalidades disponíveis**\n"
+ none_option: "• `none` — (sem sobreposição de personalidade)"
+ item: "• `{name}` — {preview}"
+ usage: "\nUso: `/personality `"
+ save_failed: "⚠️ Falha ao guardar a alteração de personalidade: {error}"
+ cleared: "🎭 Personalidade removida — a usar o comportamento base do agente.\n_(produz efeito na próxima mensagem)_"
+ set_to: "🎭 Personalidade definida como **{name}**\n_(produz efeito na próxima mensagem)_"
+ unknown: "Personalidade desconhecida: `{name}`\n\nDisponíveis: {available}"
+
+ profile:
+ header: "👤 **Perfil:** `{profile}`"
+ home: "📂 **Início:** `{home}`"
+
+ reasoning:
+ level_default: "medium (predefinido)"
+ level_disabled: "none (desativado)"
+ scope_session: "substituição de sessão"
+ scope_global: "configuração global"
+ status: "🧠 **Definições de raciocínio**\n\n**Esforço:** `{level}`\n**Âmbito:** {scope}\n**Visualização:** {display}\n\n_Uso:_ `/reasoning [--global]`"
+ display_on: "ativada ✓"
+ display_off: "desativada"
+ display_set_on: "🧠 ✓ Visualização do raciocínio: **ATIVADA**\nO pensamento do modelo será mostrado antes de cada resposta em **{platform}**."
+ display_set_off: "🧠 ✓ Visualização do raciocínio: **DESATIVADA** para **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` não é suportado. Usa `/reasoning --global` para alterar o predefinido global."
+ reset_done: "🧠 ✓ Substituição de raciocínio da sessão removida; a regressar à configuração global."
+ unknown_arg: "⚠️ Argumento desconhecido: `{arg}`\n\n**Níveis válidos:** none, minimal, low, medium, high, xhigh\n**Visualização:** show, hide\n**Persistir:** adiciona `--global` para guardar para além desta sessão"
+ set_global: "🧠 ✓ Esforço de raciocínio definido como `{effort}` (guardado na configuração)\n_(produz efeito na próxima mensagem)_"
+ set_global_save_failed: "🧠 ✓ Esforço de raciocínio definido como `{effort}` (apenas sessão — falha ao guardar a configuração)\n_(produz efeito na próxima mensagem)_"
+ set_session: "🧠 ✓ Esforço de raciocínio definido como `{effort}` (apenas sessão — adiciona `--global` para persistir)\n_(produz efeito na próxima mensagem)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp cancelado. As ferramentas MCP não foram alteradas."
+ always_followup: "ℹ️ Próximas chamadas a `/reload-mcp` serão executadas sem confirmação. Reativa através de `approvals.mcp_reload_confirm: true` em `config.yaml`."
+ confirm_prompt: "⚠️ **Confirmar /reload-mcp**\n\nRecarregar os servidores MCP reconstrói o conjunto de ferramentas desta sessão e **invalida a cache de prompt do fornecedor** — a próxima mensagem reenviará os tokens de entrada completos. Em modelos de contexto longo ou de raciocínio elevado isto pode ser dispendioso.\n\nEscolhe:\n• **Aprovar uma vez** — recarregar agora\n• **Aprovar sempre** — recarregar agora e silenciar este pedido permanentemente\n• **Cancelar** — manter as ferramentas MCP inalteradas\n\n_Alternativa em texto: responde `/approve`, `/always` ou `/cancel`._"
+ header: "🔄 **Servidores MCP recarregados**\n"
+ reconnected: "♻️ Reconectados: {names}"
+ added: "➕ Adicionados: {names}"
+ removed: "➖ Removidos: {names}"
+ none_connected: "Não há servidores MCP ligados."
+ tools_available: "\n🔧 {tools} ferramenta(s) disponíveis de {servers} servidor(es)"
+ failed: "❌ Falha ao recarregar MCP: {error}"
+
+ reload_skills:
+ header: "🔄 **Skills recarregadas**\n"
+ no_new: "Não foram detetadas novas skills."
+ total: "\n📚 {count} skill(s) disponíveis"
+ added_header: "➕ **Skills adicionadas:**"
+ removed_header: "➖ **Skills removidas:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Falha ao recarregar skills: {error}"
+
+ reset:
+ header_default: "✨ Sessão reiniciada! A começar do zero."
+ header_new: "✨ Nova sessão iniciada!"
+ header_titled: "✨ Nova sessão iniciada: {title}"
+ title_rejected: "\n⚠️ Título rejeitado: {error}"
+ title_error_untitled: "\n⚠️ {error} — sessão iniciada sem título."
+ title_empty_untitled: "\n⚠️ O título fica vazio após a limpeza — sessão iniciada sem título."
+ tip: "\n✦ Dica: {tip}"
+
+ restart:
+ in_progress: "⏳ O reinício do gateway já está em curso..."
+ restarting: "♻ A reiniciar o gateway. Se não fores notificado em 60 segundos, reinicia a partir da consola com `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "Base de dados de sessões indisponível."
+ no_named_sessions: "Não foram encontradas sessões com nome.\nUsa `/title A minha sessão` para nomear a sessão atual e depois `/resume A minha sessão` para voltar a ela."
+ list_header: "📋 **Sessões com nome**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nUso: `/resume `"
+ list_failed: "Não foi possível listar as sessões: {error}"
+ not_found: "Não foi encontrada nenhuma sessão correspondente a '**{name}**'.\nUsa `/resume` sem argumentos para ver as sessões disponíveis."
+ already_on: "📌 Já estás na sessão **{name}**."
+ switch_failed: "Falha ao mudar de sessão."
+ resumed_one: "↻ Sessão **{title}** retomada ({count} mensagem). Conversa restaurada."
+ resumed_many: "↻ Sessão **{title}** retomada ({count} mensagens). Conversa restaurada."
+ resumed_no_count: "↻ Sessão **{title}** retomada. Conversa restaurada."
+
+ retry:
+ no_previous: "Não há mensagem anterior para tentar novamente."
+
+ rollback:
+ not_enabled: "Os checkpoints não estão ativados.\nAtiva-os em config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Não foram encontrados checkpoints para {cwd}"
+ invalid_number: "Número de checkpoint inválido. Usa 1-{max}."
+ restored: "✅ Restaurado para o checkpoint {hash}: {reason}\nFoi guardado automaticamente um snapshot anterior ao rollback."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Falha ao guardar o canal principal: {error}"
+ success: "✅ Canal principal definido como **{name}** (ID: {chat_id}).\nAs tarefas cron e mensagens entre plataformas serão entregues aqui."
+
+ status:
+ header: "📊 **Estado do Hermes Gateway**"
+ session_id: "**ID da sessão:** `{session_id}`"
+ title: "**Título:** {title}"
+ created: "**Criada:** {timestamp}"
+ last_activity: "**Última atividade:** {timestamp}"
+ tokens: "**Tokens:** {tokens}"
+ agent_running: "**Agente em execução:** {state}"
+ state_yes: "Sim ⚡"
+ state_no: "Não"
+ queued: "**Seguimentos em fila:** {count}"
+ platforms: "**Plataformas ligadas:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Parado. O agente ainda não tinha começado — podes continuar esta sessão."
+ stopped: "⚡ Parado. Podes continuar esta sessão."
+ no_active: "Não há nenhuma tarefa ativa para parar."
+
+ title:
+ db_unavailable: "Base de dados de sessões indisponível."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ O título está vazio após a limpeza. Usa caracteres imprimíveis."
+ set_to: "✏️ Título da sessão definido: **{title}**"
+ not_found: "Sessão não encontrada na base de dados."
+ current_with_title: "📌 Sessão: `{session_id}`\nTítulo: **{title}**"
+ current_no_title: "📌 Sessão: `{session_id}`\nSem título. Uso: `/title O meu nome de sessão`"
+
+ topic:
+ not_telegram_dm: "O comando /topic só está disponível em chats privados do Telegram."
+ no_session_db: "Base de dados de sessões indisponível."
+ unauthorized: "Não tens autorização para usar /topic neste bot."
+ restore_needs_topic: "Para restaurar uma sessão, cria ou abre primeiro um topic do Telegram, depois envia /topic dentro desse topic. Para criar um novo topic, abre All Messages e envia qualquer mensagem aí."
+ topics_disabled: "Os topics do Telegram ainda não estão ativados para este bot.\n\nComo ativá-los:\n1. Abre @BotFather.\n2. Escolhe o teu bot.\n3. Abre Bot Settings → Threads Settings.\n4. Ativa Threaded Mode e garante que os utilizadores podem criar novas threads.\n\nDepois envia /topic novamente."
+ topics_user_disallowed: "Os topics do Telegram estão ativados, mas os utilizadores não podem criá-los.\n\nAbre @BotFather → escolhe o teu bot → Bot Settings → Threads Settings, depois desativa 'Disallow users to create new threads'.\n\nDepois envia /topic novamente."
+ enable_failed: "Falha ao ativar o modo topic do Telegram: {error}"
+ bound_status: "Este topic está associado a:\nSessão: {label}\nID: {session_id}\n\nUsa /new para substituir este topic por uma sessão nova.\nPara trabalho paralelo, abre All Messages e envia uma mensagem aí para criar outro topic."
+ thread_ready: "Os topics multi-sessão do Telegram estão ativados.\n\nEste topic será usado como uma sessão independente do Hermes. Usa /new para substituir a sessão atual deste topic. Para trabalho paralelo, abre All Messages e envia uma mensagem aí para criar outro topic."
+ untitled_session: "Sessão sem título"
+
+ undo:
+ nothing: "Nada para anular."
+ removed: "↩️ {count} mensagem(ns) anulada(s).\nRemovido: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update só está disponível em plataformas de mensagens. Executa `hermes update` a partir do terminal."
+ not_git_repo: "✗ Não é um repositório git — não é possível atualizar."
+ hermes_cmd_not_found: "✗ Não foi possível localizar o comando `hermes`. O Hermes está em execução, mas o comando de atualização não conseguiu encontrar o executável no PATH nem através do interpretador Python atual. Tenta executar `hermes update` manualmente no teu terminal."
+ start_failed: "✗ Falha ao iniciar a atualização: {error}"
+ starting: "⚕ A iniciar a atualização do Hermes… Vou transmitir o progresso aqui."
+
+ usage:
+ rate_limits: "⏱️ **Limites de taxa:** {state}"
+ header_session: "📊 **Utilização de tokens da sessão**"
+ label_model: "Modelo: `{model}`"
+ label_input_tokens: "Tokens de entrada: {count}"
+ label_cache_read: "Tokens de leitura de cache: {count}"
+ label_cache_write: "Tokens de escrita de cache: {count}"
+ label_output_tokens: "Tokens de saída: {count}"
+ label_total: "Total: {count}"
+ label_api_calls: "Chamadas à API: {count}"
+ label_cost: "Custo: {prefix}${amount}"
+ label_cost_included: "Custo: incluído"
+ label_context: "Contexto: {used} / {total} ({pct}%)"
+ label_compressions: "Compressões: {count}"
+ header_session_info: "📊 **Informações da sessão**"
+ label_messages: "Mensagens: {count}"
+ label_estimated_context: "Contexto estimado: ~{count} tokens"
+ detailed_after_first: "_(Utilização detalhada disponível após a primeira resposta do agente)_"
+ no_data: "Não há dados de utilização disponíveis para esta sessão."
+
+ verbose:
+ not_enabled: "O comando `/verbose` não está ativado para plataformas de mensagens.\n\nAtiva-o em `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Progresso de ferramentas: **OFF** — não é mostrada qualquer atividade de ferramentas."
+ mode_new: "⚙️ Progresso de ferramentas: **NEW** — mostrado quando a ferramenta muda (comprimento da pré-visualização: `display.tool_preview_length`, predefinição 40)."
+ mode_all: "⚙️ Progresso de ferramentas: **ALL** — cada chamada de ferramenta é mostrada (comprimento da pré-visualização: `display.tool_preview_length`, predefinição 40)."
+ mode_verbose: "⚙️ Progresso de ferramentas: **VERBOSE** — cada chamada de ferramenta com os argumentos completos."
+ saved_suffix: "_(guardado para **{platform}** — produz efeito na próxima mensagem)_"
+ save_failed: "_(não foi possível guardar na configuração: {error})_"
+
+ voice:
+ enabled_voice_only: "Modo de voz ativado.\nResponderei com voz quando enviares mensagens de voz.\nUsa /voice tts para receber respostas de voz em todas as mensagens."
+ disabled_text: "Modo de voz desativado. Respostas apenas em texto."
+ tts_enabled: "Auto-TTS ativado.\nTodas as respostas incluirão uma mensagem de voz."
+ status_mode: "Modo de voz: {label}"
+ status_channel: "Canal de voz: #{channel}"
+ status_participants: "Participantes: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (a falar)"
+ enabled_short: "Modo de voz ativado."
+ disabled_short: "Modo de voz desativado."
+ label_off: "Desativado (apenas texto)"
+ label_voice_only: "Ativado (resposta de voz a mensagens de voz)"
+ label_all: "TTS (resposta de voz a todas as mensagens)"
+
+ yolo:
+ disabled: "⚠️ Modo YOLO **DESATIVADO** nesta sessão — comandos perigosos exigirão aprovação."
+ enabled: "⚡ Modo YOLO **ATIVADO** nesta sessão — todos os comandos são aprovados automaticamente. Usa com precaução."
+
+ shared:
+ session_db_unavailable: "Base de dados de sessões indisponível."
+ session_db_unavailable_prefix: "Base de dados de sessões indisponível"
+ session_not_found: "Sessão não encontrada na base de dados."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/ru.yaml b/locales/ru.yaml
new file mode 100644
index 00000000000..c520362675d
--- /dev/null
+++ b/locales/ru.yaml
@@ -0,0 +1,350 @@
+# Каталог статических сообщений Hermes -- Русский
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ ОПАСНАЯ КОМАНДА: {description}"
+ choose_long: " [o]один раз | [s]сеанс | [a]всегда | [d]отклонить"
+ choose_short: " [o]один раз | [s]сеанс | [d]отклонить"
+ prompt_long: " Выбор [o/s/a/D]: "
+ prompt_short: " Выбор [o/s/D]: "
+ timeout: " ⏱ Время ожидания истекло — команда отклонена"
+ allowed_once: " ✓ Разрешено один раз"
+ allowed_session: " ✓ Разрешено для этого сеанса"
+ allowed_always: " ✓ Добавлено в постоянный список разрешённых"
+ denied: " ✗ Отклонено"
+ cancelled: " ✗ Отменено"
+ blocklist_message: "Эта команда находится в безусловном списке блокировки и не может быть одобрена."
+
+gateway:
+ approval_expired: "⚠️ Срок одобрения истёк (агент больше не ожидает). Попросите агента повторить попытку."
+ draining: "⏳ Ожидание завершения {count} активных агент(ов) перед перезапуском..."
+ goal_cleared: "✓ Цель очищена."
+ no_active_goal: "Нет активной цели."
+ config_read_failed: "⚠️ Не удалось прочитать config.yaml: {error}"
+ config_save_failed: "⚠️ Не удалось сохранить конфигурацию: {error}"
+
+ model:
+ error_prefix: "Ошибка: {error}"
+ switched: "Модель изменена на `{model}`"
+ provider_label: "Провайдер: {provider}"
+ context_label: "Контекст: {tokens} токенов"
+ max_output_label: "Макс. вывод: {tokens} токенов"
+ cost_label: "Стоимость: {cost}"
+ capabilities_label: "Возможности: {capabilities}"
+ prompt_caching_enabled: "Кеширование промптов: включено"
+ warning_prefix: "Предупреждение: {warning}"
+ saved_global: "Сохранено в config.yaml (`--global`)"
+ session_only_hint: "_(только для этого сеанса — добавьте `--global`, чтобы сохранить)_"
+ current_label: "Текущая: `{model}` на {provider}"
+ current_tag: " (текущая)"
+ more_models_suffix: " (+ещё {count})"
+ usage_switch_model: "`/model ` — сменить модель"
+ usage_switch_provider: "`/model --provider ` — сменить провайдера"
+ usage_persist: "`/model --global` — сохранить навсегда"
+
+ agents:
+ header: "🤖 **Активные агенты и задачи**"
+ active_agents: "**Активные агенты:** {count}"
+ this_chat: " · этот чат"
+ more: "... и ещё {count}"
+ running_processes: "**Выполняющиеся фоновые процессы:** {count}"
+ async_jobs: "**Асинхронные задачи шлюза:** {count}"
+ none: "Нет активных агентов или выполняющихся задач."
+ state_starting: "запускается"
+ state_running: "выполняется"
+
+ approve:
+ no_pending: "Нет команды, ожидающей одобрения."
+ once_singular: "✅ Команда одобрена. Агент возобновляет работу..."
+ once_plural: "✅ Команды одобрены ({count} команд). Агент возобновляет работу..."
+ session_singular: "✅ Команда одобрена (шаблон одобрен для этого сеанса). Агент возобновляет работу..."
+ session_plural: "✅ Команды одобрены (шаблон одобрен для этого сеанса) ({count} команд). Агент возобновляет работу..."
+ always_singular: "✅ Команда одобрена (шаблон одобрен навсегда). Агент возобновляет работу..."
+ always_plural: "✅ Команды одобрены (шаблон одобрен навсегда) ({count} команд). Агент возобновляет работу..."
+
+ background:
+ usage: "Использование: /background <запрос>\nПример: /background Сделай сводку лучших историй с HN сегодня\n\nЗапускает запрос в отдельном сеансе. Можно продолжить общение — результат появится здесь по завершении."
+ started: "🔄 Фоновая задача запущена: «{preview}»\nID задачи: {task_id}\nМожно продолжить общение — результаты появятся здесь по завершении."
+
+ branch:
+ db_unavailable: "База данных сеансов недоступна."
+ no_conversation: "Нет беседы для ответвления — сначала отправьте сообщение."
+ create_failed: "Не удалось создать ветку: {error}"
+ switch_failed: "Ветка создана, но переключиться на неё не удалось."
+ branched_one: "⑂ Создана ветка **{title}** (скопировано {count} сообщение)\nОригинал: `{parent}`\nВетка: `{new}`\nИспользуйте `/resume`, чтобы вернуться к оригиналу."
+ branched_many: "⑂ Создана ветка **{title}** (скопировано {count} сообщений)\nОригинал: `{parent}`\nВетка: `{new}`\nИспользуйте `/resume`, чтобы вернуться к оригиналу."
+
+ commands:
+ usage: "Использование: `/commands [page]`"
+ skill_header: "⚡ **Команды навыков**:"
+ default_desc: "Команда навыка"
+ none: "Нет доступных команд."
+ header: "📚 **Команды** (всего {total}, страница {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← пред."
+ nav_next: "след. → `/commands {page}`"
+ out_of_range: "_(Запрошенная страница {requested} вне диапазона, показана страница {page}.)_"
+
+ compress:
+ not_enough: "Недостаточно беседы для сжатия (нужно минимум 4 сообщения)."
+ no_provider: "Провайдер не настроен — сжатие невозможно."
+ nothing_to_do: "Пока нечего сжимать (стенограмма всё ещё полностью является защищённым контекстом)."
+ focus_line: "Фокус: \"{topic}\""
+ summary_failed: "⚠️ Не удалось сгенерировать сводку ({error}). {count} историч. сообщений было удалено и заменено заполнителем; предыдущий контекст больше нельзя восстановить. Проверьте конфигурацию модели auxiliary.compression."
+ aux_failed: "ℹ️ Настроенная модель сжатия `{model}` дала сбой ({error}). Восстановлено с помощью основной модели — контекст не повреждён — но рекомендуется проверить `auxiliary.compression.model` в config.yaml."
+ failed: "Сжатие не удалось: {error}"
+
+ debug:
+ upload_failed: "✗ Не удалось загрузить отчёт отладки: {error}"
+ header: "**Отчёт отладки загружен:**"
+ auto_delete: "⏱ Вставки автоматически удалятся через 6 часов."
+ full_logs_hint: "Для загрузки полных журналов используйте `hermes debug share` из CLI."
+ share_hint: "Поделитесь этими ссылками с командой Hermes для получения поддержки."
+
+ deny:
+ stale: "❌ Команда отклонена (одобрение устарело)."
+ no_pending: "Нет команды для отклонения."
+ denied_singular: "❌ Команда отклонена."
+ denied_plural: "❌ Команды отклонены ({count} команд)."
+
+ fast:
+ not_supported: "⚡ /fast доступен только для моделей OpenAI, поддерживающих Priority Processing."
+ status: "⚡ Priority Processing\n\nТекущий режим: `{mode}`\n\n_Использование:_ `/fast `"
+ unknown_arg: "⚠️ Неизвестный аргумент: `{arg}`\n\n**Допустимые варианты:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (сохранено в конфигурации)\n_(вступит в силу со следующего сообщения)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (только этот сеанс)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Нижний колонтитул среды выполнения: **{state}**\nПоля: `{fields}`\nПлатформа: `{platform}`"
+ usage: "Использование: `/footer [on|off|status]`"
+ saved: "📎 Нижний колонтитул среды выполнения: **{state}**{example}\n_(сохранено глобально — вступит в силу со следующего сообщения)_"
+ example_line: "\nПример: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Цели недоступны в этом сеансе."
+ no_goal_set: "Цель не задана."
+ paused: "⏸ Цель приостановлена: {goal}"
+ no_resume: "Нет цели для возобновления."
+ resumed: "▶ Цель возобновлена: {goal}\nОтправьте любое сообщение, чтобы продолжить, или подождите — я сделаю следующий шаг на следующем ходу."
+ invalid: "Недопустимая цель: {error}"
+ set: "⊙ Цель задана (бюджет {budget} ходов): {goal}\nЯ продолжу работу, пока цель не будет достигнута, вы её не приостановите/очистите, или бюджет не исчерпается.\nУправление: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Команды Hermes**\n"
+ skill_header: "\n⚡ **Команды навыков** (активных: {count}):"
+ more_use_commands: "\n... и ещё {count}. Используйте `/commands` для полного списка с постраничной разбивкой."
+
+ insights:
+ invalid_days: "Недействительное значение --days: {value}"
+ error: "Ошибка при формировании аналитики: {error}"
+
+ kanban:
+ error_prefix: "⚠ ошибка kanban: {error}"
+ subscribed_suffix: "(подписка оформлена — вы получите уведомление, когда {task_id} завершится или будет заблокирован)"
+ truncated_suffix: "… (сокращено; используйте `hermes kanban …` в терминале для полного вывода)"
+ no_output: "(нет вывода)"
+
+ personality:
+ none_configured: "В `{path}/config.yaml` не настроено ни одной личности"
+ header: "🎭 **Доступные личности**\n"
+ none_option: "• `none` — (без наложения личности)"
+ item: "• `{name}` — {preview}"
+ usage: "\nИспользование: `/personality `"
+ save_failed: "⚠️ Не удалось сохранить изменение личности: {error}"
+ cleared: "🎭 Личность очищена — используется базовое поведение агента.\n_(вступит в силу со следующего сообщения)_"
+ set_to: "🎭 Личность установлена на **{name}**\n_(вступит в силу со следующего сообщения)_"
+ unknown: "Неизвестная личность: `{name}`\n\nДоступные: {available}"
+
+ profile:
+ header: "👤 **Профиль:** `{profile}`"
+ home: "📂 **Домашний каталог:** `{home}`"
+
+ reasoning:
+ level_default: "medium (по умолчанию)"
+ level_disabled: "none (отключено)"
+ scope_session: "переопределение сеанса"
+ scope_global: "глобальная конфигурация"
+ status: "🧠 **Настройки рассуждений**\n\n**Усилия:** `{level}`\n**Область:** {scope}\n**Отображение:** {display}\n\n_Использование:_ `/reasoning [--global]`"
+ display_on: "включено ✓"
+ display_off: "выключено"
+ display_set_on: "🧠 ✓ Отображение рассуждений: **ВКЛ.**\nМысли модели будут показываться перед каждым ответом на **{platform}**."
+ display_set_off: "🧠 ✓ Отображение рассуждений: **ВЫКЛ.** для **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` не поддерживается. Используйте `/reasoning --global`, чтобы изменить глобальное значение по умолчанию."
+ reset_done: "🧠 ✓ Переопределение рассуждений для сеанса сброшено; возврат к глобальной конфигурации."
+ unknown_arg: "⚠️ Неизвестный аргумент: `{arg}`\n\n**Допустимые уровни:** none, minimal, low, medium, high, xhigh\n**Отображение:** show, hide\n**Сохранение:** добавьте `--global`, чтобы сохранить за пределами этого сеанса"
+ set_global: "🧠 ✓ Усилия рассуждений установлены на `{effort}` (сохранено в конфигурации)\n_(вступит в силу со следующего сообщения)_"
+ set_global_save_failed: "🧠 ✓ Усилия рассуждений установлены на `{effort}` (только этот сеанс — не удалось сохранить конфигурацию)\n_(вступит в силу со следующего сообщения)_"
+ set_session: "🧠 ✓ Усилия рассуждений установлены на `{effort}` (только этот сеанс — добавьте `--global`, чтобы сохранить)\n_(вступит в силу со следующего сообщения)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp отменено. MCP-инструменты без изменений."
+ always_followup: "ℹ️ Будущие вызовы `/reload-mcp` будут выполняться без подтверждения. Снова включить можно через `approvals.mcp_reload_confirm: true` в config.yaml."
+ confirm_prompt: "⚠️ **Подтверждение /reload-mcp**\n\nПерезагрузка MCP-серверов перестраивает набор инструментов для этого сеанса и **сбрасывает кеш промпта провайдера** — следующее сообщение повторно отправит все входные токены. На моделях с длинным контекстом или высоким уровнем рассуждений это может быть дорого.\n\nВыберите:\n• **Одобрить один раз** — перезагрузить сейчас\n• **Всегда одобрять** — перезагрузить и навсегда отключить этот запрос\n• **Отменить** — оставить MCP-инструменты без изменений\n\n_Текстовая альтернатива: ответьте `/approve`, `/always` или `/cancel`._"
+ header: "🔄 **MCP-серверы перезагружены**\n"
+ reconnected: "♻️ Переподключено: {names}"
+ added: "➕ Добавлено: {names}"
+ removed: "➖ Удалено: {names}"
+ none_connected: "Нет подключённых MCP-серверов."
+ tools_available: "\n🔧 {tools} инструмент(ов) доступно с {servers} сервер(ов)"
+ failed: "❌ Ошибка перезагрузки MCP: {error}"
+
+ reload_skills:
+ header: "🔄 **Навыки перезагружены**\n"
+ no_new: "Новых навыков не обнаружено."
+ total: "\n📚 {count} навык(ов) доступно"
+ added_header: "➕ **Добавленные навыки:**"
+ removed_header: "➖ **Удалённые навыки:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Ошибка перезагрузки навыков: {error}"
+
+ reset:
+ header_default: "✨ Сеанс сброшен! Начинаем с чистого листа."
+ header_new: "✨ Новый сеанс запущен!"
+ header_titled: "✨ Новый сеанс запущен: {title}"
+ title_rejected: "\n⚠️ Название отклонено: {error}"
+ title_error_untitled: "\n⚠️ {error} — сеанс запущен без названия."
+ title_empty_untitled: "\n⚠️ После очистки название пусто — сеанс запущен без названия."
+ tip: "\n✦ Совет: {tip}"
+
+ restart:
+ in_progress: "⏳ Перезапуск шлюза уже выполняется..."
+ restarting: "♻ Перезапуск шлюза. Если уведомление не придёт в течение 60 секунд, перезапустите из консоли командой `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "База данных сеансов недоступна."
+ no_named_sessions: "Именованных сеансов не найдено.\nИспользуйте `/title Мой сеанс`, чтобы назвать текущий сеанс, затем `/resume Мой сеанс`, чтобы вернуться к нему позже."
+ list_header: "📋 **Именованные сеансы**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nИспользование: `/resume <название сеанса>`"
+ list_failed: "Не удалось получить список сеансов: {error}"
+ not_found: "Сеанс, соответствующий '**{name}**', не найден.\nИспользуйте `/resume` без аргументов, чтобы увидеть доступные сеансы."
+ already_on: "📌 Уже в сеансе **{name}**."
+ switch_failed: "Не удалось переключить сеанс."
+ resumed_one: "↻ Сеанс **{title}** возобновлён ({count} сообщение). Беседа восстановлена."
+ resumed_many: "↻ Сеанс **{title}** возобновлён ({count} сообщений). Беседа восстановлена."
+ resumed_no_count: "↻ Сеанс **{title}** возобновлён. Беседа восстановлена."
+
+ retry:
+ no_previous: "Нет предыдущего сообщения для повтора."
+
+ rollback:
+ not_enabled: "Контрольные точки не включены.\nВключите в config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Контрольных точек для {cwd} не найдено"
+ invalid_number: "Недействительный номер контрольной точки. Используйте 1-{max}."
+ restored: "✅ Восстановлено до контрольной точки {hash}: {reason}\nСнимок перед откатом сохранён автоматически."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Не удалось сохранить главный канал: {error}"
+ success: "✅ Главный канал установлен на **{name}** (ID: {chat_id}).\nCron-задачи и межплатформенные сообщения будут доставляться сюда."
+
+ status:
+ header: "📊 **Состояние Hermes Gateway**"
+ session_id: "**ID сеанса:** `{session_id}`"
+ title: "**Название:** {title}"
+ created: "**Создано:** {timestamp}"
+ last_activity: "**Последняя активность:** {timestamp}"
+ tokens: "**Токены:** {tokens}"
+ agent_running: "**Агент активен:** {state}"
+ state_yes: "Да ⚡"
+ state_no: "Нет"
+ queued: "**Очередь продолжений:** {count}"
+ platforms: "**Подключённые платформы:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Остановлено. Агент ещё не начинал — вы можете продолжить этот сеанс."
+ stopped: "⚡ Остановлено. Вы можете продолжить этот сеанс."
+ no_active: "Нет активной задачи для остановки."
+
+ title:
+ db_unavailable: "База данных сеансов недоступна."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ После очистки название пусто. Используйте печатные символы."
+ set_to: "✏️ Название сеанса установлено: **{title}**"
+ not_found: "Сеанс не найден в базе данных."
+ current_with_title: "📌 Сеанс: `{session_id}`\nНазвание: **{title}**"
+ current_no_title: "📌 Сеанс: `{session_id}`\nНазвание не задано. Использование: `/title Название моего сеанса`"
+
+ topic:
+ not_telegram_dm: "Команда /topic доступна только в личных чатах Telegram."
+ no_session_db: "База данных сеансов недоступна."
+ unauthorized: "У вас нет прав использовать /topic в этом боте."
+ restore_needs_topic: "Чтобы восстановить сеанс, сначала создайте или откройте Telegram topic, затем отправьте /topic в этом topic. Чтобы создать новый topic, откройте All Messages и отправьте там любое сообщение."
+ topics_disabled: "Telegram topics ещё не включены для этого бота.\n\nКак включить:\n1. Откройте @BotFather.\n2. Выберите своего бота.\n3. Откройте Bot Settings → Threads Settings.\n4. Включите Threaded Mode и убедитесь, что пользователям разрешено создавать новые threads.\n\nЗатем снова отправьте /topic."
+ topics_user_disallowed: "Telegram topics включены, но пользователям не разрешено создавать topics.\n\nОткройте @BotFather → выберите своего бота → Bot Settings → Threads Settings, затем выключите 'Disallow users to create new threads'.\n\nЗатем снова отправьте /topic."
+ enable_failed: "Не удалось включить режим Telegram topic: {error}"
+ bound_status: "Этот topic привязан к:\nСеанс: {label}\nID: {session_id}\n\nИспользуйте /new, чтобы заменить этот topic новым сеансом.\nДля параллельной работы откройте All Messages и отправьте там сообщение, чтобы создать другой topic."
+ thread_ready: "Многосеансовые Telegram topics включены.\n\nЭтот topic будет использоваться как независимый сеанс Hermes. Используйте /new, чтобы заменить текущий сеанс этого topic. Для параллельной работы откройте All Messages и отправьте там сообщение, чтобы создать другой topic."
+ untitled_session: "Сеанс без названия"
+
+ undo:
+ nothing: "Нечего отменять."
+ removed: "↩️ Отменено сообщений: {count}.\nУдалено: «{preview}»"
+
+ update:
+ platform_not_messaging: "✗ /update доступен только на платформах обмена сообщениями. Выполните `hermes update` в терминале."
+ not_git_repo: "✗ Не git-репозиторий — обновление невозможно."
+ hermes_cmd_not_found: "✗ Не удалось найти команду `hermes`. Hermes запущен, но команда обновления не нашла исполняемый файл в PATH или через текущий интерпретатор Python. Попробуйте выполнить `hermes update` вручную в терминале."
+ start_failed: "✗ Не удалось запустить обновление: {error}"
+ starting: "⚕ Запуск обновления Hermes… Я буду транслировать прогресс сюда."
+
+ usage:
+ rate_limits: "⏱️ **Ограничения скорости:** {state}"
+ header_session: "📊 **Использование токенов сеанса**"
+ label_model: "Модель: `{model}`"
+ label_input_tokens: "Входные токены: {count}"
+ label_cache_read: "Токены чтения кеша: {count}"
+ label_cache_write: "Токены записи кеша: {count}"
+ label_output_tokens: "Выходные токены: {count}"
+ label_total: "Всего: {count}"
+ label_api_calls: "Вызовы API: {count}"
+ label_cost: "Стоимость: {prefix}${amount}"
+ label_cost_included: "Стоимость: включено"
+ label_context: "Контекст: {used} / {total} ({pct}%)"
+ label_compressions: "Сжатий: {count}"
+ header_session_info: "📊 **Информация о сеансе**"
+ label_messages: "Сообщений: {count}"
+ label_estimated_context: "Ориентировочный контекст: ~{count} токенов"
+ detailed_after_first: "_(Подробное использование доступно после первого ответа агента)_"
+ no_data: "Данные об использовании для этого сеанса отсутствуют."
+
+ verbose:
+ not_enabled: "Команда `/verbose` не включена для платформ обмена сообщениями.\n\nВключите в `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Прогресс инструментов: **OFF** — активность инструментов не показывается."
+ mode_new: "⚙️ Прогресс инструментов: **NEW** — показывается при смене инструмента (длина предпросмотра: `display.tool_preview_length`, по умолчанию 40)."
+ mode_all: "⚙️ Прогресс инструментов: **ALL** — показывается каждый вызов инструмента (длина предпросмотра: `display.tool_preview_length`, по умолчанию 40)."
+ mode_verbose: "⚙️ Прогресс инструментов: **VERBOSE** — каждый вызов инструмента с полными аргументами."
+ saved_suffix: "_(сохранено для **{platform}** — вступит в силу со следующего сообщения)_"
+ save_failed: "_(не удалось сохранить в конфигурацию: {error})_"
+
+ voice:
+ enabled_voice_only: "Голосовой режим включён.\nЯ буду отвечать голосом, когда вы отправляете голосовые сообщения.\nИспользуйте /voice tts, чтобы получать голосовые ответы на все сообщения."
+ disabled_text: "Голосовой режим отключён. Только текстовые ответы."
+ tts_enabled: "Авто-TTS включён.\nВсе ответы будут содержать голосовое сообщение."
+ status_mode: "Голосовой режим: {label}"
+ status_channel: "Голосовой канал: #{channel}"
+ status_participants: "Участники: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (говорит)"
+ enabled_short: "Голосовой режим включён."
+ disabled_short: "Голосовой режим отключён."
+ label_off: "Выкл. (только текст)"
+ label_voice_only: "Вкл. (голосовой ответ на голосовые сообщения)"
+ label_all: "TTS (голосовой ответ на все сообщения)"
+
+ yolo:
+ disabled: "⚠️ Режим YOLO для этого сеанса **ОТКЛЮЧЁН** — опасные команды потребуют одобрения."
+ enabled: "⚡ Режим YOLO для этого сеанса **ВКЛЮЧЁН** — все команды одобряются автоматически. Используйте с осторожностью."
+
+ shared:
+ session_db_unavailable: "База данных сеансов недоступна."
+ session_db_unavailable_prefix: "База данных сеансов недоступна"
+ session_not_found: "Сеанс не найден в базе данных."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/tr.yaml b/locales/tr.yaml
index cdaf0ad70e4..012854c51b3 100644
--- a/locales/tr.yaml
+++ b/locales/tr.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "Aktif hedef yok."
config_read_failed: "⚠️ config.yaml okunamadı: {error}"
config_save_failed: "⚠️ Yapılandırma kaydedilemedi: {error}"
+
+ model:
+ error_prefix: "Hata: {error}"
+ switched: "Model `{model}` olarak değiştirildi"
+ provider_label: "Sağlayıcı: {provider}"
+ context_label: "Bağlam: {tokens} token"
+ max_output_label: "Maks. çıktı: {tokens} token"
+ cost_label: "Maliyet: {cost}"
+ capabilities_label: "Yetenekler: {capabilities}"
+ prompt_caching_enabled: "Prompt önbelleği: etkin"
+ warning_prefix: "Uyarı: {warning}"
+ saved_global: "config.yaml'a kaydedildi (`--global`)"
+ session_only_hint: "_(yalnızca bu oturum — kalıcı yapmak için `--global` ekleyin)_"
+ current_label: "Geçerli: `{model}` ({provider})"
+ current_tag: " (geçerli)"
+ more_models_suffix: " (+{count} tane daha)"
+ usage_switch_model: "`/model ` — modeli değiştir"
+ usage_switch_provider: "`/model --provider ` — sağlayıcıyı değiştir"
+ usage_persist: "`/model --global` — kalıcı kaydet"
+
+ agents:
+ header: "🤖 **Aktif Ajanlar ve Görevler**"
+ active_agents: "**Aktif ajanlar:** {count}"
+ this_chat: " · bu sohbet"
+ more: "... ve {count} tane daha"
+ running_processes: "**Çalışan arka plan süreçleri:** {count}"
+ async_jobs: "**Gateway asenkron işleri:** {count}"
+ none: "Aktif ajan veya çalışan görev yok."
+ state_starting: "başlatılıyor"
+ state_running: "çalışıyor"
+
+ approve:
+ no_pending: "Onaylanacak bekleyen komut yok."
+ once_singular: "✅ Komut onaylandı. Ajan devam ediyor..."
+ once_plural: "✅ Komutlar onaylandı ({count} komut). Ajan devam ediyor..."
+ session_singular: "✅ Komut onaylandı (desen bu oturum için onaylandı). Ajan devam ediyor..."
+ session_plural: "✅ Komutlar onaylandı (desen bu oturum için onaylandı) ({count} komut). Ajan devam ediyor..."
+ always_singular: "✅ Komut onaylandı (desen kalıcı olarak onaylandı). Ajan devam ediyor..."
+ always_plural: "✅ Komutlar onaylandı (desen kalıcı olarak onaylandı) ({count} komut). Ajan devam ediyor..."
+
+ background:
+ usage: "Kullanım: /background \nÖrnek: /background Bugünün öne çıkan HN haberlerini özetle\n\nİstemi ayrı bir oturumda çalıştırır. Sohbete devam edebilirsin — sonuç tamamlandığında burada görünecek."
+ started: "🔄 Arka plan görevi başlatıldı: \"{preview}\"\nGörev kimliği: {task_id}\nSohbete devam edebilirsin — sonuçlar tamamlandığında burada görünecek."
+
+ branch:
+ db_unavailable: "Oturum veritabanı kullanılamıyor."
+ no_conversation: "Dallandırılacak konuşma yok — önce bir mesaj gönderin."
+ create_failed: "Dal oluşturulamadı: {error}"
+ switch_failed: "Dal oluşturuldu ancak ona geçilemedi."
+ branched_one: "⑂ **{title}** dalına geçildi ({count} mesaj kopyalandı)\nOrijinal: `{parent}`\nDal: `{new}`\nOrijinale geri dönmek için `/resume` kullanın."
+ branched_many: "⑂ **{title}** dalına geçildi ({count} mesaj kopyalandı)\nOrijinal: `{parent}`\nDal: `{new}`\nOrijinale geri dönmek için `/resume` kullanın."
+
+ commands:
+ usage: "Kullanım: `/commands [page]`"
+ skill_header: "⚡ **Skill Komutları**:"
+ default_desc: "Skill komutu"
+ none: "Kullanılabilir komut yok."
+ header: "📚 **Komutlar** (toplam {total}, sayfa {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← önceki"
+ nav_next: "sonraki → `/commands {page}`"
+ out_of_range: "_(İstenen sayfa {requested} aralık dışındaydı, sayfa {page} gösteriliyor.)_"
+
+ compress:
+ not_enough: "Sıkıştırmak için yeterli konuşma yok (en az 4 mesaj gerekli)."
+ no_provider: "Yapılandırılmış sağlayıcı yok — sıkıştırılamıyor."
+ nothing_to_do: "Henüz sıkıştırılacak bir şey yok (transkript hâlâ tamamen korunan bağlam)."
+ focus_line: "Odak: \"{topic}\""
+ summary_failed: "⚠️ Özet oluşturma başarısız ({error}). {count} geçmiş mesaj kaldırılıp yer tutucuyla değiştirildi; önceki bağlam artık kurtarılamaz. auxiliary.compression model yapılandırmanızı kontrol edin."
+ aux_failed: "ℹ️ Yapılandırılmış sıkıştırma modeli `{model}` başarısız oldu ({error}). Ana modelinizle kurtarıldı — bağlam sağlam — ancak config.yaml içindeki `auxiliary.compression.model` öğesini kontrol etmek isteyebilirsiniz."
+ failed: "Sıkıştırma başarısız: {error}"
+
+ debug:
+ upload_failed: "✗ Hata ayıklama raporu yüklenemedi: {error}"
+ header: "**Hata ayıklama raporu yüklendi:**"
+ auto_delete: "⏱ Paste'ler 6 saat içinde otomatik olarak silinecek."
+ full_logs_hint: "Tam günlük yüklemeleri için CLI'dan `hermes debug share` kullanın."
+ share_hint: "Destek için bu bağlantıları Hermes ekibiyle paylaşın."
+
+ deny:
+ stale: "❌ Komut reddedildi (onay geçersizdi)."
+ no_pending: "Reddedilecek bekleyen komut yok."
+ denied_singular: "❌ Komut reddedildi."
+ denied_plural: "❌ Komutlar reddedildi ({count} komut)."
+
+ fast:
+ not_supported: "⚡ /fast yalnızca Priority Processing destekleyen OpenAI modellerinde kullanılabilir."
+ status: "⚡ Priority Processing\n\nMevcut mod: `{mode}`\n\n_Kullanım:_ `/fast `"
+ unknown_arg: "⚠️ Bilinmeyen argüman: `{arg}`\n\n**Geçerli seçenekler:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (yapılandırmaya kaydedildi)\n_(sonraki mesajda geçerli olur)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (yalnızca bu oturum)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Çalışma zamanı altbilgisi: **{state}**\nAlanlar: `{fields}`\nPlatform: `{platform}`"
+ usage: "Kullanım: `/footer [on|off|status]`"
+ saved: "📎 Çalışma zamanı altbilgisi: **{state}**{example}\n_(genel olarak kaydedildi — sonraki mesajda geçerli olur)_"
+ example_line: "\nÖrnek: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Bu oturumda hedefler kullanılamıyor."
+ no_goal_set: "Hedef ayarlanmadı."
+ paused: "⏸ Hedef duraklatıldı: {goal}"
+ no_resume: "Devam ettirilecek hedef yok."
+ resumed: "▶ Hedef devam ettirildi: {goal}\nDevam etmek için herhangi bir mesaj gönderin veya bekleyin — bir sonraki turda adımı atacağım."
+ invalid: "Geçersiz hedef: {error}"
+ set: "⊙ Hedef ayarlandı ({budget} turluk bütçe): {goal}\nHedef tamamlanana, siz duraklatana/temizleyene veya bütçe tükenene kadar çalışmaya devam edeceğim.\nKontroller: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes Komutları**\n"
+ skill_header: "\n⚡ **Skill Komutları** ({count} aktif):"
+ more_use_commands: "\n... ve {count} tane daha. Tam sayfalı liste için `/commands` kullanın."
+
+ insights:
+ invalid_days: "Geçersiz --days değeri: {value}"
+ error: "Analiz oluşturulurken hata: {error}"
+
+ kanban:
+ error_prefix: "⚠ kanban hatası: {error}"
+ subscribed_suffix: "(abone olundu — {task_id} tamamlandığında veya engellendiğinde bildirim alacaksınız)"
+ truncated_suffix: "… (kısaltıldı; tam çıktı için terminalinizde `hermes kanban …` komutunu kullanın)"
+ no_output: "(çıktı yok)"
+
+ personality:
+ none_configured: "`{path}/config.yaml` içinde yapılandırılmış kişilik yok"
+ header: "🎭 **Mevcut Kişilikler**\n"
+ none_option: "• `none` — (kişilik kaplaması yok)"
+ item: "• `{name}` — {preview}"
+ usage: "\nKullanım: `/personality `"
+ save_failed: "⚠️ Kişilik değişikliği kaydedilemedi: {error}"
+ cleared: "🎭 Kişilik temizlendi — temel ajan davranışı kullanılıyor.\n_(bir sonraki mesajda etkili olur)_"
+ set_to: "🎭 Kişilik **{name}** olarak ayarlandı\n_(bir sonraki mesajda etkili olur)_"
+ unknown: "Bilinmeyen kişilik: `{name}`\n\nMevcut: {available}"
+
+ profile:
+ header: "👤 **Profil:** `{profile}`"
+ home: "📂 **Ana dizin:** `{home}`"
+
+ reasoning:
+ level_default: "medium (varsayılan)"
+ level_disabled: "none (devre dışı)"
+ scope_session: "oturum geçersiz kılma"
+ scope_global: "genel yapılandırma"
+ status: "🧠 **Akıl Yürütme Ayarları**\n\n**Güç:** `{level}`\n**Kapsam:** {scope}\n**Görüntüleme:** {display}\n\n_Kullanım:_ `/reasoning [--global]`"
+ display_on: "açık ✓"
+ display_off: "kapalı"
+ display_set_on: "🧠 ✓ Akıl yürütme görüntüleme: **AÇIK**\n**{platform}** üzerinde her yanıttan önce modelin düşüncesi gösterilecek."
+ display_set_off: "🧠 ✓ **{platform}** için akıl yürütme görüntüleme: **KAPALI**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` desteklenmiyor. Genel varsayılanı değiştirmek için `/reasoning --global` kullanın."
+ reset_done: "🧠 ✓ Oturumun akıl yürütme geçersiz kılması temizlendi; genel yapılandırmaya geri dönülüyor."
+ unknown_arg: "⚠️ Bilinmeyen argüman: `{arg}`\n\n**Geçerli seviyeler:** none, minimal, low, medium, high, xhigh\n**Görüntüleme:** show, hide\n**Kalıcı:** bu oturumun ötesinde kaydetmek için `--global` ekleyin"
+ set_global: "🧠 ✓ Akıl yürütme gücü `{effort}` olarak ayarlandı (yapılandırmaya kaydedildi)\n_(sonraki mesajda etkili)_"
+ set_global_save_failed: "🧠 ✓ Akıl yürütme gücü `{effort}` olarak ayarlandı (yalnızca bu oturum — yapılandırma kaydedilemedi)\n_(sonraki mesajda etkili)_"
+ set_session: "🧠 ✓ Akıl yürütme gücü `{effort}` olarak ayarlandı (yalnızca bu oturum — kalıcı yapmak için `--global` ekleyin)\n_(sonraki mesajda etkili)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp iptal edildi. MCP araçları değiştirilmedi."
+ always_followup: "ℹ️ Bundan sonraki `/reload-mcp` çağrıları onaysız çalışacak. `config.yaml` içinde `approvals.mcp_reload_confirm: true` ile yeniden etkinleştirebilirsiniz."
+ confirm_prompt: "⚠️ **/reload-mcp Onayı**\n\nMCP sunucularını yeniden yüklemek bu oturumdaki araç kümesini yeniden oluşturur ve **sağlayıcı prompt önbelleğini geçersiz kılar** — bir sonraki mesaj tüm giriş token'larını yeniden gönderir. Uzun bağlam veya yüksek akıl yürütmeli modellerde bu maliyetli olabilir.\n\nSeçim yapın:\n• **Bir Kez Onayla** — şimdi yeniden yükle\n• **Her Zaman Onayla** — şimdi yeniden yükle ve bu onayı kalıcı olarak sustur\n• **İptal** — MCP araçlarını değiştirme\n\n_Metin alternatifi: `/approve`, `/always` veya `/cancel` ile yanıtlayın._"
+ header: "🔄 **MCP Sunucuları Yeniden Yüklendi**\n"
+ reconnected: "♻️ Yeniden bağlanan: {names}"
+ added: "➕ Eklenen: {names}"
+ removed: "➖ Kaldırılan: {names}"
+ none_connected: "Bağlı MCP sunucusu yok."
+ tools_available: "\n🔧 {servers} sunucudan {tools} araç kullanılabilir"
+ failed: "❌ MCP yeniden yükleme başarısız: {error}"
+
+ reload_skills:
+ header: "🔄 **Beceriler Yeniden Yüklendi**\n"
+ no_new: "Yeni beceri tespit edilmedi."
+ total: "\n📚 {count} beceri kullanılabilir"
+ added_header: "➕ **Eklenen Beceriler:**"
+ removed_header: "➖ **Kaldırılan Beceriler:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Beceri yeniden yükleme başarısız: {error}"
+
+ reset:
+ header_default: "✨ Oturum sıfırlandı! Yeniden başlıyoruz."
+ header_new: "✨ Yeni oturum başlatıldı!"
+ header_titled: "✨ Yeni oturum başlatıldı: {title}"
+ title_rejected: "\n⚠️ Başlık reddedildi: {error}"
+ title_error_untitled: "\n⚠️ {error} — oturum başlıksız başlatıldı."
+ title_empty_untitled: "\n⚠️ Temizlik sonrası başlık boş — oturum başlıksız başlatıldı."
+ tip: "\n✦ İpucu: {tip}"
+
+ restart:
+ in_progress: "⏳ Gateway yeniden başlatma zaten sürüyor..."
+ restarting: "♻ Gateway yeniden başlatılıyor. 60 saniye içinde bildirim almazsanız konsoldan `hermes gateway restart` ile yeniden başlatın."
+
+ resume:
+ db_unavailable: "Oturum veritabanı kullanılamıyor."
+ no_named_sessions: "Adlandırılmış oturum bulunamadı.\nMevcut oturumu adlandırmak için `/title Oturumum`, daha sonra geri dönmek için `/resume Oturumum` kullanın."
+ list_header: "📋 **Adlandırılmış Oturumlar**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nKullanım: `/resume `"
+ list_failed: "Oturumlar listelenemedi: {error}"
+ not_found: "'**{name}**' ile eşleşen oturum bulunamadı.\nKullanılabilir oturumları görmek için argümansız `/resume` kullanın."
+ already_on: "📌 Zaten **{name}** oturumundasınız."
+ switch_failed: "Oturum değiştirilemedi."
+ resumed_one: "↻ **{title}** oturumu sürdürüldü ({count} mesaj). Konuşma geri yüklendi."
+ resumed_many: "↻ **{title}** oturumu sürdürüldü ({count} mesaj). Konuşma geri yüklendi."
+ resumed_no_count: "↻ **{title}** oturumu sürdürüldü. Konuşma geri yüklendi."
+
+ retry:
+ no_previous: "Yeniden denenecek önceki mesaj yok."
+
+ rollback:
+ not_enabled: "Kontrol noktaları etkin değil.\nconfig.yaml içinde etkinleştirin:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "{cwd} için kontrol noktası bulunamadı"
+ invalid_number: "Geçersiz kontrol noktası numarası. 1-{max} aralığını kullanın."
+ restored: "✅ {hash} kontrol noktasına geri yüklendi: {reason}\nGeri alma öncesi anlık görüntü otomatik olarak kaydedildi."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Ana kanal kaydedilemedi: {error}"
+ success: "✅ Ana kanal **{name}** (ID: {chat_id}) olarak ayarlandı.\nCron işleri ve platformlar arası mesajlar buraya iletilecek."
+
+ status:
+ header: "📊 **Hermes Gateway Durumu**"
+ session_id: "**Oturum kimliği:** `{session_id}`"
+ title: "**Başlık:** {title}"
+ created: "**Oluşturuldu:** {timestamp}"
+ last_activity: "**Son etkinlik:** {timestamp}"
+ tokens: "**Token:** {tokens}"
+ agent_running: "**Aracı çalışıyor:** {state}"
+ state_yes: "Evet ⚡"
+ state_no: "Hayır"
+ queued: "**Sıradaki devam:** {count}"
+ platforms: "**Bağlı platformlar:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Durduruldu. Ajan henüz başlamamıştı — bu oturuma devam edebilirsin."
+ stopped: "⚡ Durduruldu. Bu oturuma devam edebilirsin."
+ no_active: "Durdurulacak aktif görev yok."
+
+ title:
+ db_unavailable: "Oturum veritabanı kullanılamıyor."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Temizlemeden sonra başlık boş. Lütfen yazdırılabilir karakterler kullanın."
+ set_to: "✏️ Oturum başlığı ayarlandı: **{title}**"
+ not_found: "Oturum veritabanında bulunamadı."
+ current_with_title: "📌 Oturum: `{session_id}`\nBaşlık: **{title}**"
+ current_no_title: "📌 Oturum: `{session_id}`\nBaşlık ayarlanmamış. Kullanım: `/title Oturum Adım`"
+
+ topic:
+ not_telegram_dm: "/topic komutu yalnızca Telegram özel sohbetlerinde kullanılabilir."
+ no_session_db: "Oturum veritabanı kullanılamıyor."
+ unauthorized: "Bu bot üzerinde /topic kullanma yetkiniz yok."
+ restore_needs_topic: "Bir oturumu geri yüklemek için önce bir Telegram topic oluşturun veya açın, ardından o topic içinde /topic gönderin. Yeni bir topic oluşturmak için All Messages'ı açıp orada herhangi bir mesaj gönderin."
+ topics_disabled: "Bu bot için Telegram topic'leri henüz etkin değil.\n\nNasıl etkinleştirilir:\n1. @BotFather'ı açın.\n2. Botunuzu seçin.\n3. Bot Settings → Threads Settings'ı açın.\n4. Threaded Mode'u açın ve kullanıcıların yeni thread oluşturmasına izin verildiğinden emin olun.\n\nArdından /topic'i tekrar gönderin."
+ topics_user_disallowed: "Telegram topic'leri etkin, ancak kullanıcıların topic oluşturmasına izin verilmiyor.\n\n@BotFather → botunuz → Bot Settings → Threads Settings yolunu açın ve 'Disallow users to create new threads' seçeneğini kapatın.\n\nArdından /topic'i tekrar gönderin."
+ enable_failed: "Telegram topic modu etkinleştirilemedi: {error}"
+ bound_status: "Bu topic şuna bağlı:\nOturum: {label}\nID: {session_id}\n\nBu topic'i yeni bir oturumla değiştirmek için /new kullanın.\nParalel çalışma için All Messages'ı açıp orada bir mesaj göndererek başka bir topic oluşturun."
+ thread_ready: "Telegram çok oturumlu topic'leri etkin.\n\nBu topic bağımsız bir Hermes oturumu olarak kullanılacak. Bu topic'in mevcut oturumunu değiştirmek için /new kullanın. Paralel çalışma için All Messages'ı açıp orada bir mesaj göndererek başka bir topic oluşturun."
+ untitled_session: "Adsız oturum"
+
+ undo:
+ nothing: "Geri alınacak bir şey yok."
+ removed: "↩️ {count} mesaj geri alındı.\nKaldırıldı: \"{preview}\""
+
+ update:
+ platform_not_messaging: "✗ /update yalnızca mesajlaşma platformlarında kullanılabilir. Terminalden `hermes update` komutunu çalıştırın."
+ not_git_repo: "✗ Git deposu değil — güncellenemiyor."
+ hermes_cmd_not_found: "✗ `hermes` komutu bulunamadı. Hermes çalışıyor, ancak güncelleme komutu yürütülebilir dosyayı PATH'te veya mevcut Python yorumlayıcısı aracılığıyla bulamadı. Terminalde `hermes update` komutunu manuel olarak çalıştırmayı deneyin."
+ start_failed: "✗ Güncelleme başlatılamadı: {error}"
+ starting: "⚕ Hermes güncellemesi başlatılıyor… İlerlemeyi buraya akıtacağım."
+
+ usage:
+ rate_limits: "⏱️ **Hız Sınırları:** {state}"
+ header_session: "📊 **Oturum Token Kullanımı**"
+ label_model: "Model: `{model}`"
+ label_input_tokens: "Girdi token'ları: {count}"
+ label_cache_read: "Önbellek okuma token'ları: {count}"
+ label_cache_write: "Önbellek yazma token'ları: {count}"
+ label_output_tokens: "Çıktı token'ları: {count}"
+ label_total: "Toplam: {count}"
+ label_api_calls: "API çağrıları: {count}"
+ label_cost: "Maliyet: {prefix}${amount}"
+ label_cost_included: "Maliyet: dahil"
+ label_context: "Bağlam: {used} / {total} ({pct}%)"
+ label_compressions: "Sıkıştırmalar: {count}"
+ header_session_info: "📊 **Oturum Bilgisi**"
+ label_messages: "Mesajlar: {count}"
+ label_estimated_context: "Tahmini bağlam: ~{count} token"
+ detailed_after_first: "_(Ayrıntılı kullanım, ilk ajan yanıtından sonra kullanılabilir)_"
+ no_data: "Bu oturum için kullanım verisi yok."
+
+ verbose:
+ not_enabled: "`/verbose` komutu mesajlaşma platformlarında etkin değil.\n\n`config.yaml` içinde etkinleştirin:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Araç ilerlemesi: **OFF** — araç etkinliği gösterilmez."
+ mode_new: "⚙️ Araç ilerlemesi: **NEW** — araç değiştiğinde gösterilir (önizleme uzunluğu: `display.tool_preview_length`, varsayılan 40)."
+ mode_all: "⚙️ Araç ilerlemesi: **ALL** — her araç çağrısı gösterilir (önizleme uzunluğu: `display.tool_preview_length`, varsayılan 40)."
+ mode_verbose: "⚙️ Araç ilerlemesi: **VERBOSE** — her araç çağrısı tüm argümanlarıyla gösterilir."
+ saved_suffix: "_(**{platform}** için kaydedildi — sonraki mesajda geçerli olur)_"
+ save_failed: "_(yapılandırmaya kaydedilemedi: {error})_"
+
+ voice:
+ enabled_voice_only: "Sesli mod etkinleştirildi.\nSesli mesaj gönderdiğinizde sesli yanıt vereceğim.\nTüm mesajlara sesli yanıt almak için /voice tts kullanın."
+ disabled_text: "Sesli mod devre dışı. Yalnızca metin yanıtları."
+ tts_enabled: "Otomatik TTS etkinleştirildi.\nTüm yanıtlar bir sesli mesaj içerecek."
+ status_mode: "Sesli mod: {label}"
+ status_channel: "Ses kanalı: #{channel}"
+ status_participants: "Katılımcılar: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (konuşuyor)"
+ enabled_short: "Sesli mod etkinleştirildi."
+ disabled_short: "Sesli mod devre dışı."
+ label_off: "Kapalı (yalnızca metin)"
+ label_voice_only: "Açık (sesli mesajlara sesli yanıt)"
+ label_all: "TTS (tüm mesajlara sesli yanıt)"
+
+ yolo:
+ disabled: "⚠️ Bu oturumda YOLO modu **KAPALI** — tehlikeli komutlar onay gerektirecek."
+ enabled: "⚡ Bu oturumda YOLO modu **AÇIK** — tüm komutlar otomatik onaylanır. Dikkatli kullanın."
+
+ shared:
+ session_db_unavailable: "Oturum veritabanı kullanılamıyor."
+ session_db_unavailable_prefix: "Oturum veritabanı kullanılamıyor"
+ session_not_found: "Oturum veritabanında bulunamadı."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/uk.yaml b/locales/uk.yaml
index fce0dc0a6f8..44b011cfe83 100644
--- a/locales/uk.yaml
+++ b/locales/uk.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "Немає активної цілі."
config_read_failed: "⚠️ Не вдалося прочитати config.yaml: {error}"
config_save_failed: "⚠️ Не вдалося зберегти конфігурацію: {error}"
+
+ model:
+ error_prefix: "Помилка: {error}"
+ switched: "Модель змінено на `{model}`"
+ provider_label: "Провайдер: {provider}"
+ context_label: "Контекст: {tokens} токенів"
+ max_output_label: "Макс. вихід: {tokens} токенів"
+ cost_label: "Вартість: {cost}"
+ capabilities_label: "Можливості: {capabilities}"
+ prompt_caching_enabled: "Кешування промптів: увімкнено"
+ warning_prefix: "Попередження: {warning}"
+ saved_global: "Збережено в config.yaml (`--global`)"
+ session_only_hint: "_(лише для цього сеансу — додайте `--global`, щоб зберегти)_"
+ current_label: "Поточна: `{model}` на {provider}"
+ current_tag: " (поточна)"
+ more_models_suffix: " (+{count} ще)"
+ usage_switch_model: "`/model ` — змінити модель"
+ usage_switch_provider: "`/model --provider ` — змінити провайдера"
+ usage_persist: "`/model --global` — зберегти назавжди"
+
+ agents:
+ header: "🤖 **Активні агенти та завдання**"
+ active_agents: "**Активні агенти:** {count}"
+ this_chat: " · цей чат"
+ more: "... і ще {count}"
+ running_processes: "**Фонові процеси, що виконуються:** {count}"
+ async_jobs: "**Асинхронні задачі гейтвея:** {count}"
+ none: "Немає активних агентів або задач."
+ state_starting: "запускається"
+ state_running: "виконується"
+
+ approve:
+ no_pending: "Немає команди на схвалення."
+ once_singular: "✅ Команду схвалено. Агент відновлює роботу…"
+ once_plural: "✅ Команди схвалено ({count} команд). Агент відновлює роботу…"
+ session_singular: "✅ Команду схвалено (шаблон схвалено для цього сеансу). Агент відновлює роботу…"
+ session_plural: "✅ Команди схвалено (шаблон схвалено для цього сеансу) ({count} команд). Агент відновлює роботу…"
+ always_singular: "✅ Команду схвалено (шаблон схвалено назавжди). Агент відновлює роботу…"
+ always_plural: "✅ Команди схвалено (шаблон схвалено назавжди) ({count} команд). Агент відновлює роботу…"
+
+ background:
+ usage: "Використання: /background <запит>\nПриклад: /background Підсумуй найкращі історії з HN сьогодні\n\nЗапускає запит в окремому сеансі. Можна продовжити спілкування — результат з'явиться тут після завершення."
+ started: "🔄 Фонове завдання запущено: «{preview}»\nID завдання: {task_id}\nМожна продовжити спілкування — результати з'являться тут після завершення."
+
+ branch:
+ db_unavailable: "База даних сеансів недоступна."
+ no_conversation: "Немає розмови для розгалуження — спочатку надішліть повідомлення."
+ create_failed: "Не вдалося створити гілку: {error}"
+ switch_failed: "Гілку створено, але не вдалося переключитися на неї."
+ branched_one: "⑂ Створено гілку **{title}** (скопійовано {count} повідомлення)\nОригінал: `{parent}`\nГілка: `{new}`\nВикористайте `/resume`, щоб повернутися до оригіналу."
+ branched_many: "⑂ Створено гілку **{title}** (скопійовано {count} повідомлень)\nОригінал: `{parent}`\nГілка: `{new}`\nВикористайте `/resume`, щоб повернутися до оригіналу."
+
+ commands:
+ usage: "Використання: `/commands [page]`"
+ skill_header: "⚡ **Команди навичок**:"
+ default_desc: "Команда навички"
+ none: "Немає доступних команд."
+ header: "📚 **Команди** (всього {total}, сторінка {page}/{total_pages})"
+ nav_prev: "`/commands {page}` ← попередня"
+ nav_next: "наступна → `/commands {page}`"
+ out_of_range: "_(Запитана сторінка {requested} поза межами, показано сторінку {page}.)_"
+
+ compress:
+ not_enough: "Недостатньо розмови для стиснення (потрібно щонайменше 4 повідомлення)."
+ no_provider: "Постачальника не налаштовано — неможливо стиснути."
+ nothing_to_do: "Поки що немає що стискати (стенограма все ще є повністю захищеним контекстом)."
+ focus_line: "Фокус: \"{topic}\""
+ summary_failed: "⚠️ Не вдалося згенерувати зведення ({error}). {count} історичних повідомлень було видалено та замінено заповнювачем; попередній контекст більше не можна відновити. Перевірте конфігурацію моделі auxiliary.compression."
+ aux_failed: "ℹ️ Налаштована модель стиснення `{model}` зазнала збою ({error}). Відновлено за допомогою основної моделі — контекст не пошкоджений — але варто перевірити `auxiliary.compression.model` у config.yaml."
+ failed: "Стиснення не вдалося: {error}"
+
+ debug:
+ upload_failed: "✗ Не вдалося завантажити звіт налагодження: {error}"
+ header: "**Звіт налагодження завантажено:**"
+ auto_delete: "⏱ Вставки автоматично видаляться через 6 годин."
+ full_logs_hint: "Щоб завантажити повні журнали, використайте `hermes debug share` з CLI."
+ share_hint: "Поділіться цими посиланнями з командою Hermes для отримання підтримки."
+
+ deny:
+ stale: "❌ Команду відхилено (схвалення застаріло)."
+ no_pending: "Немає команди для відхилення."
+ denied_singular: "❌ Команду відхилено."
+ denied_plural: "❌ Команди відхилено ({count} команд)."
+
+ fast:
+ not_supported: "⚡ /fast доступний лише для моделей OpenAI, які підтримують Priority Processing."
+ status: "⚡ Priority Processing\n\nПоточний режим: `{mode}`\n\n_Використання:_ `/fast `"
+ unknown_arg: "⚠️ Невідомий аргумент: `{arg}`\n\n**Допустимі варіанти:** normal, fast, status"
+ saved: "⚡ ✓ Priority Processing: **{label}** (збережено в конфігурації)\n_(набуде чинності з наступного повідомлення)_"
+ session_only: "⚡ ✓ Priority Processing: **{label}** (лише ця сесія)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 Нижній колонтитул середовища: **{state}**\nПоля: `{fields}`\nПлатформа: `{platform}`"
+ usage: "Використання: `/footer [on|off|status]`"
+ saved: "📎 Нижній колонтитул середовища: **{state}**{example}\n_(збережено глобально — набуде чинності з наступного повідомлення)_"
+ example_line: "\nПриклад: `{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "Цілі недоступні в цій сесії."
+ no_goal_set: "Ціль не встановлено."
+ paused: "⏸ Ціль призупинено: {goal}"
+ no_resume: "Немає цілі для продовження."
+ resumed: "▶ Ціль відновлено: {goal}\nНадішліть будь-яке повідомлення, щоб продовжити, або зачекайте — я зроблю наступний крок у наступному ході."
+ invalid: "Неприпустима ціль: {error}"
+ set: "⊙ Ціль встановлено (бюджет {budget} ходів): {goal}\nЯ продовжуватиму працювати, доки ціль не буде досягнута, ви її не призупините/очистите, або бюджет не вичерпається.\nКерування: /goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Команди Hermes**\n"
+ skill_header: "\n⚡ **Команди навичок** ({count} активних):"
+ more_use_commands: "\n... і ще {count}. Використайте `/commands` для повного списку зі сторінками."
+
+ insights:
+ invalid_days: "Недійсне значення --days: {value}"
+ error: "Помилка при формуванні аналітики: {error}"
+
+ kanban:
+ error_prefix: "⚠ помилка kanban: {error}"
+ subscribed_suffix: "(підписано — ви отримаєте сповіщення, коли {task_id} завершиться або буде заблоковано)"
+ truncated_suffix: "… (скорочено; використовуйте `hermes kanban …` у терміналі для повного виводу)"
+ no_output: "(немає виводу)"
+
+ personality:
+ none_configured: "У `{path}/config.yaml` не налаштовано жодної особистості"
+ header: "🎭 **Доступні особистості**\n"
+ none_option: "• `none` — (без накладання особистості)"
+ item: "• `{name}` — {preview}"
+ usage: "\nВикористання: `/personality `"
+ save_failed: "⚠️ Не вдалося зберегти зміну особистості: {error}"
+ cleared: "🎭 Особистість очищено — використовується базова поведінка агента.\n_(набуде чинності з наступного повідомлення)_"
+ set_to: "🎭 Особистість встановлено на **{name}**\n_(набуде чинності з наступного повідомлення)_"
+ unknown: "Невідома особистість: `{name}`\n\nДоступні: {available}"
+
+ profile:
+ header: "👤 **Профіль:** `{profile}`"
+ home: "📂 **Домашня тека:** `{home}`"
+
+ reasoning:
+ level_default: "medium (за замовчуванням)"
+ level_disabled: "none (вимкнено)"
+ scope_session: "перевизначення сеансу"
+ scope_global: "глобальна конфігурація"
+ status: "🧠 **Налаштування мислення**\n\n**Зусилля:** `{level}`\n**Область:** {scope}\n**Показ:** {display}\n\n_Використання:_ `/reasoning [--global]`"
+ display_on: "увімкнено ✓"
+ display_off: "вимкнено"
+ display_set_on: "🧠 ✓ Показ мислення: **УВІМКНЕНО**\nДумки моделі будуть показуватися перед кожною відповіддю на **{platform}**."
+ display_set_off: "🧠 ✓ Показ мислення: **ВИМКНЕНО** для **{platform}**"
+ reset_global_unsupported: "⚠️ `/reasoning reset --global` не підтримується. Використовуйте `/reasoning --global`, щоб змінити глобальне значення за замовчуванням."
+ reset_done: "🧠 ✓ Перевизначення мислення для сеансу скинуто; повернення до глобальної конфігурації."
+ unknown_arg: "⚠️ Невідомий аргумент: `{arg}`\n\n**Дійсні рівні:** none, minimal, low, medium, high, xhigh\n**Показ:** show, hide\n**Зберегти:** додайте `--global`, щоб зберегти поза цим сеансом"
+ set_global: "🧠 ✓ Зусилля мислення встановлено на `{effort}` (збережено в конфігурації)\n_(набуде чинності з наступного повідомлення)_"
+ set_global_save_failed: "🧠 ✓ Зусилля мислення встановлено на `{effort}` (лише цей сеанс — не вдалося зберегти конфігурацію)\n_(набуде чинності з наступного повідомлення)_"
+ set_session: "🧠 ✓ Зусилля мислення встановлено на `{effort}` (лише цей сеанс — додайте `--global`, щоб зберегти)\n_(набуде чинності з наступного повідомлення)_"
+
+ reload_mcp:
+ cancelled: "🟡 /reload-mcp скасовано. MCP-інструменти без змін."
+ always_followup: "ℹ️ Наступні виклики `/reload-mcp` виконуватимуться без підтвердження. Увімкнути знову можна через `approvals.mcp_reload_confirm: true` у `config.yaml`."
+ confirm_prompt: "⚠️ **Підтвердження /reload-mcp**\n\nПерезавантаження MCP-серверів перебудовує набір інструментів для цього сеансу та **інвалідує кеш промпта провайдера** — наступне повідомлення повторно надішле всі вхідні токени. На моделях із довгим контекстом або високим рівнем міркувань це може бути дорого.\n\nОберіть:\n• **Схвалити один раз** — перезавантажити зараз\n• **Завжди схвалювати** — перезавантажити та назавжди приховати цей запит\n• **Скасувати** — залишити MCP-інструменти без змін\n\n_Текстова альтернатива: відповідайте `/approve`, `/always` або `/cancel`._"
+ header: "🔄 **MCP-сервери перезавантажено**\n"
+ reconnected: "♻️ Перепідключено: {names}"
+ added: "➕ Додано: {names}"
+ removed: "➖ Видалено: {names}"
+ none_connected: "Немає підключених MCP-серверів."
+ tools_available: "\n🔧 {tools} інструмент(ів) доступно з {servers} сервер(ів)"
+ failed: "❌ Помилка перезавантаження MCP: {error}"
+
+ reload_skills:
+ header: "🔄 **Навички перезавантажено**\n"
+ no_new: "Нових навичок не виявлено."
+ total: "\n📚 {count} навичок(и) доступно"
+ added_header: "➕ **Додані навички:**"
+ removed_header: "➖ **Видалені навички:**"
+ item_with_desc: " - {name}: {desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ Помилка перезавантаження навичок: {error}"
+
+ reset:
+ header_default: "✨ Сесію скинуто! Починаємо з чистого аркуша."
+ header_new: "✨ Нову сесію запущено!"
+ header_titled: "✨ Нову сесію запущено: {title}"
+ title_rejected: "\n⚠️ Назву відхилено: {error}"
+ title_error_untitled: "\n⚠️ {error} — сесію запущено без назви."
+ title_empty_untitled: "\n⚠️ Після очищення назва порожня — сесію запущено без назви."
+ tip: "\n✦ Порада: {tip}"
+
+ restart:
+ in_progress: "⏳ Перезапуск гейтвея вже виконується..."
+ restarting: "♻ Перезапуск гейтвея. Якщо ви не отримаєте сповіщення протягом 60 секунд, перезапустіть із консолі командою `hermes gateway restart`."
+
+ resume:
+ db_unavailable: "База даних сеансів недоступна."
+ no_named_sessions: "Іменованих сеансів не знайдено.\nВикористайте `/title Мій сеанс`, щоб назвати поточний сеанс, потім `/resume Мій сеанс`, щоб повернутися до нього."
+ list_header: "📋 **Іменовані сеанси**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\nВикористання: `/resume <назва сеансу>`"
+ list_failed: "Не вдалося отримати список сеансів: {error}"
+ not_found: "Сеанс, що відповідає '**{name}**', не знайдено.\nВикористайте `/resume` без аргументів, щоб побачити доступні сеанси."
+ already_on: "📌 Уже в сеансі **{name}**."
+ switch_failed: "Не вдалося переключити сеанс."
+ resumed_one: "↻ Сеанс **{title}** відновлено ({count} повідомлення). Розмову відновлено."
+ resumed_many: "↻ Сеанс **{title}** відновлено ({count} повідомлень). Розмову відновлено."
+ resumed_no_count: "↻ Сеанс **{title}** відновлено. Розмову відновлено."
+
+ retry:
+ no_previous: "Немає попереднього повідомлення для повторення."
+
+ rollback:
+ not_enabled: "Контрольні точки не ввімкнено.\nУвімкніть у config.yaml:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "Контрольних точок для {cwd} не знайдено"
+ invalid_number: "Недійсний номер контрольної точки. Використовуйте 1-{max}."
+ restored: "✅ Відновлено до контрольної точки {hash}: {reason}\nЗнімок перед відкатом збережено автоматично."
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "Не вдалося зберегти головний канал: {error}"
+ success: "✅ Головний канал встановлено на **{name}** (ID: {chat_id}).\nCron-завдання та міжплатформні повідомлення доставлятимуться сюди."
+
+ status:
+ header: "📊 **Стан Hermes Gateway**"
+ session_id: "**ID сесії:** `{session_id}`"
+ title: "**Назва:** {title}"
+ created: "**Створено:** {timestamp}"
+ last_activity: "**Остання активність:** {timestamp}"
+ tokens: "**Токени:** {tokens}"
+ agent_running: "**Агент активний:** {state}"
+ state_yes: "Так ⚡"
+ state_no: "Ні"
+ queued: "**Черга продовжень:** {count}"
+ platforms: "**Підключені платформи:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ Зупинено. Агент ще не починав — можна продовжити цей сеанс."
+ stopped: "⚡ Зупинено. Можна продовжити цей сеанс."
+ no_active: "Немає активного завдання для зупинки."
+
+ title:
+ db_unavailable: "База даних сеансів недоступна."
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ Після очищення назва порожня. Використовуйте друковані символи."
+ set_to: "✏️ Назву сеансу встановлено: **{title}**"
+ not_found: "Сеанс не знайдено в базі даних."
+ current_with_title: "📌 Сеанс: `{session_id}`\nНазва: **{title}**"
+ current_no_title: "📌 Сеанс: `{session_id}`\nНазву не встановлено. Використання: `/title Назва мого сеансу`"
+
+ topic:
+ not_telegram_dm: "Команда /topic доступна лише в приватних чатах Telegram."
+ no_session_db: "База даних сесій недоступна."
+ unauthorized: "Ви не маєте дозволу використовувати /topic у цьому боті."
+ restore_needs_topic: "Щоб відновити сесію, спочатку створіть або відкрийте Telegram topic, а потім надішліть /topic у цьому topic. Щоб створити новий topic, відкрийте All Messages і надішліть там будь-яке повідомлення."
+ topics_disabled: "Telegram topics ще не ввімкнено для цього бота.\n\nЯк увімкнути:\n1. Відкрийте @BotFather.\n2. Виберіть свого бота.\n3. Відкрийте Bot Settings → Threads Settings.\n4. Увімкніть Threaded Mode і переконайтеся, що користувачам дозволено створювати нові threads.\n\nПотім надішліть /topic знову."
+ topics_user_disallowed: "Telegram topics увімкнено, але користувачам не дозволено створювати topics.\n\nВідкрийте @BotFather → виберіть свого бота → Bot Settings → Threads Settings, потім вимкніть 'Disallow users to create new threads'.\n\nПотім надішліть /topic знову."
+ enable_failed: "Не вдалося ввімкнути режим Telegram topic: {error}"
+ bound_status: "Цей topic пов'язано з:\nСесія: {label}\nID: {session_id}\n\nВикористовуйте /new, щоб замінити цей topic новою сесією.\nДля паралельної роботи відкрийте All Messages і надішліть там повідомлення, щоб створити інший topic."
+ thread_ready: "Багатосесійні Telegram topics увімкнено.\n\nЦей topic використовуватиметься як незалежна сесія Hermes. Використовуйте /new, щоб замінити поточну сесію цього topic. Для паралельної роботи відкрийте All Messages і надішліть там повідомлення, щоб створити інший topic."
+ untitled_session: "Сесія без назви"
+
+ undo:
+ nothing: "Немає чого скасовувати."
+ removed: "↩️ Скасовано {count} повідомлень.\nВидалено: «{preview}»"
+
+ update:
+ platform_not_messaging: "✗ /update доступний лише на платформах обміну повідомленнями. Виконайте `hermes update` у терміналі."
+ not_git_repo: "✗ Не git-репозиторій — оновлення неможливе."
+ hermes_cmd_not_found: "✗ Не вдалося знайти команду `hermes`. Hermes запущено, але команда оновлення не знайшла виконуваний файл у PATH або через поточний інтерпретатор Python. Спробуйте виконати `hermes update` вручну у вашому терміналі."
+ start_failed: "✗ Не вдалося запустити оновлення: {error}"
+ starting: "⚕ Запуск оновлення Hermes… Я транслюватиму прогрес сюди."
+
+ usage:
+ rate_limits: "⏱️ **Обмеження швидкості:** {state}"
+ header_session: "📊 **Використання токенів сеансу**"
+ label_model: "Модель: `{model}`"
+ label_input_tokens: "Вхідні токени: {count}"
+ label_cache_read: "Токени читання кешу: {count}"
+ label_cache_write: "Токени запису кешу: {count}"
+ label_output_tokens: "Вихідні токени: {count}"
+ label_total: "Усього: {count}"
+ label_api_calls: "Виклики API: {count}"
+ label_cost: "Вартість: {prefix}${amount}"
+ label_cost_included: "Вартість: включено"
+ label_context: "Контекст: {used} / {total} ({pct}%)"
+ label_compressions: "Стиснень: {count}"
+ header_session_info: "📊 **Інформація про сеанс**"
+ label_messages: "Повідомлень: {count}"
+ label_estimated_context: "Орієнтовний контекст: ~{count} токенів"
+ detailed_after_first: "_(Детальне використання доступне після першої відповіді агента)_"
+ no_data: "Дані про використання для цього сеансу відсутні."
+
+ verbose:
+ not_enabled: "Команду `/verbose` не ввімкнено для платформ обміну повідомленнями.\n\nУвімкніть у `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ Прогрес інструментів: **OFF** — активність інструментів не показується."
+ mode_new: "⚙️ Прогрес інструментів: **NEW** — показується при зміні інструмента (довжина попереднього перегляду: `display.tool_preview_length`, за замовчуванням 40)."
+ mode_all: "⚙️ Прогрес інструментів: **ALL** — показується кожен виклик інструмента (довжина попереднього перегляду: `display.tool_preview_length`, за замовчуванням 40)."
+ mode_verbose: "⚙️ Прогрес інструментів: **VERBOSE** — кожен виклик інструмента з повними аргументами."
+ saved_suffix: "_(збережено для **{platform}** — набуде чинності з наступного повідомлення)_"
+ save_failed: "_(не вдалося зберегти у конфігурацію: {error})_"
+
+ voice:
+ enabled_voice_only: "Голосовий режим увімкнено.\nЯ відповідатиму голосом, коли ви надсилатимете голосові повідомлення.\nВикористайте /voice tts, щоб отримувати голосові відповіді на всі повідомлення."
+ disabled_text: "Голосовий режим вимкнено. Лише текстові відповіді."
+ tts_enabled: "Авто-TTS увімкнено.\nУсі відповіді міститимуть голосове повідомлення."
+ status_mode: "Голосовий режим: {label}"
+ status_channel: "Голосовий канал: #{channel}"
+ status_participants: "Учасники: {count}"
+ status_member: " - {name}{status}"
+ speaking: " (говорить)"
+ enabled_short: "Голосовий режим увімкнено."
+ disabled_short: "Голосовий режим вимкнено."
+ label_off: "Вимкнено (лише текст)"
+ label_voice_only: "Увімкнено (голосова відповідь на голосові повідомлення)"
+ label_all: "TTS (голосова відповідь на всі повідомлення)"
+
+ yolo:
+ disabled: "⚠️ Режим YOLO для цього сеансу **ВИМКНЕНО** — небезпечні команди потребуватимуть схвалення."
+ enabled: "⚡ Режим YOLO для цього сеансу **УВІМКНЕНО** — усі команди схвалюються автоматично. Використовуйте з обережністю."
+
+ shared:
+ session_db_unavailable: "База даних сеансів недоступна."
+ session_db_unavailable_prefix: "База даних сеансів недоступна"
+ session_not_found: "Сеанс не знайдено в базі даних."
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/zh-hant.yaml b/locales/zh-hant.yaml
new file mode 100644
index 00000000000..362ea298de8
--- /dev/null
+++ b/locales/zh-hant.yaml
@@ -0,0 +1,350 @@
+# Hermes 靜態訊息目錄 -- 繁體中文(台灣/香港)
+# See locales/en.yaml for the source of truth; keep keys in sync.
+
+approval:
+ dangerous_header: "⚠️ 危險指令: {description}"
+ choose_long: " [o]僅此一次 | [s]本次工作階段 | [a]永久允許 | [d]拒絕"
+ choose_short: " [o]僅此一次 | [s]本次工作階段 | [d]拒絕"
+ prompt_long: " 選擇 [o/s/a/D]: "
+ prompt_short: " 選擇 [o/s/D]: "
+ timeout: " ⏱ 逾時 — 已拒絕指令"
+ allowed_once: " ✓ 本次允許"
+ allowed_session: " ✓ 本次工作階段內允許"
+ allowed_always: " ✓ 已加入永久允許清單"
+ denied: " ✗ 已拒絕"
+ cancelled: " ✗ 已取消"
+ blocklist_message: "此指令位於無條件封鎖清單中,無法被批准。"
+
+gateway:
+ approval_expired: "⚠️ 批准已逾期(代理不再等待)。請讓代理重試。"
+ draining: "⏳ 正在等待 {count} 個活躍代理結束後重新啟動..."
+ goal_cleared: "✓ 目標已清除。"
+ no_active_goal: "目前沒有作用中的目標。"
+ config_read_failed: "⚠️ 無法讀取 config.yaml:{error}"
+ config_save_failed: "⚠️ 無法儲存設定:{error}"
+
+ model:
+ error_prefix: "錯誤:{error}"
+ switched: "已切換模型為 `{model}`"
+ provider_label: "提供方:{provider}"
+ context_label: "上下文:{tokens} tokens"
+ max_output_label: "最大輸出:{tokens} tokens"
+ cost_label: "費用:{cost}"
+ capabilities_label: "能力:{capabilities}"
+ prompt_caching_enabled: "提示快取:已啟用"
+ warning_prefix: "警告:{warning}"
+ saved_global: "已儲存到 config.yaml(`--global`)"
+ session_only_hint: "_(僅本次工作階段有效 — 加上 `--global` 可永久儲存)_"
+ current_label: "目前:`{model}`({provider})"
+ current_tag: "(目前)"
+ more_models_suffix: "(還有 {count} 個)"
+ usage_switch_model: "`/model ` — 切換模型"
+ usage_switch_provider: "`/model --provider ` — 切換提供方"
+ usage_persist: "`/model --global` — 永久儲存"
+
+ agents:
+ header: "🤖 **作用中的代理與任務**"
+ active_agents: "**作用中代理:** {count}"
+ this_chat: " · 目前聊天"
+ more: "... 還有 {count} 個"
+ running_processes: "**執行中的背景程序:** {count}"
+ async_jobs: "**閘道非同步任務:** {count}"
+ none: "沒有作用中的代理或執行中的任務。"
+ state_starting: "啟動中"
+ state_running: "執行中"
+
+ approve:
+ no_pending: "沒有待批准的指令。"
+ once_singular: "✅ 指令已批准。代理正在恢復…"
+ once_plural: "✅ 指令已批准({count} 條指令)。代理正在恢復…"
+ session_singular: "✅ 指令已批准(本次工作階段內允許該模式)。代理正在恢復…"
+ session_plural: "✅ 指令已批准(本次工作階段內允許該模式)({count} 條指令)。代理正在恢復…"
+ always_singular: "✅ 指令已批准(永久允許該模式)。代理正在恢復…"
+ always_plural: "✅ 指令已批准(永久允許該模式)({count} 條指令)。代理正在恢復…"
+
+ background:
+ usage: "用法:/background <提示>\n範例:/background 摘要今天 HN 上的熱門故事\n\n在獨立工作階段中執行該提示。你可以繼續聊天 — 完成後結果將顯示於此。"
+ started: "🔄 背景任務已啟動:「{preview}」\n任務 ID:{task_id}\n你可以繼續聊天 — 完成後結果將顯示於此。"
+
+ branch:
+ db_unavailable: "工作階段資料庫無法使用。"
+ no_conversation: "沒有可分支的對話 — 請先傳送一則訊息。"
+ create_failed: "建立分支失敗:{error}"
+ switch_failed: "分支已建立,但無法切換到該分支。"
+ branched_one: "⑂ 已分支至 **{title}**(已複製 {count} 則訊息)\n原始:`{parent}`\n分支:`{new}`\n使用 `/resume` 切換回原始工作階段。"
+ branched_many: "⑂ 已分支至 **{title}**(已複製 {count} 則訊息)\n原始:`{parent}`\n分支:`{new}`\n使用 `/resume` 切換回原始工作階段。"
+
+ commands:
+ usage: "用法:`/commands [page]`"
+ skill_header: "⚡ **技能指令**:"
+ default_desc: "技能指令"
+ none: "沒有可用的指令。"
+ header: "📚 **指令**(共 {total} 個,第 {page}/{total_pages} 頁)"
+ nav_prev: "`/commands {page}` ← 上一頁"
+ nav_next: "下一頁 → `/commands {page}`"
+ out_of_range: "_(請求的第 {requested} 頁超出範圍,顯示第 {page} 頁。)_"
+
+ compress:
+ not_enough: "對話內容不足,無法壓縮(至少需要 4 則訊息)。"
+ no_provider: "未設定提供方 — 無法壓縮。"
+ nothing_to_do: "目前沒有可壓縮的內容(對話記錄仍全部為受保護的上下文)。"
+ focus_line: "聚焦:\"{topic}\""
+ summary_failed: "⚠️ 摘要產生失敗({error})。{count} 則歷史訊息已被移除並以佔位符取代;先前的上下文已無法復原。建議檢查 auxiliary.compression 模型設定。"
+ aux_failed: "ℹ️ 設定的壓縮模型 `{model}` 失敗({error})。已使用主要模型復原 — 上下文完整 — 但您可能想檢查 config.yaml 中的 `auxiliary.compression.model`。"
+ failed: "壓縮失敗:{error}"
+
+ debug:
+ upload_failed: "✗ 無法上傳除錯報告:{error}"
+ header: "**除錯報告已上傳:**"
+ auto_delete: "⏱ 貼上的內容將於 6 小時後自動刪除。"
+ full_logs_hint: "如需上傳完整紀錄,請在 CLI 中使用 `hermes debug share`。"
+ share_hint: "請將這些連結分享給 Hermes 團隊以取得支援。"
+
+ deny:
+ stale: "❌ 指令已拒絕(批准已過期)。"
+ no_pending: "沒有待拒絕的指令。"
+ denied_singular: "❌ 指令已拒絕。"
+ denied_plural: "❌ 指令已拒絕({count} 條指令)。"
+
+ fast:
+ not_supported: "⚡ /fast 僅適用於支援 Priority Processing 的 OpenAI 模型。"
+ status: "⚡ Priority Processing\n\n目前模式:`{mode}`\n\n_用法:_ `/fast `"
+ unknown_arg: "⚠️ 未知參數:`{arg}`\n\n**有效選項:** normal、fast、status"
+ saved: "⚡ ✓ Priority Processing:**{label}**(已儲存到設定)\n_(下一則訊息生效)_"
+ session_only: "⚡ ✓ Priority Processing:**{label}**(僅本次工作階段)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 執行階段頁尾:**{state}**\n欄位:`{fields}`\n平台:`{platform}`"
+ usage: "用法:`/footer [on|off|status]`"
+ saved: "📎 執行階段頁尾:**{state}**{example}\n_(已全域儲存 — 下一則訊息生效)_"
+ example_line: "\n範例:`{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "此工作階段不支援目標功能。"
+ no_goal_set: "未設定目標。"
+ paused: "⏸ 目標已暫停:{goal}"
+ no_resume: "沒有可恢復的目標。"
+ resumed: "▶ 目標已恢復:{goal}\n傳送任意訊息繼續,或等待 — 我會在下一輪繼續推進。"
+ invalid: "無效目標:{error}"
+ set: "⊙ 目標已設定({budget} 輪預算):{goal}\n我會持續工作直到目標完成、你暫停/清除目標,或預算耗盡。\n控制指令:/goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes 指令**\n"
+ skill_header: "\n⚡ **技能指令**({count} 個作用中):"
+ more_use_commands: "\n... 還有 {count} 個。使用 `/commands` 檢視完整分頁清單。"
+
+ insights:
+ invalid_days: "無效的 --days 值:{value}"
+ error: "產生洞察時發生錯誤:{error}"
+
+ kanban:
+ error_prefix: "⚠ kanban 錯誤:{error}"
+ subscribed_suffix: "(已訂閱 — 當 {task_id} 完成或被封鎖時將通知您)"
+ truncated_suffix: "…(已截斷;如需完整輸出請在終端機執行 `hermes kanban …`)"
+ no_output: "(無輸出)"
+
+ personality:
+ none_configured: "`{path}/config.yaml` 中未設定人格"
+ header: "🎭 **可用人格**\n"
+ none_option: "• `none` —(不套用人格覆寫)"
+ item: "• `{name}` — {preview}"
+ usage: "\n用法:`/personality `"
+ save_failed: "⚠️ 儲存人格變更失敗:{error}"
+ cleared: "🎭 已清除人格 — 使用基礎代理行為。\n_(下一則訊息生效)_"
+ set_to: "🎭 人格已設定為 **{name}**\n_(下一則訊息生效)_"
+ unknown: "未知人格:`{name}`\n\n可用:{available}"
+
+ profile:
+ header: "👤 **設定檔:** `{profile}`"
+ home: "📂 **主目錄:** `{home}`"
+
+ reasoning:
+ level_default: "medium(預設)"
+ level_disabled: "none(已停用)"
+ scope_session: "工作階段覆寫"
+ scope_global: "全域設定"
+ status: "🧠 **推理設定**\n\n**強度:** `{level}`\n**範圍:** {scope}\n**顯示:** {display}\n\n_用法:_ `/reasoning [--global]`"
+ display_on: "開啟 ✓"
+ display_off: "關閉"
+ display_set_on: "🧠 ✓ 推理顯示:**開啟**\n在 **{platform}** 上每次回應前將顯示模型的思考過程。"
+ display_set_off: "🧠 ✓ **{platform}** 上的推理顯示:**關閉**"
+ reset_global_unsupported: "⚠️ 不支援 `/reasoning reset --global`。請使用 `/reasoning --global` 變更全域預設值。"
+ reset_done: "🧠 ✓ 已清除本工作階段的推理覆寫;回退至全域設定。"
+ unknown_arg: "⚠️ 未知參數:`{arg}`\n\n**有效級別:** none, minimal, low, medium, high, xhigh\n**顯示:** show, hide\n**持久化:** 加上 `--global` 可跨工作階段儲存"
+ set_global: "🧠 ✓ 推理強度已設定為 `{effort}`(已儲存到設定)\n_(下一則訊息生效)_"
+ set_global_save_failed: "🧠 ✓ 推理強度已設定為 `{effort}`(僅本工作階段 — 設定儲存失敗)\n_(下一則訊息生效)_"
+ set_session: "🧠 ✓ 推理強度已設定為 `{effort}`(僅本工作階段 — 加上 `--global` 可持久化)\n_(下一則訊息生效)_"
+
+ reload_mcp:
+ cancelled: "🟡 已取消 /reload-mcp。MCP 工具未變更。"
+ always_followup: "ℹ️ 後續 `/reload-mcp` 呼叫將不再要求確認。可在 `config.yaml` 中將 `approvals.mcp_reload_confirm: true` 重新啟用。"
+ confirm_prompt: "⚠️ **確認 /reload-mcp**\n\n重新載入 MCP 伺服器會為本工作階段重建工具集,並**使提供方提示快取失效** — 下一則訊息將重新傳送完整輸入 token。在長上下文或高推理模型上,這可能成本較高。\n\n請選擇:\n• **批准一次** — 立即重新載入\n• **永遠批准** — 立即重新載入並永久關閉此提示\n• **取消** — 保持 MCP 工具不變\n\n_文字備援:回覆 `/approve`、`/always` 或 `/cancel`。_"
+ header: "🔄 **MCP 伺服器已重新載入**\n"
+ reconnected: "♻️ 已重新連線:{names}"
+ added: "➕ 已新增:{names}"
+ removed: "➖ 已移除:{names}"
+ none_connected: "沒有已連線的 MCP 伺服器。"
+ tools_available: "\n🔧 來自 {servers} 個伺服器的 {tools} 個工具可用"
+ failed: "❌ MCP 重新載入失敗:{error}"
+
+ reload_skills:
+ header: "🔄 **技能已重新載入**\n"
+ no_new: "未偵測到新技能。"
+ total: "\n📚 {count} 個技能可用"
+ added_header: "➕ **新增技能:**"
+ removed_header: "➖ **移除技能:**"
+ item_with_desc: " - {name}:{desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ 技能重新載入失敗:{error}"
+
+ reset:
+ header_default: "✨ 工作階段已重設!重新開始。"
+ header_new: "✨ 新工作階段已啟動!"
+ header_titled: "✨ 新工作階段已啟動:{title}"
+ title_rejected: "\n⚠️ 標題遭拒絕:{error}"
+ title_error_untitled: "\n⚠️ {error} — 工作階段以未命名方式啟動。"
+ title_empty_untitled: "\n⚠️ 清理後標題為空 — 工作階段以未命名方式啟動。"
+ tip: "\n✦ 提示:{tip}"
+
+ restart:
+ in_progress: "⏳ 閘道重新啟動已在進行中……"
+ restarting: "♻ 正在重新啟動閘道。如果 60 秒內未收到通知,請在主控台執行 `hermes gateway restart` 重新啟動。"
+
+ resume:
+ db_unavailable: "工作階段資料庫無法使用。"
+ no_named_sessions: "找不到已命名的工作階段。\n使用 `/title 我的工作階段` 為目前工作階段命名,然後使用 `/resume 我的工作階段` 返回。"
+ list_header: "📋 **已命名工作階段**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\n用法:`/resume <工作階段名稱>`"
+ list_failed: "無法列出工作階段:{error}"
+ not_found: "找不到符合 '**{name}**' 的工作階段。\n使用不帶參數的 `/resume` 檢視可用的工作階段。"
+ already_on: "📌 已在工作階段 **{name}** 上。"
+ switch_failed: "切換工作階段失敗。"
+ resumed_one: "↻ 已恢復工作階段 **{title}**({count} 則訊息)。對話已還原。"
+ resumed_many: "↻ 已恢復工作階段 **{title}**({count} 則訊息)。對話已還原。"
+ resumed_no_count: "↻ 已恢復工作階段 **{title}**。對話已還原。"
+
+ retry:
+ no_previous: "沒有可重試的上一則訊息。"
+
+ rollback:
+ not_enabled: "檢查點未啟用。\n請在 config.yaml 中啟用:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "找不到 {cwd} 的檢查點"
+ invalid_number: "無效的檢查點編號。請使用 1-{max}。"
+ restored: "✅ 已還原至檢查點 {hash}:{reason}\n已自動儲存回復前的快照。"
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "無法儲存主頻道:{error}"
+ success: "✅ 主頻道已設定為 **{name}**(ID:{chat_id})。\n排程任務和跨平台訊息將傳送至此處。"
+
+ status:
+ header: "📊 **Hermes 閘道狀態**"
+ session_id: "**工作階段 ID:** `{session_id}`"
+ title: "**標題:** {title}"
+ created: "**建立時間:** {timestamp}"
+ last_activity: "**最近活動:** {timestamp}"
+ tokens: "**Token 數:** {tokens}"
+ agent_running: "**代理執行中:** {state}"
+ state_yes: "是 ⚡"
+ state_no: "否"
+ queued: "**排隊中的後續:** {count}"
+ platforms: "**已連線平台:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ 已停止。代理尚未啟動 — 你可以繼續此工作階段。"
+ stopped: "⚡ 已停止。你可以繼續此工作階段。"
+ no_active: "沒有可停止的作用中任務。"
+
+ title:
+ db_unavailable: "工作階段資料庫無法使用。"
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ 清理後標題為空。請使用可列印字元。"
+ set_to: "✏️ 已設定工作階段標題:**{title}**"
+ not_found: "在資料庫中找不到此工作階段。"
+ current_with_title: "📌 工作階段:`{session_id}`\n標題:**{title}**"
+ current_no_title: "📌 工作階段:`{session_id}`\n尚未設定標題。用法:`/title 我的工作階段名稱`"
+
+ topic:
+ not_telegram_dm: "/topic 指令僅在 Telegram 私人聊天中可用。"
+ no_session_db: "工作階段資料庫無法使用。"
+ unauthorized: "您無權在此 bot 上使用 /topic。"
+ restore_needs_topic: "若要恢復工作階段,請先建立或開啟一個 Telegram topic,然後在該 topic 中傳送 /topic 。若要建立新 topic,請開啟 All Messages 並在其中傳送任意訊息。"
+ topics_disabled: "此 bot 尚未啟用 Telegram topics。\n\n啟用方法:\n1. 開啟 @BotFather。\n2. 選擇您的 bot。\n3. 開啟 Bot Settings → Threads Settings。\n4. 開啟 Threaded Mode,並確保允許使用者建立新 thread。\n\n然後再次傳送 /topic。"
+ topics_user_disallowed: "Telegram topics 已啟用,但不允許使用者建立 topics。\n\n開啟 @BotFather → 選擇您的 bot → Bot Settings → Threads Settings,然後關閉 'Disallow users to create new threads'。\n\n然後再次傳送 /topic。"
+ enable_failed: "啟用 Telegram topic 模式失敗:{error}"
+ bound_status: "此 topic 已連結至:\n工作階段:{label}\nID:{session_id}\n\n使用 /new 將此 topic 取代為新工作階段。\n如需平行作業,請開啟 All Messages 並在其中傳送訊息以建立另一個 topic。"
+ thread_ready: "Telegram 多工作階段 topics 已啟用。\n\n此 topic 將作為獨立的 Hermes 工作階段使用。使用 /new 取代此 topic 目前的工作階段。如需平行作業,請開啟 All Messages 並在其中傳送訊息以建立另一個 topic。"
+ untitled_session: "未命名工作階段"
+
+ undo:
+ nothing: "沒有可復原的內容。"
+ removed: "↩️ 已復原 {count} 則訊息。\n已移除:「{preview}」"
+
+ update:
+ platform_not_messaging: "✗ /update 僅在訊息平台上可用。請在終端機執行 `hermes update`。"
+ not_git_repo: "✗ 不是 git 儲存庫 — 無法更新。"
+ hermes_cmd_not_found: "✗ 找不到 `hermes` 指令。Hermes 正在執行,但更新指令無法在 PATH 上或透過目前的 Python 解譯器找到執行檔。請嘗試在終端機中手動執行 `hermes update`。"
+ start_failed: "✗ 啟動更新失敗:{error}"
+ starting: "⚕ 正在啟動 Hermes 更新…… 進度將在此處顯示。"
+
+ usage:
+ rate_limits: "⏱️ **速率限制:** {state}"
+ header_session: "📊 **工作階段 token 使用情況**"
+ label_model: "模型:`{model}`"
+ label_input_tokens: "輸入 token:{count}"
+ label_cache_read: "快取讀取 token:{count}"
+ label_cache_write: "快取寫入 token:{count}"
+ label_output_tokens: "輸出 token:{count}"
+ label_total: "總計:{count}"
+ label_api_calls: "API 呼叫次數:{count}"
+ label_cost: "費用:{prefix}${amount}"
+ label_cost_included: "費用:已包含"
+ label_context: "上下文:{used} / {total}({pct}%)"
+ label_compressions: "壓縮次數:{count}"
+ header_session_info: "📊 **工作階段資訊**"
+ label_messages: "訊息數:{count}"
+ label_estimated_context: "預估上下文:~{count} 個 token"
+ detailed_after_first: "_(首次代理回應後可檢視詳細使用情況)_"
+ no_data: "此工作階段沒有可用的使用資料。"
+
+ verbose:
+ not_enabled: "`/verbose` 指令未在訊息平台上啟用。\n\n請在 `config.yaml` 中啟用:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ 工具進度:**OFF** — 不顯示任何工具活動。"
+ mode_new: "⚙️ 工具進度:**NEW** — 工具變更時顯示(預覽長度:`display.tool_preview_length`,預設 40)。"
+ mode_all: "⚙️ 工具進度:**ALL** — 顯示每次工具呼叫(預覽長度:`display.tool_preview_length`,預設 40)。"
+ mode_verbose: "⚙️ 工具進度:**VERBOSE** — 顯示每次工具呼叫及完整參數。"
+ saved_suffix: "_(已為 **{platform}** 儲存 — 下一則訊息生效)_"
+ save_failed: "_(無法儲存到設定:{error})_"
+
+ voice:
+ enabled_voice_only: "語音模式已啟用。\n當你傳送語音訊息時,我會以語音回覆。\n使用 /voice tts 讓所有訊息都收到語音回覆。"
+ disabled_text: "語音模式已停用。僅文字回覆。"
+ tts_enabled: "自動 TTS 已啟用。\n所有回覆都將包含一則語音訊息。"
+ status_mode: "語音模式:{label}"
+ status_channel: "語音頻道:#{channel}"
+ status_participants: "參與人數:{count}"
+ status_member: " - {name}{status}"
+ speaking: "(正在說話)"
+ enabled_short: "語音模式已啟用。"
+ disabled_short: "語音模式已停用。"
+ label_off: "關閉(僅文字)"
+ label_voice_only: "開啟(僅對語音訊息進行語音回覆)"
+ label_all: "TTS(對所有訊息進行語音回覆)"
+
+ yolo:
+ disabled: "⚠️ 本工作階段 YOLO 模式 **已關閉** — 危險指令將需要批准。"
+ enabled: "⚡ 本工作階段 YOLO 模式 **已開啟** — 所有指令自動批准。請謹慎使用。"
+
+ shared:
+ session_db_unavailable: "工作階段資料庫無法使用。"
+ session_db_unavailable_prefix: "工作階段資料庫無法使用"
+ session_not_found: "資料庫中找不到此工作階段。"
+ warn_passthrough: "⚠️ {error}"
diff --git a/locales/zh.yaml b/locales/zh.yaml
index 7cd9a4f3214..7859a1a203c 100644
--- a/locales/zh.yaml
+++ b/locales/zh.yaml
@@ -22,3 +22,329 @@ gateway:
no_active_goal: "当前没有活跃的目标。"
config_read_failed: "⚠️ 无法读取 config.yaml:{error}"
config_save_failed: "⚠️ 无法保存配置:{error}"
+
+ model:
+ error_prefix: "错误:{error}"
+ switched: "已切换模型为 `{model}`"
+ provider_label: "提供方:{provider}"
+ context_label: "上下文:{tokens} tokens"
+ max_output_label: "最大输出:{tokens} tokens"
+ cost_label: "费用:{cost}"
+ capabilities_label: "能力:{capabilities}"
+ prompt_caching_enabled: "提示词缓存:已启用"
+ warning_prefix: "警告:{warning}"
+ saved_global: "已保存到 config.yaml(`--global`)"
+ session_only_hint: "_(仅本次会话有效 — 添加 `--global` 可永久保存)_"
+ current_label: "当前:`{model}`({provider})"
+ current_tag: "(当前)"
+ more_models_suffix: "(还有 {count} 个)"
+ usage_switch_model: "`/model ` — 切换模型"
+ usage_switch_provider: "`/model --provider ` — 切换提供方"
+ usage_persist: "`/model --global` — 永久保存"
+
+ agents:
+ header: "🤖 **活跃代理与任务**"
+ active_agents: "**活跃代理:** {count}"
+ this_chat: " · 当前聊天"
+ more: "... 还有 {count} 个"
+ running_processes: "**运行中的后台进程:** {count}"
+ async_jobs: "**网关异步任务:** {count}"
+ none: "没有活跃的代理或运行中的任务。"
+ state_starting: "启动中"
+ state_running: "运行中"
+
+ approve:
+ no_pending: "没有待批准的命令。"
+ once_singular: "✅ 命令已批准。代理正在恢复…"
+ once_plural: "✅ 命令已批准({count} 条命令)。代理正在恢复…"
+ session_singular: "✅ 命令已批准(本次会话内允许该模式)。代理正在恢复…"
+ session_plural: "✅ 命令已批准(本次会话内允许该模式)({count} 条命令)。代理正在恢复…"
+ always_singular: "✅ 命令已批准(永久允许该模式)。代理正在恢复…"
+ always_plural: "✅ 命令已批准(永久允许该模式)({count} 条命令)。代理正在恢复…"
+
+ background:
+ usage: "用法:/background <提示>\n示例:/background 总结今天 HN 上热门的故事\n\n在独立会话中运行该提示。你可以继续聊天 — 结果完成后将在此显示。"
+ started: "🔄 后台任务已启动:「{preview}」\n任务 ID:{task_id}\n你可以继续聊天 — 完成后结果将在此显示。"
+
+ branch:
+ db_unavailable: "会话数据库不可用。"
+ no_conversation: "没有可分支的对话 — 请先发送一条消息。"
+ create_failed: "创建分支失败:{error}"
+ switch_failed: "分支已创建,但无法切换到它。"
+ branched_one: "⑂ 已分支到 **{title}**(已复制 {count} 条消息)\n原始:`{parent}`\n分支:`{new}`\n使用 `/resume` 切换回原始会话。"
+ branched_many: "⑂ 已分支到 **{title}**(已复制 {count} 条消息)\n原始:`{parent}`\n分支:`{new}`\n使用 `/resume` 切换回原始会话。"
+
+ commands:
+ usage: "用法:`/commands [page]`"
+ skill_header: "⚡ **技能命令**:"
+ default_desc: "技能命令"
+ none: "没有可用的命令。"
+ header: "📚 **命令**(共 {total} 个,第 {page}/{total_pages} 页)"
+ nav_prev: "`/commands {page}` ← 上一页"
+ nav_next: "下一页 → `/commands {page}`"
+ out_of_range: "_(请求的第 {requested} 页超出范围,显示第 {page} 页。)_"
+
+ compress:
+ not_enough: "对话内容不足,无法压缩(至少需要 4 条消息)。"
+ no_provider: "未配置提供方 — 无法压缩。"
+ nothing_to_do: "暂无可压缩内容(对话记录仍全部为受保护上下文)。"
+ focus_line: "聚焦:\"{topic}\""
+ summary_failed: "⚠️ 摘要生成失败({error})。{count} 条历史消息已被移除并替换为占位符;之前的上下文已无法恢复。建议检查 auxiliary.compression 模型配置。"
+ aux_failed: "ℹ️ 配置的压缩模型 `{model}` 失败({error})。已使用主模型恢复 — 上下文完好 — 但您可能想检查 config.yaml 中的 `auxiliary.compression.model`。"
+ failed: "压缩失败:{error}"
+
+ debug:
+ upload_failed: "✗ 无法上传调试报告:{error}"
+ header: "**调试报告已上传:**"
+ auto_delete: "⏱ 粘贴内容将在 6 小时后自动删除。"
+ full_logs_hint: "如需上传完整日志,请在 CLI 中使用 `hermes debug share`。"
+ share_hint: "请将这些链接分享给 Hermes 团队以获得支持。"
+
+ deny:
+ stale: "❌ 命令已拒绝(批准已过期)。"
+ no_pending: "没有待拒绝的命令。"
+ denied_singular: "❌ 命令已拒绝。"
+ denied_plural: "❌ 命令已拒绝({count} 条命令)。"
+
+ fast:
+ not_supported: "⚡ /fast 仅适用于支持优先处理(Priority Processing)的 OpenAI 模型。"
+ status: "⚡ 优先处理\n\n当前模式:`{mode}`\n\n_用法:_ `/fast `"
+ unknown_arg: "⚠️ 未知参数:`{arg}`\n\n**有效选项:** normal、fast、status"
+ saved: "⚡ ✓ 优先处理:**{label}**(已保存到配置)\n_(下一条消息生效)_"
+ session_only: "⚡ ✓ 优先处理:**{label}**(仅本次会话)"
+ label_fast: "FAST"
+ label_normal: "NORMAL"
+ status_fast: "fast"
+ status_normal: "normal"
+
+ footer:
+ status: "📎 运行时页脚:**{state}**\n字段:`{fields}`\n平台:`{platform}`"
+ usage: "用法:`/footer [on|off|status]`"
+ saved: "📎 运行时页脚:**{state}**{example}\n_(已全局保存 — 下一条消息生效)_"
+ example_line: "\n示例:`{preview}`"
+ state_on: "ON"
+ state_off: "OFF"
+
+ goal:
+ unavailable: "此会话不支持目标功能。"
+ no_goal_set: "未设置目标。"
+ paused: "⏸ 目标已暂停:{goal}"
+ no_resume: "没有可恢复的目标。"
+ resumed: "▶ 目标已恢复:{goal}\n发送任意消息继续,或等待 — 我会在下一轮继续推进。"
+ invalid: "无效目标:{error}"
+ set: "⊙ 目标已设置({budget} 轮预算):{goal}\n我将持续工作直到目标完成、你暂停/清除它,或预算耗尽。\n控制命令:/goal status · /goal pause · /goal resume · /goal clear"
+
+ help:
+ header: "📖 **Hermes 命令**\n"
+ skill_header: "\n⚡ **技能命令**({count} 个活跃):"
+ more_use_commands: "\n... 还有 {count} 个。使用 `/commands` 查看完整分页列表。"
+
+ insights:
+ invalid_days: "无效的 --days 值:{value}"
+ error: "生成洞察时出错:{error}"
+
+ kanban:
+ error_prefix: "⚠ kanban 错误:{error}"
+ subscribed_suffix: "(已订阅 — 当 {task_id} 完成或被阻塞时将通知您)"
+ truncated_suffix: "…(已截断;如需完整输出请在终端运行 `hermes kanban …`)"
+ no_output: "(无输出)"
+
+ personality:
+ none_configured: "`{path}/config.yaml` 中未配置人格设定"
+ header: "🎭 **可用人格**\n"
+ none_option: "• `none` — (不应用人格覆盖)"
+ item: "• `{name}` — {preview}"
+ usage: "\n用法:`/personality `"
+ save_failed: "⚠️ 保存人格变更失败:{error}"
+ cleared: "🎭 已清除人格 — 使用基础代理行为。\n_(在下一条消息时生效)_"
+ set_to: "🎭 人格已设置为 **{name}**\n_(在下一条消息时生效)_"
+ unknown: "未知人格:`{name}`\n\n可用:{available}"
+
+ profile:
+ header: "👤 **配置文件:** `{profile}`"
+ home: "📂 **主目录:** `{home}`"
+
+ reasoning:
+ level_default: "medium(默认)"
+ level_disabled: "none(已禁用)"
+ scope_session: "会话覆盖"
+ scope_global: "全局配置"
+ status: "🧠 **推理设置**\n\n**强度:** `{level}`\n**作用域:** {scope}\n**显示:** {display}\n\n_用法:_ `/reasoning [--global]`"
+ display_on: "开 ✓"
+ display_off: "关"
+ display_set_on: "🧠 ✓ 推理显示:**开启**\n在 **{platform}** 上每次响应前将显示模型的思考过程。"
+ display_set_off: "🧠 ✓ **{platform}** 上的推理显示:**关闭**"
+ reset_global_unsupported: "⚠️ 不支持 `/reasoning reset --global`。请使用 `/reasoning --global` 修改全局默认值。"
+ reset_done: "🧠 ✓ 已清除本会话的推理覆盖;回退到全局配置。"
+ unknown_arg: "⚠️ 未知参数:`{arg}`\n\n**有效级别:** none, minimal, low, medium, high, xhigh\n**显示:** show, hide\n**持久化:** 添加 `--global` 以跨会话保存"
+ set_global: "🧠 ✓ 推理强度已设置为 `{effort}`(已保存到配置)\n_(下一条消息生效)_"
+ set_global_save_failed: "🧠 ✓ 推理强度已设置为 `{effort}`(仅本会话 — 配置保存失败)\n_(下一条消息生效)_"
+ set_session: "🧠 ✓ 推理强度已设置为 `{effort}`(仅本会话 — 添加 `--global` 以持久化)\n_(下一条消息生效)_"
+
+ reload_mcp:
+ cancelled: "🟡 已取消 /reload-mcp。MCP 工具未更改。"
+ always_followup: "ℹ️ 后续 `/reload-mcp` 调用将不再确认。可在 `config.yaml` 中将 `approvals.mcp_reload_confirm: true` 重新启用。"
+ confirm_prompt: "⚠️ **确认 /reload-mcp**\n\n重新加载 MCP 服务器会为本会话重建工具集,并**使提供方提示词缓存失效** — 下一条消息将重新发送完整输入令牌。在长上下文或高推理模型上,这可能开销较大。\n\n请选择:\n• **批准一次** — 立即重新加载\n• **始终批准** — 立即重新加载并永久静默此提示\n• **取消** — 保持 MCP 工具不变\n\n_文本备用:回复 `/approve`、`/always` 或 `/cancel`。_"
+ header: "🔄 **MCP 服务器已重新加载**\n"
+ reconnected: "♻️ 已重新连接:{names}"
+ added: "➕ 已添加:{names}"
+ removed: "➖ 已移除:{names}"
+ none_connected: "没有连接的 MCP 服务器。"
+ tools_available: "\n🔧 来自 {servers} 个服务器的 {tools} 个工具可用"
+ failed: "❌ MCP 重新加载失败:{error}"
+
+ reload_skills:
+ header: "🔄 **技能已重新加载**\n"
+ no_new: "未检测到新技能。"
+ total: "\n📚 {count} 个技能可用"
+ added_header: "➕ **新增技能:**"
+ removed_header: "➖ **移除技能:**"
+ item_with_desc: " - {name}:{desc}"
+ item_no_desc: " - {name}"
+ failed: "❌ 技能重新加载失败:{error}"
+
+ reset:
+ header_default: "✨ 会话已重置!重新开始。"
+ header_new: "✨ 新会话已启动!"
+ header_titled: "✨ 新会话已启动:{title}"
+ title_rejected: "\n⚠️ 标题被拒绝:{error}"
+ title_error_untitled: "\n⚠️ {error} — 会话以未命名方式启动。"
+ title_empty_untitled: "\n⚠️ 清理后标题为空 — 会话以未命名方式启动。"
+ tip: "\n✦ 提示:{tip}"
+
+ restart:
+ in_progress: "⏳ 网关重启已在进行中……"
+ restarting: "♻ 正在重启网关。如果 60 秒内没有收到通知,请在控制台运行 `hermes gateway restart` 重启。"
+
+ resume:
+ db_unavailable: "会话数据库不可用。"
+ no_named_sessions: "未找到已命名的会话。\n使用 `/title 我的会话` 为当前会话命名,然后用 `/resume 我的会话` 返回。"
+ list_header: "📋 **已命名会话**\n"
+ list_item: "• **{title}**{preview_part}"
+ list_preview_suffix: " — _{preview}_"
+ list_footer: "\n用法:`/resume <会话名称>`"
+ list_failed: "无法列出会话:{error}"
+ not_found: "未找到匹配 '**{name}**' 的会话。\n使用不带参数的 `/resume` 查看可用会话。"
+ already_on: "📌 已在会话 **{name}** 上。"
+ switch_failed: "切换会话失败。"
+ resumed_one: "↻ 已恢复会话 **{title}**({count} 条消息)。对话已还原。"
+ resumed_many: "↻ 已恢复会话 **{title}**({count} 条消息)。对话已还原。"
+ resumed_no_count: "↻ 已恢复会话 **{title}**。对话已还原。"
+
+ retry:
+ no_previous: "没有可重试的上一条消息。"
+
+ rollback:
+ not_enabled: "检查点未启用。\n请在 config.yaml 中启用:\n```\ncheckpoints:\n enabled: true\n```"
+ none_found: "未找到 {cwd} 的检查点"
+ invalid_number: "无效的检查点编号。请使用 1-{max}。"
+ restored: "✅ 已恢复到检查点 {hash}:{reason}\n已自动保存回滚前的快照。"
+ restore_failed: "❌ {error}"
+
+ set_home:
+ save_failed: "无法保存主频道:{error}"
+ success: "✅ 主频道已设置为 **{name}**(ID:{chat_id})。\n定时任务和跨平台消息将发送到此处。"
+
+ status:
+ header: "📊 **Hermes 网关状态**"
+ session_id: "**会话 ID:** `{session_id}`"
+ title: "**标题:** {title}"
+ created: "**创建时间:** {timestamp}"
+ last_activity: "**最近活动:** {timestamp}"
+ tokens: "**Token 数:** {tokens}"
+ agent_running: "**代理运行中:** {state}"
+ state_yes: "是 ⚡"
+ state_no: "否"
+ queued: "**排队的后续:** {count}"
+ platforms: "**已连接平台:** {platforms}"
+
+ stop:
+ stopped_pending: "⚡ 已停止。代理尚未启动 — 你可以继续此会话。"
+ stopped: "⚡ 已停止。你可以继续此会话。"
+ no_active: "没有可停止的活跃任务。"
+
+ title:
+ db_unavailable: "会话数据库不可用。"
+ warn_prefix: "⚠️ {error}"
+ empty_after_clean: "⚠️ 清理后标题为空。请使用可打印字符。"
+ set_to: "✏️ 已设置会话标题:**{title}**"
+ not_found: "未在数据库中找到该会话。"
+ current_with_title: "📌 会话:`{session_id}`\n标题:**{title}**"
+ current_no_title: "📌 会话:`{session_id}`\n尚未设置标题。用法:`/title 我的会话名称`"
+
+ topic:
+ not_telegram_dm: "/topic 命令仅在 Telegram 私聊中可用。"
+ no_session_db: "会话数据库不可用。"
+ unauthorized: "您无权在此 bot 上使用 /topic。"
+ restore_needs_topic: "若要恢复会话,请先创建或打开一个 Telegram topic,然后在该 topic 中发送 /topic 。要创建新 topic,请打开 All Messages 并在其中发送任意消息。"
+ topics_disabled: "此 bot 尚未启用 Telegram topics。\n\n启用方法:\n1. 打开 @BotFather。\n2. 选择您的 bot。\n3. 打开 Bot Settings → Threads Settings。\n4. 开启 Threaded Mode,并确保允许用户创建新线程。\n\n然后再次发送 /topic。"
+ topics_user_disallowed: "Telegram topics 已启用,但不允许用户创建 topics。\n\n打开 @BotFather → 选择您的 bot → Bot Settings → Threads Settings,然后关闭 'Disallow users to create new threads'。\n\n然后再次发送 /topic。"
+ enable_failed: "启用 Telegram topic 模式失败:{error}"
+ bound_status: "此 topic 已关联到:\n会话:{label}\nID:{session_id}\n\n使用 /new 将此 topic 替换为新会话。\n如需并行工作,请打开 All Messages 并在其中发送消息以创建另一个 topic。"
+ thread_ready: "Telegram 多会话 topics 已启用。\n\n此 topic 将作为独立的 Hermes 会话使用。使用 /new 替换此 topic 的当前会话。如需并行工作,请打开 All Messages 并在其中发送消息以创建另一个 topic。"
+ untitled_session: "未命名会话"
+
+ undo:
+ nothing: "没有可撤销的内容。"
+ removed: "↩️ 已撤销 {count} 条消息。\n已移除:「{preview}」"
+
+ update:
+ platform_not_messaging: "✗ /update 仅在消息平台可用。请在终端运行 `hermes update`。"
+ not_git_repo: "✗ 不是 git 仓库 — 无法更新。"
+ hermes_cmd_not_found: "✗ 无法找到 `hermes` 命令。Hermes 正在运行,但更新命令无法在 PATH 上或通过当前 Python 解释器找到可执行文件。请尝试在终端中手动运行 `hermes update`。"
+ start_failed: "✗ 启动更新失败:{error}"
+ starting: "⚕ 正在启动 Hermes 更新…… 进度将在此处显示。"
+
+ usage:
+ rate_limits: "⏱️ **速率限制:** {state}"
+ header_session: "📊 **会话令牌使用情况**"
+ label_model: "模型:`{model}`"
+ label_input_tokens: "输入令牌:{count}"
+ label_cache_read: "缓存读取令牌:{count}"
+ label_cache_write: "缓存写入令牌:{count}"
+ label_output_tokens: "输出令牌:{count}"
+ label_total: "总计:{count}"
+ label_api_calls: "API 调用次数:{count}"
+ label_cost: "费用:{prefix}${amount}"
+ label_cost_included: "费用:已包含"
+ label_context: "上下文:{used} / {total}({pct}%)"
+ label_compressions: "压缩次数:{count}"
+ header_session_info: "📊 **会话信息**"
+ label_messages: "消息数:{count}"
+ label_estimated_context: "估计上下文:~{count} 个令牌"
+ detailed_after_first: "_(首次代理响应后可查看详细使用情况)_"
+ no_data: "此会话暂无使用数据。"
+
+ verbose:
+ not_enabled: "`/verbose` 命令未在消息平台启用。\n\n请在 `config.yaml` 中启用:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
+ mode_off: "⚙️ 工具进度:**OFF** — 不显示任何工具活动。"
+ mode_new: "⚙️ 工具进度:**NEW** — 工具变化时显示(预览长度:`display.tool_preview_length`,默认 40)。"
+ mode_all: "⚙️ 工具进度:**ALL** — 显示每次工具调用(预览长度:`display.tool_preview_length`,默认 40)。"
+ mode_verbose: "⚙️ 工具进度:**VERBOSE** — 显示每次工具调用及完整参数。"
+ saved_suffix: "_(已为 **{platform}** 保存 — 下一条消息生效)_"
+ save_failed: "_(无法保存到配置:{error})_"
+
+ voice:
+ enabled_voice_only: "语音模式已启用。\n当你发送语音消息时,我会用语音回复。\n使用 /voice tts 让所有消息都收到语音回复。"
+ disabled_text: "语音模式已禁用。仅文本回复。"
+ tts_enabled: "自动 TTS 已启用。\n所有回复都将包含一条语音消息。"
+ status_mode: "语音模式:{label}"
+ status_channel: "语音频道:#{channel}"
+ status_participants: "参与人数:{count}"
+ status_member: " - {name}{status}"
+ speaking: "(正在说话)"
+ enabled_short: "语音模式已启用。"
+ disabled_short: "语音模式已禁用。"
+ label_off: "关闭(仅文本)"
+ label_voice_only: "开启(仅对语音消息进行语音回复)"
+ label_all: "TTS(对所有消息进行语音回复)"
+
+ yolo:
+ disabled: "⚠️ 本会话 YOLO 模式 **已关闭** — 危险命令将需要批准。"
+ enabled: "⚡ 本会话 YOLO 模式 **已开启** — 所有命令自动批准。请谨慎使用。"
+
+ shared:
+ session_db_unavailable: "会话数据库不可用。"
+ session_db_unavailable_prefix: "会话数据库不可用"
+ session_not_found: "数据库中未找到该会话。"
+ warn_passthrough: "⚠️ {error}"
diff --git a/plugins/hermes-achievements/dashboard/dist/index.js b/plugins/hermes-achievements/dashboard/dist/index.js
index d30f34e11e9..e51227991d9 100644
--- a/plugins/hermes-achievements/dashboard/dist/index.js
+++ b/plugins/hermes-achievements/dashboard/dist/index.js
@@ -12,6 +12,35 @@
const hooks = SDK.hooks;
const C = SDK.components;
const cn = SDK.utils.cn;
+ // useI18n is a hook so each component that needs translations calls it
+ // locally (see AchievementsPage, AchievementCard, ShareDialog, LoadingPage).
+ // Older host dashboards may not expose useI18n yet; fall back to a no-op
+ // shim that returns en values so the bundle still renders against an older
+ // host SDK. English fallback strings live alongside each call site.
+ const useI18n = SDK.useI18n || function () { return { t: { achievements: null }, locale: "en" }; };
+
+ // Resolve a translation by dotted path (e.g. "card.share_text"); fall back to
+ // the English string passed in. Used inside components after they call
+ // useI18n() so they can still render against an older host SDK that doesn't
+ // expose the achievements namespace yet.
+ function tx(t, path, fallback, vars) {
+ let node = t && t.achievements;
+ if (node) {
+ const parts = path.split(".");
+ for (let i = 0; i < parts.length; i++) {
+ if (node && typeof node === "object" && parts[i] in node) {
+ node = node[parts[i]];
+ } else { node = null; break; }
+ }
+ }
+ let str = (typeof node === "string") ? node : fallback;
+ if (vars) {
+ for (const k in vars) {
+ str = str.replace(new RegExp("\\{" + k + "\\}", "g"), vars[k]);
+ }
+ }
+ return str;
+ }
const LUCIDE = {"flame":"","avalanche":"\n ","nodes":"\n \n \n \n ","rocket":"\n \n \n ","branch":"\n \n \n ","daemon":"\n ","clock":"\n ","warning":"\n \n ","wine":"\n \n \n ","scroll":"\n \n \n ","plug":"\n \n \n \n \n ","lock":"\n \n ","package_skull":"\n \n \n \n ","restart":"\n \n \n ","key":"\n ","colon":"\n ","container":"\n \n \n \n ","melting_clock":"\n \n ","pencil":"\n ","blueprint":"\n \n \n \n ","pixel":"\n \n \n \n ","ship":"\n \n \n \n ","spark_cursor":"\n \n \n \n ","needle":"","hammer_scroll":"\n \n ","anvil":"\n \n \n \n ","crystal":"\n \n ","palace":"\n \n \n \n \n ","dragon":"","antenna":"\n \n \n \n \n \n ","puzzle":"","rewind":"\n ","spiral":"\n \n \n \n ","quote":"\n ","compass":"\n ","browser":"\n \n ","terminal":"\n ","wand":"\n \n \n \n \n \n \n ","folder":"\n \n ","eye":"\n ","wave":"","swap":"\n \n \n ","router":"\n \n \n \n \n ","codex":"\n \n ","prism":"\n \n ","marathon":"\n \n ","calendar":"\n \n \n \n \n \n \n \n \n ","moon":"","cache":"\n \n ","secret":"\n \n "};
@@ -249,6 +278,7 @@
}
function ShareDialog({ achievement, onClose }) {
+ const { t } = useI18n();
const [status, setStatus] = hooks.useState("rendering"); // rendering | ready | copied | error
const [errorMsg, setErrorMsg] = hooks.useState(null);
const [previewUrl, setPreviewUrl] = hooks.useState(null);
@@ -290,7 +320,7 @@
if (!blobRef.current) return;
try {
if (!navigator.clipboard || !window.ClipboardItem) {
- throw new Error("Clipboard image copy not supported in this browser — use Download instead.");
+ throw new Error(tx(t, "share.clipboard_unsupported", "Clipboard image copy not supported in this browser — use Download instead."));
}
await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blobRef.current }),
@@ -308,8 +338,11 @@
// paste in the same flow.
function tweetText() {
const tierPart = achievement.tier ? (achievement.tier + " tier ") : "";
- return "Just unlocked " + tierPart + "\"" + achievement.name + "\" in Hermes Agent ☤\n\n" +
- "@NousResearch · https://hermes-agent.nousresearch.com";
+ const tmpl = tx(t, "share.tweet_text", "Just unlocked {tier_part}\"{name}\" in Hermes Agent ☤", {
+ tier_part: tierPart,
+ name: achievement.name,
+ });
+ return tmpl + "\n\n@NousResearch · https://hermes-agent.nousresearch.com";
}
function shareOnX() {
@@ -321,36 +354,36 @@
className: "ha-share-backdrop",
onClick: function (e) { if (e.target === e.currentTarget) onClose(); },
},
- React.createElement("div", { className: "ha-share-dialog", role: "dialog", "aria-label": "Share achievement" },
+ React.createElement("div", { className: "ha-share-dialog", role: "dialog", "aria-label": tx(t, "share.dialog_label", "Share achievement") },
React.createElement("div", { className: "ha-share-head" },
- React.createElement("strong", null, "Share: " + achievement.name),
- React.createElement("button", { className: "ha-share-close", onClick: onClose, "aria-label": "Close" }, "×")
+ React.createElement("strong", null, tx(t, "share.header", "Share: {name}", { name: achievement.name })),
+ React.createElement("button", { className: "ha-share-close", onClick: onClose, "aria-label": tx(t, "share.close", "Close") }, "×")
),
React.createElement("div", { className: "ha-share-preview" },
- status === "rendering" && React.createElement("div", { className: "ha-share-placeholder" }, "Rendering…"),
- previewUrl && React.createElement("img", { src: previewUrl, alt: achievement.name + " share card" })
+ status === "rendering" && React.createElement("div", { className: "ha-share-placeholder" }, tx(t, "share.rendering", "Rendering…")),
+ previewUrl && React.createElement("img", { src: previewUrl, alt: tx(t, "share.card_alt", "{name} share card", { name: achievement.name }) })
),
- status === "error" && React.createElement("div", { className: "ha-share-error" }, errorMsg || "Something went wrong."),
+ status === "error" && React.createElement("div", { className: "ha-share-error" }, errorMsg || tx(t, "share.error_generic", "Something went wrong.")),
React.createElement("div", { className: "ha-share-actions" },
React.createElement("button", {
className: "ha-share-btn ha-share-btn-primary",
onClick: shareOnX,
- title: "Opens X with a pre-filled post",
- }, "Share on X"),
+ title: tx(t, "share.x_title", "Opens X with a pre-filled post"),
+ }, tx(t, "share.x_button", "Share on X")),
React.createElement("button", {
className: "ha-share-btn",
onClick: copyToClipboard,
disabled: status !== "ready" && status !== "copied",
- title: "Copy the image to paste into your post",
- }, status === "copied" ? "Copied ✓" : "Copy image"),
+ title: tx(t, "share.copy_title", "Copy the image to paste into your post"),
+ }, status === "copied" ? tx(t, "share.copied", "Copied ✓") : tx(t, "share.copy_button", "Copy image")),
React.createElement("button", {
className: "ha-share-btn",
onClick: download,
disabled: status !== "ready" && status !== "copied",
- }, "Download PNG")
+ }, tx(t, "share.download_button", "Download PNG"))
),
React.createElement("p", { className: "ha-share-hint" },
- "Share on X opens a pre-filled post in a new tab. Click Copy image first if you want the 1200×630 badge attached — X lets you paste it right into the tweet composer. Download PNG saves the file for use anywhere."
+ tx(t, "share.hint", "Share on X opens a pre-filled post in a new tab. Click Copy image first if you want the 1200×630 badge attached — X lets you paste it right into the tweet composer. Download PNG saves the file for use anywhere.")
)
)
);
@@ -408,24 +441,32 @@
}
function LoadingPage() {
+ const { t } = useI18n();
return React.createElement("div", { className: "ha-page ha-page-loading" },
React.createElement("section", { className: "ha-hero ha-loading-hero" },
React.createElement("div", null,
- React.createElement("div", { className: "ha-kicker" }, "Agentic Gamerscore"),
- React.createElement("h1", null, "Hermes Achievements"),
- React.createElement("p", null, "Scanning Hermes session history. First scan can take 5–10 seconds on large histories.")
+ React.createElement("div", { className: "ha-kicker" }, tx(t, "hero.kicker", "Agentic Gamerscore")),
+ React.createElement("h1", null, tx(t, "hero.title", "Hermes Achievements")),
+ React.createElement("p", null, tx(t, "hero.scan_subtitle", "Scanning Hermes session history. First scan can take 5–10 seconds on large histories."))
),
React.createElement("div", { className: "ha-scan-status", role: "status", "aria-live": "polite" },
React.createElement("span", { className: "ha-scan-pulse", "aria-hidden": "true" }),
React.createElement("div", null,
- React.createElement("strong", null, "Building achievement profile…"),
- React.createElement("p", null, "Reading sessions, tool calls, model metadata, and unlock state.")
+ React.createElement("strong", null, tx(t, "scan.building_headline", "Building achievement profile…")),
+ React.createElement("p", null, tx(t, "scan.building_detail", "Reading sessions, tool calls, model metadata, and unlock state."))
)
)
),
React.createElement("div", { className: "ha-stats" },
- ["Unlocked", "Discovered", "Secrets", "Highest tier", "Latest"].map(function (label) {
- return React.createElement(C.Card, { key: label, className: "ha-stat ha-skeleton-stat" },
+ [
+ { key: "stats.unlocked", fallback: "Unlocked" },
+ { key: "stats.discovered", fallback: "Discovered" },
+ { key: "stats.secrets", fallback: "Secrets" },
+ { key: "stats.highest_tier", fallback: "Highest tier" },
+ { key: "stats.latest", fallback: "Latest" },
+ ].map(function (entry) {
+ const label = tx(t, entry.key, entry.fallback);
+ return React.createElement(C.Card, { key: entry.key, className: "ha-stat ha-skeleton-stat" },
React.createElement(C.CardContent, { className: "ha-stat-content" },
React.createElement("div", { className: "ha-stat-label" }, label),
React.createElement("div", { className: "ha-skeleton ha-skeleton-stat-value" }),
@@ -436,12 +477,12 @@
),
React.createElement("section", { className: "ha-guide ha-loading-guide" },
React.createElement("div", null,
- React.createElement("strong", null, "Scan status"),
- React.createElement("p", null, "Hermes is scanning local history once, then cards will appear automatically. Nothing is stuck if this takes a few seconds.")
+ React.createElement("strong", null, tx(t, "guide.scan_status_header", "Scan status")),
+ React.createElement("p", null, tx(t, "guide.scan_status_body", "Hermes is scanning local history once, then cards will appear automatically. Nothing is stuck if this takes a few seconds."))
),
React.createElement("div", null,
- React.createElement("strong", null, "What is scanned"),
- React.createElement("p", null, "Sessions, tool calls, model metadata, errors, achievements, and local unlock state.")
+ React.createElement("strong", null, tx(t, "guide.what_scanned_header", "What is scanned")),
+ React.createElement("p", null, tx(t, "guide.what_scanned_body", "Sessions, tool calls, model metadata, errors, achievements, and local unlock state."))
)
),
React.createElement("section", { className: "ha-grid" }, [0, 1, 2, 3, 4, 5].map(function (i) {
@@ -452,14 +493,30 @@
function AchievementCard({ achievement }) {
+ const { t } = useI18n();
const unlocked = achievement.unlocked;
const progress = achievement.progress || 0;
const pct = achievement.progress_pct || (unlocked ? 100 : 0);
const state = achievement.state || (unlocked ? "unlocked" : "discovered");
- const stateLabel = state === "unlocked" ? "Unlocked" : (state === "secret" ? "Secret" : "Discovered");
+ const stateLabel = state === "unlocked"
+ ? tx(t, "state.unlocked", "Unlocked")
+ : (state === "secret" ? tx(t, "state.secret", "Secret") : tx(t, "state.discovered", "Discovered"));
const targetTier = achievement.next_tier || achievement.tier;
- const tierLabel = achievement.tier ? achievement.tier : (targetTier ? "Target " + targetTier : (state === "secret" ? "Hidden" : (unlocked ? "Complete" : "Objective")));
- const progressText = state === "secret" ? "hidden" : (progress + (achievement.next_threshold ? " / " + achievement.next_threshold : ""));
+ let tierLabel;
+ if (achievement.tier) {
+ tierLabel = achievement.tier;
+ } else if (targetTier) {
+ tierLabel = tx(t, "tier.target", "Target {tier}", { tier: targetTier });
+ } else if (state === "secret") {
+ tierLabel = tx(t, "tier.hidden", "Hidden");
+ } else if (unlocked) {
+ tierLabel = tx(t, "tier.complete", "Complete");
+ } else {
+ tierLabel = tx(t, "tier.objective", "Objective");
+ }
+ const progressText = state === "secret"
+ ? tx(t, "progress.hidden", "hidden")
+ : (progress + (achievement.next_threshold ? " / " + achievement.next_threshold : ""));
const [shareOpen, setShareOpen] = hooks.useState(false);
return React.createElement(C.Card, { className: cn("ha-card", "ha-state-" + state, tierClass(achievement.tier || achievement.next_tier)) },
React.createElement(C.CardContent, { className: "ha-card-content" },
@@ -475,21 +532,23 @@
state === "unlocked" && React.createElement("button", {
className: "ha-share-trigger",
onClick: function () { setShareOpen(true); },
- title: "Share this achievement",
- "aria-label": "Share " + achievement.name,
- }, "Share")
+ title: tx(t, "card.share_title", "Share this achievement"),
+ "aria-label": tx(t, "card.share_label", "Share {name}", { name: achievement.name }),
+ }, tx(t, "card.share_text", "Share"))
)
),
React.createElement("p", { className: "ha-description" }, achievement.description),
achievement.criteria && React.createElement("details", { className: "ha-criteria" },
- React.createElement("summary", null, state === "secret" ? "How to reveal" : "What counts"),
+ React.createElement("summary", null, state === "secret"
+ ? tx(t, "card.how_to_reveal", "How to reveal")
+ : tx(t, "card.what_counts", "What counts")),
React.createElement("p", null, achievement.criteria)
),
React.createElement("div", { className: "ha-evidence-slot" },
achievement.evidence ? React.createElement("div", { className: "ha-evidence" },
- React.createElement("span", { className: "ha-evidence-label" }, "Evidence"),
- React.createElement("span", { className: "ha-evidence-title" }, achievement.evidence.title || achievement.evidence.session_id || "session")
- ) : React.createElement("div", { className: "ha-evidence ha-evidence-empty", "aria-hidden": "true" }, "No evidence yet")
+ React.createElement("span", { className: "ha-evidence-label" }, tx(t, "card.evidence_label", "Evidence")),
+ React.createElement("span", { className: "ha-evidence-title" }, achievement.evidence.title || achievement.evidence.session_id || tx(t, "card.evidence_session_fallback", "session"))
+ ) : React.createElement("div", { className: "ha-evidence ha-evidence-empty", "aria-hidden": "true" }, tx(t, "card.no_evidence", "No evidence yet"))
),
React.createElement("div", { className: "ha-progress-row" },
React.createElement("div", { className: "ha-progress-track" },
@@ -506,6 +565,7 @@
}
function AchievementsPage() {
+ const { t } = useI18n();
const [data, setData] = hooks.useState(null);
const [loading, setLoading] = hooks.useState(true);
const [error, setError] = hooks.useState(null);
@@ -554,7 +614,7 @@
const discovered = achievements.filter(function (a) { return a.state === "discovered"; });
const secret = achievements.filter(function (a) { return a.state === "secret"; });
const latest = unlocked.slice().sort(function (a, b) { return (b.unlocked_at || 0) - (a.unlocked_at || 0); }).slice(0, 5);
- const highest = ["Olympian", "Diamond", "Gold", "Silver", "Copper"].find(function (tier) { return unlocked.some(function (a) { return a.tier === tier; }); }) || "None yet";
+ const highest = ["Olympian", "Diamond", "Gold", "Silver", "Copper"].find(function (tier) { return unlocked.some(function (a) { return a.tier === tier; }); }) || tx(t, "stats.none_yet", "None yet");
// Build the in-progress scan banner once so the JSX below stays readable.
// Shows nothing when the scan is idle. When a scan is running it renders
@@ -568,11 +628,15 @@
const total = Number(meta.sessions_expected_total || 0);
const pct = total > 0 ? Math.max(0, Math.min(100, Math.floor((scanned / total) * 100))) : 0;
const headline = scanMode === "pending"
- ? "Starting achievement scan…"
- : "Building achievement profile…";
+ ? tx(t, "scan.starting_headline", "Starting achievement scan…")
+ : tx(t, "scan.building_headline", "Building achievement profile…");
const detail = total > 0
- ? ("Scanned " + scanned.toLocaleString() + " of " + total.toLocaleString() + " sessions · " + pct + "%. Badges unlock as more history streams in.")
- : "Reading sessions, tool calls, model metadata, and unlock state. Badges appear here as they unlock.";
+ ? tx(t, "scan.progress_detail", "Scanned {scanned} of {total} sessions · {pct}%. Badges unlock as more history streams in.", {
+ scanned: scanned.toLocaleString(),
+ total: total.toLocaleString(),
+ pct: String(pct),
+ })
+ : tx(t, "scan.idle_detail", "Reading sessions, tool calls, model metadata, and unlock state. Badges appear here as they unlock.");
scanBanner = React.createElement("section", { className: "ha-scan-banner", role: "status", "aria-live": "polite" },
React.createElement("div", { className: "ha-scan-banner-head" },
React.createElement("span", { className: "ha-scan-pulse", "aria-hidden": "true" }),
@@ -591,44 +655,57 @@
return React.createElement(LoadingPage, null);
}
+ // Translate the "All" category pill but keep the underlying state ("All")
+ // as the canonical key the API matches against.
+ const allCategoryLabel = tx(t, "filters.all_categories", "All");
+ const visibilityLabels = {
+ all: tx(t, "filters.visibility_all", "all"),
+ unlocked: tx(t, "filters.visibility_unlocked", "unlocked"),
+ discovered: tx(t, "filters.visibility_discovered", "discovered"),
+ secret: tx(t, "filters.visibility_secret", "secret"),
+ };
+
return React.createElement("div", { className: "ha-page" },
React.createElement("section", { className: "ha-hero" },
React.createElement("div", null,
- React.createElement("div", { className: "ha-kicker" }, "Agentic Gamerscore"),
- React.createElement("h1", null, "Hermes Achievements"),
- React.createElement("p", null, "Collectible Hermes badges earned from real session history. Known unfinished achievements are shown as Discovered; Secret achievements stay hidden until the first matching behavior appears.")
+ React.createElement("div", { className: "ha-kicker" }, tx(t, "hero.kicker", "Agentic Gamerscore")),
+ React.createElement("h1", null, tx(t, "hero.title", "Hermes Achievements")),
+ React.createElement("p", null, tx(t, "hero.subtitle", "Collectible Hermes badges earned from real session history. Known unfinished achievements are shown as Discovered; Secret achievements stay hidden until the first matching behavior appears."))
),
- React.createElement(C.Button, { onClick: load, className: "ha-refresh" }, "Rescan")
+ React.createElement(C.Button, { onClick: load, className: "ha-refresh" }, tx(t, "actions.rescan", "Rescan"))
),
scanBanner,
error && React.createElement(C.Card, { className: "ha-error" }, React.createElement(C.CardContent, null, String(error))),
React.createElement("div", { className: "ha-stats" },
- React.createElement(StatCard, { label: "Unlocked", value: (data ? data.unlocked_count : 0) + " / " + (data ? data.total_count : 0), hint: "earned badges" }),
- React.createElement(StatCard, { label: "Discovered", value: discovered.length, hint: "known, not earned yet" }),
- React.createElement(StatCard, { label: "Secrets", value: secret.length, hint: "hidden until first signal" }),
- React.createElement(StatCard, { label: "Highest tier", value: highest, hint: "Copper → Silver → Gold → Diamond → Olympian" }),
- React.createElement(StatCard, { label: "Latest", value: latest[0] ? latest[0].name : "None yet", hint: latest[0] ? latest[0].category : "run Hermes more" })
+ React.createElement(StatCard, { label: tx(t, "stats.unlocked", "Unlocked"), value: (data ? data.unlocked_count : 0) + " / " + (data ? data.total_count : 0), hint: tx(t, "stats.unlocked_hint", "earned badges") }),
+ React.createElement(StatCard, { label: tx(t, "stats.discovered", "Discovered"), value: discovered.length, hint: tx(t, "stats.discovered_hint", "known, not earned yet") }),
+ React.createElement(StatCard, { label: tx(t, "stats.secrets", "Secrets"), value: secret.length, hint: tx(t, "stats.secrets_hint", "hidden until first signal") }),
+ React.createElement(StatCard, { label: tx(t, "stats.highest_tier", "Highest tier"), value: highest, hint: tx(t, "stats.highest_tier_hint", "Copper → Silver → Gold → Diamond → Olympian") }),
+ React.createElement(StatCard, { label: tx(t, "stats.latest", "Latest"), value: latest[0] ? latest[0].name : tx(t, "stats.none_yet", "None yet"), hint: latest[0] ? latest[0].category : tx(t, "stats.latest_hint_empty", "run Hermes more") })
),
React.createElement("section", { className: "ha-guide" },
React.createElement("div", null,
- React.createElement("strong", null, "Tiers"),
+ React.createElement("strong", null, tx(t, "guide.tiers_header", "Tiers")),
React.createElement(TierLegend, null)
),
React.createElement("div", null,
- React.createElement("strong", null, "Secret achievements"),
- React.createElement("p", null, "Secrets hide their exact trigger. Once Hermes sees a related signal, the card becomes Discovered and shows its requirement.")
+ React.createElement("strong", null, tx(t, "guide.secret_header", "Secret achievements")),
+ React.createElement("p", null, tx(t, "guide.secret_body", "Secrets hide their exact trigger. Once Hermes sees a related signal, the card becomes Discovered and shows its requirement."))
)
),
React.createElement("div", { className: "ha-toolbar" },
React.createElement("div", { className: "ha-pills" }, categories.map(function (cat) {
- return React.createElement("button", { key: cat, onClick: function () { setCategory(cat); }, className: cat === category ? "active" : "" }, cat);
+ // Render the localized "All" pill but keep the underlying value
+ // unchanged so the filter logic still compares against "All".
+ const pillLabel = cat === "All" ? allCategoryLabel : cat;
+ return React.createElement("button", { key: cat, onClick: function () { setCategory(cat); }, className: cat === category ? "active" : "" }, pillLabel);
})),
React.createElement("div", { className: "ha-pills" }, ["all", "unlocked", "discovered", "secret"].map(function (v) {
- return React.createElement("button", { key: v, onClick: function () { setVisibility(v); }, className: v === visibility ? "active" : "" }, v);
+ return React.createElement("button", { key: v, onClick: function () { setVisibility(v); }, className: v === visibility ? "active" : "" }, visibilityLabels[v] || v);
}))
),
latest.length > 0 && React.createElement("section", { className: "ha-latest" },
- React.createElement("h2", null, "Recent unlocks"),
+ React.createElement("h2", null, tx(t, "latest.header", "Recent unlocks")),
React.createElement("div", { className: "ha-latest-row" }, latest.map(function (a) {
return React.createElement("div", { key: a.id, className: cn("ha-chip", tierClass(a.tier)) },
React.createElement("span", { className: "ha-chip-icon" }, React.createElement(AchievementIcon, { icon: a.icon || "secret" })),
@@ -638,8 +715,8 @@
),
visibility === "secret" && visible.length === 0 && React.createElement(C.Card, { className: "ha-secret-empty" },
React.createElement(C.CardContent, { className: "ha-secret-empty-content" },
- React.createElement("strong", null, "No hidden secrets left in this scan."),
- React.createElement("p", null, "Clue: secrets usually start from unusual failure or power-user patterns — port conflicts, permission walls, missing env vars, YAML mistakes, Docker collisions, rollback/checkpoint use, cache hits, or tiny fixes after lots of red text.")
+ React.createElement("strong", null, tx(t, "empty.no_secrets_header", "No hidden secrets left in this scan.")),
+ React.createElement("p", null, tx(t, "empty.no_secrets_body", "Clue: secrets usually start from unusual failure or power-user patterns — port conflicts, permission walls, missing env vars, YAML mistakes, Docker collisions, rollback/checkpoint use, cache hits, or tiny fixes after lots of red text."))
)
),
React.createElement("section", { className: "ha-grid" }, visible.map(function (a) {
diff --git a/plugins/kanban/dashboard/dist/index.js b/plugins/kanban/dashboard/dist/index.js
index 71ec6b8dc51..1f5f0591758 100644
--- a/plugins/kanban/dashboard/dist/index.js
+++ b/plugins/kanban/dashboard/dist/index.js
@@ -24,9 +24,39 @@
const { useState, useEffect, useCallback, useMemo, useRef } = SDK.hooks;
const { cn, timeAgo } = SDK.utils;
+ // useI18n is a hook each component calls locally. Older host dashboards
+ // may not expose it yet; fall back to a shim so the bundle still renders
+ // English against an older host SDK. English fallback strings live
+ // alongside each call site (passed as the third arg of tx()).
+ const useI18n = SDK.useI18n || function () { return { t: { kanban: null }, locale: "en" }; };
+
+ // Resolve a translation by dotted path under the kanban namespace
+ // (e.g. "columnLabels.triage"); fall back to the English string passed in.
+ function tx(t, path, fallback, vars) {
+ let node = t && t.kanban;
+ if (node) {
+ const parts = path.split(".");
+ for (let i = 0; i < parts.length; i++) {
+ if (node && typeof node === "object" && parts[i] in node) {
+ node = node[parts[i]];
+ } else { node = null; break; }
+ }
+ }
+ let str = (typeof node === "string") ? node : fallback;
+ if (vars) {
+ for (const k in vars) {
+ str = str.replace(new RegExp("\\{" + k + "\\}", "g"), vars[k]);
+ }
+ }
+ return str;
+ }
+
// Order matches BOARD_COLUMNS in plugin_api.py.
const COLUMN_ORDER = ["triage", "todo", "ready", "running", "blocked", "done"];
- const COLUMN_LABEL = {
+ // English fallback dictionaries — used when the i18n catalog is missing
+ // a key, and as defaults for the get*() helpers below so callers running
+ // outside any React component (where there's no `t`) still get sane text.
+ const FALLBACK_COLUMN_LABEL = {
triage: "Triage",
todo: "Todo",
ready: "Ready",
@@ -35,7 +65,7 @@
done: "Done",
archived: "Archived",
};
- const COLUMN_HELP = {
+ const FALLBACK_COLUMN_HELP = {
triage: "Raw ideas — a specifier will flesh out the spec",
todo: "Waiting on dependencies or unassigned",
ready: "Assigned and waiting for a dispatcher tick",
@@ -44,6 +74,42 @@
done: "Completed",
archived: "Archived",
};
+ const FALLBACK_DESTRUCTIVE = {
+ done: "Mark this task as done? The worker's claim is released and dependent children become ready.",
+ archived: "Archive this task? It disappears from the default board view.",
+ blocked: "Mark this task as blocked? The worker's claim is released.",
+ };
+ const FALLBACK_DIAGNOSTIC_EVENT_LABELS = {
+ completion_blocked_hallucination: "⚠ Completion blocked — phantom card ids",
+ suspected_hallucinated_references: "⚠ Prose referenced phantom card ids",
+ };
+ const DIAGNOSTIC_EVENT_KIND_KEYS = {
+ completion_blocked_hallucination: "completionBlockedHallucination",
+ suspected_hallucinated_references: "suspectedHallucinatedReferences",
+ };
+ const DESTRUCTIVE_KEYS = {
+ done: "confirmDone",
+ archived: "confirmArchive",
+ blocked: "confirmBlocked",
+ };
+
+ function getColumnLabel(t, status) {
+ return tx(t, "columnLabels." + status, FALLBACK_COLUMN_LABEL[status] || status);
+ }
+ function getColumnHelp(t, status) {
+ return tx(t, "columnHelp." + status, FALLBACK_COLUMN_HELP[status] || "");
+ }
+ function getDestructiveConfirm(t, status) {
+ const key = DESTRUCTIVE_KEYS[status];
+ if (!key) return null;
+ return tx(t, key, FALLBACK_DESTRUCTIVE[status]);
+ }
+ function getDiagnosticEventLabel(t, kind) {
+ const key = DIAGNOSTIC_EVENT_KIND_KEYS[kind];
+ if (!key) return null;
+ return tx(t, key, FALLBACK_DIAGNOSTIC_EVENT_LABELS[kind]);
+ }
+
const COLUMN_DOT = {
triage: "hermes-kanban-dot-triage",
todo: "hermes-kanban-dot-todo",
@@ -54,22 +120,8 @@
archived: "hermes-kanban-dot-archived",
};
- const DESTRUCTIVE_TRANSITIONS = {
- done: "Mark this task as done? The worker's claim is released and dependent children become ready.",
- archived: "Archive this task? It disappears from the default board view.",
- blocked: "Mark this task as blocked? The worker's claim is released.",
- };
-
- // Diagnostic kind labels for the events-tab callout. Event kinds emitted
- // by the kernel get a human-readable header when we detect them in the
- // events list; add new entries here as new diagnostic event kinds land.
- const DIAGNOSTIC_EVENT_LABELS = {
- completion_blocked_hallucination: "⚠ Completion blocked — phantom card ids",
- suspected_hallucinated_references: "⚠ Prose referenced phantom card ids",
- };
-
function isDiagnosticEvent(kind) {
- return Object.prototype.hasOwnProperty.call(DIAGNOSTIC_EVENT_LABELS, kind);
+ return Object.prototype.hasOwnProperty.call(FALLBACK_DIAGNOSTIC_EVENT_LABELS, kind);
}
function phantomIdsFromEvent(ev) {
@@ -78,17 +130,22 @@
return p.phantom_cards || p.phantom_refs || [];
}
- function withCompletionSummary(patch, count) {
+ // Takes an optional `t` so the prompt/alert text is localised. Callers
+ // outside React components can pass null and fall through to English.
+ function withCompletionSummary(patch, count, t) {
if (!patch || patch.status !== "done") return patch;
const label = count && count > 1 ? `${count} selected task(s)` : "this task";
const value = window.prompt(
- `Completion summary for ${label}. This is stored as the task result.`,
+ tx(t, "completionSummary",
+ "Completion summary for {label}. This is stored as the task result.",
+ { label: label }),
"",
);
if (value === null) return null;
const summary = value.trim();
if (!summary) {
- window.alert("Completion summary is required before marking a task done.");
+ window.alert(tx(t, "completionSummaryRequired",
+ "Completion summary is required before marking a task done."));
return null;
}
return Object.assign({}, patch, { result: summary, summary });
@@ -314,6 +371,24 @@
// Error boundary
// -------------------------------------------------------------------------
+ // Wrap the boundary's fallback in a tiny function component so we can
+ // call useI18n() — class components can't use hooks directly.
+ function ErrorBoundaryFallback(props) {
+ const { t } = useI18n();
+ return h(Card, null,
+ h(CardContent, { className: "p-6 text-sm" },
+ h("div", { className: "text-destructive font-semibold mb-1" },
+ tx(t, "renderingError", "Kanban tab hit a rendering error")),
+ h("div", { className: "text-muted-foreground text-xs mb-3" },
+ props.message),
+ h(Button, {
+ onClick: props.onReset,
+ size: "sm",
+ }, tx(t, "reloadView", "Reload view")),
+ ),
+ );
+ }
+
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { error: null }; }
static getDerivedStateFromError(error) { return { error }; }
@@ -323,18 +398,10 @@
}
render() {
if (this.state.error) {
- return h(Card, null,
- h(CardContent, { className: "p-6 text-sm" },
- h("div", { className: "text-destructive font-semibold mb-1" },
- "Kanban tab hit a rendering error"),
- h("div", { className: "text-muted-foreground text-xs mb-3" },
- String(this.state.error && this.state.error.message || this.state.error)),
- h(Button, {
- onClick: () => this.setState({ error: null }),
- size: "sm",
- }, "Reload view"),
- ),
- );
+ return h(ErrorBoundaryFallback, {
+ message: String(this.state.error && this.state.error.message || this.state.error),
+ onReset: () => this.setState({ error: null }),
+ });
}
return this.props.children;
}
@@ -345,6 +412,7 @@
// -------------------------------------------------------------------------
function KanbanPage() {
+ const { t } = useI18n();
const [board, setBoard] = useState(() => readSelectedBoard() || "default");
const [boardList, setBoardList] = useState([]); // [{slug, name, counts, ...}]
const [showNewBoard, setShowNewBoard] = useState(false);
@@ -497,7 +565,8 @@
ws.onclose = function (ev) {
if (wsClosedRef.current) return;
if (ev && ev.code === 1008) {
- setError("WebSocket auth failed — reload the page to refresh the session token.");
+ setError(tx(t, "wsAuthFailed",
+ "WebSocket auth failed — reload the page to refresh the session token."));
return;
}
const delay = Math.min(wsBackoffRef.current, 30000);
@@ -534,9 +603,9 @@
// --- actions ------------------------------------------------------------
const moveTask = useCallback(function (taskId, newStatus) {
- const confirmMsg = DESTRUCTIVE_TRANSITIONS[newStatus];
+ const confirmMsg = getDestructiveConfirm(t, newStatus);
if (confirmMsg && !window.confirm(confirmMsg)) return;
- const patch = withCompletionSummary({ status: newStatus }, 1);
+ const patch = withCompletionSummary({ status: newStatus }, 1, t);
if (!patch) return;
setBoardData(function (b) {
if (!b) return b;
@@ -559,10 +628,10 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
}).catch(function (err) {
- setError(`Move failed: ${err.message || err}`);
+ setError(tx(t, "moveFailed", "Move failed: ") + (err.message || err));
loadBoard();
});
- }, [loadBoard, board]);
+ }, [loadBoard, board, t]);
const createTask = useCallback(function (body) {
return SDK.fetchJSON(withBoard(`${API}/tasks`, board), {
@@ -575,13 +644,13 @@
// the task was created successfully — but the user should know
// their ready task will sit idle until the gateway is up.
if (res && res.warning) {
- setError("Task created, but: " + res.warning);
+ setError(tx(t, "taskCreatedWarning", "Task created, but: ") + res.warning);
}
loadBoard();
loadBoardList(); // refresh counts in the switcher
return res;
});
- }, [loadBoard, loadBoardList, board]);
+ }, [loadBoard, loadBoardList, board, t]);
const toggleSelected = useCallback(function (id, additive) {
setSelectedIds(function (prev) {
@@ -596,7 +665,7 @@
const applyBulk = useCallback(function (patch, confirmMsg) {
if (selectedIds.size === 0) return;
if (confirmMsg && !window.confirm(confirmMsg)) return;
- const finalPatch = withCompletionSummary(patch, selectedIds.size);
+ const finalPatch = withCompletionSummary(patch, selectedIds.size, t);
if (!finalPatch) return;
const body = Object.assign({ ids: Array.from(selectedIds) }, finalPatch);
SDK.fetchJSON(withBoard(`${API}/tasks/bulk`, board), {
@@ -607,14 +676,15 @@
.then(function (res) {
const failed = (res.results || []).filter(function (r) { return !r.ok; });
if (failed.length > 0) {
- setError(`Bulk: ${failed.length} of ${res.results.length} failed: ` +
+ setError(tx(t, "bulkFailed", "Bulk: ") +
+ `${failed.length} of ${res.results.length} failed: ` +
failed.slice(0, 3).map(function (f) { return `${f.id} (${f.error})`; }).join("; "));
}
clearSelected();
loadBoard();
})
.catch(function (e) { setError(String(e.message || e)); });
- }, [selectedIds, loadBoard, clearSelected, board]);
+ }, [selectedIds, loadBoard, clearSelected, board, t]);
// --- board switching ----------------------------------------------------
const switchBoard = useCallback(function (nextSlug) {
@@ -655,15 +725,16 @@
// --- render -------------------------------------------------------------
if (loading && !boardData) {
return h("div", { className: "p-8 text-sm text-muted-foreground" },
- "Loading Kanban board…");
+ tx(t, "loading", "Loading Kanban board…"));
}
if (error && !boardData) {
return h(Card, null,
h(CardContent, { className: "p-6" },
h("div", { className: "text-sm text-destructive" },
- "Failed to load Kanban board: ", error),
+ tx(t, "loadFailed", "Failed to load Kanban board: "), error),
h("div", { className: "text-xs text-muted-foreground mt-2" },
- "The backend auto-creates kanban.db on first read. If this persists, check the dashboard logs."),
+ tx(t, "loadFailedHint",
+ "The backend auto-creates kanban.db on first read. If this persists, check the dashboard logs.")),
),
);
}
@@ -771,6 +842,7 @@
}
function AttentionStrip(props) {
+ const { t } = useI18n();
const [expanded, setExpanded] = useState(false);
const [dismissed, setDismissed] = useState(false);
const diagTasks = useMemo(
@@ -780,8 +852,8 @@
if (dismissed || diagTasks.length === 0) return null;
// Pick the highest severity present so we can colour the strip.
let topSev = "warning";
- for (const t of diagTasks) {
- const s = (t.warnings && t.warnings.highest_severity) || "warning";
+ for (const td of diagTasks) {
+ const s = (td.warnings && td.warnings.highest_severity) || "warning";
if (s === "critical") { topSev = "critical"; break; }
if (s === "error" && topSev !== "critical") topSev = "error";
}
@@ -796,14 +868,15 @@
topSev === "critical" ? "!!!" : topSev === "error" ? "!!" : "⚠"),
h("span", { className: "hermes-kanban-attention-text" },
diagTasks.length === 1
- ? "1 task needs attention"
- : `${diagTasks.length} tasks need attention`,
+ ? tx(t, "taskNeedsAttention", "1 task needs attention")
+ : tx(t, "tasksNeedAttention", "{n} tasks need attention",
+ { n: diagTasks.length }),
),
h("button", {
className: "hermes-kanban-attention-toggle",
onClick: function () { setExpanded(function (x) { return !x; }); },
type: "button",
- }, expanded ? "Hide" : "Show"),
+ }, expanded ? tx(t, "hide", "Hide") : tx(t, "show", "Show")),
h("button", {
className: "hermes-kanban-attention-dismiss",
onClick: function () { setDismissed(true); },
@@ -813,11 +886,11 @@
),
expanded
? h("div", { className: "hermes-kanban-attention-list" },
- diagTasks.map(function (t) {
- const sev = (t.warnings && t.warnings.highest_severity) || "warning";
- const kinds = t.warnings && t.warnings.kinds ? Object.keys(t.warnings.kinds) : [];
+ diagTasks.map(function (task) {
+ const sev = (task.warnings && task.warnings.highest_severity) || "warning";
+ const kinds = task.warnings && task.warnings.kinds ? Object.keys(task.warnings.kinds) : [];
return h("div", {
- key: t.id,
+ key: task.id,
className: cn(
"hermes-kanban-attention-row",
"hermes-kanban-attention-row--" + sev,
@@ -825,19 +898,19 @@
},
h("span", { className: "hermes-kanban-attention-row-sev" },
sev === "critical" ? "!!!" : sev === "error" ? "!!" : "⚠"),
- h("span", { className: "hermes-kanban-attention-row-id" }, t.id),
+ h("span", { className: "hermes-kanban-attention-row-id" }, task.id),
h("span", { className: "hermes-kanban-attention-row-title" },
- t.title || "(untitled)"),
+ task.title || tx(t, "untitled", "(untitled)")),
h("span", { className: "hermes-kanban-attention-row-meta" },
- t.assignee ? "@" + t.assignee : "unassigned",
+ task.assignee ? "@" + task.assignee : tx(t, "unassigned", "unassigned"),
" \u00b7 ",
- kinds.length > 0 ? kinds.join(", ") : "diagnostic",
+ kinds.length > 0 ? kinds.join(", ") : tx(t, "diagnostic", "diagnostic"),
),
h("button", {
className: "hermes-kanban-attention-row-btn",
- onClick: function () { props.onOpen(t.id); },
+ onClick: function () { props.onOpen(task.id); },
type: "button",
- }, "Open"),
+ }, tx(t, "open", "Open")),
);
}),
)
@@ -864,6 +937,7 @@
// -------------------------------------------------------------------------
function DiagnosticActionButton(props) {
+ const { t } = useI18n();
const { action, onExec, busy, extra } = props;
const label = (action.suggested ? "\u2606 " : "") + action.label;
const cls = cn(
@@ -885,8 +959,8 @@
disabled: busy,
onClick: function () { onExec(action); },
type: "button",
- title: "Copy command to clipboard",
- }, (extra && extra.copied) ? "Copied" : label);
+ title: tx(t, "copyCommand", "Copy command to clipboard"),
+ }, (extra && extra.copied) ? tx(t, "copied", "Copied") : label);
}
if (action.kind === "comment") {
return h("button", {
@@ -909,6 +983,7 @@
}
function DiagnosticCard(props) {
+ const { t } = useI18n();
const { diag, task, boardSlug, assignees, onRefresh } = props;
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState(null);
@@ -953,10 +1028,11 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "ready" }),
}).then(function () {
- setMsg({ ok: true, text: `Unblocked ${task.id}. Task is ready for the next tick.` });
+ setMsg({ ok: true, text: tx(t, "unblockedMessage",
+ "Unblocked {id}. Task is ready for the next tick.", { id: task.id }) });
if (onRefresh) onRefresh();
}).catch(function (err) {
- setMsg({ ok: false, text: `Unblock failed: ${err.message || err}` });
+ setMsg({ ok: false, text: tx(t, "unblockFailed", "Unblock failed: ") + (err.message || err) });
}).then(function () { setBusy(false); });
return;
}
@@ -968,16 +1044,17 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reason: `recovery action for ${diag.kind}` }),
}).then(function () {
- setMsg({ ok: true, text: `Reclaimed ${task.id}. Task is back to ready.` });
+ setMsg({ ok: true, text: tx(t, "reclaimedMessage",
+ "Reclaimed {id}. Task is back to ready.", { id: task.id }) });
if (onRefresh) onRefresh();
}).catch(function (err) {
- setMsg({ ok: false, text: `Reclaim failed: ${err.message || err}` });
+ setMsg({ ok: false, text: tx(t, "reclaimFailed", "Reclaim failed: ") + (err.message || err) });
}).then(function () { setBusy(false); });
return;
}
if (action.kind === "reassign") {
if (!reassignProfile) {
- setMsg({ ok: false, text: "Pick a profile first." });
+ setMsg({ ok: false, text: tx(t, "pickProfileFirst", "Pick a profile first.") });
return;
}
setBusy(true); setMsg(null);
@@ -994,11 +1071,12 @@
}).then(function () {
setMsg({
ok: true,
- text: `Reassigned ${task.id} to ${reassignProfile}.`,
+ text: tx(t, "reassignedMessage", "Reassigned {id} to {profile}.",
+ { id: task.id, profile: reassignProfile }),
});
if (onRefresh) onRefresh();
}).catch(function (err) {
- setMsg({ ok: false, text: `Reassign failed: ${err.message || err}` });
+ setMsg({ ok: false, text: tx(t, "reassignFailed", "Reassign failed: ") + (err.message || err) });
}).then(function () { setBusy(false); });
return;
}
@@ -1049,7 +1127,7 @@
reassignAction
? h("div", { className: "hermes-kanban-diag-reassign-row" },
h("span", { className: "hermes-kanban-diag-reassign-label" },
- "Reassign to:"),
+ tx(t, "reassignTo", "Reassign to:")),
h("select", {
className: "hermes-kanban-recovery-select",
value: reassignProfile,
@@ -1088,6 +1166,7 @@
}
function DiagnosticsSection(props) {
+ const { t } = useI18n();
const diags = props.diagnostics || [];
const hasOpenDiags = diags.length > 0;
const [open, setOpen] = useState(hasOpenDiags);
@@ -1104,14 +1183,14 @@
h("span", { className: "hermes-kanban-section-head" },
hasOpenDiags
? h("span", { className: "hermes-kanban-section-head-warning" },
- `\u26a0 Diagnostics (${diags.length})`)
- : "Diagnostics",
+ `\u26a0 ${tx(t, "diagnostics", "Diagnostics")} (${diags.length})`)
+ : tx(t, "diagnostics", "Diagnostics"),
),
h("button", {
className: "hermes-kanban-section-toggle",
onClick: function () { setOpen(function (x) { return !x; }); },
type: "button",
- }, open ? "Hide" : "Show"),
+ }, open ? tx(t, "hide", "Hide") : tx(t, "show", "Show")),
),
open
? h("div", { className: "hermes-kanban-diag-list" },
@@ -1149,6 +1228,7 @@
}
function BoardSwitcher(props) {
+ const { t } = useI18n();
const list = props.boardList || [];
const current = list.find(function (b) { return b.slug === props.board; });
const currentName = current && current.name ? current.name : props.board;
@@ -1165,13 +1245,13 @@
if (!shouldShow) {
return h("div", {
className: "hermes-kanban-boardswitcher-compact",
- title: "Boards let you separate unrelated streams of work",
+ title: tx(t, "boardSwitcherHint", "Boards let you separate unrelated streams of work"),
},
h(Button, {
onClick: props.onNewClick,
size: "sm",
className: "h-7 text-xs",
- }, "+ New board"),
+ }, tx(t, "newBoard", "+ New board")),
h(DocsLink, null),
);
}
@@ -1180,7 +1260,7 @@
h("div", { className: "hermes-kanban-boardswitcher-inner" },
h("div", { className: "flex flex-col gap-0.5" },
h("div", { className: "text-[11px] uppercase tracking-wider text-muted-foreground" },
- "Board"),
+ tx(t, "board", "Board")),
h("div", { className: "flex items-center gap-2" },
h(Select, Object.assign({
value: props.board,
@@ -1206,26 +1286,26 @@
size: "sm",
className: "h-8",
title: "Create a new board. Useful when you want an unrelated work stream (different project, different team, isolated scratch area).",
- }, "+ New board"),
+ }, tx(t, "newBoard", "+ New board")),
props.board !== "default"
? h(Button, {
onClick: function () {
- const msg =
- `Archive board '${currentName}'? ` +
- `It will be moved to boards/_archived/ so you can recover it later. ` +
- `Tasks on this board will no longer appear anywhere in the UI.`;
+ const msg = tx(t, "archiveBoardConfirm",
+ "Archive board '{name}'? It will be moved to boards/_archived/ so you can recover it later. Tasks on this board will no longer appear anywhere in the UI.",
+ { name: currentName });
if (window.confirm(msg)) props.onDeleteBoard(props.board);
},
size: "sm",
className: "h-8",
- title: "Archive this board",
- }, "Archive")
+ title: tx(t, "archiveBoardTitle", "Archive this board"),
+ }, tx(t, "archive", "Archive"))
: null,
),
);
}
function NewBoardDialog(props) {
+ const { t } = useI18n();
const [slug, setSlug] = useState("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
@@ -1269,14 +1349,16 @@
className: "hermes-kanban-dialog",
onSubmit: onSubmit,
},
- h("div", { className: "hermes-kanban-dialog-title" }, "New board"),
+ h("div", { className: "hermes-kanban-dialog-title" },
+ tx(t, "newBoardTitle", "New board")),
h("div", { className: "text-xs text-muted-foreground mb-2" },
- "Boards let you separate unrelated streams of work — one per project, repo, or domain. Workers on one board never see another board's tasks."),
+ tx(t, "newBoardDescription",
+ "Boards let you separate unrelated streams of work — one per project, repo, or domain. Workers on one board never see another board's tasks.")),
h("div", { className: "flex flex-col gap-3" },
h("div", { className: "flex flex-col gap-1" },
- h(Label, { className: "text-xs" }, "Slug ",
+ h(Label, { className: "text-xs" }, tx(t, "slug", "Slug"), " ",
h("span", { className: "text-muted-foreground" },
- "— lowercase, hyphens, e.g. atm10-server")),
+ tx(t, "slugHint", "— lowercase, hyphens, e.g. atm10-server"))),
h(Input, {
value: slug,
onChange: function (e) { setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9\-_]/g, "-")); },
@@ -1286,18 +1368,20 @@
}),
),
h("div", { className: "flex flex-col gap-1" },
- h(Label, { className: "text-xs" }, "Display name ",
- h("span", { className: "text-muted-foreground" }, "(optional)")),
+ h(Label, { className: "text-xs" }, tx(t, "displayName", "Display name"), " ",
+ h("span", { className: "text-muted-foreground" },
+ tx(t, "displayNameHint", "(optional)"))),
h(Input, {
value: name,
onChange: function (e) { setName(e.target.value); },
- placeholder: autoName || "Display name",
+ placeholder: autoName || tx(t, "displayName", "Display name"),
className: "h-8",
}),
),
h("div", { className: "flex flex-col gap-1" },
- h(Label, { className: "text-xs" }, "Description ",
- h("span", { className: "text-muted-foreground" }, "(optional)")),
+ h(Label, { className: "text-xs" }, tx(t, "description", "Description"), " ",
+ h("span", { className: "text-muted-foreground" },
+ tx(t, "descriptionHint", "(optional)"))),
h(Input, {
value: description,
onChange: function (e) { setDescription(e.target.value); },
@@ -1306,8 +1390,9 @@
}),
),
h("div", { className: "flex flex-col gap-1" },
- h(Label, { className: "text-xs" }, "Icon ",
- h("span", { className: "text-muted-foreground" }, "(single character or emoji)")),
+ h(Label, { className: "text-xs" }, tx(t, "icon", "Icon"), " ",
+ h("span", { className: "text-muted-foreground" },
+ tx(t, "iconHint", "(single character or emoji)"))),
h(Input, {
value: icon,
onChange: function (e) { setIcon(e.target.value.slice(0, 4)); },
@@ -1321,7 +1406,7 @@
checked: switchTo,
onChange: function (e) { setSwitchTo(e.target.checked); },
}),
- "Switch to this board after creating it",
+ tx(t, "switchAfterCreate", "Switch to this board after creating it"),
),
),
err ? h("div", { className: "text-xs text-destructive mt-2" }, err) : null,
@@ -1331,12 +1416,12 @@
onClick: props.onCancel,
size: "sm",
disabled: submitting,
- }, "Cancel"),
+ }, tx(t, "cancel", "Cancel")),
h(Button, {
type: "submit",
size: "sm",
disabled: submitting || !slug.trim(),
- }, submitting ? "Creating…" : "Create board"),
+ }, submitting ? tx(t, "creating", "Creating…") : tx(t, "createBoard", "Create board")),
),
),
);
@@ -1347,14 +1432,15 @@
// -------------------------------------------------------------------------
function BoardToolbar(props) {
+ const { t } = useI18n();
const tenants = (props.board && props.board.tenants) || [];
const assignees = (props.board && props.board.assignees) || [];
return h("div", { className: "flex flex-wrap items-end gap-3" },
h("div", { className: "flex flex-col gap-1",
title: "Fuzzy-match tasks by id, title, or description. Matches across all columns." },
- h(Label, { className: "text-xs text-muted-foreground" }, "Search"),
+ h(Label, { className: "text-xs text-muted-foreground" }, tx(t, "search", "Search")),
h(Input, {
- placeholder: "Filter cards…",
+ placeholder: tx(t, "filterCards", "Filter cards…"),
value: props.search,
onChange: function (e) { props.setSearch(e.target.value); },
className: "w-56 h-8",
@@ -1362,25 +1448,25 @@
),
h("div", { className: "flex flex-col gap-1",
title: "Tenants are free-form tags on a task (e.g. customer, project, team). Set them via the task drawer or kanban_create." },
- h(Label, { className: "text-xs text-muted-foreground" }, "Tenant"),
+ h(Label, { className: "text-xs text-muted-foreground" }, tx(t, "tenant", "Tenant")),
h(Select, Object.assign({
value: props.tenantFilter,
className: "h-8",
}, selectChangeHandler(props.setTenantFilter)),
- h(SelectOption, { value: "" }, "All tenants"),
- tenants.map(function (t) {
- return h(SelectOption, { key: t, value: t }, t);
+ h(SelectOption, { value: "" }, tx(t, "allTenants", "All tenants")),
+ tenants.map(function (tn) {
+ return h(SelectOption, { key: tn, value: tn }, tn);
}),
),
),
h("div", { className: "flex flex-col gap-1",
title: "Filter by assigned Hermes profile. Profiles are the named agent identities that claim and work on tasks." },
- h(Label, { className: "text-xs text-muted-foreground" }, "Assignee"),
+ h(Label, { className: "text-xs text-muted-foreground" }, tx(t, "assignee", "Assignee")),
h(Select, Object.assign({
value: props.assigneeFilter,
className: "h-8",
}, selectChangeHandler(props.setAssigneeFilter)),
- h(SelectOption, { value: "" }, "All profiles"),
+ h(SelectOption, { value: "" }, tx(t, "allProfiles", "All profiles")),
assignees.map(function (a) {
return h(SelectOption, { key: a, value: a }, a);
}),
@@ -1393,7 +1479,7 @@
checked: props.includeArchived,
onChange: function (e) { props.setIncludeArchived(e.target.checked); },
}),
- "Show archived",
+ tx(t, "showArchived", "Show archived"),
),
h("label", { className: "flex items-center gap-2 text-xs",
title: "Group the Running column by assigned profile" },
@@ -1402,19 +1488,19 @@
checked: props.laneByProfile,
onChange: function (e) { props.setLaneByProfile(e.target.checked); },
}),
- "Lanes by profile",
+ tx(t, "lanesByProfile", "Lanes by profile"),
),
h("div", { className: "flex-1" }),
h(Button, {
onClick: props.onNudgeDispatch,
size: "sm",
title: "Wake the dispatcher to claim ready tasks now instead of waiting for the next tick. Use this after adding tasks if you want them picked up immediately.",
- }, "Nudge dispatcher"),
+ }, tx(t, "nudgeDispatcher", "Nudge dispatcher")),
h(Button, {
onClick: props.onRefresh,
size: "sm",
title: "Reload the board from the database. The board auto-refreshes on task events; this is for forcing a re-read.",
- }, "Refresh"),
+ }, tx(t, "refresh", "Refresh")),
);
}
@@ -1423,10 +1509,11 @@
// -------------------------------------------------------------------------
function BulkActionBar(props) {
+ const { t } = useI18n();
const [assignee, setAssignee] = useState("");
return h("div", { className: "hermes-kanban-bulk" },
h("span", { className: "hermes-kanban-bulk-count" },
- `${props.count} selected`),
+ `${props.count} ${tx(t, "selected", "selected")}`),
h(Button, {
onClick: function () { props.onApply({ status: "ready" }); },
size: "sm",
@@ -1435,19 +1522,19 @@
h(Button, {
onClick: function () {
props.onApply({ status: "done" },
- `Mark ${props.count} task(s) as done?`);
+ tx(t, "markDone", "Mark {n} task(s) as done?", { n: props.count }));
},
size: "sm",
title: "Mark selected tasks as done. Releases any claims and unblocks dependent children. You'll be asked for a completion summary.",
- }, "Complete"),
+ }, tx(t, "complete", "Complete")),
h(Button, {
onClick: function () {
props.onApply({ archive: true },
- `Archive ${props.count} task(s)?`);
+ tx(t, "markArchived", "Archive {n} task(s)?", { n: props.count }));
},
size: "sm",
title: "Archive selected tasks. They disappear from the default board view but remain in the database.",
- }, "Archive"),
+ }, tx(t, "archive", "Archive")),
h("div", { className: "hermes-kanban-bulk-reassign",
title: "Reassign selected tasks to a different Hermes profile. Pick a profile (or unassign) and click Apply." },
h(Select, {
@@ -1470,14 +1557,14 @@
disabled: !assignee,
size: "sm",
title: "Apply the selected assignee to all selected tasks.",
- }, "Apply"),
+ }, tx(t, "apply", "Apply")),
),
h("div", { className: "flex-1" }),
h(Button, {
onClick: props.onClear,
size: "sm",
title: "Deselect all tasks and hide this bar.",
- }, "Clear"),
+ }, tx(t, "clear", "Clear")),
);
}
@@ -1504,6 +1591,7 @@
}
function Column(props) {
+ const { t } = useI18n();
const [dragOver, setDragOver] = useState(false);
const [showCreate, setShowCreate] = useState(false);
const colRef = useRef(null);
@@ -1537,15 +1625,18 @@
const lanes = useMemo(function () {
if (!props.laneByProfile || props.column.name !== "running") return null;
const byProfile = {};
- for (const t of props.column.tasks) {
- const key = t.assignee || "(unassigned)";
- (byProfile[key] = byProfile[key] || []).push(t);
+ for (const tk of props.column.tasks) {
+ const key = tk.assignee || "(unassigned)";
+ (byProfile[key] = byProfile[key] || []).push(tk);
}
return Object.keys(byProfile).sort().map(function (k) {
return { assignee: k, tasks: byProfile[k] };
});
}, [props.column, props.laneByProfile]);
+ const colHelp = getColumnHelp(t, props.column.name);
+ const colLabel = getColumnLabel(t, props.column.name);
+
return h("div", {
ref: colRef,
"data-kanban-column": props.column.name,
@@ -1558,22 +1649,22 @@
onDrop: handleDrop,
},
h("div", { className: "hermes-kanban-column-header",
- title: COLUMN_HELP[props.column.name] || "" },
+ title: colHelp || "" },
h("span", { className: cn("hermes-kanban-dot", COLUMN_DOT[props.column.name]) }),
h("span", { className: "hermes-kanban-column-label" },
- COLUMN_LABEL[props.column.name] || props.column.name),
+ colLabel || props.column.name),
h("span", { className: "hermes-kanban-column-count",
title: `${props.column.tasks.length} task${props.column.tasks.length === 1 ? "" : "s"} in this column` },
props.column.tasks.length),
h("button", {
type: "button",
className: "hermes-kanban-column-add",
- title: "Create task in this column",
+ title: tx(t, "createTask", "Create task in this column"),
onClick: function () { setShowCreate(function (v) { return !v; }); },
}, showCreate ? "×" : "+"),
),
h("div", { className: "hermes-kanban-column-sub" },
- COLUMN_HELP[props.column.name] || ""),
+ colHelp || ""),
showCreate ? h(InlineCreate, {
columnName: props.column.name,
allTasks: props.allTasks,
@@ -1584,7 +1675,7 @@
}) : null,
h("div", { className: "hermes-kanban-column-body" },
props.column.tasks.length === 0
- ? h("div", { className: "hermes-kanban-empty" }, "— no tasks —")
+ ? h("div", { className: "hermes-kanban-empty" }, tx(t, "noTasks", "— no tasks —"))
: lanes
? lanes.map(function (lane) {
return h("div", { key: lane.assignee, className: "hermes-kanban-lane" },
@@ -1592,20 +1683,20 @@
h("span", { className: "hermes-kanban-lane-name" }, lane.assignee),
h("span", { className: "hermes-kanban-lane-count" }, lane.tasks.length),
),
- lane.tasks.map(function (t) {
+ lane.tasks.map(function (tk) {
return h(TaskCard, {
- key: t.id, task: t,
- selected: props.selectedIds.has(t.id),
+ key: tk.id, task: tk,
+ selected: props.selectedIds.has(tk.id),
toggleSelected: props.toggleSelected,
onOpen: props.onOpen,
});
}),
);
})
- : props.column.tasks.map(function (t) {
+ : props.column.tasks.map(function (tk) {
return h(TaskCard, {
- key: t.id, task: t,
- selected: props.selectedIds.has(t.id),
+ key: tk.id, task: tk,
+ selected: props.selectedIds.has(tk.id),
toggleSelected: props.toggleSelected,
onOpen: props.onOpen,
});
@@ -1640,6 +1731,7 @@
}
function TaskCard(props) {
+ const { t: i18n } = useI18n();
const t = props.task;
const cardRef = useRef(null);
@@ -1688,7 +1780,7 @@
checked: props.selected,
onChange: handleCheckbox,
onClick: function (e) { e.stopPropagation(); },
- title: "Select for bulk actions",
+ title: tx(i18n, "selectForBulk", "Select for bulk actions"),
}),
h("span", { className: "hermes-kanban-card-id",
title: `Task id: ${t.id}. Use this id with kanban_show, /kanban show, or hermes kanban show.` }, t.id),
@@ -1725,13 +1817,15 @@
}, `${progress.done}/${progress.total}`)
: null,
),
- h("div", { className: "hermes-kanban-card-title" }, t.title || "(untitled)"),
+ h("div", { className: "hermes-kanban-card-title" },
+ t.title || tx(i18n, "untitled", "(untitled)")),
h("div", { className: "hermes-kanban-card-row hermes-kanban-card-meta" },
t.assignee
? h("span", { className: "hermes-kanban-assignee",
title: `Assigned to Hermes profile @${t.assignee}` }, "@", t.assignee)
: h("span", { className: "hermes-kanban-unassigned",
- title: "No profile assigned. The dispatcher will pick one from available profiles when the task is Ready." }, "unassigned"),
+ title: "No profile assigned. The dispatcher will pick one from available profiles when the task is Ready." },
+ tx(i18n, "unassigned", "unassigned")),
t.comment_count > 0
? h("span", { className: "hermes-kanban-count",
title: `${t.comment_count} comment${t.comment_count === 1 ? "" : "s"} on this task` }, "💬 ", t.comment_count)
@@ -1755,6 +1849,7 @@
// -------------------------------------------------------------------------
function InlineCreate(props) {
+ const { t } = useI18n();
const [title, setTitle] = useState("");
const [assignee, setAssignee] = useState("");
const [priority, setPriority] = useState(0);
@@ -1799,8 +1894,9 @@
const showPathInput = workspaceKind !== "scratch";
const pathPlaceholder = workspaceKind === "dir"
- ? "workspace path (required, e.g. ~/projects/my-app)"
- : "workspace path (optional, derived from assignee if blank)";
+ ? tx(t, "workspacePathDir", "workspace path (required, e.g. ~/projects/my-app)")
+ : tx(t, "workspacePathOptional",
+ "workspace path (optional, derived from assignee if blank)");
return h("div", { className: "hermes-kanban-inline-create" },
h("textarea", {
@@ -1811,8 +1907,8 @@
if (e.key === "Escape") props.onCancel();
},
placeholder: props.columnName === "triage"
- ? "Rough idea — AI will spec it…"
- : "New task title…",
+ ? tx(t, "triagePlaceholder", "Rough idea — AI will spec it…")
+ : tx(t, "taskTitlePlaceholder", "New task title…"),
autoFocus: true,
className: "text-sm min-h-[2rem] max-h-32 resize-y w-full border border-input bg-transparent px-2 py-1 rounded-md focus:outline-none focus:ring-2 focus:ring-ring",
rows: 2,
@@ -1821,7 +1917,9 @@
h(Input, {
value: assignee,
onChange: function (e) { setAssignee(e.target.value); },
- placeholder: props.columnName === "triage" ? "specifier" : "assignee",
+ placeholder: props.columnName === "triage"
+ ? tx(t, "specifier", "specifier")
+ : tx(t, "assigneePlaceholder", "assignee"),
className: "h-7 text-xs flex-1",
title: props.columnName === "triage"
? "Hermes profile that will spec this task (default: the dispatcher's configured specifier). Leave blank to let the dispatcher pick."
@@ -1839,7 +1937,8 @@
h(Input, {
value: skills,
onChange: function (e) { setSkills(e.target.value); },
- placeholder: "skills (optional, comma-separated): translation, github-code-review",
+ placeholder: tx(t, "skillsPlaceholder",
+ "skills (optional, comma-separated): translation, github-code-review"),
title: "Force-load these skills into the worker (in addition to the built-in kanban-worker).",
className: "h-7 text-xs",
}),
@@ -1867,10 +1966,10 @@
className: "h-7 text-xs",
title: "Optional parent task. A child stays blocked in its current column until the parent is marked done.",
},
- h(SelectOption, { value: "" }, "— no parent —"),
- (props.allTasks || []).map(function (t) {
- return h(SelectOption, { key: t.id, value: t.id },
- `${t.id} — ${(t.title || "").slice(0, 50)}`);
+ h(SelectOption, { value: "" }, tx(t, "noParent", "— no parent —")),
+ (props.allTasks || []).map(function (task) {
+ return h(SelectOption, { key: task.id, value: task.id },
+ `${task.id} — ${(task.title || "").slice(0, 50)}`);
}),
),
h("div", { className: "flex gap-2" },
@@ -1881,7 +1980,7 @@
h(Button, {
onClick: props.onCancel,
size: "sm",
- }, "Cancel"),
+ }, tx(t, "cancel", "Cancel")),
),
);
}
@@ -1891,6 +1990,7 @@
// -------------------------------------------------------------------------
function TaskDrawer(props) {
+ const { t } = useI18n();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
@@ -2056,10 +2156,11 @@
type: "button",
onClick: props.onClose,
className: "hermes-kanban-drawer-close",
- title: "Close (Esc)",
+ title: tx(t, "close", "Close (Esc)"),
}, "×"),
),
- loading ? h("div", { className: "p-4 text-sm text-muted-foreground" }, "Loading…") :
+ loading ? h("div", { className: "p-4 text-sm text-muted-foreground" },
+ tx(t, "loadingDetail", "Loading…")) :
err ? h("div", { className: "p-4 text-sm text-destructive" }, err) :
data ? h(TaskDetail, {
data, editing, setEditing,
@@ -2087,19 +2188,20 @@
e.preventDefault(); handleComment();
}
},
- placeholder: "Add a comment… (Enter to submit)",
+ placeholder: tx(t, "addComment", "Add a comment… (Enter to submit)"),
className: "h-8 text-sm flex-1",
}),
h(Button, {
onClick: handleComment,
size: "sm",
- }, "Comment"),
+ }, tx(t, "comment", "Comment")),
) : null,
),
);
}
function TaskDetail(props) {
+ const { t: i18n } = useI18n();
const t = props.data.task;
const comments = props.data.comments || [];
const events = props.data.events || [];
@@ -2118,24 +2220,24 @@
})
: h("span", {
className: "hermes-kanban-drawer-title-text",
- title: "Click to edit",
+ title: tx(i18n, "clickToEdit", "Click to edit"),
onClick: function () { props.setEditing(true); },
- }, t.title || "(untitled)"),
+ }, t.title || tx(i18n, "untitled", "(untitled)")),
),
h("div", { className: "hermes-kanban-drawer-meta" },
- h(MetaRow, { label: "Status", value: t.status }),
+ h(MetaRow, { label: tx(i18n, "status", "Status"), value: t.status }),
h(AssigneeEditor, { task: t, onPatch: props.onPatch }),
h(PriorityEditor, { task: t, onPatch: props.onPatch }),
- t.tenant ? h(MetaRow, { label: "Tenant", value: t.tenant }) : null,
+ t.tenant ? h(MetaRow, { label: tx(i18n, "tenant", "Tenant"), value: t.tenant }) : null,
h(MetaRow, {
- label: "Workspace",
+ label: tx(i18n, "workspace", "Workspace"),
value: `${t.workspace_kind}${t.workspace_path ? ": " + t.workspace_path : ""}`,
}),
(t.skills && t.skills.length > 0) ? h(MetaRow, {
- label: "Skills",
+ label: tx(i18n, "skills", "Skills"),
value: t.skills.join(", "),
}) : null,
- t.created_by ? h(MetaRow, { label: "Created by", value: t.created_by }) : null,
+ t.created_by ? h(MetaRow, { label: tx(i18n, "createdBy", "Created by"), value: t.created_by }) : null,
),
h(StatusActions, {
task: t,
@@ -2168,13 +2270,15 @@
onRemoveChild: props.onRemoveChild,
}),
t.result ? h("div", { className: "hermes-kanban-section" },
- h("div", { className: "hermes-kanban-section-head" }, "Result"),
+ h("div", { className: "hermes-kanban-section-head" }, tx(i18n, "result", "Result")),
h(MarkdownBlock, { source: t.result, enabled: props.renderMarkdown }),
) : null,
h("div", { className: "hermes-kanban-section" },
- h("div", { className: "hermes-kanban-section-head" }, `Comments (${comments.length})`),
+ h("div", { className: "hermes-kanban-section-head" },
+ `${tx(i18n, "comments", "Comments")} (${comments.length})`),
comments.length === 0
- ? h("div", { className: "text-xs text-muted-foreground" }, "— no comments —")
+ ? h("div", { className: "text-xs text-muted-foreground" },
+ tx(i18n, "noComments", "— no comments —"))
: comments.map(function (c) {
return h("div", { key: c.id, className: "hermes-kanban-comment" },
h("div", { className: "hermes-kanban-comment-head" },
@@ -2187,7 +2291,8 @@
}),
),
h("div", { className: "hermes-kanban-section" },
- h("div", { className: "hermes-kanban-section-head" }, `Events (${events.length})`),
+ h("div", { className: "hermes-kanban-section-head" },
+ `${tx(i18n, "events", "Events")} (${events.length})`),
events.slice().reverse().slice(0, 20).map(function (e) {
const isDiag = isDiagnosticEvent(e.kind);
const phantoms = isDiag ? phantomIdsFromEvent(e) : [];
@@ -2202,7 +2307,7 @@
? h("div", { className: "hermes-kanban-event-header" },
h("span", { className: "hermes-kanban-event-warning-icon" }, "⚠"),
h("span", { className: "hermes-kanban-event-warning-label" },
- DIAGNOSTIC_EVENT_LABELS[e.kind] || e.kind),
+ getDiagnosticEventLabel(i18n, e.kind) || e.kind),
h("span", { className: "hermes-kanban-event-ago" },
timeAgo ? timeAgo(e.created_at) : ""),
)
@@ -2214,7 +2319,7 @@
isDiag && phantoms.length > 0
? h("div", { className: "hermes-kanban-event-phantom-row" },
h("span", { className: "hermes-kanban-event-phantom-label" },
- "Phantom ids:"),
+ tx(i18n, "phantomIds", "Phantom ids:")),
phantoms.map(function (pid) {
return h("code", {
key: pid,
@@ -2239,6 +2344,7 @@
// active run if any. Each row shows profile / outcome / elapsed /
// summary. Collapsed by default when there are more than three runs.
function RunHistorySection(props) {
+ const { t } = useI18n();
const runs = props.runs || [];
const [expanded, setExpanded] = useState(false);
if (runs.length === 0) return null;
@@ -2257,13 +2363,13 @@
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head-row" },
h("span", { className: "hermes-kanban-section-head" },
- `Run history (${runs.length})`),
+ `${tx(t, "runHistory", "Run history")} (${runs.length})`),
!showAll
? h("button", {
type: "button",
onClick: function () { setExpanded(true); },
className: "hermes-kanban-edit-link",
- title: "Show all attempts",
+ title: tx(t, "showAllAttempts", "Show all attempts"),
}, `+${runs.length - 3} earlier`)
: null,
),
@@ -2274,9 +2380,9 @@
return h("div", { key: r.id, className: cn("hermes-kanban-run", outcomeClass) },
h("div", { className: "hermes-kanban-run-head" },
h("span", { className: "hermes-kanban-run-outcome" },
- r.ended_at ? (r.outcome || r.status || "ended") : "active"),
+ r.ended_at ? (r.outcome || r.status || tx(t, "ended", "ended")) : tx(t, "active", "active")),
h("span", { className: "hermes-kanban-run-profile" },
- r.profile ? `@${r.profile}` : "(no profile)"),
+ r.profile ? `@${r.profile}` : tx(t, "noProfile", "(no profile)")),
h("span", { className: "hermes-kanban-run-elapsed" }, fmtElapsed(r)),
h("span", { className: "hermes-kanban-run-ago" },
timeAgo ? timeAgo(r.started_at) : ""),
@@ -2298,6 +2404,7 @@
// Worker log: loads lazily (one GET on mount), refresh button, tail cap.
function WorkerLogSection(props) {
+ const { t } = useI18n();
const [state, setState] = useState({ loading: false, data: null, err: null });
const load = useCallback(function () {
setState({ loading: true, data: null, err: null });
@@ -2313,12 +2420,14 @@
const data = state.data;
let body;
if (state.loading) {
- body = h("div", { className: "text-xs text-muted-foreground" }, "Loading log…");
+ body = h("div", { className: "text-xs text-muted-foreground" },
+ tx(t, "loadingLog", "Loading log…"));
} else if (state.err) {
body = h("div", { className: "text-xs text-destructive" }, state.err);
} else if (!data || !data.exists) {
body = h("div", { className: "text-xs text-muted-foreground italic" },
- "— no worker log yet (task hasn't spawned or log was rotated away) —");
+ tx(t, "noWorkerLog",
+ "— no worker log yet (task hasn't spawned or log was rotated away) —"));
} else {
body = h("pre", { className: "hermes-kanban-pre hermes-kanban-log" },
data.content || "(empty)");
@@ -2327,7 +2436,7 @@
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head-row" },
h("span", { className: "hermes-kanban-section-head" },
- "Worker log" + (data && data.size_bytes ? ` (${data.size_bytes} B)` : "")),
+ tx(t, "workerLog", "Worker log") + (data && data.size_bytes ? ` (${data.size_bytes} B)` : "")),
h("button", {
type: "button",
onClick: load,
@@ -2338,7 +2447,9 @@
body,
data && data.truncated
? h("div", { className: "text-xs text-muted-foreground" },
- "(showing last 100 KB — full log at ", data.path, ")")
+ tx(t, "logTruncated", "(showing last 100 KB — full log at "),
+ data.path,
+ tx(t, "logAt", ")"))
: null,
);
}
@@ -2351,11 +2462,12 @@
}
function TitleEditor(props) {
+ const { t } = useI18n();
const [v, setV] = useState(props.initial);
const save = function () {
- const t = v.trim();
- if (!t) return;
- props.onSave(t);
+ const trimmed = v.trim();
+ if (!trimmed) return;
+ props.onSave(trimmed);
};
return h("div", { className: "hermes-kanban-edit-row" },
h(Input, {
@@ -2369,32 +2481,33 @@
}),
h(Button, { onClick: save,
size: "sm",
- }, "Save"),
+ }, tx(t, "save", "Save")),
h(Button, { onClick: props.onCancel,
size: "sm",
- }, "Cancel"),
+ }, tx(t, "cancel", "Cancel")),
);
}
function AssigneeEditor(props) {
+ const { t } = useI18n();
const [editing, setEditing] = useState(false);
const [v, setV] = useState(props.task.assignee || "");
useEffect(function () { setV(props.task.assignee || ""); }, [props.task.assignee]);
if (!editing) {
return h("div", { className: "hermes-kanban-meta-row" },
- h("span", { className: "hermes-kanban-meta-label" }, "Assignee"),
+ h("span", { className: "hermes-kanban-meta-label" }, tx(t, "assignee", "Assignee")),
h("span", {
className: "hermes-kanban-meta-value hermes-kanban-editable",
onClick: function () { setEditing(true); },
- title: "Click to edit",
- }, props.task.assignee || "unassigned"),
+ title: tx(t, "clickToEditAssignee", "Click to edit assignee"),
+ }, props.task.assignee || tx(t, "unassigned", "unassigned")),
);
}
const save = function () {
props.onPatch({ assignee: v.trim() || "" }).then(function () { setEditing(false); });
};
return h("div", { className: "hermes-kanban-meta-row" },
- h("span", { className: "hermes-kanban-meta-label" }, "Assignee"),
+ h("span", { className: "hermes-kanban-meta-label" }, tx(t, "assignee", "Assignee")),
h(Input, {
value: v, autoFocus: true,
onChange: function (e) { setV(e.target.value); },
@@ -2402,23 +2515,24 @@
if (e.key === "Enter") { e.preventDefault(); save(); }
if (e.key === "Escape") setEditing(false);
},
- placeholder: "(empty = unassign)",
+ placeholder: tx(t, "emptyAssignee", "(empty = unassign)"),
className: "h-7 text-xs flex-1",
}),
);
}
function PriorityEditor(props) {
+ const { t } = useI18n();
const [editing, setEditing] = useState(false);
const [v, setV] = useState(String(props.task.priority || 0));
useEffect(function () { setV(String(props.task.priority || 0)); }, [props.task.priority]);
if (!editing) {
return h("div", { className: "hermes-kanban-meta-row" },
- h("span", { className: "hermes-kanban-meta-label" }, "Priority"),
+ h("span", { className: "hermes-kanban-meta-label" }, tx(t, "priority", "Priority")),
h("span", {
className: "hermes-kanban-meta-value hermes-kanban-editable",
onClick: function () { setEditing(true); },
- title: "Click to edit",
+ title: tx(t, "clickToEdit", "Click to edit"),
}, String(props.task.priority)),
);
}
@@ -2426,7 +2540,7 @@
props.onPatch({ priority: Number(v) || 0 }).then(function () { setEditing(false); });
};
return h("div", { className: "hermes-kanban-meta-row" },
- h("span", { className: "hermes-kanban-meta-label" }, "Priority"),
+ h("span", { className: "hermes-kanban-meta-label" }, tx(t, "priority", "Priority")),
h(Input, {
type: "number", value: v, autoFocus: true,
onChange: function (e) { setV(e.target.value); },
@@ -2440,6 +2554,7 @@
}
function BodyEditor(props) {
+ const { t } = useI18n();
const [editing, setEditing] = useState(false);
const [v, setV] = useState(props.task.body || "");
useEffect(function () { setV(props.task.body || ""); }, [props.task.body]);
@@ -2448,22 +2563,22 @@
};
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head-row" },
- h("span", { className: "hermes-kanban-section-head" }, "Description"),
+ h("span", { className: "hermes-kanban-section-head" }, tx(t, "description", "Description")),
editing
? h("div", { className: "flex gap-1" },
h(Button, { onClick: save,
size: "sm",
- }, "Save"),
+ }, tx(t, "save", "Save")),
h(Button, { onClick: function () { setEditing(false); setV(props.task.body || ""); },
size: "sm",
- }, "Cancel"),
+ }, tx(t, "cancel", "Cancel")),
)
: h("button", {
type: "button",
onClick: function () { setEditing(true); },
className: "hermes-kanban-edit-link",
title: "Edit description",
- }, "edit"),
+ }, tx(t, "edit", "edit")),
),
editing
? h("textarea", {
@@ -2474,30 +2589,32 @@
})
: props.task.body
? h(MarkdownBlock, { source: props.task.body, enabled: props.renderMarkdown })
- : h("div", { className: "text-xs text-muted-foreground italic" }, "— no description —"),
+ : h("div", { className: "text-xs text-muted-foreground italic" },
+ tx(t, "noDescription", "— no description —")),
);
}
function DependencyEditor(props) {
+ const { t } = useI18n();
const { task, links, allTasks } = props;
const [newParent, setNewParent] = useState("");
const [newChild, setNewChild] = useState("");
// Filter out self + existing links when offering the "add" dropdown.
const candidatesFor = function (excludeSet) {
- return (allTasks || []).filter(function (t) {
- return t.id !== task.id && !excludeSet.has(t.id);
+ return (allTasks || []).filter(function (tk) {
+ return tk.id !== task.id && !excludeSet.has(tk.id);
});
};
const parentExclude = new Set([task.id, ...(links.parents || [])]);
const childExclude = new Set([task.id, ...(links.children || [])]);
return h("div", { className: "hermes-kanban-section" },
- h("div", { className: "hermes-kanban-section-head" }, "Dependencies"),
+ h("div", { className: "hermes-kanban-section-head" }, tx(t, "dependencies", "Dependencies")),
h("div", { className: "hermes-kanban-deps-row" },
- h("span", { className: "hermes-kanban-deps-label" }, "Parents:"),
+ h("span", { className: "hermes-kanban-deps-label" }, tx(t, "parents", "Parents:")),
h("div", { className: "hermes-kanban-deps-chips" },
(links.parents || []).length === 0
- ? h("span", { className: "hermes-kanban-deps-empty" }, "none")
+ ? h("span", { className: "hermes-kanban-deps-empty" }, tx(t, "none", "none"))
: (links.parents || []).map(function (id) {
return h("span", { key: id, className: "hermes-kanban-dep-chip" },
id,
@@ -2505,7 +2622,7 @@
type: "button",
className: "hermes-kanban-dep-chip-x",
onClick: function () { props.onRemoveParent(id); },
- title: "Remove dependency",
+ title: tx(t, "removeDependency", "Remove dependency"),
}, "×"),
);
}),
@@ -2516,10 +2633,10 @@
value: newParent,
className: "h-7 text-xs flex-1",
}, selectChangeHandler(setNewParent)),
- h(SelectOption, { value: "" }, "— add parent —"),
- candidatesFor(parentExclude).map(function (t) {
- return h(SelectOption, { key: t.id, value: t.id },
- `${t.id} — ${(t.title || "").slice(0, 50)}`);
+ h(SelectOption, { value: "" }, tx(t, "addParent", "— add parent —")),
+ candidatesFor(parentExclude).map(function (tk) {
+ return h(SelectOption, { key: tk.id, value: tk.id },
+ `${tk.id} — ${(tk.title || "").slice(0, 50)}`);
}),
),
h(Button, {
@@ -2532,10 +2649,10 @@
}, "+ parent"),
),
h("div", { className: "hermes-kanban-deps-row" },
- h("span", { className: "hermes-kanban-deps-label" }, "Children:"),
+ h("span", { className: "hermes-kanban-deps-label" }, tx(t, "children", "Children:")),
h("div", { className: "hermes-kanban-deps-chips" },
(links.children || []).length === 0
- ? h("span", { className: "hermes-kanban-deps-empty" }, "none")
+ ? h("span", { className: "hermes-kanban-deps-empty" }, tx(t, "none", "none"))
: (links.children || []).map(function (id) {
return h("span", { key: id, className: "hermes-kanban-dep-chip" },
id,
@@ -2543,7 +2660,7 @@
type: "button",
className: "hermes-kanban-dep-chip-x",
onClick: function () { props.onRemoveChild(id); },
- title: "Remove dependency",
+ title: tx(t, "removeDependency", "Remove dependency"),
}, "×"),
);
}),
@@ -2554,10 +2671,10 @@
value: newChild,
className: "h-7 text-xs flex-1",
}, selectChangeHandler(setNewChild)),
- h(SelectOption, { value: "" }, "— add child —"),
- candidatesFor(childExclude).map(function (t) {
- return h(SelectOption, { key: t.id, value: t.id },
- `${t.id} — ${(t.title || "").slice(0, 50)}`);
+ h(SelectOption, { value: "" }, tx(t, "addChild", "— add child —")),
+ candidatesFor(childExclude).map(function (tk) {
+ return h(SelectOption, { key: tk.id, value: tk.id },
+ `${tk.id} — ${(tk.title || "").slice(0, 50)}`);
}),
),
h(Button, {
@@ -2573,7 +2690,8 @@
}
function StatusActions(props) {
- const t = props.task;
+ const { t } = useI18n();
+ const task = props.task;
const [specifyBusy, setSpecifyBusy] = useState(false);
const [specifyMsg, setSpecifyMsg] = useState(null);
const b = function (label, patch, enabled, confirmMsg) {
@@ -2588,7 +2706,7 @@
// one column where an auxiliary LLM pass is meaningful. Elsewhere
// the backend would return ok:false with "not in triage" anyway,
// so hiding the button keeps the action row uncluttered.
- const specifyButton = (t.status === "triage" && props.onSpecify)
+ const specifyButton = (task.status === "triage" && props.onSpecify)
? h(Button, {
onClick: function () {
if (specifyBusy) return;
@@ -2623,21 +2741,21 @@
return h("div", null,
h("div", { className: "hermes-kanban-actions" },
specifyButton,
- b("→ triage", { status: "triage" }, t.status !== "triage"),
- b("→ ready", { status: "ready" }, t.status !== "ready"),
+ b("→ triage", { status: "triage" }, task.status !== "triage"),
+ b("→ ready", { status: "ready" }, task.status !== "ready"),
// No direct → running button: /tasks/:id PATCH rejects status=running
// with 400 (issue #19535). Tasks enter running only through the
// dispatcher's claim_task path, which atomically creates the run row,
// claim lock, and worker process metadata.
- b("Block", { status: "blocked" },
- t.status === "running" || t.status === "ready",
- DESTRUCTIVE_TRANSITIONS.blocked),
- b("Unblock", { status: "ready" }, t.status === "blocked"),
- b("Complete", { status: "done" },
- t.status === "running" || t.status === "ready" || t.status === "blocked",
- DESTRUCTIVE_TRANSITIONS.done),
- b("Archive", { status: "archived" }, t.status !== "archived",
- DESTRUCTIVE_TRANSITIONS.archived),
+ b(tx(t, "block", "Block"), { status: "blocked" },
+ task.status === "running" || task.status === "ready",
+ getDestructiveConfirm(t, "blocked")),
+ b(tx(t, "unblock", "Unblock"), { status: "ready" }, task.status === "blocked"),
+ b(tx(t, "complete", "Complete"), { status: "done" },
+ task.status === "running" || task.status === "ready" || task.status === "blocked",
+ getDestructiveConfirm(t, "done")),
+ b(tx(t, "archive", "Archive"), { status: "archived" }, task.status !== "archived",
+ getDestructiveConfirm(t, "archived")),
),
specifyMsg ? h("div", {
className: specifyMsg.ok
@@ -2654,19 +2772,21 @@
// renders when no platforms have a home configured — this section stays
// invisible for users who haven't set one up.
function HomeSubsSection(props) {
+ const { t } = useI18n();
const channels = props.homeChannels || [];
if (channels.length === 0) return null;
const busy = props.homeBusy || {};
return h("div", { className: "hermes-kanban-section" },
h("div", { className: "hermes-kanban-section-head" },
- "Notify home channels"),
+ tx(t, "notifyHomeChannels", "Notify home channels")),
h("div", { className: "hermes-kanban-home-subs" },
channels.map(function (hc) {
const isBusy = !!busy[hc.platform];
const label = hc.subscribed ? "✓ " + hc.platform : hc.platform;
+ const target = `${hc.name} (${hc.chat_id}${hc.thread_id ? " / " + hc.thread_id : ""})`;
const title = hc.subscribed
- ? `Sending updates to ${hc.name} (${hc.chat_id}${hc.thread_id ? " / " + hc.thread_id : ""}). Click to stop.`
- : `Send completed / blocked / gave_up notifications to ${hc.name} (${hc.chat_id}${hc.thread_id ? " / " + hc.thread_id : ""}).`;
+ ? `${tx(t, "sendingUpdates", "Sending updates to")} ${target}. Click to stop.`
+ : `${tx(t, "sendNotifications", "Send completed / blocked / gave_up notifications to")} ${target}.`;
return h(Button, {
key: hc.platform,
size: "sm",
diff --git a/tests/agent/test_i18n.py b/tests/agent/test_i18n.py
index f59d3fb430d..6c374ebf487 100644
--- a/tests/agent/test_i18n.py
+++ b/tests/agent/test_i18n.py
@@ -156,7 +156,12 @@ def test_t_missing_key_in_non_english_falls_back_to_english(tmp_path, monkeypatc
(fake_locales / "zh.yaml").write_text("# intentionally empty\n", encoding="utf-8")
monkeypatch.setattr(i18n, "_locales_dir", lambda: fake_locales)
i18n.reset_language_cache()
- assert i18n.t("foo", lang="zh") == "English Foo"
+ try:
+ assert i18n.t("foo", lang="zh") == "English Foo"
+ finally:
+ # Clear the cache on teardown so subsequent tests don't see the
+ # fake "foo: English Foo" catalog instead of the real locales/*.yaml.
+ i18n.reset_language_cache()
def test_t_unknown_language_uses_english():
diff --git a/web/src/components/LanguageSwitcher.tsx b/web/src/components/LanguageSwitcher.tsx
index dc477021ee8..74a16b1068f 100644
--- a/web/src/components/LanguageSwitcher.tsx
+++ b/web/src/components/LanguageSwitcher.tsx
@@ -1,36 +1,100 @@
+import { useState, useRef, useEffect } from "react";
import { Button } from "@nous-research/ui/ui/components/button";
import { Typography } from "@/components/NouiTypography";
import { useI18n } from "@/i18n/context";
+import { LOCALE_META } from "@/i18n";
+import type { Locale } from "@/i18n";
/**
- * Compact language toggle — shows a clickable flag that switches between
- * English and Chinese. Persists choice to localStorage.
+ * Language picker — shows the current language's flag + endonym, opens a
+ * dropdown of all supported locales when clicked. Persists choice to
+ * localStorage via the I18n context.
+ *
+ * Replaces the older two-state EN↔ZH toggle now that we ship 16 locales
+ * (en, zh, zh-hant, ja, de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu).
*/
export function LanguageSwitcher() {
const { locale, setLocale, t } = useI18n();
+ const [open, setOpen] = useState(false);
+ const containerRef = useRef(null);
- const toggle = () => setLocale(locale === "en" ? "zh" : "en");
+ // Close on outside click / Escape so the dropdown doesn't trap the user.
+ useEffect(() => {
+ if (!open) return;
+
+ function onPointerDown(e: PointerEvent) {
+ if (!containerRef.current) return;
+ if (!containerRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ }
+ }
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") setOpen(false);
+ }
+
+ document.addEventListener("pointerdown", onPointerDown);
+ document.addEventListener("keydown", onKey);
+ return () => {
+ document.removeEventListener("pointerdown", onPointerDown);
+ document.removeEventListener("keydown", onKey);
+ };
+ }, [open]);
+
+ const current = LOCALE_META[locale];
+ const allLocales = Object.entries(LOCALE_META) as Array<[Locale, typeof current]>;
return (
-