feat: localize api keys page
This commit is contained in:
+32
-31
@@ -48,12 +48,12 @@
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
|
||||
<div class="ws-dropdown-label">Your workspaces</div>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
Create workspace
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
<span>⌘K</span>
|
||||
</div>
|
||||
|
||||
<button class="nav-icon-btn" title="Notifications">
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
|
||||
@@ -116,12 +116,12 @@
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="apikeys.title">API Keys</h1>
|
||||
<p class="page-subtitle">Keys authenticate external MCP clients and workspace automation against Crank.</p>
|
||||
<p class="page-subtitle" data-i18n="apikeys.subtitle">Keys authenticate external MCP clients and workspace automation against Crank.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" id="btn-create-key" type="button">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
New key
|
||||
<span data-i18n="apikeys.new">Create key</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,19 +132,20 @@
|
||||
<path d="M8 11V8M8 5.5V5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Keys are only shown once.</strong> Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.
|
||||
<strong data-i18n="apikeys.callout.title">Keys are only shown once.</strong>
|
||||
<span data-i18n="apikeys.callout.body">Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title">Active keys</div>
|
||||
<div class="section-card-title" data-i18n="apikeys.active.title">Active keys</div>
|
||||
<div class="section-card-subtitle">3 active · 1 revoked</div>
|
||||
</div>
|
||||
<div class="filter-bar-search" style="max-width: 220px; margin: 0;">
|
||||
<svg width="13" height="13" style="position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--text-muted);" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215zM11.5 7a4.499 4.499 0 11-8.997 0A4.499 4.499 0 0111.5 7z"/></svg>
|
||||
<input type="text" id="key-search" placeholder="Filter keys…" autocomplete="off">
|
||||
<input type="text" id="key-search" data-i18n-ph="apikeys.search" placeholder="Filter keys…" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,12 +153,12 @@
|
||||
<table class="data-table" id="keys-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Key prefix</th>
|
||||
<th>Scopes</th>
|
||||
<th>Created</th>
|
||||
<th>Last used</th>
|
||||
<th>Status</th>
|
||||
<th data-i18n="apikeys.th.name">Name</th>
|
||||
<th data-i18n="apikeys.th.prefix">Key prefix</th>
|
||||
<th data-i18n="apikeys.th.scopes">Scopes</th>
|
||||
<th data-i18n="apikeys.th.created">Created</th>
|
||||
<th data-i18n="apikeys.th.last">Last used</th>
|
||||
<th data-i18n="apikeys.th.status">Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -169,26 +170,26 @@
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title">Scope reference</div>
|
||||
<div class="section-card-title" data-i18n="apikeys.scope_ref">Scope reference</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;display:flex;align-items:center;gap:6px;">
|
||||
<span class="badge badge-scope">read</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;">Initialize MCP sessions, ping the server, and list tools for a workspace agent.</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;" data-i18n="apikeys.scope.read">Initialize MCP sessions, ping the server, and list tools for a workspace agent.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;">
|
||||
<span class="badge badge-scope">write</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;">Execute `tools/call` requests against published agent toolsets.</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;" data-i18n="apikeys.scope.write">Execute `tools/call` requests against published agent toolsets.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;">
|
||||
<span class="badge badge-scope">deploy</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;">Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;" data-i18n="apikeys.scope.deploy">Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,7 +200,7 @@
|
||||
<div class="modal-overlay" id="modal-create">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Create API key</span>
|
||||
<span class="modal-title" data-i18n="apikeys.modal.title">Create API key</span>
|
||||
<button class="modal-close" id="modal-close-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
@@ -208,12 +209,12 @@
|
||||
</div>
|
||||
<div class="modal-body" id="modal-form-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Key name</label>
|
||||
<input class="field-input" id="new-key-name" type="text" placeholder="e.g. Production, CI pipeline" autocomplete="off">
|
||||
<div class="field-hint">A descriptive label to identify the key. Only visible to admins.</div>
|
||||
<label class="field-label" data-i18n="apikeys.modal.name">Key name</label>
|
||||
<input class="field-input" id="new-key-name" type="text" data-i18n-ph="apikeys.modal.name_placeholder" placeholder="e.g. Production, CI pipeline" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="apikeys.modal.name_hint">A descriptive label to identify the key. Only visible to admins.</div>
|
||||
</div>
|
||||
<div class="field-group" style="margin-bottom:0;">
|
||||
<label class="field-label">Scopes</label>
|
||||
<label class="field-label" data-i18n="apikeys.modal.scopes">Scopes</label>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;margin-top:2px;" id="scope-checkboxes">
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,11 +224,11 @@
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
||||
</svg>
|
||||
<div><strong>Copy this key now.</strong> It won't be shown again.</div>
|
||||
<div><strong data-i18n="apikeys.modal.reveal_title">Copy this key now.</strong> <span data-i18n="apikeys.modal.reveal_body">It won't be shown again.</span></div>
|
||||
</div>
|
||||
<div class="key-reveal">
|
||||
<span class="key-mask" id="reveal-key-value">mcp_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>
|
||||
<button class="key-copy" id="copy-key-btn" type="button" title="Copy to clipboard">
|
||||
<button class="key-copy" id="copy-key-btn" type="button" data-i18n-title="apikeys.modal.copy" title="Copy to clipboard">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="5" y="5" width="9" height="9" rx="1.5"/>
|
||||
<path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/>
|
||||
@@ -236,11 +237,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="modal-footer-create">
|
||||
<button class="btn-secondary" id="modal-cancel-btn" type="button">Cancel</button>
|
||||
<button class="btn-primary" id="modal-confirm-btn" type="button">Create key</button>
|
||||
<button class="btn-secondary" id="modal-cancel-btn" type="button" data-i18n="apikeys.modal.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="modal-confirm-btn" type="button" data-i18n="apikeys.modal.create">Create key</button>
|
||||
</div>
|
||||
<div class="modal-footer" id="modal-footer-done" style="display:none;">
|
||||
<button class="btn-primary" id="modal-done-btn" type="button">Done — I've copied the key</button>
|
||||
<button class="btn-primary" id="modal-done-btn" type="button" data-i18n="apikeys.modal.done">Done — I've copied the key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,11 +268,11 @@
|
||||
<td class="col-status"></td>
|
||||
<td class="col-actions">
|
||||
<span class="actions-active">
|
||||
<button class="btn-icon" title="Copy key prefix"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/></svg></button>
|
||||
<button class="btn-icon danger" title="Revoke key"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.5"/><path d="M5.5 10.5l5-5M10.5 10.5l-5-5"/></svg></button>
|
||||
<button class="btn-icon" data-i18n-title="apikeys.action.copy_prefix" title="Copy key prefix"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/></svg></button>
|
||||
<button class="btn-icon danger" data-i18n-title="apikeys.action.revoke" title="Revoke key"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.5"/><path d="M5.5 10.5l5-5M10.5 10.5l-5-5"/></svg></button>
|
||||
</span>
|
||||
<span class="actions-revoked" style="display:none;">
|
||||
<button class="btn-icon" title="Delete"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h4M2.5 4h11M5 4l.5 9h5l.5-9"/></svg></button>
|
||||
<button class="btn-icon" data-i18n-title="apikeys.action.delete" title="Delete"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h4M2.5 4h11M5 4l.5 9h5l.5-9"/></svg></button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+52
-26
@@ -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
@@ -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': 'Логи',
|
||||
|
||||
Reference in New Issue
Block a user