feat: add internationalization (i18n) to web dashboard — English + Chinese (#9453)

Add a lightweight i18n system to the web dashboard with English (default) and
Chinese language support. A language switcher with flag icons is placed in the
header bar, allowing users to toggle between languages. The choice persists
to localStorage.

Implementation:
- src/i18n/ — types, translation files (en.ts, zh.ts), React context + hook
- LanguageSwitcher component shows the *other* language's flag as the toggle
- I18nProvider wraps the app in main.tsx
- All 8 pages + OAuth components updated to use t() translation calls
- Zero new dependencies — pure React context + localStorage
This commit is contained in:
Teknium 2026-04-13 23:19:13 -07:00 committed by GitHub
parent 19199cd38d
commit a2ea237db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1715 additions and 977 deletions

275
web/src/i18n/en.ts Normal file
View file

@ -0,0 +1,275 @@
import type { Translations } from "./types";
export const en: Translations = {
common: {
save: "Save",
saving: "Saving...",
cancel: "Cancel",
close: "Close",
delete: "Delete",
refresh: "Refresh",
retry: "Retry",
search: "Search...",
loading: "Loading...",
create: "Create",
creating: "Creating...",
set: "Set",
replace: "Replace",
clear: "Clear",
live: "Live",
off: "Off",
enabled: "enabled",
disabled: "disabled",
active: "active",
inactive: "inactive",
unknown: "unknown",
untitled: "Untitled",
none: "None",
form: "Form",
noResults: "No results",
of: "of",
page: "Page",
msgs: "msgs",
tools: "tools",
match: "match",
other: "Other",
configured: "configured",
removed: "removed",
failedToToggle: "Failed to toggle",
failedToRemove: "Failed to remove",
failedToReveal: "Failed to reveal",
collapse: "Collapse",
expand: "Expand",
general: "General",
messaging: "Messaging",
},
app: {
brand: "Hermes Agent",
brandShort: "HA",
webUi: "Web UI",
footer: {
name: "Hermes Agent",
org: "Nous Research",
},
nav: {
status: "Status",
sessions: "Sessions",
analytics: "Analytics",
logs: "Logs",
cron: "Cron",
skills: "Skills",
config: "Config",
keys: "Keys",
},
},
status: {
agent: "Agent",
gateway: "Gateway",
activeSessions: "Active Sessions",
recentSessions: "Recent Sessions",
connectedPlatforms: "Connected Platforms",
running: "Running",
starting: "Starting",
failed: "Failed",
stopped: "Stopped",
connected: "Connected",
disconnected: "Disconnected",
error: "Error",
notRunning: "Not running",
startFailed: "Start failed",
pid: "PID",
noneRunning: "None",
gatewayFailedToStart: "Gateway failed to start",
lastUpdate: "Last update",
platformError: "error",
platformDisconnected: "disconnected",
},
sessions: {
title: "Sessions",
searchPlaceholder: "Search message content...",
noSessions: "No sessions yet",
noMatch: "No sessions match your search",
startConversation: "Start a conversation to see it here",
noMessages: "No messages",
untitledSession: "Untitled session",
deleteSession: "Delete session",
previousPage: "Previous page",
nextPage: "Next page",
roles: {
user: "User",
assistant: "Assistant",
system: "System",
tool: "Tool",
},
},
analytics: {
period: "Period:",
totalTokens: "Total Tokens",
totalSessions: "Total Sessions",
apiCalls: "API Calls",
dailyTokenUsage: "Daily Token Usage",
dailyBreakdown: "Daily Breakdown",
perModelBreakdown: "Per-Model Breakdown",
input: "Input",
output: "Output",
total: "Total",
noUsageData: "No usage data for this period",
startSession: "Start a session to see analytics here",
date: "Date",
model: "Model",
tokens: "Tokens",
perDayAvg: "/day avg",
acrossModels: "across {count} models",
inOut: "{input} in / {output} out",
},
logs: {
title: "Logs",
autoRefresh: "Auto-refresh",
file: "File",
level: "Level",
component: "Component",
lines: "Lines",
noLogLines: "No log lines found",
},
cron: {
newJob: "New Cron Job",
nameOptional: "Name (optional)",
namePlaceholder: "e.g. Daily summary",
prompt: "Prompt",
promptPlaceholder: "What should the agent do on each run?",
schedule: "Schedule (cron expression)",
schedulePlaceholder: "0 9 * * *",
deliverTo: "Deliver to",
scheduledJobs: "Scheduled Jobs",
noJobs: "No cron jobs configured. Create one above.",
last: "Last",
next: "Next",
pause: "Pause",
resume: "Resume",
triggerNow: "Trigger now",
delivery: {
local: "Local",
telegram: "Telegram",
discord: "Discord",
slack: "Slack",
email: "Email",
},
},
skills: {
title: "Skills",
searchPlaceholder: "Search skills and toolsets...",
enabledOf: "{enabled}/{total} enabled",
all: "All",
noSkills: "No skills found. Skills are loaded from ~/.hermes/skills/",
noSkillsMatch: "No skills match your search or filter.",
skillCount: "{count} skill{s}",
noDescription: "No description available.",
toolsets: "Toolsets",
noToolsetsMatch: "No toolsets match the search.",
setupNeeded: "Setup needed",
disabledForCli: "Disabled for CLI",
more: "+{count} more",
},
config: {
configPath: "~/.hermes/config.yaml",
exportConfig: "Export config as JSON",
importConfig: "Import config from JSON",
resetDefaults: "Reset to defaults",
rawYaml: "Raw YAML Configuration",
searchResults: "Search Results",
fields: "field{s}",
noFieldsMatch: 'No fields match "{query}"',
configSaved: "Configuration saved",
yamlConfigSaved: "YAML config saved",
failedToSave: "Failed to save",
failedToSaveYaml: "Failed to save YAML",
failedToLoadRaw: "Failed to load raw config",
configImported: "Config imported — review and save",
invalidJson: "Invalid JSON file",
categories: {
general: "General",
agent: "Agent",
terminal: "Terminal",
display: "Display",
delegation: "Delegation",
memory: "Memory",
compression: "Compression",
security: "Security",
browser: "Browser",
voice: "Voice",
tts: "Text-to-Speech",
stt: "Speech-to-Text",
logging: "Logging",
discord: "Discord",
auxiliary: "Auxiliary",
},
},
env: {
description: "Manage API keys and secrets stored in",
changesNote: "Changes are saved to disk immediately. Active sessions pick up new keys automatically.",
hideAdvanced: "Hide Advanced",
showAdvanced: "Show Advanced",
llmProviders: "LLM Providers",
providersConfigured: "{configured} of {total} providers configured",
getKey: "Get key",
notConfigured: "{count} not configured",
notSet: "Not set",
keysCount: "{count} key{s}",
enterValue: "Enter value...",
replaceCurrentValue: "Replace current value ({preview})",
showValue: "Show real value",
hideValue: "Hide value",
},
oauth: {
title: "Provider Logins (OAuth)",
providerLogins: "Provider Logins (OAuth)",
description: "{connected} of {total} OAuth providers connected. Login flows currently run via the CLI; click Copy command and paste into a terminal to set up.",
connected: "Connected",
expired: "Expired",
notConnected: "Not connected. Run {command} in a terminal.",
runInTerminal: "in a terminal.",
noProviders: "No OAuth-capable providers detected.",
login: "Login",
disconnect: "Disconnect",
managedExternally: "Managed externally",
copied: "Copied ✓",
cli: "CLI",
copyCliCommand: "Copy CLI command (for external / fallback)",
connect: "Connect",
sessionExpires: "Session expires in {time}",
initiatingLogin: "Initiating login flow…",
exchangingCode: "Exchanging code for tokens…",
connectedClosing: "Connected! Closing…",
loginFailed: "Login failed.",
sessionExpired: "Session expired. Click Retry to start a new login.",
reOpenAuth: "Re-open auth page",
reOpenVerification: "Re-open verification page",
submitCode: "Submit code",
pasteCode: "Paste authorization code (with #state suffix is fine)",
waitingAuth: "Waiting for you to authorize in the browser…",
enterCodePrompt: "A new tab opened. Enter this code if prompted:",
pkceStep1: "A new tab opened to claude.ai. Sign in and click Authorize.",
pkceStep2: "Copy the authorization code shown after authorizing.",
pkceStep3: "Paste it below and submit.",
flowLabels: {
pkce: "Browser login (PKCE)",
device_code: "Device code",
external: "External CLI",
},
expiresIn: "expires in {time}",
},
language: {
switchTo: "Switch to Chinese",
},
};