From 2d7c43c438e9f28444c8dcc1909925db22df9e3b Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Thu, 2 Apr 2026 23:04:10 +0300 Subject: [PATCH] feat: localize api keys page --- apps/ui/html/api-keys.html | 63 +++++++++++---------- apps/ui/js/api-keys.js | 78 +++++++++++++++++--------- apps/ui/js/i18n.js | 112 ++++++++++++++++++++++++++++++++++++- 3 files changed, 194 insertions(+), 59 deletions(-) diff --git a/apps/ui/html/api-keys.html b/apps/ui/html/api-keys.html index 9e61256..64d13a0 100644 --- a/apps/ui/html/api-keys.html +++ b/apps/ui/html/api-keys.html @@ -48,12 +48,12 @@ @@ -75,7 +75,7 @@ ⌘K - @@ -116,12 +116,12 @@ @@ -132,19 +132,20 @@
- Keys are only shown once. Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication. + Keys are only shown once. + Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.
-
Active keys
+
Active keys
3 active · 1 revoked
@@ -152,12 +153,12 @@ - - - - - - + + + + + + @@ -169,26 +170,26 @@
-
Scope reference
+
Scope reference
read
-
Initialize MCP sessions, ping the server, and list tools for a workspace agent.
+
Initialize MCP sessions, ping the server, and list tools for a workspace agent.
write
-
Execute `tools/call` requests against published agent toolsets.
+
Execute `tools/call` requests against published agent toolsets.
deploy
-
Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.
+
Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.
@@ -199,7 +200,7 @@ @@ -267,11 +268,11 @@ diff --git a/apps/ui/js/api-keys.js b/apps/ui/js/api-keys.js index e551181..a7f1f8e 100644 --- a/apps/ui/js/api-keys.js +++ b/apps/ui/js/api-keys.js @@ -5,11 +5,19 @@ var search = ''; var SCOPES = ['read', 'write', 'deploy']; var SCOPE_DESC = { - read: 'Initialize MCP sessions and list tools', - write: 'Execute published tools through MCP', - deploy: 'Reserved for deploy-scoped automation', + read: 'apikeys.scope.read', + write: 'apikeys.scope.write', + deploy: 'apikeys.scope.deploy', }; +function tKey(key) { + return window.t ? t(key) : key; +} + +function tfKey(key, vars) { + return window.tf ? tf(key, vars) : tKey(key); +} + function mapKeyRecord(record) { var apiKey = record.api_key || {}; return { @@ -18,7 +26,7 @@ function mapKeyRecord(record) { prefix: apiKey.prefix || '', scopes: apiKey.scopes || [], created: apiKey.created_at ? apiKey.created_at.slice(0, 10) : '—', - lastUsed: apiKey.last_used_at ? apiKey.last_used_at.slice(0, 10) : 'Never', + lastUsed: apiKey.last_used_at ? apiKey.last_used_at.slice(0, 10) : tKey('apikeys.last_used.never'), status: apiKey.status || 'active', }; } @@ -36,7 +44,7 @@ async function loadKeys() { if (!currentWorkspaceId || !window.CrankApi) { KEYS = []; setTableLoading(false); - renderTable('Workspace or API is unavailable'); + renderTable(tKey('apikeys.error.api')); return; } @@ -48,7 +56,7 @@ async function loadKeys() { } catch (error) { KEYS = []; setTableLoading(false); - renderTable(error.message || 'Failed to load API keys'); + renderTable(error.message || tKey('apikeys.error.load')); } } @@ -60,13 +68,13 @@ function setTableLoading(on) { var td = document.createElement('td'); td.colSpan = 7; td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);'; - td.textContent = 'Loading…'; + td.textContent = tKey('apikeys.loading'); tr.appendChild(td); tbody.appendChild(tr); } async function revokeKey(id) { - if (!confirm('Revoke this key? All requests using it will fail immediately.')) return; + if (!confirm(tKey('apikeys.confirm.revoke'))) return; try { var key = KEYS.find(function(item) { return item.id === id; }); @@ -74,19 +82,22 @@ async function revokeKey(id) { await loadKeys(); if (window.CrankUi) { window.CrankUi.success( - (key ? key.name : 'API key') + ' was revoked.', - 'Key revoked' + tfKey('apikeys.toast.revoke_message', { name: key ? key.name : '' }), + tKey('apikeys.toast.revoke_title') ); } } catch (error) { if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to revoke key', 'Revoke failed'); + window.CrankUi.error( + error.message || tKey('apikeys.toast.revoke_error_message'), + tKey('apikeys.toast.revoke_error_title') + ); } } } async function deleteKey(id) { - if (!confirm('Delete this key? This cannot be undone.')) return; + if (!confirm(tKey('apikeys.confirm.delete'))) return; try { var key = KEYS.find(function(item) { return item.id === id; }); @@ -94,13 +105,16 @@ async function deleteKey(id) { await loadKeys(); if (window.CrankUi) { window.CrankUi.success( - (key ? key.name : 'API key') + ' was deleted.', - 'Key deleted' + tfKey('apikeys.toast.delete_message', { name: key ? key.name : '' }), + tKey('apikeys.toast.delete_title') ); } } catch (error) { if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to delete key', 'Delete failed'); + window.CrankUi.error( + error.message || tKey('apikeys.toast.delete_error_message'), + tKey('apikeys.toast.delete_error_title') + ); } } } @@ -126,7 +140,7 @@ function renderScopes() { input.dataset.scope = scope; if (selectedScopes.has(scope)) input.checked = true; node.querySelector('.scope-checkbox-name').textContent = scope; - node.querySelector('.scope-checkbox-desc').textContent = SCOPE_DESC[scope]; + node.querySelector('.scope-checkbox-desc').textContent = tKey(SCOPE_DESC[scope]); input.addEventListener('change', function() { if (this.checked) selectedScopes.add(this.dataset.scope); else selectedScopes.delete(this.dataset.scope); @@ -148,7 +162,7 @@ function renderTable(errorMessage) { if (subtitle) { var active = KEYS.filter(function(key) { return key.status === 'active'; }).length; var revoked = KEYS.filter(function(key) { return key.status === 'revoked'; }).length; - subtitle.textContent = active + ' active · ' + revoked + ' revoked'; + subtitle.textContent = tfKey('apikeys.active.subtitle', { active: active, revoked: revoked }); } if (errorMessage) { @@ -167,7 +181,7 @@ function renderTable(errorMessage) { var td = document.createElement('td'); td.colSpan = 7; td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);'; - td.textContent = KEYS.length ? 'No keys match your search' : 'No API keys yet'; + td.textContent = KEYS.length ? tKey('apikeys.empty.search') : tKey('apikeys.empty.none'); empty.appendChild(td); tbody.appendChild(empty); return; @@ -191,7 +205,7 @@ function renderTable(errorMessage) { var badge = document.createElement('span'); badge.className = key.status === 'active' ? 'badge badge-active' : 'badge badge-revoked'; - badge.textContent = key.status === 'active' ? 'Active' : 'Revoked'; + badge.textContent = key.status === 'active' ? tKey('apikeys.status.active') : tKey('apikeys.status.revoked'); node.querySelector('.col-status').appendChild(badge); var actionsActive = node.querySelector('.actions-active'); @@ -251,14 +265,17 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun } if (!selectedScopes.size) { if (window.CrankUi) { - window.CrankUi.info('Select at least one scope before creating a key.', 'Missing scope'); + window.CrankUi.info( + tKey('apikeys.toast.scope_missing_message'), + tKey('apikeys.toast.scope_missing_title') + ); } return; } try { button.disabled = true; - button.textContent = 'Creating…'; + button.textContent = tKey('apikeys.creating'); var created = await createKey(name, Array.from(selectedScopes)); KEYS.unshift(created.record); document.getElementById('reveal-key-value').textContent = created.rawKey; @@ -268,15 +285,21 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun document.getElementById('modal-footer-done').style.display = ''; renderTable(); if (window.CrankUi) { - window.CrankUi.success('Copy the key now. It will not be shown again.', 'API key created'); + window.CrankUi.success( + tKey('apikeys.toast.create_message'), + tKey('apikeys.toast.create_title') + ); } } catch (error) { if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to create key', 'Key creation failed'); + window.CrankUi.error( + error.message || tKey('apikeys.toast.create_error_message'), + tKey('apikeys.toast.create_error_title') + ); } } finally { button.disabled = false; - button.textContent = 'Create key'; + button.textContent = tKey('apikeys.modal.create'); } }); @@ -289,7 +312,10 @@ document.getElementById('copy-key-btn').addEventListener('click', function() { } this.innerHTML = ''; if (window.CrankUi) { - window.CrankUi.info('Store the raw key securely. It cannot be revealed again.', 'API key copied'); + window.CrankUi.info( + tKey('apikeys.toast.copy_message'), + tKey('apikeys.toast.copy_title') + ); } }); @@ -303,7 +329,7 @@ function copyPrefix(prefix) { navigator.clipboard.writeText(prefix).catch(function() {}); } if (window.CrankUi) { - window.CrankUi.info(prefix, 'Key prefix copied'); + window.CrankUi.info(prefix, tKey('apikeys.toast.prefix_title')); } } diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js index 801bc4e..5f77eea 100644 --- a/apps/ui/js/i18n.js +++ b/apps/ui/js/i18n.js @@ -92,12 +92,66 @@ var TRANSLATIONS = { // API Keys page 'apikeys.title': 'API Keys', - 'apikeys.subtitle': 'Manage access tokens for external integrations', + 'apikeys.subtitle': 'Keys authenticate external MCP clients and workspace automation against Crank.', 'apikeys.new': 'Create key', 'apikeys.th.name': 'NAME', 'apikeys.th.scope': 'SCOPE', 'apikeys.th.created':'CREATED', 'apikeys.th.last': 'LAST USED', + 'apikeys.callout.title': 'Keys are only shown once.', + 'apikeys.callout.body': 'Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.', + 'apikeys.active.title': 'Active keys', + 'apikeys.active.subtitle': '{active} active · {revoked} revoked', + 'apikeys.search': 'Filter keys…', + 'apikeys.th.prefix': 'KEY PREFIX', + 'apikeys.th.scopes': 'SCOPES', + 'apikeys.th.status': 'STATUS', + 'apikeys.scope_ref': 'Scope reference', + 'apikeys.scope.read': 'Initialize MCP sessions, ping the server, and list tools for a workspace agent.', + 'apikeys.scope.write': 'Execute `tools/call` requests against published agent toolsets.', + 'apikeys.scope.deploy': 'Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.', + 'apikeys.modal.title': 'Create API key', + 'apikeys.modal.name': 'Key name', + 'apikeys.modal.name_hint': 'A descriptive label to identify the key. Only visible to admins.', + 'apikeys.modal.name_placeholder': 'e.g. Production, CI pipeline', + 'apikeys.modal.scopes': 'Scopes', + 'apikeys.modal.reveal_title': 'Copy this key now.', + 'apikeys.modal.reveal_body': "It won't be shown again.", + 'apikeys.modal.copy': 'Copy to clipboard', + 'apikeys.modal.cancel': 'Cancel', + 'apikeys.modal.create': 'Create key', + 'apikeys.modal.done': "Done — I've copied the key", + 'apikeys.status.active': 'Active', + 'apikeys.status.revoked': 'Revoked', + 'apikeys.last_used.never': 'Never', + 'apikeys.empty.none': 'No API keys yet', + 'apikeys.empty.search': 'No keys match your search', + 'apikeys.loading': 'Loading…', + 'apikeys.error.api': 'Workspace or API is unavailable', + 'apikeys.error.load': 'Failed to load API keys', + 'apikeys.confirm.revoke': 'Revoke this key? All requests using it will fail immediately.', + 'apikeys.confirm.delete': 'Delete this key? This cannot be undone.', + 'apikeys.toast.revoke_title': 'Key revoked', + 'apikeys.toast.revoke_message': '{name} was revoked.', + 'apikeys.toast.revoke_error_title': 'Revoke failed', + 'apikeys.toast.revoke_error_message': 'Failed to revoke key', + 'apikeys.toast.delete_title': 'Key deleted', + 'apikeys.toast.delete_message': '{name} was deleted.', + 'apikeys.toast.delete_error_title': 'Delete failed', + 'apikeys.toast.delete_error_message': 'Failed to delete key', + 'apikeys.toast.scope_missing_title': 'Missing scope', + 'apikeys.toast.scope_missing_message': 'Select at least one scope before creating a key.', + 'apikeys.toast.create_title': 'API key created', + 'apikeys.toast.create_message': 'Copy the key now. It will not be shown again.', + 'apikeys.toast.create_error_title': 'Key creation failed', + 'apikeys.toast.create_error_message': 'Failed to create key', + 'apikeys.toast.copy_title': 'API key copied', + 'apikeys.toast.copy_message': 'Store the raw key securely. It cannot be revealed again.', + 'apikeys.toast.prefix_title': 'Key prefix copied', + 'apikeys.action.copy_prefix': 'Copy key prefix', + 'apikeys.action.revoke': 'Revoke key', + 'apikeys.action.delete': 'Delete', + 'apikeys.creating': 'Creating…', // Logs page 'logs.title': 'Logs', @@ -329,12 +383,66 @@ var TRANSLATIONS = { // API Keys page 'apikeys.title': 'API Ключи', - 'apikeys.subtitle': 'Управление токенами доступа для внешних интеграций', + 'apikeys.subtitle': 'Ключи аутентифицируют внешние MCP-клиенты и автоматизацию воркспейса в Crank.', 'apikeys.new': 'Создать ключ', 'apikeys.th.name': 'НАЗВАНИЕ', 'apikeys.th.scope': 'ОБЛАСТЬ', 'apikeys.th.created':'СОЗДАН', 'apikeys.th.last': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ', + 'apikeys.callout.title': 'Ключ показывается только один раз.', + 'apikeys.callout.body': 'Скопируйте и сохраните его сразу после создания — Crank хранит только хэш. `Последнее использование` обновляется после успешной MCP-аутентификации.', + 'apikeys.active.title': 'Активные ключи', + 'apikeys.active.subtitle': '{active} активных · {revoked} отозванных', + 'apikeys.search': 'Фильтр ключей…', + 'apikeys.th.prefix': 'ПРЕФИКС КЛЮЧА', + 'apikeys.th.scopes': 'ОБЛАСТИ', + 'apikeys.th.status': 'СТАТУС', + 'apikeys.scope_ref': 'Справка по областям', + 'apikeys.scope.read': 'Инициализация MCP-сессий, ping сервера и получение списка инструментов для агента воркспейса.', + 'apikeys.scope.write': 'Выполнение `tools/call` для опубликованных наборов инструментов агента.', + 'apikeys.scope.deploy': 'Зарезервировано для deploy-автоматизации. Сейчас также разрешает MCP read/write сценарии.', + 'apikeys.modal.title': 'Создать API-ключ', + 'apikeys.modal.name': 'Имя ключа', + 'apikeys.modal.name_hint': 'Понятная метка для идентификации ключа. Видна только администраторам.', + 'apikeys.modal.name_placeholder': 'например, Production, CI pipeline', + 'apikeys.modal.scopes': 'Области', + 'apikeys.modal.reveal_title': 'Скопируйте ключ сейчас.', + 'apikeys.modal.reveal_body': 'Повторно он не будет показан.', + 'apikeys.modal.copy': 'Скопировать', + 'apikeys.modal.cancel': 'Отмена', + 'apikeys.modal.create': 'Создать ключ', + 'apikeys.modal.done': 'Готово — ключ скопирован', + 'apikeys.status.active': 'Активен', + 'apikeys.status.revoked': 'Отозван', + 'apikeys.last_used.never': 'Никогда', + 'apikeys.empty.none': 'API-ключей пока нет', + 'apikeys.empty.search': 'Нет ключей по текущему поиску', + 'apikeys.loading': 'Загрузка…', + 'apikeys.error.api': 'Воркспейс или API недоступен', + 'apikeys.error.load': 'Не удалось загрузить API-ключи', + 'apikeys.confirm.revoke': 'Отозвать этот ключ? Все запросы с ним сразу начнут падать.', + 'apikeys.confirm.delete': 'Удалить этот ключ? Действие нельзя отменить.', + 'apikeys.toast.revoke_title': 'Ключ отозван', + 'apikeys.toast.revoke_message': 'Ключ {name} отозван.', + 'apikeys.toast.revoke_error_title': 'Не удалось отозвать', + 'apikeys.toast.revoke_error_message': 'Не удалось отозвать ключ', + 'apikeys.toast.delete_title': 'Ключ удален', + 'apikeys.toast.delete_message': 'Ключ {name} удален.', + 'apikeys.toast.delete_error_title': 'Не удалось удалить', + 'apikeys.toast.delete_error_message': 'Не удалось удалить ключ', + 'apikeys.toast.scope_missing_title': 'Не выбрана область', + 'apikeys.toast.scope_missing_message': 'Выберите хотя бы одну область перед созданием ключа.', + 'apikeys.toast.create_title': 'API-ключ создан', + 'apikeys.toast.create_message': 'Скопируйте ключ сейчас. Повторно он не будет показан.', + 'apikeys.toast.create_error_title': 'Не удалось создать ключ', + 'apikeys.toast.create_error_message': 'Не удалось создать ключ', + 'apikeys.toast.copy_title': 'API-ключ скопирован', + 'apikeys.toast.copy_message': 'Сохраните исходный ключ в надежном месте. Повторно показать его нельзя.', + 'apikeys.toast.prefix_title': 'Префикс ключа скопирован', + 'apikeys.action.copy_prefix': 'Скопировать префикс ключа', + 'apikeys.action.revoke': 'Отозвать ключ', + 'apikeys.action.delete': 'Удалить', + 'apikeys.creating': 'Создание…', // Logs page 'logs.title': 'Логи',
NameKey prefixScopesCreatedLast usedStatusNameKey prefixScopesCreatedLast usedStatus
- - + +