From 12b43542a9d0b26ad5d140e091e7de7875930e55 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Thu, 2 Apr 2026 23:00:12 +0300 Subject: [PATCH] feat: localize agents page --- apps/ui/html/agents.html | 111 +++++++++++++------------- apps/ui/js/agents.js | 75 +++++++++++------ apps/ui/js/i18n.js | 168 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 80 deletions(-) diff --git a/apps/ui/html/agents.html b/apps/ui/html/agents.html index 277372e..825837c 100644 --- a/apps/ui/html/agents.html +++ b/apps/ui/html/agents.html @@ -36,12 +36,12 @@ @@ -61,7 +61,7 @@ ⌘K - @@ -100,36 +100,36 @@
-
Total agents
+
Total agents
-
across this workspace
+
across this workspace
-
Published
+
Published
-
+
-
Operations exposed
+
Operations exposed
-
+
-
Calls today
+
Calls today
-
all agents combined
+
all agents combined
@@ -139,6 +139,7 @@
- of agents +
@@ -154,17 +155,17 @@
- Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all ? operations in this workspace. + Each agent gets its own MCP endpoint.
- Loading agents… + Loading agents…
-
Failed to load agents
+
Failed to load agents
@@ -173,18 +174,18 @@
-
No agents yet
-
Create your first agent to get a dedicated MCP endpoint with a curated set of tools.
+
No agents yet
+
Create your first agent to get a dedicated MCP endpoint with a curated set of tools.
-
No agents match ""
-
Try a different search term.
+
No agents match
+
Try a different search term.
@@ -212,17 +213,17 @@
- tools + tools
- API keys + API keys
- calls today + calls today
@@ -230,18 +231,18 @@
-
@@ -275,8 +276,8 @@
-
-
+
+
@@ -330,16 +331,16 @@
-
Operations
- +
Operations
+
-
Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.
+
Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.
- No operations match "" +
- You've selected tools. LLMs perform best with <15 tools per agent. Consider splitting into separate agents by use-case. + You've selected tools.
@@ -381,11 +382,11 @@ diff --git a/apps/ui/js/agents.js b/apps/ui/js/agents.js index 125c926..3128d33 100644 --- a/apps/ui/js/agents.js +++ b/apps/ui/js/agents.js @@ -35,6 +35,10 @@ function mapOperation(operation) { }; } +function currentLocale() { + return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US'; +} + document.addEventListener('alpine:init', function() { Alpine.data('agents', function() { return { @@ -94,7 +98,7 @@ document.addEventListener('alpine:init', function() { this.agents = []; this.operations = []; this.loading = false; - this.loadError = 'Workspace or API is unavailable'; + this.loadError = this.tKey('agents.error.api'); return; } @@ -108,7 +112,7 @@ document.addEventListener('alpine:init', function() { } catch (error) { this.agents = []; this.operations = []; - this.loadError = error.message || 'Failed to load agents'; + this.loadError = error.message || this.tKey('agents.error.load'); } this.loading = false; @@ -290,21 +294,29 @@ document.addEventListener('alpine:init', function() { await this.reload(); if (window.CrankUi) { window.CrankUi.success( - this.form.display_name + ' was saved with ' + this.form.selectedOps.length + ' tool bindings.', - this.drawerMode === 'create' ? 'Agent created' : 'Agent updated' + this.tfKey('agents.toast.saved_message', { + name: this.form.display_name, + count: this.form.selectedOps.length + }), + this.drawerMode === 'create' + ? this.tKey('agents.toast.saved_title_create') + : this.tKey('agents.toast.saved_title_update') ); } this.closeDrawer(); } catch (error) { if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to save agent', 'Agent update failed'); + window.CrankUi.error( + error.message || this.tKey('agents.toast.save_error_message'), + this.tKey('agents.toast.save_error_title') + ); } this.saving = false; } }, async deleteAgent(id) { - if (!confirm('Delete this agent? This cannot be undone.')) return; + if (!confirm(this.tKey('agents.toast.delete_confirm'))) return; try { var agent = this.agents.find(function(item) { return item.id === id; }); @@ -312,13 +324,16 @@ document.addEventListener('alpine:init', function() { await this.reload(); if (window.CrankUi) { window.CrankUi.success( - (agent ? agent.display_name : 'Agent') + ' was deleted.', - 'Agent deleted' + this.tfKey('agents.toast.delete_message', { name: agent ? agent.display_name : '' }), + this.tKey('agents.toast.delete_title') ); } } catch (error) { if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to delete agent', 'Delete failed'); + window.CrankUi.error( + error.message || this.tKey('agents.toast.delete_error_message'), + this.tKey('agents.toast.delete_error_title') + ); } } }, @@ -340,37 +355,39 @@ document.addEventListener('alpine:init', function() { } await this.reload(); if (window.CrankUi) { - var actionLabel = action === 'publish' - ? 'published' - : action === 'unpublish' - ? 'returned to draft' - : 'archived'; window.CrankUi.success( - agent.display_name + ' was ' + actionLabel + '.', - 'Agent lifecycle updated' + action === 'publish' + ? this.tfKey('agents.toast.lifecycle_publish', { name: agent.display_name }) + : action === 'unpublish' + ? this.tfKey('agents.toast.lifecycle_unpublish', { name: agent.display_name }) + : this.tfKey('agents.toast.lifecycle_archive', { name: agent.display_name }), + this.tKey('agents.toast.lifecycle_title') ); } } catch (error) { if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to update agent lifecycle', 'Lifecycle update failed'); + window.CrankUi.error( + error.message || this.tKey('agents.toast.lifecycle_error_message'), + this.tKey('agents.toast.lifecycle_error_title') + ); } } }, lifecycleAction(agent) { if (agent.raw_status === 'published') { - return { key: 'unpublish', label: 'Unpublish' }; + return { key: 'unpublish', label: this.tKey('agents.lifecycle.unpublish') }; } if (agent.raw_status === 'archived') { - return { key: 'unpublish', label: 'Restore draft' }; + return { key: 'unpublish', label: this.tKey('agents.lifecycle.restore_draft') }; } - return { key: 'publish', label: 'Publish' }; + return { key: 'publish', label: this.tKey('agents.lifecycle.publish') }; }, lifecycleLabel(agent) { - if (agent.raw_status === 'published') return 'Published'; - if (agent.raw_status === 'archived') return 'Archived'; - return 'Draft'; + if (agent.raw_status === 'published') return this.tKey('agents.lifecycle.published'); + if (agent.raw_status === 'archived') return this.tKey('agents.lifecycle.archived'); + return this.tKey('agents.lifecycle.draft'); }, mcpEndpoint(agent) { @@ -384,7 +401,7 @@ document.addEventListener('alpine:init', function() { var endpoint = this.mcpEndpoint(agent); navigator.clipboard.writeText(endpoint).catch(function() {}); if (window.CrankUi) { - window.CrankUi.info(endpoint, 'MCP endpoint copied'); + window.CrankUi.info(endpoint, this.tKey('agents.toast.endpoint_title')); } }, @@ -406,7 +423,7 @@ document.addEventListener('alpine:init', function() { formatDate(dateStr) { if (!dateStr) return '—'; - return new Date(dateStr).toLocaleDateString('en-US', { + return new Date(dateStr).toLocaleDateString(currentLocale(), { month: 'short', day: 'numeric', year: 'numeric', @@ -417,6 +434,14 @@ document.addEventListener('alpine:init', function() { if (!value) return '0'; return value >= 1000 ? (value / 1000).toFixed(1) + 'k' : String(value); }, + + tKey(key) { + return window.t ? t(key) : key; + }, + + tfKey(key, vars) { + return window.tf ? tf(key, vars) : this.tKey(key); + }, }; }); }); diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js index cba11c1..801bc4e 100644 --- a/apps/ui/js/i18n.js +++ b/apps/ui/js/i18n.js @@ -13,6 +13,9 @@ var TRANSLATIONS = { 'nav.usage': 'Usage', 'nav.settings': 'Settings', 'nav.logout': 'Log out', + 'nav.workspaces': 'Your workspaces', + 'nav.create_workspace': 'Create workspace', + 'nav.notifications': 'Notifications', // Operations page 'ops.title': 'Operations', @@ -148,6 +151,87 @@ var TRANSLATIONS = { 'btn.revoke': 'Revoke', 'btn.copy': 'Copy', + // Agents page + 'agents.title': 'Agents', + 'agents.subtitle': 'Named MCP endpoints that expose a curated subset of operations to an LLM', + 'agents.new': 'New agent', + 'agents.stats.total': 'Total agents', + 'agents.stats.total_sub': 'across this workspace', + 'agents.stats.published': 'Published', + 'agents.stats.published_sub': '{value}% of total', + 'agents.stats.tools': 'Operations exposed', + 'agents.stats.tools_sub': 'across {count} agents', + 'agents.stats.calls': 'Calls today', + 'agents.stats.calls_sub': 'all agents combined', + 'agents.search': 'Search agents...', + 'agents.results': '{shown} of {total} agents', + 'agents.callout': 'Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.', + 'agents.loading': 'Loading agents…', + 'agents.error.title': 'Failed to load agents', + 'agents.error.api': 'Workspace or API is unavailable', + 'agents.error.load': 'Failed to load agents', + 'agents.empty.initial.title': 'No agents yet', + 'agents.empty.initial.sub': 'Create your first agent to get a dedicated MCP endpoint with a curated set of tools.', + 'agents.empty.search.title': 'No agents match "{query}"', + 'agents.empty.search.sub': 'Try a different search term.', + 'agents.card.tools': 'tools', + 'agents.card.keys': 'API keys', + 'agents.card.calls_today': 'calls today', + 'agents.card.created': 'Created {date}', + 'agents.card.copy_endpoint': 'Copy endpoint', + 'agents.action.edit': 'Edit', + 'agents.action.archive': 'Archive', + 'agents.action.delete': 'Delete', + 'agents.lifecycle.publish': 'Publish', + 'agents.lifecycle.unpublish': 'Unpublish', + 'agents.lifecycle.restore_draft': 'Restore draft', + 'agents.lifecycle.published': 'Published', + 'agents.lifecycle.archived': 'Archived', + 'agents.lifecycle.draft': 'Draft', + 'agents.drawer.new_title': 'New agent', + 'agents.drawer.edit_title': 'Edit agent', + 'agents.drawer.new_sub': 'Configure a named MCP endpoint for your LLM', + 'agents.drawer.edit_sub': 'Update agent settings and tool selection', + 'agents.drawer.identity': 'Identity', + 'agents.drawer.display_name': 'Display name', + 'agents.drawer.slug': 'Slug', + 'agents.drawer.description': 'Description', + 'agents.drawer.optional': '(optional)', + 'agents.drawer.required': 'required', + 'agents.drawer.status': 'Status', + 'agents.drawer.endpoint': 'MCP endpoint', + 'agents.drawer.placeholder.name': 'Customer Support', + 'agents.drawer.placeholder.slug': 'customer-support', + 'agents.drawer.placeholder.description': 'What does this agent do? What LLM or use-case is it for?', + 'agents.drawer.operations': 'Operations', + 'agents.drawer.selected': '{count} selected', + 'agents.drawer.operations_sub': 'Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.', + 'agents.drawer.filter_ops': 'Filter operations…', + 'agents.drawer.ops_no_match': 'No operations match "{query}"', + 'agents.drawer.ops_selected': '{count} operations selected', + 'agents.drawer.clear_all': 'Clear all', + 'agents.drawer.recommendation': "You've selected {count} tools. LLMs perform best with <15 tools per agent. Consider splitting into separate agents by use-case.", + 'agents.drawer.cancel': 'Cancel', + 'agents.drawer.create': 'Create agent', + 'agents.drawer.save': 'Save changes', + 'agents.toast.saved_title_create': 'Agent created', + 'agents.toast.saved_title_update': 'Agent updated', + 'agents.toast.saved_message': '{name} was saved with {count} tool bindings.', + 'agents.toast.save_error_title': 'Agent update failed', + 'agents.toast.save_error_message': 'Failed to save agent', + 'agents.toast.delete_confirm': 'Delete this agent? This cannot be undone.', + 'agents.toast.delete_title': 'Agent deleted', + 'agents.toast.delete_message': '{name} was deleted.', + 'agents.toast.delete_error_title': 'Delete failed', + 'agents.toast.delete_error_message': 'Failed to delete agent', + 'agents.toast.lifecycle_title': 'Agent lifecycle updated', + 'agents.toast.lifecycle_publish': '{name} was published.', + 'agents.toast.lifecycle_unpublish': '{name} was returned to draft.', + 'agents.toast.lifecycle_archive': '{name} was archived.', + 'agents.toast.lifecycle_error_title': 'Lifecycle update failed', + 'agents.toast.lifecycle_error_message': 'Failed to update agent lifecycle', + 'agents.toast.endpoint_title': 'MCP endpoint copied', + // Login 'login.title': 'Sign in', 'login.subtitle': 'Welcome back to your workspace', @@ -166,6 +250,9 @@ var TRANSLATIONS = { 'nav.usage': 'Использование', 'nav.settings': 'Настройки', 'nav.logout': 'Выйти', + 'nav.workspaces': 'Ваши воркспейсы', + 'nav.create_workspace': 'Создать воркспейс', + 'nav.notifications': 'Уведомления', // Operations page 'ops.title': 'Операции', @@ -301,6 +388,87 @@ var TRANSLATIONS = { 'btn.revoke': 'Отозвать', 'btn.copy': 'Копировать', + // Agents page + 'agents.title': 'Агенты', + 'agents.subtitle': 'Именованные MCP endpoint-ы, открывающие LLM ограниченный набор операций', + 'agents.new': 'Новый агент', + 'agents.stats.total': 'Всего агентов', + 'agents.stats.total_sub': 'в этом воркспейсе', + 'agents.stats.published': 'Опубликованы', + 'agents.stats.published_sub': '{value}% от общего числа', + 'agents.stats.tools': 'Открытых операций', + 'agents.stats.tools_sub': 'в {count} агентах', + 'agents.stats.calls': 'Вызовов сегодня', + 'agents.stats.calls_sub': 'по всем агентам', + 'agents.search': 'Поиск агентов...', + 'agents.results': '{shown} из {total} агентов', + 'agents.callout': 'У каждого агента свой MCP endpoint. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.', + 'agents.loading': 'Загрузка агентов…', + 'agents.error.title': 'Не удалось загрузить агентов', + 'agents.error.api': 'Воркспейс или API недоступен', + 'agents.error.load': 'Не удалось загрузить агентов', + 'agents.empty.initial.title': 'Агентов пока нет', + 'agents.empty.initial.sub': 'Создайте первого агента, чтобы получить отдельный MCP endpoint с нужным набором инструментов.', + 'agents.empty.search.title': 'Нет агентов по запросу "{query}"', + 'agents.empty.search.sub': 'Попробуйте другой поисковый запрос.', + 'agents.card.tools': 'инструментов', + 'agents.card.keys': 'API-ключей', + 'agents.card.calls_today': 'вызовов сегодня', + 'agents.card.created': 'Создан {date}', + 'agents.card.copy_endpoint': 'Скопировать endpoint', + 'agents.action.edit': 'Редактировать', + 'agents.action.archive': 'Архивировать', + 'agents.action.delete': 'Удалить', + 'agents.lifecycle.publish': 'Опубликовать', + 'agents.lifecycle.unpublish': 'Снять с публикации', + 'agents.lifecycle.restore_draft': 'Вернуть в черновик', + 'agents.lifecycle.published': 'Опубликован', + 'agents.lifecycle.archived': 'Архивирован', + 'agents.lifecycle.draft': 'Черновик', + 'agents.drawer.new_title': 'Новый агент', + 'agents.drawer.edit_title': 'Редактировать агента', + 'agents.drawer.new_sub': 'Настройте именованный MCP endpoint для вашей LLM', + 'agents.drawer.edit_sub': 'Обновите настройки агента и выбор инструментов', + 'agents.drawer.identity': 'Идентичность', + 'agents.drawer.display_name': 'Отображаемое имя', + 'agents.drawer.slug': 'Slug', + 'agents.drawer.description': 'Описание', + 'agents.drawer.optional': '(необязательно)', + 'agents.drawer.required': 'обязательно', + 'agents.drawer.status': 'Статус', + 'agents.drawer.endpoint': 'MCP endpoint', + 'agents.drawer.placeholder.name': 'Customer Support', + 'agents.drawer.placeholder.slug': 'customer-support', + 'agents.drawer.placeholder.description': 'Что делает этот агент? Для какой LLM или сценария он нужен?', + 'agents.drawer.operations': 'Операции', + 'agents.drawer.selected': 'Выбрано: {count}', + 'agents.drawer.operations_sub': 'Выберите операции, которые открывает этот агент. LLM, подключенная к агенту, увидит только эти инструменты.', + 'agents.drawer.filter_ops': 'Фильтр операций…', + 'agents.drawer.ops_no_match': 'Нет операций по запросу "{query}"', + 'agents.drawer.ops_selected': 'Выбрано операций: {count}', + 'agents.drawer.clear_all': 'Очистить все', + 'agents.drawer.recommendation': 'Сейчас выбрано {count} инструментов. LLM лучше работает, когда у агента меньше 15 инструментов. Подумайте о разбиении по сценариям.', + 'agents.drawer.cancel': 'Отмена', + 'agents.drawer.create': 'Создать агента', + 'agents.drawer.save': 'Сохранить изменения', + 'agents.toast.saved_title_create': 'Агент создан', + 'agents.toast.saved_title_update': 'Агент обновлен', + 'agents.toast.saved_message': '{name} сохранен, привязано инструментов: {count}.', + 'agents.toast.save_error_title': 'Не удалось обновить агента', + 'agents.toast.save_error_message': 'Не удалось сохранить агента', + 'agents.toast.delete_confirm': 'Удалить этого агента? Действие нельзя отменить.', + 'agents.toast.delete_title': 'Агент удален', + 'agents.toast.delete_message': 'Агент {name} удален.', + 'agents.toast.delete_error_title': 'Не удалось удалить', + 'agents.toast.delete_error_message': 'Не удалось удалить агента', + 'agents.toast.lifecycle_title': 'Жизненный цикл агента обновлен', + 'agents.toast.lifecycle_publish': '{name} опубликован.', + 'agents.toast.lifecycle_unpublish': '{name} возвращен в черновик.', + 'agents.toast.lifecycle_archive': '{name} архивирован.', + 'agents.toast.lifecycle_error_title': 'Не удалось обновить жизненный цикл', + 'agents.toast.lifecycle_error_message': 'Не удалось обновить состояние агента', + 'agents.toast.endpoint_title': 'MCP endpoint скопирован', + // Login 'login.title': 'Войти', 'login.subtitle': 'Добро пожаловать обратно',