diff --git a/plugins/hermes-achievements/dashboard/dist/index.js b/plugins/hermes-achievements/dashboard/dist/index.js index 5be8a3f1dd1d..ad57212c4620 100644 --- a/plugins/hermes-achievements/dashboard/dist/index.js +++ b/plugins/hermes-achievements/dashboard/dist/index.js @@ -42,6 +42,53 @@ return str; } + // Achievement definitions keep English copy as an API fallback. Locales use + // the stable ACHIEVEMENTS id directly, so translating a badge never changes + // its unlock state or requires a locale-specific scan snapshot. + function localizedCategory(t, category) { + const categories = t && t.achievements && t.achievements.categories; + return (categories && categories[category]) || category; + } + + function localizedAchievement(t, achievement) { + const catalog = t && t.achievements && t.achievements.catalog; + const copy = catalog && catalog[achievement.id]; + const category = localizedCategory(t, achievement.category); + if (!copy && category === achievement.category) return achievement; + return Object.assign({}, achievement, { + name: (copy && copy.name) || achievement.name, + description: (copy && copy.description) || achievement.description, + category: category, + }); + } + + function localizedMetric(t, achievement, metric) { + const metrics = t && t.achievements && t.achievements.metrics; + const spec = achievement.criteria_spec || {}; + return (metrics && metrics[metric]) || (spec.metric_labels && spec.metric_labels[metric]) || null; + } + + function localizedCriteria(t, achievement) { + const spec = achievement.criteria_spec; + if (!spec || !spec.kind) return achievement.criteria; + if (spec.kind === "secret_hidden") { + return tx(t, "criteria.secret_hidden", "Secret: exact requirement hidden until Hermes sees the first matching signal. Keep using Hermes across debugging, tools, memory, skills, plugins, and model workflows to reveal it."); + } + if (spec.kind === "tiered") { + const metric = localizedMetric(t, achievement, spec.metric); + const ladder = (spec.tiers || []).map(function (tier) { return tier.name + " " + tier.threshold; }).join(", "); + return metric && ladder ? tx(t, "criteria.tiered", "Requirement: {metric}. Tier ladder: {ladder}.", { metric: metric, ladder: ladder }) : achievement.criteria; + } + if (spec.kind === "requirements") { + const requirements = (spec.requirements || []).map(function (requirement) { + const metric = localizedMetric(t, achievement, requirement.metric); + return metric && (metric + " ≥" + (requirement.gte || 1)); + }); + return requirements.length && requirements.every(Boolean) ? tx(t, "criteria.requirements", "Requirement: {requirements}.", { requirements: requirements.join("; ") }) : achievement.criteria; + } + return achievement.criteria; + } + 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 "}; const tierClass = function (tier) { @@ -276,6 +323,7 @@ function ShareDialog({ achievement, onClose }) { const { t } = useI18n(); + const displayAchievement = localizedAchievement(t, achievement); const [status, setStatus] = hooks.useState("rendering"); // rendering | ready | copied | error const [errorMsg, setErrorMsg] = hooks.useState(null); const [previewUrl, setPreviewUrl] = hooks.useState(null); @@ -284,7 +332,7 @@ hooks.useEffect(function () { let cancelled = false; let createdUrl = null; - buildShareImage(achievement).then(function (blob) { + buildShareImage(displayAchievement).then(function (blob) { if (cancelled) return; blobRef.current = blob; createdUrl = URL.createObjectURL(blob); @@ -299,7 +347,7 @@ cancelled = true; if (createdUrl) URL.revokeObjectURL(createdUrl); }; - }, [achievement.id]); + }, [achievement.id, displayAchievement.name, displayAchievement.description]); function download() { if (!blobRef.current) return; @@ -337,7 +385,7 @@ const tierPart = achievement.tier ? (achievement.tier + " tier ") : ""; const tmpl = tx(t, "share.tweet_text", "Just unlocked {tier_part}\"{name}\" in Hermes Agent ☤", { tier_part: tierPart, - name: achievement.name, + name: displayAchievement.name, }); return tmpl + "\n\n@NousResearch · https://hermes-agent.nousresearch.com"; } @@ -353,12 +401,12 @@ }, 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, tx(t, "share.header", "Share: {name}", { name: achievement.name })), + React.createElement("strong", null, tx(t, "share.header", "Share: {name}", { name: displayAchievement.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" }, tx(t, "share.rendering", "Rendering…")), - previewUrl && React.createElement("img", { src: previewUrl, alt: tx(t, "share.card_alt", "{name} share card", { name: achievement.name }) }) + previewUrl && React.createElement("img", { src: previewUrl, alt: tx(t, "share.card_alt", "{name} share card", { name: displayAchievement.name }) }) ), 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" }, @@ -491,6 +539,7 @@ function AchievementCard({ achievement }) { const { t } = useI18n(); + const displayAchievement = localizedAchievement(t, achievement); const unlocked = achievement.unlocked; const progress = achievement.progress || 0; const pct = achievement.progress_pct || (unlocked ? 100 : 0); @@ -520,8 +569,8 @@ React.createElement("div", { className: "ha-card-head" }, React.createElement("div", { className: "ha-icon" }, React.createElement(AchievementIcon, { icon: achievement.icon || "secret" })), React.createElement("div", { className: "ha-card-title-wrap" }, - React.createElement("div", { className: "ha-card-title" }, achievement.name), - React.createElement("div", { className: "ha-card-category" }, achievement.category) + React.createElement("div", { className: "ha-card-title" }, displayAchievement.name), + React.createElement("div", { className: "ha-card-category" }, displayAchievement.category) ), React.createElement("div", { className: "ha-badges" }, React.createElement("span", { className: "ha-state-badge" }, stateLabel), @@ -530,16 +579,16 @@ className: "ha-share-trigger", onClick: function () { setShareOpen(true); }, title: tx(t, "card.share_title", "Share this achievement"), - "aria-label": tx(t, "card.share_label", "Share {name}", { name: achievement.name }), + "aria-label": tx(t, "card.share_label", "Share {name}", { name: displayAchievement.name }), }, tx(t, "card.share_text", "Share")) ) ), - React.createElement("p", { className: "ha-description" }, achievement.description), + React.createElement("p", { className: "ha-description" }, displayAchievement.description), achievement.criteria && React.createElement("details", { className: "ha-criteria" }, 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("p", null, localizedCriteria(t, achievement)) ), React.createElement("div", { className: "ha-evidence-slot" }, achievement.evidence ? React.createElement("div", { className: "ha-evidence" }, @@ -678,7 +727,7 @@ 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(StatCard, { label: tx(t, "stats.latest", "Latest"), value: latest[0] ? localizedAchievement(t, latest[0]).name : tx(t, "stats.none_yet", "None yet"), hint: latest[0] ? localizedAchievement(t, latest[0]).category : tx(t, "stats.latest_hint_empty", "run Hermes more") }) ), React.createElement("section", { className: "ha-guide" }, React.createElement("div", null, @@ -694,7 +743,7 @@ React.createElement("div", { className: "ha-pills" }, categories.map(function (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; + const pillLabel = cat === "All" ? allCategoryLabel : localizedCategory(t, 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) { @@ -706,7 +755,7 @@ 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" })), - a.name + localizedAchievement(t, a).name ); })) ), diff --git a/plugins/hermes-achievements/dashboard/plugin_api.py b/plugins/hermes-achievements/dashboard/plugin_api.py index b419efc6c27f..32c8d42e438e 100644 --- a/plugins/hermes-achievements/dashboard/plugin_api.py +++ b/plugins/hermes-achievements/dashboard/plugin_api.py @@ -531,6 +531,30 @@ def metric_label(metric: str) -> str: return METRIC_LABELS.get(metric, metric.replace("_", " ")) +def criteria_spec_for(definition: Dict[str, Any]) -> Dict[str, Any]: + """Return locale-neutral criteria data for dashboard clients. + + ``criteria`` remains an English fallback for older plugin bundles, while + this structure lets a locale-aware client render the same requirement + without putting a locale into the scan snapshot cache. + """ + if definition.get("secret") and definition.get("state") == "secret": + return {"kind": "secret_hidden"} + if "threshold_metric" in definition: + metric = definition["threshold_metric"] + return { + "kind": "tiered", + "metric": metric, + "metric_labels": {metric: metric_label(metric)}, + "tiers": [dict(tier) for tier in definition.get("tiers", [])], + } + requirements = definition.get("requirements") or [] + if requirements: + metrics = {requirement["metric"]: metric_label(requirement["metric"]) for requirement in requirements} + return {"kind": "requirements", "requirements": [dict(requirement) for requirement in requirements], "metric_labels": metrics} + return {"kind": "matching_workflow"} + + def criteria_for(definition: Dict[str, Any]) -> str: if definition.get("secret") and definition.get("state") == "secret": return "Secret: exact requirement hidden until Hermes sees the first matching signal. Keep using Hermes across debugging, tools, memory, skills, plugins, and model workflows to reveal it." @@ -551,9 +575,11 @@ def criteria_for(definition: Dict[str, Any]) -> str: def display_achievement(item: Dict[str, Any]) -> Dict[str, Any]: clean = dict(item) + criteria_spec = criteria_spec_for(clean) if clean.get("state") == "secret": - return {**clean, "name": "???", "description": "Secret achievement: hidden until Hermes detects the first relevant behavior in your session history.", "criteria": criteria_for(clean), "icon": "secret"} + return {**clean, "name": "???", "description": "Secret achievement: hidden until Hermes detects the first relevant behavior in your session history.", "criteria": criteria_for(clean), "criteria_spec": criteria_spec, "icon": "secret"} clean["criteria"] = criteria_for(clean) + clean["criteria_spec"] = criteria_spec return clean diff --git a/plugins/hermes-achievements/tests/test_achievement_engine.py b/plugins/hermes-achievements/tests/test_achievement_engine.py index 22342095269c..9edb2fd5e12d 100644 --- a/plugins/hermes-achievements/tests/test_achievement_engine.py +++ b/plugins/hermes-achievements/tests/test_achievement_engine.py @@ -73,6 +73,34 @@ def test_secret_achievement_stays_hidden_without_progress(self): self.assertEqual(result["state"], "secret") self.assertEqual(display["name"], "???") self.assertNotIn("Permission", display["description"]) + self.assertEqual(display["criteria_spec"], {"kind": "secret_hidden"}) + + def test_display_achievement_includes_locale_neutral_criteria_spec(self): + definition = { + "id": "terminal_goblin", + "threshold_metric": "total_terminal_calls", + "tiers": [{"name": "Copper", "threshold": 50}], + } + + display = plugin_api.display_achievement({**definition, "state": "discovered"}) + + self.assertEqual(display["criteria_spec"], { + "kind": "tiered", + "metric": "total_terminal_calls", + "metric_labels": {"total_terminal_calls": "lifetime terminal calls"}, + "tiers": [{"name": "Copper", "threshold": 50}], + }) + + def test_requirement_criteria_spec_contains_metric_label_fallbacks(self): + display = plugin_api.display_achievement({ + "id": "full_send", + "state": "discovered", + "requirements": [{"metric": "total_terminal_calls", "gte": 10}], + }) + + self.assertEqual(display["criteria_spec"]["metric_labels"], { + "total_terminal_calls": "lifetime terminal calls", + }) def test_multi_condition_unlock_requires_all_requirements(self): definition = { diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index a8be9310835a..5d888f57159e 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -556,6 +556,147 @@ export const en: Translations = { }, achievements: { + catalog: { + let_him_cook: { name: "Let Him Cook", description: "Let Hermes run a serious autonomous tool chain in one session." }, + autonomous_avalanche: { name: "Autonomous Avalanche", description: "Accumulate a lifetime avalanche of Hermes tool calls across sessions." }, + toolchain_maxxer: { name: "Toolchain Maxxer", description: "Use a wide spread of distinct Hermes tools in one session." }, + full_send: { name: "Full Send", description: "Terminal, files, and web/browser all get involved in one real run." }, + subagent_commander: { name: "Subagent Commander", description: "Coordinate delegated agent work." }, + background_process_enjoyer: { name: "Background Process Enjoyer", description: "Start or control enough long-running processes to deserve the title." }, + cron_necromancer: { name: "Cron Necromancer", description: "Raise scheduled autonomous jobs from the dead." }, + red_text_connoisseur: { name: "Red Text Connoisseur", description: "Encounter enough errors to develop a palate for red text." }, + stack_trace_sommelier: { name: "Stack Trace Sommelier", description: "Taste tracebacks by the flight, not by the sip." }, + actually_read_the_logs: { name: "Actually Read The Logs", description: "Inspect logs repeatedly instead of guessing." }, + port_3000_taken: { name: "Port 3000 Is Taken", description: "Discover dev-server port conflict patterns enough times to become numb." }, + permission_denied_any_percent: { name: "Permission Denied Any%", description: "Speedrun into permission walls." }, + dependency_hell_tourist: { name: "Dependency Hell Tourist", description: "Package installs fail, then somehow life continues." }, + the_fix_was_restarting: { name: "The Fix Was Restarting It", description: "Restart after enough error clusters to call it a technique." }, + forgot_the_env_var: { name: "Forgot The Env Var", description: "Auth or configuration failed because an environment variable was missing." }, + yaml_colon_incident: { name: "YAML Colon Incident", description: "Configuration syntax bites back." }, + docker_name_collision: { name: "Docker Name Collision", description: "A container name already exists. Of course it does." }, + supposed_to_be_quick: { name: "This Was Supposed To Be Quick", description: "A tiny ask becomes an entire expedition." }, + one_more_small_change: { name: "One More Small Change", description: "Make enough file edits in one session to invalidate the phrase small change." }, + vibe_architect: { name: "Vibe Architect", description: "Touch a broad surface area in one project session." }, + pixel_goblin: { name: "Pixel Goblin", description: "Do sustained frontend, CSS, SVG, or visual tuning." }, + ship_first_ask_later: { name: "Ship First, Ask Later", description: "Git activity after a serious tool chain." }, + css_exorcist: { name: "CSS Exorcist", description: "Cast repeated styling demons out of the interface." }, + one_character_fix: { name: "One Character Fix", description: "A tiny edit after a pile of errors. Painful. Beautiful." }, + skillsmith: { name: "Skillsmith", description: "Work with Hermes skills enough to leave fingerprints." }, + skill_issue_skill_created: { name: "Skill Issue? Skill Created.", description: "Create or patch durable procedures instead of repeating yourself." }, + memory_keeper: { name: "Memory Keeper", description: "Persist durable knowledge with memory or Mnemosyne." }, + memory_palace: { name: "Memory Palace", description: "Build a serious durable-memory trail." }, + context_dragon: { name: "Context Dragon", description: "Brush against compression, huge context, or token pressure repeatedly." }, + gateway_dweller: { name: "Gateway Dweller", description: "Live through gateway-connected Hermes workflows." }, + plugin_goblin: { name: "Plugin Goblin", description: "Use or develop plugins enough that the dashboard notices." }, + rollback_wizard: { name: "Rollback Wizard", description: "Invoke rollback/checkpoint recovery magic." }, + rabbit_hole_certified: { name: "Rabbit Hole Certified", description: "Search or extract enough web content to qualify as a research spiral." }, + citation_goblin: { name: "Citation Goblin", description: "Extract enough web pages to become a tiny librarian." }, + docs_archaeologist: { name: "Docs Archaeologist", description: "Dig through documentation sources over and over." }, + browser_possession: { name: "Browser Possession", description: "Possess a browser through automation repeatedly." }, + terminal_goblin: { name: "Terminal Goblin", description: "Spend serious time in shell-land." }, + patch_wizard: { name: "Patch Wizard", description: "Bend files to your will with targeted patches." }, + file_archaeologist: { name: "File Archaeologist", description: "Dig through the filesystem with reads and searches." }, + image_whisperer: { name: "Image Whisperer", description: "Use image generation or vision tools enough for visual work." }, + voice_of_the_machine: { name: "Voice Of The Machine", description: "Use text-to-speech or voice tooling repeatedly." }, + model_hopper: { name: "Model Hopper", description: "Switch or inspect providers/models enough to count as a habit." }, + openrouter_enjoyer: { name: "OpenRouter Enjoyer", description: "Route model work through OpenRouter repeatedly." }, + codex_conjurer: { name: "Codex Conjurer", description: "Summon Codex-flavored assistance often enough for a ritual." }, + multi_model_mage: { name: "Multi-Model Mage", description: "Use a real spread of distinct model names across Hermes history." }, + five_model_flight: { name: "Five-Model Flight", description: "Try at least five distinct LLMs instead of marrying the first model that answers." }, + provider_polyglot: { name: "Provider Polyglot", description: "Use models from multiple providers across Hermes history." }, + model_sommelier: { name: "Model Sommelier", description: "Taste enough model/provider conversations to develop preferences." }, + claude_confidant: { name: "Claude Confidant", description: "Bring Claude-flavored reasoning into the workflow repeatedly." }, + gemini_cartographer: { name: "Gemini Cartographer", description: "Map enough Gemini-related workflows to know the terrain." }, + open_weights_pilgrim: { name: "Open Weights Pilgrim", description: "Actually chat with local/open-weight models through Hermes session metadata." }, + toolset_cartographer: { name: "Toolset Cartographer", description: "Navigate Hermes toolsets deliberately instead of treating tools as a blur." }, + config_surgeon: { name: "Config Surgeon", description: "Operate on real config files, manifests, env files, and dashboard settings without flinching." }, + rebase_acrobat: { name: "Rebase Acrobat", description: "Handle real git history surgery: rebase, conflict, merge, fetch, push." }, + test_suite_tamer: { name: "Test Suite Tamer", description: "Run enough verification commands that green text becomes part of the ritual." }, + screenshot_hunter: { name: "Screenshot Hunter", description: "Capture, inspect, and polish visual proof instead of just claiming it works." }, + marathon_operator: { name: "Marathon Operator", description: "Accumulate a serious number of Hermes sessions." }, + weekend_warrior: { name: "Weekend Warrior", description: "Run Hermes on weekends enough times to make it a lifestyle." }, + night_shift_operator: { name: "Night Shift Operator", description: "Run sessions during gremlin hours repeatedly." }, + cache_hit_appreciator: { name: "Cache Hit Appreciator", description: "Notice or benefit from prompt/cache behavior." }, + }, + metrics: { + max_tool_calls_in_session: "tool calls in one session", + max_distinct_tools_in_session: "distinct Hermes tools used in one session", + max_terminal_calls_in_session: "terminal calls in one session", + max_file_tool_calls_in_session: "file/search/patch calls in one session", + max_web_browser_calls_in_session: "web search/extract or browser calls in one session", + max_messages_in_session: "messages in one session", + max_files_touched_in_session: "files touched in one session", + total_delegate_calls: "lifetime delegate_task calls", + total_process_calls: "lifetime background process operations", + total_cron_calls: "lifetime scheduled-job operations", + total_errors: "error/failed/traceback messages observed", + traceback_events: "traceback or exception mentions", + log_read_events: "log inspections", + port_conflict_events: "dev-server port conflict detections", + permission_denied_events: "permission-denied errors", + install_error_events: "package-install failures", + install_success_events: "successful package installs after package work", + restart_after_error_events: "restart/reload actions after error clusters", + env_var_error_events: "missing auth/config/environment-variable events", + yaml_error_events: "YAML/config parse incidents", + docker_conflict_events: "Docker/container-name conflicts", + frontend_activity_events: "frontend/CSS/SVG/React activity mentions", + css_activity_events: "CSS, styling, Tailwind, or className activity", + git_events: "git workflow commands", + tiny_patch_after_errors_events: "tiny typo-style fixes after error clusters", + skill_events: "Hermes skill mentions or tool use", + skill_manage_events: "skill_manage create/patch/delete operations", + memory_events: "memory or Mnemosyne tool events", + memory_write_events: "durable memory writes", + context_events: "context, compression, token, or cache-pressure mentions", + gateway_events: "gateway/API/chat-platform activity", + plugin_events: "dashboard plugin development or usage signals", + rollback_events: "rollback/checkpoint recovery mentions", + docs_activity_events: "documentation/README/docs activity", + model_events: "model/provider-related activity", + openrouter_events: "OpenRouter mentions", + codex_events: "Codex mentions", + cache_events: "prompt-cache/cache-hit mentions", + total_web_calls: "lifetime web_search/web_extract calls", + total_web_extract_calls: "lifetime web_extract calls", + browser_calls: "lifetime browser automation calls", + total_tool_calls: "lifetime Hermes tool calls", + total_terminal_calls: "lifetime terminal calls", + total_patch_calls: "lifetime targeted patch edits", + total_file_reads_searches: "lifetime read_file/search_files calls", + image_vision_calls: "image generation or vision tool calls", + tts_calls: "text-to-speech or voice tool calls", + distinct_model_count: "distinct model names seen in session metadata", + distinct_provider_count: "distinct model providers inferred from session metadata", + claude_events: "Claude/Anthropic model mentions", + gemini_events: "Gemini/Google model mentions", + local_model_events: "local/open-weight model mentions", + local_model_chat_sessions: "Hermes sessions whose model metadata is local/open-weight", + toolset_events: "toolset or tool-family mentions", + config_events: "configuration/environment/manifest activity", + git_history_events: "git history operations such as rebase, merge, fetch, push, or tag", + test_events: "test/check/verification command mentions", + screenshot_events: "screenshot, Playwright, PNG, or vision-inspection activity", + release_events: "release, version, publish, or git tag events", + session_count: "Hermes sessions", + weekend_sessions: "sessions started on weekends", + night_sessions: "sessions started late night or before dawn", + }, + criteria: { + tiered: "Requirement: {metric}. Tier ladder: {ladder}.", + requirements: "Requirement: {requirements}.", + secret_hidden: "Secret: exact requirement hidden until Hermes sees the first matching signal. Keep using Hermes across debugging, tools, memory, skills, plugins, and model workflows to reveal it.", + }, + categories: { + "Agent Autonomy": "Agent Autonomy", + "Debugging Chaos": "Debugging Chaos", + "Vibe Coding": "Vibe Coding", + "Hermes Native": "Hermes Native", + "Research/Web": "Research/Web", + "Tool Mastery": "Tool Mastery", + "Model Lore": "Model Lore", + "Lifestyle": "Lifestyle", + }, hero: { kicker: "Agentic Gamerscore", title: "Hermes Achievements", diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index 6a888226adbf..28a08caf01fa 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -578,6 +578,21 @@ export interface Translations { // ── Achievements plugin (plugins/hermes-achievements) ── achievements: { + /** Optional achievement copy keyed by the stable ACHIEVEMENTS id. */ + catalog?: Record; + /** Optional labels keyed by METRIC_LABELS keys. */ + metrics?: Record; + /** Optional templates for locale-aware achievement criteria. */ + criteria?: { + tiered?: string; + requirements?: string; + secret_hidden?: string; + }; + /** Optional labels keyed by the canonical achievement category. */ + categories?: Record; hero: { kicker: string; title: string;