feat: localize api keys page

This commit is contained in:
a.tolmachev
2026-04-02 23:04:10 +03:00
parent 12b43542a9
commit 2d7c43c438
3 changed files with 194 additions and 59 deletions
+52 -26
View File
@@ -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 = '<svg width="13" height="13"><use href="' + (window.APP_BASE || '') + 'icons/general/check.svg#icon"/></svg>';
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'));
}
}
+110 -2
View File
@@ -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': 'Логи',