From d061ae4201811065a780bd2700ef480f8476b389 Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 15:38:30 -0400
Subject: [PATCH 1/2] ci(release): pin GNU Linux release runners back to Ubuntu
22.04 (#3211)
---
.github/workflows/pub-release.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pub-release.yml b/.github/workflows/pub-release.yml
index 3798d6253..d0129cfb8 100644
--- a/.github/workflows/pub-release.yml
+++ b/.github/workflows/pub-release.yml
@@ -202,7 +202,7 @@ jobs:
include:
# Keep GNU Linux release artifacts on Ubuntu 22.04 to preserve
# a broadly compatible GLIBC baseline for user distributions.
- - os: ubuntu-22.04
+ - os: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2204]
target: x86_64-unknown-linux-gnu
artifact: zeroclaw
archive_ext: tar.gz
@@ -217,7 +217,7 @@ jobs:
linker_env: ""
linker: ""
use_cross: true
- - os: ubuntu-22.04
+ - os: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2204]
target: aarch64-unknown-linux-gnu
artifact: zeroclaw
archive_ext: tar.gz
@@ -232,7 +232,7 @@ jobs:
linker_env: ""
linker: ""
use_cross: true
- - os: ubuntu-22.04
+ - os: [self-hosted, Linux, X64, blacksmith-2vcpu-ubuntu-2204]
target: armv7-unknown-linux-gnueabihf
artifact: zeroclaw
archive_ext: tar.gz
From e3c39f64db4e29ff5a9f64d09b2af8781bce19ad Mon Sep 17 00:00:00 2001
From: Argenis
Date: Wed, 11 Mar 2026 15:39:08 -0400
Subject: [PATCH 2/2] feat(web): localize gateway pages for i18n (#3210)
---
web/src/App.tsx | 49 +++-
web/src/lib/i18n.ts | 450 +++++++++++++++++++++++++++++++++
web/src/pages/AgentChat.tsx | 34 ++-
web/src/pages/Config.tsx | 18 +-
web/src/pages/Cost.tsx | 43 ++--
web/src/pages/Cron.tsx | 89 +++----
web/src/pages/Dashboard.tsx | 49 ++--
web/src/pages/Doctor.tsx | 48 ++--
web/src/pages/Integrations.tsx | 151 ++++++-----
web/src/pages/Logs.tsx | 25 +-
web/src/pages/Memory.tsx | 81 +++---
web/src/pages/Tools.tsx | 21 +-
12 files changed, 795 insertions(+), 263 deletions(-)
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 9f4b77fcc..594f23a31 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -12,7 +12,7 @@ import Cost from './pages/Cost';
import Logs from './pages/Logs';
import Doctor from './pages/Doctor';
import { AuthProvider, useAuth } from './hooks/useAuth';
-import { setLocale, type Locale } from './lib/i18n';
+import { setLocale, t, useLocale, type Locale } from './lib/i18n';
// Locale context
interface LocaleContextType {
@@ -21,12 +21,25 @@ interface LocaleContextType {
}
export const LocaleContext = createContext({
- locale: 'tr',
+ locale: 'en',
setAppLocale: (_locale: Locale) => {},
});
export const useLocaleContext = () => useContext(LocaleContext);
+const LOCALE_STORAGE_KEY = 'zeroclaw.web.locale';
+
+function readStoredLocale(): Locale | null {
+ if (typeof window === 'undefined') {
+ return null;
+ }
+
+ const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
+ return stored === 'en' || stored === 'tr' || stored === 'zh-CN'
+ ? stored
+ : null;
+}
+
// Pairing dialog component
function PairingDialog({ onPair }: { onPair: (code: string) => Promise }) {
const [code, setCode] = useState('');
@@ -40,7 +53,7 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise })
try {
await onPair(code);
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Pairing failed');
+ setError(err instanceof Error ? err.message : t('auth.pairing_failed'));
} finally {
setLoading(false);
}
@@ -51,14 +64,14 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise })
ZeroClaw
-
Enter the pairing code from your terminal
+
{t('auth.enter_code_terminal')}
@@ -81,9 +94,29 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise })
function AppContent() {
const { isAuthenticated, loading, pair, logout } = useAuth();
- const [locale, setLocaleState] = useState('tr');
+ const { locale: detectedLocale } = useLocale();
+ const [locale, setLocaleState] = useState(() => readStoredLocale() ?? detectedLocale);
+ const [hasStoredLocale, setHasStoredLocale] = useState(() => readStoredLocale() !== null);
+
+ useEffect(() => {
+ if (hasStoredLocale) {
+ const storedLocale = readStoredLocale();
+ if (storedLocale) {
+ setLocaleState(storedLocale);
+ setLocale(storedLocale);
+ }
+ return;
+ }
+
+ setLocaleState(detectedLocale);
+ setLocale(detectedLocale);
+ }, [detectedLocale, hasStoredLocale]);
const setAppLocale = (newLocale: Locale) => {
+ if (typeof window !== 'undefined') {
+ window.localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
+ }
+ setHasStoredLocale(true);
setLocaleState(newLocale);
setLocale(newLocale);
};
@@ -100,7 +133,7 @@ function AppContent() {
if (loading) {
return (
-
Connecting...
+
{t('agent.connecting')}
);
}
diff --git a/web/src/lib/i18n.ts b/web/src/lib/i18n.ts
index 0ebb95ecc..69cfe5d50 100644
--- a/web/src/lib/i18n.ts
+++ b/web/src/lib/i18n.ts
@@ -37,6 +37,15 @@ const translations: Record> = {
'dashboard.overview': 'Overview',
'dashboard.system_info': 'System Information',
'dashboard.quick_actions': 'Quick Actions',
+ 'dashboard.provider_model': 'Provider / Model',
+ 'dashboard.since_restart': 'Since last restart',
+ 'dashboard.cost_overview': 'Cost Overview',
+ 'dashboard.active_channels': 'Active Channels',
+ 'dashboard.no_channels': 'No channels configured',
+ 'dashboard.component_health': 'Component Health',
+ 'dashboard.no_components': 'No components reporting',
+ 'dashboard.restarts': 'Restarts: {count}',
+ 'dashboard.load_error': 'Failed to load dashboard: {error}',
// Agent / Chat
'agent.title': 'Agent Chat',
@@ -49,6 +58,17 @@ const translations: Record> = {
'agent.thinking': 'Thinking...',
'agent.tool_call': 'Tool Call',
'agent.tool_result': 'Tool Result',
+ 'agent.empty_title': 'ZeroClaw Agent',
+ 'agent.empty_subtitle': 'Send a message to start the conversation',
+ 'agent.connection_error': 'Connection error. Attempting to reconnect...',
+ 'agent.send_failed': 'Failed to send message. Please try again.',
+ 'agent.done_without_text': 'Tool execution completed, but no final response text was returned.',
+ 'agent.tool_call_message': '[Tool Call] {name}({args})',
+ 'agent.tool_result_message': '[Tool Result] {output}',
+ 'agent.error_message': '[Error] {message}',
+ 'agent.typing': 'Typing...',
+ 'agent.unknown_tool': 'unknown',
+ 'agent.unknown_error': 'Unknown error',
// Tools
'tools.title': 'Available Tools',
@@ -58,6 +78,11 @@ const translations: Record> = {
'tools.search': 'Search tools...',
'tools.empty': 'No tools available.',
'tools.count': 'Total tools',
+ 'tools.agent_tools': 'Agent Tools',
+ 'tools.cli_tools': 'CLI Tools',
+ 'tools.parameter_schema': 'Parameter Schema',
+ 'tools.no_match': 'No tools match your search.',
+ 'tools.load_error': 'Failed to load tools: {error}',
// Cron
'cron.title': 'Scheduled Jobs',
@@ -74,6 +99,18 @@ const translations: Record> = {
'cron.enabled': 'Enabled',
'cron.empty': 'No scheduled jobs.',
'cron.confirm_delete': 'Are you sure you want to delete this job?',
+ 'cron.load_error': 'Failed to load cron jobs: {error}',
+ 'cron.required_fields': 'Schedule and command are required.',
+ 'cron.add_error': 'Failed to add job',
+ 'cron.delete_error': 'Failed to delete job',
+ 'cron.modal_title': 'Add Cron Job',
+ 'cron.name_optional': 'Name (optional)',
+ 'cron.name_placeholder': 'e.g. Daily cleanup',
+ 'cron.schedule_placeholder': 'e.g. 0 0 * * * (cron expression)',
+ 'cron.command_placeholder': 'e.g. cleanup --older-than 7d',
+ 'cron.adding': 'Adding...',
+ 'cron.empty_configured': 'No scheduled tasks configured.',
+ 'cron.delete_prompt': 'Delete?',
// Integrations
'integrations.title': 'Integrations',
@@ -86,6 +123,58 @@ const translations: Record> = {
'integrations.empty': 'No integrations found.',
'integrations.activate': 'Activate',
'integrations.deactivate': 'Deactivate',
+ 'integrations.load_error': 'Failed to load integrations: {error}',
+ 'integrations.current_provider': 'current provider',
+ 'integrations.custom_model_hint_scoped': 'Format: anthropic/claude-sonnet-4-6',
+ 'integrations.custom_model_hint_generic': 'Format: claude-sonnet-4-6 (or provider/model when required)',
+ 'integrations.field_required': '{field} is required.',
+ 'integrations.custom_value_required':
+ 'Enter a custom value for {field} or choose a recommended model.',
+ 'integrations.no_changes': 'No changes to save.',
+ 'integrations.confirm_switch_provider':
+ 'Switch default AI provider from {current} to {target}?',
+ 'integrations.confirm_switch_provider_with_model':
+ 'Switch default AI provider from {current} to {target} and set model to {model}?',
+ 'integrations.credentials_saved': '{name} credentials saved.',
+ 'integrations.save_error': 'Failed to save credentials',
+ 'integrations.stale_save':
+ 'Configuration changed elsewhere. Refreshed latest settings; re-enter values and save again.',
+ 'integrations.model_updated': 'Model updated to {model} for {name}.',
+ 'integrations.update_model_error': 'Failed to update model',
+ 'integrations.stale_model':
+ 'Configuration changed elsewhere. Refreshed latest settings; choose the model again.',
+ 'integrations.default_summary': 'default: {model}',
+ 'integrations.default_only': 'default',
+ 'integrations.default_badge': 'Default',
+ 'integrations.configured_badge': 'Configured',
+ 'integrations.current_model': 'Current model',
+ 'integrations.quick_model_help': 'For custom model IDs, use Edit Keys.',
+ 'integrations.default_provider_configured': 'Default provider configured',
+ 'integrations.provider_configured': 'Provider configured',
+ 'integrations.credentials_configured': 'Credentials configured',
+ 'integrations.credentials_not_configured': 'Credentials not configured',
+ 'integrations.edit_keys': 'Edit Keys',
+ 'integrations.configure': 'Configure',
+ 'integrations.configure_title': 'Configure {name}',
+ 'integrations.configure_intro_update': 'Enter only fields you want to update.',
+ 'integrations.configure_intro_new': 'Enter required fields to configure this integration.',
+ 'integrations.default_provider_notice_prefix':
+ 'Saving here updates credentials and switches your default AI provider to',
+ 'integrations.default_provider_notice_suffix':
+ 'For advanced provider settings, use',
+ 'integrations.keep_current_model': 'Keep current model',
+ 'integrations.keep_current_model_with_value': 'Keep current model ({model})',
+ 'integrations.select_recommended_model': 'Select a recommended model',
+ 'integrations.custom_model': 'Custom model...',
+ 'integrations.clear_current_model': 'Clear current model',
+ 'integrations.pick_model_help':
+ 'Pick a recommended model or choose Custom model. {hint}.',
+ 'integrations.current_value': 'Current value:',
+ 'integrations.replace_current_placeholder': 'Enter a new value to replace current',
+ 'integrations.enter_value_placeholder': 'Enter value',
+ 'integrations.keep_current_placeholder': 'Type new value, or leave empty to keep current',
+ 'integrations.save_activate': 'Save & Activate',
+ 'integrations.save_keys': 'Save Keys',
// Memory
'memory.title': 'Memory Store',
@@ -101,6 +190,16 @@ const translations: Record> = {
'memory.empty': 'No memory entries found.',
'memory.confirm_delete': 'Are you sure you want to delete this memory entry?',
'memory.all_categories': 'All Categories',
+ 'memory.load_error': 'Failed to load memory: {error}',
+ 'memory.required_fields': 'Key and content are required.',
+ 'memory.store_error': 'Failed to store memory',
+ 'memory.delete_error': 'Failed to delete memory',
+ 'memory.add_button': 'Add Memory',
+ 'memory.key_placeholder': 'e.g. user_preferences',
+ 'memory.content_placeholder': 'Memory content...',
+ 'memory.category_optional': 'Category (optional)',
+ 'memory.category_placeholder': 'e.g. preferences, context, facts',
+ 'memory.delete_prompt': 'Delete?',
// Config
'config.title': 'Configuration',
@@ -110,6 +209,10 @@ const translations: Record> = {
'config.error': 'Failed to save configuration.',
'config.loading': 'Loading configuration...',
'config.editor_placeholder': 'TOML configuration...',
+ 'config.sensitive_title': 'Sensitive fields are masked',
+ 'config.sensitive_body':
+ 'API keys, tokens, and passwords are hidden for security. To update a masked field, replace the entire masked value with your new value.',
+ 'config.editor_title': 'TOML Configuration',
// Cost
'cost.title': 'Cost Tracker',
@@ -123,6 +226,14 @@ const translations: Record> = {
'cost.tokens': 'Tokens',
'cost.requests': 'Requests',
'cost.usd': 'Cost (USD)',
+ 'cost.load_error': 'Failed to load cost data: {error}',
+ 'cost.total_requests': 'Total Requests',
+ 'cost.token_statistics': 'Token Statistics',
+ 'cost.average_tokens_per_request': 'Avg Tokens / Request',
+ 'cost.cost_per_1k_tokens': 'Cost per 1K Tokens',
+ 'cost.model_breakdown': 'Model Breakdown',
+ 'cost.no_models': 'No model data available.',
+ 'cost.share': 'Share',
// Logs
'logs.title': 'Live Logs',
@@ -133,6 +244,11 @@ const translations: Record> = {
'logs.empty': 'No log entries.',
'logs.connected': 'Connected to event stream.',
'logs.disconnected': 'Disconnected from event stream.',
+ 'logs.jump_to_bottom': 'Jump to bottom',
+ 'logs.events': 'events',
+ 'logs.filter_label': 'Filter:',
+ 'logs.paused_empty': 'Log streaming is paused.',
+ 'logs.waiting_empty': 'Waiting for events...',
// Doctor
'doctor.title': 'System Diagnostics',
@@ -146,6 +262,13 @@ const translations: Record> = {
'doctor.message': 'Message',
'doctor.empty': 'No diagnostics have been run yet.',
'doctor.summary': 'Diagnostic Summary',
+ 'doctor.run_error': 'Failed to run diagnostics',
+ 'doctor.running_short': 'Running...',
+ 'doctor.may_take_seconds': 'This may take a few seconds.',
+ 'doctor.issues_found': 'Issues Found',
+ 'doctor.warnings': 'Warnings',
+ 'doctor.all_clear': 'All Clear',
+ 'doctor.empty_help': 'Click "Run Diagnostics" to check your ZeroClaw installation.',
// Auth / Pairing
'auth.pair': 'Pair Device',
@@ -155,6 +278,9 @@ const translations: Record> = {
'auth.pairing_success': 'Pairing successful!',
'auth.pairing_failed': 'Pairing failed. Please try again.',
'auth.enter_code': 'Enter your pairing code to connect to the agent.',
+ 'auth.enter_code_terminal': 'Enter the pairing code from your terminal',
+ 'auth.code_placeholder': '6-digit code',
+ 'auth.pairing_progress': 'Pairing...',
// Common
'common.loading': 'Loading...',
@@ -178,6 +304,27 @@ const translations: Record> = {
'common.status': 'Status',
'common.created': 'Created',
'common.updated': 'Updated',
+ 'common.saving': 'Saving...',
+ 'common.adding': 'Adding...',
+ 'common.enabled': 'Enabled',
+ 'common.disabled': 'Disabled',
+ 'common.active': 'Active',
+ 'common.inactive': 'Inactive',
+ 'common.optional': 'optional',
+ 'common.id': 'ID',
+ 'common.path': 'Path',
+ 'common.version': 'Version',
+ 'common.all': 'All',
+ 'common.unknown': 'Unknown',
+ 'common.current': 'Current',
+ 'common.current_value': 'Current value',
+ 'common.apply': 'Apply',
+ 'common.configured': 'Configured',
+ 'common.configure': 'Configure',
+ 'common.default': 'Default',
+ 'common.events': 'events',
+ 'common.lines': 'lines',
+ 'common.select': 'Select',
// Health
'health.title': 'System Health',
@@ -220,6 +367,15 @@ const translations: Record> = {
'dashboard.overview': 'Genel Bakis',
'dashboard.system_info': 'Sistem Bilgisi',
'dashboard.quick_actions': 'Hizli Islemler',
+ 'dashboard.provider_model': 'Saglayici / Model',
+ 'dashboard.since_restart': 'Son yeniden baslatmadan beri',
+ 'dashboard.cost_overview': 'Maliyet Ozeti',
+ 'dashboard.active_channels': 'Aktif Kanallar',
+ 'dashboard.no_channels': 'Yapilandirilmis kanal yok',
+ 'dashboard.component_health': 'Bilesen Sagligi',
+ 'dashboard.no_components': 'Raporlayan bilesen yok',
+ 'dashboard.restarts': 'Yeniden baslatmalar: {count}',
+ 'dashboard.load_error': 'Kontrol paneli yuklenemedi: {error}',
// Agent / Chat
'agent.title': 'Ajan Sohbet',
@@ -232,6 +388,17 @@ const translations: Record> = {
'agent.thinking': 'Dusunuyor...',
'agent.tool_call': 'Arac Cagrisi',
'agent.tool_result': 'Arac Sonucu',
+ 'agent.empty_title': 'ZeroClaw Ajan',
+ 'agent.empty_subtitle': 'Sohbeti baslatmak icin bir mesaj gonderin',
+ 'agent.connection_error': 'Baglanti hatasi. Yeniden baglanmayi deniyor...',
+ 'agent.send_failed': 'Mesaj gonderilemedi. Lutfen tekrar deneyin.',
+ 'agent.done_without_text': 'Arac calismasi tamamlandi ancak son yanit metni dondurulmedi.',
+ 'agent.tool_call_message': '[Arac Cagrisi] {name}({args})',
+ 'agent.tool_result_message': '[Arac Sonucu] {output}',
+ 'agent.error_message': '[Hata] {message}',
+ 'agent.typing': 'Yaziyor...',
+ 'agent.unknown_tool': 'bilinmeyen',
+ 'agent.unknown_error': 'Bilinmeyen hata',
// Tools
'tools.title': 'Mevcut Araclar',
@@ -241,6 +408,11 @@ const translations: Record> = {
'tools.search': 'Arac ara...',
'tools.empty': 'Mevcut arac yok.',
'tools.count': 'Toplam arac',
+ 'tools.agent_tools': 'Ajan Araclari',
+ 'tools.cli_tools': 'CLI Araclari',
+ 'tools.parameter_schema': 'Parametre Semasi',
+ 'tools.no_match': 'Aramanizla eslesen arac yok.',
+ 'tools.load_error': 'Araclar yuklenemedi: {error}',
// Cron
'cron.title': 'Zamanlanmis Gorevler',
@@ -257,6 +429,18 @@ const translations: Record> = {
'cron.enabled': 'Etkin',
'cron.empty': 'Zamanlanmis gorev yok.',
'cron.confirm_delete': 'Bu gorevi silmek istediginizden emin misiniz?',
+ 'cron.load_error': 'Zamanlanmis gorevler yuklenemedi: {error}',
+ 'cron.required_fields': 'Zamanlama ve komut gereklidir.',
+ 'cron.add_error': 'Gorev eklenemedi',
+ 'cron.delete_error': 'Gorev silinemedi',
+ 'cron.modal_title': 'Cron Gorevi Ekle',
+ 'cron.name_optional': 'Ad (istege bagli)',
+ 'cron.name_placeholder': 'ornegin Gunluk temizleme',
+ 'cron.schedule_placeholder': 'ornegin 0 0 * * * (cron ifadesi)',
+ 'cron.command_placeholder': 'ornegin cleanup --older-than 7d',
+ 'cron.adding': 'Ekleniyor...',
+ 'cron.empty_configured': 'Yapilandirilmis zamanlanmis gorev yok.',
+ 'cron.delete_prompt': 'Silinsin mi?',
// Integrations
'integrations.title': 'Entegrasyonlar',
@@ -269,6 +453,60 @@ const translations: Record> = {
'integrations.empty': 'Entegrasyon bulunamadi.',
'integrations.activate': 'Etkinlestir',
'integrations.deactivate': 'Devre Disi Birak',
+ 'integrations.load_error': 'Entegrasyonlar yuklenemedi: {error}',
+ 'integrations.current_provider': 'mevcut saglayici',
+ 'integrations.custom_model_hint_scoped': 'Format: anthropic/claude-sonnet-4-6',
+ 'integrations.custom_model_hint_generic':
+ 'Format: claude-sonnet-4-6 (gerektiginde provider/model)',
+ 'integrations.field_required': '{field} gereklidir.',
+ 'integrations.custom_value_required':
+ '{field} icin ozel bir deger girin veya onerilen bir model secin.',
+ 'integrations.no_changes': 'Kaydedilecek degisiklik yok.',
+ 'integrations.confirm_switch_provider':
+ 'Varsayilan AI saglayicisini {current} konumundan {target} konumuna gecirmek istiyor musunuz?',
+ 'integrations.confirm_switch_provider_with_model':
+ 'Varsayilan AI saglayicisini {current} konumundan {target} konumuna gecirip modeli {model} olarak ayarlamak istiyor musunuz?',
+ 'integrations.credentials_saved': '{name} kimlik bilgileri kaydedildi.',
+ 'integrations.save_error': 'Kimlik bilgileri kaydedilemedi',
+ 'integrations.stale_save':
+ 'Yapilandirma baska yerde degisti. En guncel ayarlar yenilendi; degerleri yeniden girip tekrar kaydedin.',
+ 'integrations.model_updated': '{name} icin model {model} olarak guncellendi.',
+ 'integrations.update_model_error': 'Model guncellenemedi',
+ 'integrations.stale_model':
+ 'Yapilandirma baska yerde degisti. En guncel ayarlar yenilendi; modeli yeniden secin.',
+ 'integrations.default_summary': 'varsayilan: {model}',
+ 'integrations.default_only': 'varsayilan',
+ 'integrations.default_badge': 'Varsayilan',
+ 'integrations.configured_badge': 'Yapilandirildi',
+ 'integrations.current_model': 'Guncel model',
+ 'integrations.quick_model_help': 'Ozel model kimlikleri icin Anahtarlari Duzenle secenegini kullanin.',
+ 'integrations.default_provider_configured': 'Varsayilan saglayici yapilandirildi',
+ 'integrations.provider_configured': 'Saglayici yapilandirildi',
+ 'integrations.credentials_configured': 'Kimlik bilgileri yapilandirildi',
+ 'integrations.credentials_not_configured': 'Kimlik bilgileri yapilandirilmadi',
+ 'integrations.edit_keys': 'Anahtarlari Duzenle',
+ 'integrations.configure': 'Yapilandir',
+ 'integrations.configure_title': '{name} Yapilandir',
+ 'integrations.configure_intro_update': 'Yalnizca guncellemek istediginiz alanlari girin.',
+ 'integrations.configure_intro_new': 'Bu entegrasyonu yapilandirmak icin gerekli alanlari girin.',
+ 'integrations.default_provider_notice_prefix':
+ 'Burada kaydetmek kimlik bilgilerini gunceller ve varsayilan AI saglayicinizi su olarak degistirir:',
+ 'integrations.default_provider_notice_suffix':
+ 'Gelismis saglayici ayarlari icin sunu kullanin:',
+ 'integrations.keep_current_model': 'Guncel modeli koru',
+ 'integrations.keep_current_model_with_value': 'Guncel modeli koru ({model})',
+ 'integrations.select_recommended_model': 'Onerilen bir model secin',
+ 'integrations.custom_model': 'Ozel model...',
+ 'integrations.clear_current_model': 'Guncel modeli temizle',
+ 'integrations.pick_model_help':
+ 'Onerilen bir model secin veya Ozel model secenegini kullanin. {hint}.',
+ 'integrations.current_value': 'Guncel deger:',
+ 'integrations.replace_current_placeholder': 'Gunceli degistirmek icin yeni bir deger girin',
+ 'integrations.enter_value_placeholder': 'Deger girin',
+ 'integrations.keep_current_placeholder':
+ 'Yeni deger yazin veya gunceli korumak icin bos birakin',
+ 'integrations.save_activate': 'Kaydet ve Etkinlestir',
+ 'integrations.save_keys': 'Anahtarlari Kaydet',
// Memory
'memory.title': 'Hafiza Deposu',
@@ -284,6 +522,16 @@ const translations: Record> = {
'memory.empty': 'Hafiza kaydi bulunamadi.',
'memory.confirm_delete': 'Bu hafiza kaydini silmek istediginizden emin misiniz?',
'memory.all_categories': 'Tum Kategoriler',
+ 'memory.load_error': 'Hafiza yuklenemedi: {error}',
+ 'memory.required_fields': 'Anahtar ve icerik gereklidir.',
+ 'memory.store_error': 'Hafiza kaydedilemedi',
+ 'memory.delete_error': 'Hafiza silinemedi',
+ 'memory.add_button': 'Hafiza Ekle',
+ 'memory.key_placeholder': 'ornegin user_preferences',
+ 'memory.content_placeholder': 'Hafiza icerigi...',
+ 'memory.category_optional': 'Kategori (istege bagli)',
+ 'memory.category_placeholder': 'ornegin preferences, context, facts',
+ 'memory.delete_prompt': 'Silinsin mi?',
// Config
'config.title': 'Yapilandirma',
@@ -293,6 +541,10 @@ const translations: Record> = {
'config.error': 'Yapilandirma kaydedilemedi.',
'config.loading': 'Yapilandirma yukleniyor...',
'config.editor_placeholder': 'TOML yapilandirmasi...',
+ 'config.sensitive_title': 'Hassas alanlar maskelenir',
+ 'config.sensitive_body':
+ 'Guvenlik icin API anahtarlari, tokenlar ve parolalar gizlenir. Maskeli bir alani guncellemek icin tum maskeli degeri yeni degerinizle degistirin.',
+ 'config.editor_title': 'TOML Yapilandirmasi',
// Cost
'cost.title': 'Maliyet Takibi',
@@ -306,6 +558,14 @@ const translations: Record> = {
'cost.tokens': 'Token',
'cost.requests': 'Istekler',
'cost.usd': 'Maliyet (USD)',
+ 'cost.load_error': 'Maliyet verileri yuklenemedi: {error}',
+ 'cost.total_requests': 'Toplam Istek',
+ 'cost.token_statistics': 'Token Istatistikleri',
+ 'cost.average_tokens_per_request': 'Istek Basina Ort. Token',
+ 'cost.cost_per_1k_tokens': '1K Token Basina Maliyet',
+ 'cost.model_breakdown': 'Model Dagilimi',
+ 'cost.no_models': 'Model verisi yok.',
+ 'cost.share': 'Pay',
// Logs
'logs.title': 'Canli Kayitlar',
@@ -316,6 +576,11 @@ const translations: Record> = {
'logs.empty': 'Kayit girisi yok.',
'logs.connected': 'Olay akisina baglandi.',
'logs.disconnected': 'Olay akisi baglantisi kesildi.',
+ 'logs.jump_to_bottom': 'En alta git',
+ 'logs.events': 'olay',
+ 'logs.filter_label': 'Filtre:',
+ 'logs.paused_empty': 'Kayit akisi duraklatildi.',
+ 'logs.waiting_empty': 'Olaylar bekleniyor...',
// Doctor
'doctor.title': 'Sistem Teshisleri',
@@ -329,6 +594,13 @@ const translations: Record> = {
'doctor.message': 'Mesaj',
'doctor.empty': 'Henuz teshis calistirilmadi.',
'doctor.summary': 'Teshis Ozeti',
+ 'doctor.run_error': 'Teshisler calistirilamadi',
+ 'doctor.running_short': 'Calisiyor...',
+ 'doctor.may_take_seconds': 'Bu islem birkac saniye surebilir.',
+ 'doctor.issues_found': 'Sorunlar Bulundu',
+ 'doctor.warnings': 'Uyarilar',
+ 'doctor.all_clear': 'Her Sey Yolunda',
+ 'doctor.empty_help': 'ZeroClaw kurulumunuzu kontrol etmek icin "Teshis Calistir" dugmesine tiklayin.',
// Auth / Pairing
'auth.pair': 'Cihaz Esle',
@@ -338,6 +610,9 @@ const translations: Record> = {
'auth.pairing_success': 'Eslestirme basarili!',
'auth.pairing_failed': 'Eslestirme basarisiz. Lutfen tekrar deneyin.',
'auth.enter_code': 'Ajana baglanmak icin eslestirme kodunuzu girin.',
+ 'auth.enter_code_terminal': 'Terminalinizdeki eslestirme kodunu girin',
+ 'auth.code_placeholder': '6 haneli kod',
+ 'auth.pairing_progress': 'Eslestiriliyor...',
// Common
'common.loading': 'Yukleniyor...',
@@ -361,6 +636,27 @@ const translations: Record> = {
'common.status': 'Durum',
'common.created': 'Olusturulma',
'common.updated': 'Guncellenme',
+ 'common.saving': 'Kaydediliyor...',
+ 'common.adding': 'Ekleniyor...',
+ 'common.enabled': 'Etkin',
+ 'common.disabled': 'Devre disi',
+ 'common.active': 'Aktif',
+ 'common.inactive': 'Pasif',
+ 'common.optional': 'istege bagli',
+ 'common.id': 'Kimlik',
+ 'common.path': 'Yol',
+ 'common.version': 'Surum',
+ 'common.all': 'Tum',
+ 'common.unknown': 'Bilinmeyen',
+ 'common.current': 'Guncel',
+ 'common.current_value': 'Guncel deger',
+ 'common.apply': 'Uygula',
+ 'common.configured': 'Yapilandirildi',
+ 'common.configure': 'Yapilandir',
+ 'common.default': 'Varsayilan',
+ 'common.events': 'olay',
+ 'common.lines': 'satir',
+ 'common.select': 'Sec',
// Health
'health.title': 'Sistem Sagligi',
@@ -403,6 +699,15 @@ const translations: Record> = {
'dashboard.overview': '总览',
'dashboard.system_info': '系统信息',
'dashboard.quick_actions': '快捷操作',
+ 'dashboard.provider_model': '提供商 / 模型',
+ 'dashboard.since_restart': '自上次重启以来',
+ 'dashboard.cost_overview': '成本总览',
+ 'dashboard.active_channels': '活跃渠道',
+ 'dashboard.no_channels': '未配置任何渠道',
+ 'dashboard.component_health': '组件健康状态',
+ 'dashboard.no_components': '没有组件上报状态',
+ 'dashboard.restarts': '重启次数:{count}',
+ 'dashboard.load_error': '加载仪表盘失败:{error}',
// Agent / Chat
'agent.title': '智能体聊天',
@@ -415,6 +720,17 @@ const translations: Record> = {
'agent.thinking': '思考中...',
'agent.tool_call': '工具调用',
'agent.tool_result': '工具结果',
+ 'agent.empty_title': 'ZeroClaw 智能体',
+ 'agent.empty_subtitle': '发送一条消息以开始对话',
+ 'agent.connection_error': '连接出错,正在尝试重新连接...',
+ 'agent.send_failed': '发送消息失败,请重试。',
+ 'agent.done_without_text': '工具执行已完成,但未返回最终响应文本。',
+ 'agent.tool_call_message': '[工具调用] {name}({args})',
+ 'agent.tool_result_message': '[工具结果] {output}',
+ 'agent.error_message': '[错误] {message}',
+ 'agent.typing': '输入中...',
+ 'agent.unknown_tool': '未知',
+ 'agent.unknown_error': '未知错误',
// Tools
'tools.title': '可用工具',
@@ -424,6 +740,11 @@ const translations: Record> = {
'tools.search': '搜索工具...',
'tools.empty': '暂无可用工具。',
'tools.count': '工具总数',
+ 'tools.agent_tools': '智能体工具',
+ 'tools.cli_tools': 'CLI 工具',
+ 'tools.parameter_schema': '参数结构',
+ 'tools.no_match': '没有工具符合你的搜索。',
+ 'tools.load_error': '加载工具失败:{error}',
// Cron
'cron.title': '定时任务',
@@ -440,6 +761,18 @@ const translations: Record> = {
'cron.enabled': '已启用',
'cron.empty': '暂无定时任务。',
'cron.confirm_delete': '确定要删除此任务吗?',
+ 'cron.load_error': '加载定时任务失败:{error}',
+ 'cron.required_fields': '计划和命令为必填项。',
+ 'cron.add_error': '添加任务失败',
+ 'cron.delete_error': '删除任务失败',
+ 'cron.modal_title': '添加 Cron 任务',
+ 'cron.name_optional': '名称(可选)',
+ 'cron.name_placeholder': '例如:每日清理',
+ 'cron.schedule_placeholder': '例如:0 0 * * *(cron 表达式)',
+ 'cron.command_placeholder': '例如:cleanup --older-than 7d',
+ 'cron.adding': '添加中...',
+ 'cron.empty_configured': '尚未配置定时任务。',
+ 'cron.delete_prompt': '删除?',
// Integrations
'integrations.title': '集成',
@@ -452,6 +785,55 @@ const translations: Record> = {
'integrations.empty': '未找到集成。',
'integrations.activate': '激活',
'integrations.deactivate': '停用',
+ 'integrations.load_error': '加载集成失败:{error}',
+ 'integrations.current_provider': '当前提供商',
+ 'integrations.custom_model_hint_scoped': '格式:anthropic/claude-sonnet-4-6',
+ 'integrations.custom_model_hint_generic':
+ '格式:claude-sonnet-4-6(需要时使用 provider/model)',
+ 'integrations.field_required': '{field} 为必填项。',
+ 'integrations.custom_value_required': '请为 {field} 输入自定义值,或选择推荐模型。',
+ 'integrations.no_changes': '没有可保存的更改。',
+ 'integrations.confirm_switch_provider': '要将默认 AI 提供商从 {current} 切换到 {target} 吗?',
+ 'integrations.confirm_switch_provider_with_model':
+ '要将默认 AI 提供商从 {current} 切换到 {target},并将模型设置为 {model} 吗?',
+ 'integrations.credentials_saved': '{name} 凭据已保存。',
+ 'integrations.save_error': '保存凭据失败',
+ 'integrations.stale_save':
+ '配置已在其他地方发生变化。已刷新最新设置;请重新输入值后再次保存。',
+ 'integrations.model_updated': '{name} 的模型已更新为 {model}。',
+ 'integrations.update_model_error': '更新模型失败',
+ 'integrations.stale_model':
+ '配置已在其他地方发生变化。已刷新最新设置;请重新选择模型。',
+ 'integrations.default_summary': '默认:{model}',
+ 'integrations.default_only': '默认',
+ 'integrations.default_badge': '默认',
+ 'integrations.configured_badge': '已配置',
+ 'integrations.current_model': '当前模型',
+ 'integrations.quick_model_help': '如需自定义模型 ID,请使用“编辑密钥”。',
+ 'integrations.default_provider_configured': '默认提供商已配置',
+ 'integrations.provider_configured': '提供商已配置',
+ 'integrations.credentials_configured': '凭据已配置',
+ 'integrations.credentials_not_configured': '凭据未配置',
+ 'integrations.edit_keys': '编辑密钥',
+ 'integrations.configure': '配置',
+ 'integrations.configure_title': '配置 {name}',
+ 'integrations.configure_intro_update': '只输入你想更新的字段。',
+ 'integrations.configure_intro_new': '输入必填字段以配置此集成。',
+ 'integrations.default_provider_notice_prefix':
+ '在此保存会更新凭据,并将你的默认 AI 提供商切换为',
+ 'integrations.default_provider_notice_suffix': '如需高级提供商设置,请使用',
+ 'integrations.keep_current_model': '保留当前模型',
+ 'integrations.keep_current_model_with_value': '保留当前模型({model})',
+ 'integrations.select_recommended_model': '选择推荐模型',
+ 'integrations.custom_model': '自定义模型...',
+ 'integrations.clear_current_model': '清除当前模型',
+ 'integrations.pick_model_help': '请选择推荐模型或使用自定义模型。{hint}。',
+ 'integrations.current_value': '当前值:',
+ 'integrations.replace_current_placeholder': '输入新值以替换当前值',
+ 'integrations.enter_value_placeholder': '输入值',
+ 'integrations.keep_current_placeholder': '输入新值,或留空以保留当前值',
+ 'integrations.save_activate': '保存并激活',
+ 'integrations.save_keys': '保存密钥',
// Memory
'memory.title': '记忆存储',
@@ -467,6 +849,16 @@ const translations: Record> = {
'memory.empty': '未找到记忆条目。',
'memory.confirm_delete': '确定要删除此记忆条目吗?',
'memory.all_categories': '全部分类',
+ 'memory.load_error': '加载记忆失败:{error}',
+ 'memory.required_fields': '键和内容为必填项。',
+ 'memory.store_error': '存储记忆失败',
+ 'memory.delete_error': '删除记忆失败',
+ 'memory.add_button': '添加记忆',
+ 'memory.key_placeholder': '例如:user_preferences',
+ 'memory.content_placeholder': '记忆内容...',
+ 'memory.category_optional': '分类(可选)',
+ 'memory.category_placeholder': '例如:preferences、context、facts',
+ 'memory.delete_prompt': '删除?',
// Config
'config.title': '配置',
@@ -476,6 +868,10 @@ const translations: Record> = {
'config.error': '配置保存失败。',
'config.loading': '配置加载中...',
'config.editor_placeholder': 'TOML 配置...',
+ 'config.sensitive_title': '敏感字段已隐藏',
+ 'config.sensitive_body':
+ '为了安全,API 密钥、令牌和密码会被隐藏。要更新已隐藏的字段,请用你的新值替换整个隐藏值。',
+ 'config.editor_title': 'TOML 配置',
// Cost
'cost.title': '成本追踪',
@@ -489,6 +885,14 @@ const translations: Record> = {
'cost.tokens': 'Token',
'cost.requests': '请求',
'cost.usd': '成本(USD)',
+ 'cost.load_error': '加载成本数据失败:{error}',
+ 'cost.total_requests': '总请求数',
+ 'cost.token_statistics': 'Token 统计',
+ 'cost.average_tokens_per_request': '每次请求平均 Token',
+ 'cost.cost_per_1k_tokens': '每 1K Token 成本',
+ 'cost.model_breakdown': '模型拆分',
+ 'cost.no_models': '暂无模型数据。',
+ 'cost.share': '占比',
// Logs
'logs.title': '实时日志',
@@ -499,6 +903,11 @@ const translations: Record> = {
'logs.empty': '暂无日志条目。',
'logs.connected': '已连接到事件流。',
'logs.disconnected': '与事件流断开连接。',
+ 'logs.jump_to_bottom': '跳到末尾',
+ 'logs.events': '事件',
+ 'logs.filter_label': '筛选:',
+ 'logs.paused_empty': '日志流已暂停。',
+ 'logs.waiting_empty': '正在等待事件...',
// Doctor
'doctor.title': '系统诊断',
@@ -512,6 +921,13 @@ const translations: Record> = {
'doctor.message': '消息',
'doctor.empty': '尚未运行诊断。',
'doctor.summary': '诊断摘要',
+ 'doctor.run_error': '运行诊断失败',
+ 'doctor.running_short': '运行中...',
+ 'doctor.may_take_seconds': '这可能需要几秒钟。',
+ 'doctor.issues_found': '发现问题',
+ 'doctor.warnings': '警告',
+ 'doctor.all_clear': '一切正常',
+ 'doctor.empty_help': '点击“运行诊断”以检查你的 ZeroClaw 安装。',
// Auth / Pairing
'auth.pair': '设备配对',
@@ -521,6 +937,9 @@ const translations: Record> = {
'auth.pairing_success': '配对成功!',
'auth.pairing_failed': '配对失败,请重试。',
'auth.enter_code': '输入配对码以连接到智能体。',
+ 'auth.enter_code_terminal': '输入终端中的配对码',
+ 'auth.code_placeholder': '6 位代码',
+ 'auth.pairing_progress': '配对中...',
// Common
'common.loading': '加载中...',
@@ -544,6 +963,27 @@ const translations: Record> = {
'common.status': '状态',
'common.created': '创建时间',
'common.updated': '更新时间',
+ 'common.saving': '保存中...',
+ 'common.adding': '添加中...',
+ 'common.enabled': '已启用',
+ 'common.disabled': '已禁用',
+ 'common.active': '激活',
+ 'common.inactive': '未激活',
+ 'common.optional': '可选',
+ 'common.id': 'ID',
+ 'common.path': '路径',
+ 'common.version': '版本',
+ 'common.all': '全部',
+ 'common.unknown': '未知',
+ 'common.current': '当前',
+ 'common.current_value': '当前值',
+ 'common.apply': '应用',
+ 'common.configured': '已配置',
+ 'common.configure': '配置',
+ 'common.default': '默认',
+ 'common.events': '事件',
+ 'common.lines': '行',
+ 'common.select': '选择',
// Health
'health.title': '系统健康',
@@ -584,6 +1024,16 @@ export function t(key: string): string {
return translations[currentLocale]?.[key] ?? translations.en[key] ?? key;
}
+export function tf(
+ key: string,
+ values: Record,
+): string {
+ return Object.entries(values).reduce((result, [name, value]) => {
+ const token = `{${name}}`;
+ return result.split(token).join(String(value));
+ }, t(key));
+}
+
/**
* Get the translation for a specific locale. Falls back to English, then to the
* raw key.
diff --git a/web/src/pages/AgentChat.tsx b/web/src/pages/AgentChat.tsx
index a926c34b5..0a275bca1 100644
--- a/web/src/pages/AgentChat.tsx
+++ b/web/src/pages/AgentChat.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import { Send, Bot, User, AlertCircle } from 'lucide-react';
import type { WsMessage } from '@/types/api';
import { WebSocketClient } from '@/lib/ws';
+import { getLocale, t, tf } from '@/lib/i18n';
interface ChatMessage {
id: string;
@@ -18,8 +19,6 @@ interface PersistedChatMessage {
}
let fallbackMessageIdCounter = 0;
-const EMPTY_DONE_FALLBACK =
- 'Tool execution completed, but no final response text was returned.';
const CHAT_HISTORY_STORAGE_KEY = 'zeroclaw.agent_chat.messages.v1';
const MAX_PERSISTED_MESSAGES = 500;
@@ -124,7 +123,7 @@ export default function AgentChat() {
};
ws.onError = () => {
- setError('Connection error. Attempting to reconnect...');
+ setError(t('agent.connection_error'));
};
ws.onMessage = (msg: WsMessage) => {
@@ -137,7 +136,7 @@ export default function AgentChat() {
case 'message':
case 'done': {
const content = (msg.full_response ?? msg.content ?? pendingContentRef.current ?? '').trim();
- const finalContent = content || EMPTY_DONE_FALLBACK;
+ const finalContent = content || t('agent.done_without_text');
setMessages((prev) => [
...prev,
@@ -160,7 +159,10 @@ export default function AgentChat() {
{
id: makeMessageId(),
role: 'agent',
- content: `[Tool Call] ${msg.name ?? 'unknown'}(${JSON.stringify(msg.args ?? {})})`,
+ content: tf('agent.tool_call_message', {
+ name: msg.name ?? t('agent.unknown_tool'),
+ args: JSON.stringify(msg.args ?? {}),
+ }),
timestamp: new Date(),
},
]);
@@ -172,7 +174,9 @@ export default function AgentChat() {
{
id: makeMessageId(),
role: 'agent',
- content: `[Tool Result] ${msg.output ?? ''}`,
+ content: tf('agent.tool_result_message', {
+ output: msg.output ?? '',
+ }),
timestamp: new Date(),
},
]);
@@ -184,7 +188,9 @@ export default function AgentChat() {
{
id: makeMessageId(),
role: 'agent',
- content: `[Error] ${msg.message ?? 'Unknown error'}`,
+ content: tf('agent.error_message', {
+ message: msg.message ?? t('agent.unknown_error'),
+ }),
timestamp: new Date(),
},
]);
@@ -229,7 +235,7 @@ export default function AgentChat() {
setTyping(true);
pendingContentRef.current = '';
} catch {
- setError('Failed to send message. Please try again.');
+ setError(t('agent.send_failed'));
}
setInput('');
@@ -258,8 +264,8 @@ export default function AgentChat() {
{messages.length === 0 && (
-
ZeroClaw Agent
-
Send a message to start the conversation
+
{t('agent.empty_title')}
+
{t('agent.empty_subtitle')}
)}
@@ -296,7 +302,7 @@ export default function AgentChat() {
msg.role === 'user' ? 'text-blue-200' : 'text-gray-500'
}`}
>
- {msg.timestamp.toLocaleTimeString()}
+ {msg.timestamp.toLocaleTimeString(getLocale())}
@@ -313,7 +319,7 @@ export default function AgentChat() {
- Typing...
+ {t('agent.typing')}
)}
@@ -331,7 +337,7 @@ export default function AgentChat() {
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
- placeholder={connected ? 'Type a message...' : 'Connecting...'}
+ placeholder={connected ? t('agent.placeholder') : t('agent.connecting')}
disabled={!connected}
className="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
/>
@@ -351,7 +357,7 @@ export default function AgentChat() {
}`}
/>
- {connected ? 'Connected' : 'Disconnected'}
+ {connected ? t('agent.connected') : t('agent.disconnected')}
diff --git a/web/src/pages/Config.tsx b/web/src/pages/Config.tsx
index 17a4868d2..eb89945c0 100644
--- a/web/src/pages/Config.tsx
+++ b/web/src/pages/Config.tsx
@@ -7,6 +7,7 @@ import {
ShieldAlert,
} from 'lucide-react';
import { getConfig, putConfig } from '@/lib/api';
+import { t } from '@/lib/i18n';
export default function Config() {
const [config, setConfig] = useState('');
@@ -31,9 +32,9 @@ export default function Config() {
setSuccess(null);
try {
await putConfig(config);
- setSuccess('Configuration saved successfully.');
+ setSuccess(t('config.saved'));
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Failed to save configuration');
+ setError(err instanceof Error ? err.message : t('config.error'));
} finally {
setSaving(false);
}
@@ -60,7 +61,7 @@ export default function Config() {
-
Configuration
+ {t('config.title')}
- {saving ? 'Saving...' : 'Save'}
+ {saving ? t('common.saving') : t('common.save')}
@@ -77,11 +78,10 @@ export default function Config() {
- Sensitive fields are masked
+ {t('config.sensitive_title')}
- API keys, tokens, and passwords are hidden for security. To update a
- masked field, replace the entire masked value with your new value.
+ {t('config.sensitive_body')}
@@ -106,10 +106,10 @@ export default function Config() {
- TOML Configuration
+ {t('config.editor_title')}
- {config.split('\n').length} lines
+ {config.split('\n').length} {t('common.lines')}
);
@@ -53,7 +54,7 @@ export default function Cost() {
- Session Cost
+ {t('cost.session')}
{formatUSD(cost.session_cost_usd)}
@@ -65,7 +66,7 @@ export default function Cost() {
- Daily Cost
+ {t('cost.daily')}
{formatUSD(cost.daily_cost_usd)}
@@ -77,7 +78,7 @@ export default function Cost() {
- Monthly Cost
+ {t('cost.monthly')}
{formatUSD(cost.monthly_cost_usd)}
@@ -89,10 +90,10 @@ export default function Cost() {
- Total Requests
+ {t('cost.total_requests')}
- {cost.request_count.toLocaleString()}
+ {cost.request_count.toLocaleString(getLocale())}
@@ -100,25 +101,25 @@ export default function Cost() {
{/* Token Statistics */}
- Token Statistics
+ {t('cost.token_statistics')}
-
Total Tokens
+
{t('cost.total_tokens')}
- {cost.total_tokens.toLocaleString()}
+ {cost.total_tokens.toLocaleString(getLocale())}
-
Avg Tokens / Request
+
{t('cost.average_tokens_per_request')}
{cost.request_count > 0
- ? Math.round(cost.total_tokens / cost.request_count).toLocaleString()
+ ? Math.round(cost.total_tokens / cost.request_count).toLocaleString(getLocale())
: '0'}
-
Cost per 1K Tokens
+
{t('cost.cost_per_1k_tokens')}
{cost.total_tokens > 0
? formatUSD((cost.monthly_cost_usd / cost.total_tokens) * 1000)
@@ -132,12 +133,12 @@ export default function Cost() {
- Model Breakdown
+ {t('cost.model_breakdown')}
{models.length === 0 ? (
- No model data available.
+ {t('cost.no_models')}
) : (
@@ -145,19 +146,19 @@ export default function Cost() {
- Model
+ {t('cost.model')}
- Cost
+ {t('cost.usd')}
- Tokens
+ {t('cost.tokens')}
- Requests
+ {t('cost.requests')}
- Share
+ {t('cost.share')}
@@ -181,10 +182,10 @@ export default function Cost() {
{formatUSD(m.cost_usd)}
- {m.total_tokens.toLocaleString()}
+ {m.total_tokens.toLocaleString(getLocale())}
- {m.request_count.toLocaleString()}
+ {m.request_count.toLocaleString(getLocale())}
diff --git a/web/src/pages/Cron.tsx b/web/src/pages/Cron.tsx
index ed4451472..cadd60d84 100644
--- a/web/src/pages/Cron.tsx
+++ b/web/src/pages/Cron.tsx
@@ -10,11 +10,12 @@ import {
} from 'lucide-react';
import type { CronJob } from '@/types/api';
import { getCronJobs, addCronJob, deleteCronJob } from '@/lib/api';
+import { getLocale, t, tf } from '@/lib/i18n';
function formatDate(iso: string | null): string {
if (!iso) return '-';
const d = new Date(iso);
- return d.toLocaleString();
+ return d.toLocaleString(getLocale());
}
export default function Cron() {
@@ -45,7 +46,7 @@ export default function Cron() {
const handleAdd = async () => {
if (!formSchedule.trim() || !formCommand.trim()) {
- setFormError('Schedule and command are required.');
+ setFormError(t('cron.required_fields'));
return;
}
setSubmitting(true);
@@ -62,7 +63,7 @@ export default function Cron() {
setFormSchedule('');
setFormCommand('');
} catch (err: unknown) {
- setFormError(err instanceof Error ? err.message : 'Failed to add job');
+ setFormError(err instanceof Error ? err.message : t('cron.add_error'));
} finally {
setSubmitting(false);
}
@@ -73,7 +74,7 @@ export default function Cron() {
await deleteCronJob(id);
setJobs((prev) => prev.filter((j) => j.id !== id));
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Failed to delete job');
+ setError(err instanceof Error ? err.message : t('cron.delete_error'));
} finally {
setConfirmDelete(null);
}
@@ -97,7 +98,7 @@ export default function Cron() {
return (
- Failed to load cron jobs: {error}
+ {tf('cron.load_error', { error })}
);
@@ -118,7 +119,7 @@ export default function Cron() {
- Scheduled Tasks ({jobs.length})
+ {t('cron.title')} ({jobs.length})
- Add Job
+ {t('cron.add')}
@@ -135,7 +136,7 @@ export default function Cron() {
-
Add Cron Job
+
{t('cron.modal_title')}
{
setShowForm(false);
@@ -156,37 +157,37 @@ export default function Cron() {
@@ -218,35 +219,35 @@ export default function Cron() {
{jobs.length === 0 ? (
-
No scheduled tasks configured.
+
{t('cron.empty_configured')}
) : (
-
-
- ID
-
-
- Name
-
-
- Command
-
-
- Next Run
-
-
- Last Status
-
-
- Enabled
-
-
- Actions
-
-
+
+
+ {t('common.id')}
+
+
+ {t('cron.name')}
+
+
+ {t('cron.command')}
+
+
+ {t('cron.next_run')}
+
+
+ {t('cron.last_status')}
+
+
+ {t('cron.enabled')}
+
+
+ {t('common.actions')}
+
+
{jobs.map((job) => (
@@ -282,24 +283,24 @@ export default function Cron() {
: 'bg-gray-800 text-gray-500 border border-gray-700'
}`}
>
- {job.enabled ? 'Enabled' : 'Disabled'}
+ {job.enabled ? t('common.enabled') : t('common.disabled')}
{confirmDelete === job.id ? (
- Delete?
+ {t('cron.delete_prompt')}
handleDelete(job.id)}
className="text-red-400 hover:text-red-300 text-xs font-medium"
>
- Yes
+ {t('common.yes')}
setConfirmDelete(null)}
className="text-gray-400 hover:text-white text-xs font-medium"
>
- No
+ {t('common.no')}
) : (
diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx
index 2df44e667..f76bf188d 100644
--- a/web/src/pages/Dashboard.tsx
+++ b/web/src/pages/Dashboard.tsx
@@ -10,6 +10,7 @@ import {
} from 'lucide-react';
import type { StatusResponse, CostSummary } from '@/types/api';
import { getStatus, getCost } from '@/lib/api';
+import { getLocale, t, tf } from '@/lib/i18n';
function formatUptime(seconds: number): string {
const d = Math.floor(seconds / 86400);
@@ -70,7 +71,7 @@ export default function Dashboard() {
return (
- Failed to load dashboard: {error}
+ {tf('dashboard.load_error', { error })}
);
@@ -95,10 +96,10 @@ export default function Dashboard() {
- Provider / Model
+ {t('dashboard.provider_model')}
- {status.provider ?? 'Unknown'}
+ {status.provider ?? t('common.unknown')}
{status.model}
@@ -108,12 +109,12 @@ export default function Dashboard() {
- Uptime
+ {t('dashboard.uptime')}
{formatUptime(status.uptime_seconds)}
- Since last restart
+ {t('dashboard.since_restart')}
@@ -121,12 +122,14 @@ export default function Dashboard() {
-
Gateway Port
+
{t('dashboard.gateway_port')}
:{status.gateway_port}
- Locale: {status.locale}
+
+ {t('dashboard.locale')}: {status.locale}
+
@@ -134,13 +137,13 @@ export default function Dashboard() {
-
Memory Backend
+
{t('dashboard.memory_backend')}
{status.memory_backend}
- Paired: {status.paired ? 'Yes' : 'No'}
+ {t('dashboard.paired')}: {status.paired ? t('common.yes') : t('common.no')}
@@ -150,13 +153,13 @@ export default function Dashboard() {
-
Cost Overview
+ {t('dashboard.cost_overview')}
{[
- { label: 'Session', value: cost.session_cost_usd, color: 'bg-blue-500' },
- { label: 'Daily', value: cost.daily_cost_usd, color: 'bg-green-500' },
- { label: 'Monthly', value: cost.monthly_cost_usd, color: 'bg-purple-500' },
+ { label: t('cost.session'), value: cost.session_cost_usd, color: 'bg-blue-500' },
+ { label: t('cost.daily'), value: cost.daily_cost_usd, color: 'bg-green-500' },
+ { label: t('cost.monthly'), value: cost.monthly_cost_usd, color: 'bg-purple-500' },
].map(({ label, value, color }) => (
@@ -173,12 +176,12 @@ export default function Dashboard() {
))}
- Total Tokens
- {cost.total_tokens.toLocaleString()}
+ {t('cost.total_tokens')}
+ {cost.total_tokens.toLocaleString(getLocale())}
- Requests
- {cost.request_count.toLocaleString()}
+ {t('cost.requests')}
+ {cost.request_count.toLocaleString(getLocale())}
@@ -186,11 +189,11 @@ export default function Dashboard() {
-
Active Channels
+ {t('dashboard.active_channels')}
{Object.entries(status.channels).length === 0 ? (
-
No channels configured
+
{t('dashboard.no_channels')}
) : (
Object.entries(status.channels).map(([name, active]) => (
- {active ? 'Active' : 'Inactive'}
+ {active ? t('common.active') : t('common.inactive')}
@@ -218,11 +221,11 @@ export default function Dashboard() {
-
Component Health
+
{t('dashboard.component_health')}
{Object.entries(status.health.components).length === 0 ? (
-
No components reporting
+
{t('dashboard.no_components')}
) : (
Object.entries(status.health.components).map(([name, comp]) => (
{comp.status}
{comp.restart_count > 0 && (
- Restarts: {comp.restart_count}
+ {tf('dashboard.restarts', { count: comp.restart_count })}
)}
diff --git a/web/src/pages/Doctor.tsx b/web/src/pages/Doctor.tsx
index 5305b8c68..b3b9a056e 100644
--- a/web/src/pages/Doctor.tsx
+++ b/web/src/pages/Doctor.tsx
@@ -9,6 +9,7 @@ import {
} from 'lucide-react';
import type { DiagResult } from '@/types/api';
import { runDoctor } from '@/lib/api';
+import { t } from '@/lib/i18n';
function severityIcon(severity: DiagResult['severity']) {
switch (severity) {
@@ -43,6 +44,17 @@ function severityBg(severity: DiagResult['severity']): string {
}
}
+function severityLabel(severity: DiagResult['severity']): string {
+ switch (severity) {
+ case 'ok':
+ return t('doctor.ok');
+ case 'warn':
+ return t('doctor.warn');
+ case 'error':
+ return t('doctor.error');
+ }
+}
+
export default function Doctor() {
const [results, setResults] = useState
(null);
const [loading, setLoading] = useState(false);
@@ -56,7 +68,7 @@ export default function Doctor() {
const data = await runDoctor();
setResults(data);
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Failed to run diagnostics');
+ setError(err instanceof Error ? err.message : t('doctor.run_error'));
} finally {
setLoading(false);
}
@@ -82,7 +94,7 @@ export default function Doctor() {
-
Diagnostics
+ {t('doctor.title')}
- Running...
+ {t('doctor.running_short')}
>
) : (
<>
- Run Diagnostics
+ {t('doctor.run')}
>
)}
@@ -114,9 +126,9 @@ export default function Doctor() {
{loading && (
-
Running diagnostics...
+
{t('doctor.running')}
- This may take a few seconds.
+ {t('doctor.may_take_seconds')}
)}
@@ -129,27 +141,21 @@ export default function Doctor() {
- {okCount} ok
+ {okCount} {t('doctor.ok')}
- {warnCount}{' '}
-
- warning{warnCount !== 1 ? 's' : ''}
-
+ {warnCount} {t('doctor.warn')}
- {errorCount}{' '}
-
- error{errorCount !== 1 ? 's' : ''}
-
+ {errorCount} {t('doctor.error')}
@@ -157,15 +163,15 @@ export default function Doctor() {
{errorCount > 0 ? (
- Issues Found
+ {t('doctor.issues_found')}
) : warnCount > 0 ? (
- Warnings
+ {t('doctor.warnings')}
) : (
- All Clear
+ {t('doctor.all_clear')}
)}
@@ -191,7 +197,7 @@ export default function Doctor() {
{result.message}
- {result.severity}
+ {severityLabel(result.severity)}
@@ -206,9 +212,9 @@ export default function Doctor() {
{!results && !loading && !error && (
-
System Diagnostics
+
{t('doctor.title')}
- Click "Run Diagnostics" to check your ZeroClaw installation.
+ {t('doctor.empty_help')}
)}
diff --git a/web/src/pages/Integrations.tsx b/web/src/pages/Integrations.tsx
index e4b1e5fcf..2f383cdd1 100644
--- a/web/src/pages/Integrations.tsx
+++ b/web/src/pages/Integrations.tsx
@@ -13,25 +13,26 @@ import {
getStatus,
putIntegrationCredentials,
} from '@/lib/api';
+import { t, tf } from '@/lib/i18n';
function statusBadge(status: Integration['status']) {
switch (status) {
case 'Active':
return {
icon: Check,
- label: 'Active',
+ label: t('integrations.active'),
classes: 'bg-green-900/40 text-green-400 border-green-700/50',
};
case 'Available':
return {
icon: Zap,
- label: 'Available',
+ label: t('integrations.available'),
classes: 'bg-blue-900/40 text-blue-400 border-blue-700/50',
};
case 'ComingSoon':
return {
icon: Clock,
- label: 'Coming Soon',
+ label: t('integrations.coming_soon'),
classes: 'bg-gray-800 text-gray-400 border-gray-700',
};
}
@@ -70,9 +71,9 @@ const FALLBACK_MODEL_OPTIONS: Record = {
function customModelFormatHint(integrationId: string): string {
if (integrationId === 'openrouter' || integrationId === 'vercel') {
- return 'Format: anthropic/claude-sonnet-4-6';
+ return t('integrations.custom_model_hint_scoped');
}
- return 'Format: claude-sonnet-4-6 (or provider/model when required)';
+ return t('integrations.custom_model_hint_generic');
}
function modelOptionsForField(
@@ -175,7 +176,7 @@ export default function Integrations() {
setRuntimeStatus(status ? { model: status.model } : null);
return nextSettingsByName;
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Failed to load integrations');
+ setError(err instanceof Error ? err.message : t('common.error'));
setActiveAiIntegrationId(null);
setRuntimeStatus(null);
return null;
@@ -252,7 +253,7 @@ export default function Integrations() {
if (isSelectField) {
if (value === SELECT_KEEP) {
if (field.required && !field.has_value) {
- setSaveError(`${field.label} is required.`);
+ setSaveError(tf('integrations.field_required', { field: field.label }));
return;
}
if (isDirty) {
@@ -268,12 +269,14 @@ export default function Integrations() {
const trimmed = resolvedValue.trim();
if (isSelectField && value === SELECT_CUSTOM && !trimmed) {
- setSaveError(`Enter a custom value for ${field.label} or choose a recommended model.`);
+ setSaveError(
+ tf('integrations.custom_value_required', { field: field.label }),
+ );
return;
}
if (field.required && !trimmed && !field.has_value) {
- setSaveError(`${field.label} is required.`);
+ setSaveError(tf('integrations.field_required', { field: field.label }));
return;
}
@@ -289,7 +292,7 @@ export default function Integrations() {
Object.keys(payload).length === 0 &&
!activeEditor.activates_default_provider
) {
- setSaveError('No changes to save.');
+ setSaveError(t('integrations.no_changes'));
return;
}
@@ -298,9 +301,13 @@ export default function Integrations() {
activeAiIntegrationId &&
activeEditor.id !== activeAiIntegrationId
) {
- const currentProvider = activeAiIntegration?.name ?? 'current provider';
+ const currentProvider =
+ activeAiIntegration?.name ?? t('integrations.current_provider');
const confirmed = window.confirm(
- `Switch default AI provider from ${currentProvider} to ${activeEditor.name}?`,
+ tf('integrations.confirm_switch_provider', {
+ current: currentProvider,
+ target: activeEditor.name,
+ }),
);
if (!confirmed) {
return;
@@ -315,10 +322,10 @@ export default function Integrations() {
});
await loadData(false);
- setSaveSuccess(`${activeEditor.name} credentials saved.`);
+ setSaveSuccess(tf('integrations.credentials_saved', { name: activeEditor.name }));
closeEditor();
} catch (err: unknown) {
- const message = err instanceof Error ? err.message : 'Failed to save credentials';
+ const message = err instanceof Error ? err.message : t('integrations.save_error');
if (message.includes('API 409')) {
const refreshed = await loadData(false);
if (refreshed) {
@@ -331,7 +338,7 @@ export default function Integrations() {
}
}
setSaveError(
- 'Configuration changed elsewhere. Refreshed latest settings; re-enter values and save again.',
+ t('integrations.stale_save'),
);
} else {
setSaveError(message);
@@ -357,9 +364,14 @@ export default function Integrations() {
!isActiveDefaultProvider &&
integration.id !== activeAiIntegrationId
) {
- const currentProvider = activeAiIntegration?.name ?? 'current provider';
+ const currentProvider =
+ activeAiIntegration?.name ?? t('integrations.current_provider');
const confirmed = window.confirm(
- `Switch default AI provider from ${currentProvider} to ${integration.name} and set model to ${trimmedTarget}?`,
+ tf('integrations.confirm_switch_provider_with_model', {
+ current: currentProvider,
+ target: integration.name,
+ model: trimmedTarget,
+ }),
);
if (!confirmed) {
return;
@@ -378,19 +390,22 @@ export default function Integrations() {
});
await loadData(false);
- setSaveSuccess(`Model updated to ${trimmedTarget} for ${integration.name}.`);
+ setSaveSuccess(
+ tf('integrations.model_updated', {
+ model: trimmedTarget,
+ name: integration.name,
+ }),
+ );
setQuickModelDrafts((prev) => {
const next = { ...prev };
delete next[integration.id];
return next;
});
} catch (err: unknown) {
- const message = err instanceof Error ? err.message : 'Failed to update model';
+ const message = err instanceof Error ? err.message : t('integrations.update_model_error');
if (message.includes('API 409')) {
await loadData(false);
- setQuickModelError(
- 'Configuration changed elsewhere. Refreshed latest settings; choose the model again.',
- );
+ setQuickModelError(t('integrations.stale_model'));
} else {
setQuickModelError(message);
}
@@ -421,7 +436,7 @@ export default function Integrations() {
return (
- Failed to load integrations: {error}
+ {tf('integrations.load_error', { error })}
);
@@ -441,7 +456,7 @@ export default function Integrations() {
- Integrations ({integrations.length})
+ {t('integrations.title')} ({integrations.length})
@@ -469,7 +484,7 @@ export default function Integrations() {
: 'bg-gray-900 text-gray-400 border border-gray-700 hover:bg-gray-800 hover:text-white'
}`}
>
- {cat === 'all' ? 'All' : formatCategory(cat)}
+ {cat === 'all' ? t('common.all') : formatCategory(cat)}
))}
@@ -478,7 +493,7 @@ export default function Integrations() {
{Object.keys(grouped).length === 0 ? (
-
No integrations found.
+
{t('integrations.empty')}
) : (
Object.entries(grouped)
@@ -510,8 +525,8 @@ export default function Integrations() {
const modelSummary = currentModel
? currentModel
: fallbackModel
- ? `default: ${fallbackModel}`
- : 'default';
+ ? tf('integrations.default_summary', { model: fallbackModel })
+ : t('integrations.default_only');
const modelBaseline = currentModel ?? fallbackModel ?? '';
const quickDraft = editable
? quickModelDrafts[editable.id] ?? modelBaseline
@@ -555,7 +570,9 @@ export default function Integrations() {
: 'bg-gray-800 text-gray-300 border-gray-700'
}`}
>
- {isActiveDefaultProvider ? 'Default' : 'Configured'}
+ {isActiveDefaultProvider
+ ? t('integrations.default_badge')
+ : t('integrations.configured_badge')}
)}
- Current model
+ {t('integrations.current_model')}
{modelSummary}
@@ -615,11 +632,13 @@ export default function Integrations() {
}
className="px-2.5 py-1.5 rounded-lg text-xs font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50"
>
- {quickModelSavingId === editable.id ? 'Saving...' : 'Apply'}
+ {quickModelSavingId === editable.id
+ ? t('common.saving')
+ : t('common.apply')}
- For custom model IDs, use Edit Keys.
+ {t('integrations.quick_model_help')}
)}
@@ -632,17 +651,19 @@ export default function Integrations() {
{editable.configured
? editable.activates_default_provider
? isActiveDefaultProvider
- ? 'Default provider configured'
- : 'Provider configured'
- : 'Credentials configured'
- : 'Credentials not configured'}
+ ? t('integrations.default_provider_configured')
+ : t('integrations.provider_configured')
+ : t('integrations.credentials_configured')
+ : t('integrations.credentials_not_configured')}
openEditor(editable)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-blue-700/70 bg-blue-900/30 hover:bg-blue-900/50 text-blue-300 text-xs font-medium transition-colors"
>
- {editable.configured ? 'Edit Keys' : 'Configure'}
+ {editable.configured
+ ? t('integrations.edit_keys')
+ : t('integrations.configure')}
)}
@@ -667,19 +688,19 @@ export default function Integrations() {
- Configure {activeEditor.name}
+ {tf('integrations.configure_title', { name: activeEditor.name })}
{activeEditor.configured
- ? 'Enter only fields you want to update.'
- : 'Enter required fields to configure this integration.'}
+ ? t('integrations.configure_intro_update')
+ : t('integrations.configure_intro_new')}
@@ -688,10 +709,11 @@ export default function Integrations() {
{activeEditor.activates_default_provider && (
- Saving here updates credentials and switches your default AI provider to{' '}
- {activeEditor.name} . For advanced provider settings, use{' '}
+ {t('integrations.default_provider_notice_prefix')}{' '}
+ {activeEditor.name} .{' '}
+ {t('integrations.default_provider_notice_suffix')}{' '}
- Configuration
+ {t('config.title')}
.
@@ -712,8 +734,10 @@ export default function Integrations() {
field.current_value?.trim() ||
(activeEditorIsDefaultProvider ? runtimeStatus?.model?.trim() || '' : '');
const keepCurrentLabel = currentModelValue
- ? `Keep current model (${currentModelValue})`
- : 'Keep current model';
+ ? tf('integrations.keep_current_model_with_value', {
+ model: currentModelValue,
+ })
+ : t('integrations.keep_current_model');
return (
@@ -722,7 +746,7 @@ export default function Integrations() {
{field.required &&
* }
{field.has_value && (
- Configured
+ {t('common.configured')}
)}
@@ -737,7 +761,7 @@ export default function Integrations() {
{keepCurrentLabel}
) : (
- Select a recommended model
+ {t('integrations.select_recommended_model')}
)}
{selectOptions.map((option) => (
@@ -745,8 +769,12 @@ export default function Integrations() {
{option}
))}
-
Custom model...
- {field.has_value &&
Clear current model }
+
{t('integrations.custom_model')}
+ {field.has_value && (
+
+ {t('integrations.clear_current_model')}
+
+ )}
{fieldValues[field.key] === SELECT_CUSTOM && (
@@ -760,14 +788,17 @@ export default function Integrations() {
)}
- Pick a recommended model or choose Custom model. {customModelFormatHint(activeEditor.id)}.
+ {tf('integrations.pick_model_help', {
+ hint: customModelFormatHint(activeEditor.id),
+ })}
) : (
{maskedSecretValue && (
- Current value: {maskedSecretValue}
+ {t('integrations.current_value')}{' '}
+ {maskedSecretValue}
)}
@@ -805,7 +836,7 @@ export default function Integrations() {
disabled={saving}
className="px-4 py-2 rounded-lg text-sm border border-gray-700 text-gray-300 hover:bg-gray-800 transition-colors disabled:opacity-50"
>
- Cancel
+ {t('common.cancel')}
{saving
- ? 'Saving...'
+ ? t('common.saving')
: activeEditor.activates_default_provider
- ? 'Save & Activate'
- : 'Save Keys'}
+ ? t('integrations.save_activate')
+ : t('integrations.save_keys')}
diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx
index af1330583..aae19dcde 100644
--- a/web/src/pages/Logs.tsx
+++ b/web/src/pages/Logs.tsx
@@ -8,10 +8,11 @@ import {
} from 'lucide-react';
import type { SSEEvent } from '@/types/api';
import { SSEClient } from '@/lib/sse';
+import { getLocale, t } from '@/lib/i18n';
function formatTimestamp(ts?: string): string {
- if (!ts) return new Date().toLocaleTimeString();
- return new Date(ts).toLocaleTimeString();
+ if (!ts) return new Date().toLocaleTimeString(getLocale());
+ return new Date(ts).toLocaleTimeString(getLocale());
}
function eventTypeBadgeColor(type: string): string {
@@ -138,7 +139,7 @@ export default function Logs() {
-
Live Logs
+
{t('logs.title')}
- {connected ? 'Connected' : 'Disconnected'}
+ {connected ? t('agent.connected') : t('agent.disconnected')}
- {filteredEntries.length} events
+ {filteredEntries.length} {t('logs.events')}
@@ -166,11 +167,11 @@ export default function Logs() {
>
{paused ? (
<>
-
Resume
+
{t('logs.resume')}
>
) : (
<>
-
Pause
+
{t('logs.pause')}
>
)}
@@ -182,7 +183,7 @@ export default function Logs() {
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-blue-600 hover:bg-blue-700 text-white transition-colors"
>
- Jump to bottom
+ {t('logs.jump_to_bottom')}
)}
@@ -192,7 +193,7 @@ export default function Logs() {
{allTypes.length > 0 && (
- Filter:
+ {t('logs.filter_label')}
{allTypes.map((type) => (
setTypeFilters(new Set())}
className="text-xs text-blue-400 hover:text-blue-300 flex-shrink-0 ml-1"
>
- Clear
+ {t('logs.clear')}
)}
@@ -228,9 +229,7 @@ export default function Logs() {
- {paused
- ? 'Log streaming is paused.'
- : 'Waiting for events...'}
+ {paused ? t('logs.paused_empty') : t('logs.waiting_empty')}
) : (
diff --git a/web/src/pages/Memory.tsx b/web/src/pages/Memory.tsx
index 59258eaf9..0179b08d4 100644
--- a/web/src/pages/Memory.tsx
+++ b/web/src/pages/Memory.tsx
@@ -9,6 +9,7 @@ import {
} from 'lucide-react';
import type { MemoryEntry } from '@/types/api';
import { getMemory, storeMemory, deleteMemory } from '@/lib/api';
+import { getLocale, t, tf } from '@/lib/i18n';
function truncate(text: string, max: number): string {
if (text.length <= max) return text;
@@ -17,7 +18,7 @@ function truncate(text: string, max: number): string {
function formatDate(iso: string): string {
const d = new Date(iso);
- return d.toLocaleString();
+ return d.toLocaleString(getLocale());
}
export default function Memory() {
@@ -60,7 +61,7 @@ export default function Memory() {
const handleAdd = async () => {
if (!formKey.trim() || !formContent.trim()) {
- setFormError('Key and content are required.');
+ setFormError(t('memory.required_fields'));
return;
}
setSubmitting(true);
@@ -77,7 +78,7 @@ export default function Memory() {
setFormContent('');
setFormCategory('');
} catch (err: unknown) {
- setFormError(err instanceof Error ? err.message : 'Failed to store memory');
+ setFormError(err instanceof Error ? err.message : t('memory.store_error'));
} finally {
setSubmitting(false);
}
@@ -88,7 +89,7 @@ export default function Memory() {
await deleteMemory(key);
setEntries((prev) => prev.filter((e) => e.key !== key));
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Failed to delete memory');
+ setError(err instanceof Error ? err.message : t('memory.delete_error'));
} finally {
setConfirmDelete(null);
}
@@ -98,7 +99,7 @@ export default function Memory() {
return (
- Failed to load memory: {error}
+ {tf('memory.load_error', { error })}
);
@@ -111,7 +112,7 @@ export default function Memory() {
- Memory ({entries.length})
+ {t('memory.title')} ({entries.length})
- Add Memory
+ {t('memory.add_button')}
@@ -132,7 +133,7 @@ export default function Memory() {
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown}
- placeholder="Search memory entries..."
+ placeholder={t('memory.search')}
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
@@ -143,7 +144,7 @@ export default function Memory() {
onChange={(e) => setCategoryFilter(e.target.value)}
className="bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-8 py-2.5 text-sm text-white appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
>
- All Categories
+ {t('memory.all_categories')}
{categories.map((cat) => (
{cat}
@@ -155,7 +156,7 @@ export default function Memory() {
onClick={handleSearch}
className="px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
- Search
+ {t('common.search')}
@@ -171,7 +172,7 @@ export default function Memory() {
-
Add Memory
+
{t('memory.add_button')}
{
setShowForm(false);
@@ -192,37 +193,37 @@ export default function Memory() {
@@ -258,29 +259,29 @@ export default function Memory() {
) : entries.length === 0 ? (
-
No memory entries found.
+
{t('memory.empty')}
) : (
-
-
- Key
-
-
- Content
-
-
- Category
-
-
- Timestamp
-
-
- Actions
-
-
+
+
+ {t('memory.key')}
+
+
+ {t('memory.content')}
+
+
+ {t('memory.category')}
+
+
+ {t('memory.timestamp')}
+
+
+ {t('common.actions')}
+
+
{entries.map((entry) => (
@@ -307,18 +308,18 @@ export default function Memory() {
{confirmDelete === entry.key ? (
- Delete?
+ {t('memory.delete_prompt')}
handleDelete(entry.key)}
className="text-red-400 hover:text-red-300 text-xs font-medium"
>
- Yes
+ {t('common.yes')}
setConfirmDelete(null)}
className="text-gray-400 hover:text-white text-xs font-medium"
>
- No
+ {t('common.no')}
) : (
diff --git a/web/src/pages/Tools.tsx b/web/src/pages/Tools.tsx
index dc10677dd..bd047455f 100644
--- a/web/src/pages/Tools.tsx
+++ b/web/src/pages/Tools.tsx
@@ -9,6 +9,7 @@ import {
} from 'lucide-react';
import type { ToolSpec, CliTool } from '@/types/api';
import { getTools, getCliTools } from '@/lib/api';
+import { t, tf } from '@/lib/i18n';
export default function Tools() {
const [tools, setTools] = useState([]);
@@ -44,7 +45,7 @@ export default function Tools() {
return (
- Failed to load tools: {error}
+ {tf('tools.load_error', { error })}
);
@@ -67,7 +68,7 @@ export default function Tools() {
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
- placeholder="Search tools..."
+ placeholder={t('tools.search')}
className="w-full bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
@@ -77,12 +78,12 @@ export default function Tools() {
- Agent Tools ({filtered.length})
+ {t('tools.agent_tools')} ({filtered.length})
{filtered.length === 0 ? (
- No tools match your search.
+ {t('tools.no_match')}
) : (
{filtered.map((tool) => {
@@ -119,7 +120,7 @@ export default function Tools() {
{isExpanded && tool.parameters && (
- Parameter Schema
+ {t('tools.parameter_schema')}
{JSON.stringify(tool.parameters, null, 2)}
@@ -139,7 +140,7 @@ export default function Tools() {
- CLI Tools ({filteredCli.length})
+ {t('tools.cli_tools')} ({filteredCli.length})
@@ -148,16 +149,16 @@ export default function Tools() {
- Name
+ {t('common.name')}
- Path
+ {t('common.path')}
- Version
+ {t('common.version')}
- Category
+ {t('integrations.category')}