feat: localize settings workspace and wizard

This commit is contained in:
a.tolmachev
2026-04-03 00:03:40 +03:00
parent 581d50c33c
commit 140548bb20
16 changed files with 763 additions and 247 deletions
+37 -28
View File
@@ -9,6 +9,12 @@ var workspaceFormState = {
invitations: [],
currentUserId: null,
};
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function normalizeSlug(value) {
return value
@@ -26,9 +32,10 @@ function uiRoleToApi(value) {
}
function roleLabel(value) {
if (value === 'operator') {
return 'Developer';
}
if (value === 'operator') return tKey('workspace_setup.role.operator');
if (value === 'owner') return tKey('workspace_setup.role.owner');
if (value === 'admin') return tKey('workspace_setup.role.admin');
if (value === 'viewer') return tKey('workspace_setup.role.viewer');
return value.charAt(0).toUpperCase() + value.slice(1);
}
@@ -142,7 +149,9 @@ function renderMembers() {
workspaceFormState.memberships.forEach(function (membership) {
addMemberRow(container, membership);
});
label.textContent = workspaceFormState.memberships.length + ' member' + (workspaceFormState.memberships.length === 1 ? '' : 's');
label.textContent = workspaceFormState.memberships.length === 1
? tKey('workspace_setup.member_count_one')
: tfKey('workspace_setup.member_count', { count: workspaceFormState.memberships.length });
}
function renderInvitations() {
@@ -165,7 +174,7 @@ function renderInvitations() {
pending.style.display = '';
pending.innerHTML =
'<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">' +
'Pending invites <span id="pending-count" style="background:rgba(139,148,158,0.15);border-radius:10px;padding:1px 7px;font-size:10px;">' + items.length + '</span>' +
tKey('workspace_setup.members.pending') + ' <span id="pending-count" style="background:rgba(139,148,158,0.15);border-radius:10px;padding:1px 7px;font-size:10px;">' + items.length + '</span>' +
'</div>';
items.forEach(function (record) {
@@ -176,7 +185,7 @@ function renderInvitations() {
'<div class="member-avatar" style="background:var(--bg-canvas);border:1.5px dashed var(--border);color:var(--text-muted);font-size:16px;">?</div>' +
'<div class="member-info">' +
'<div class="member-name">' + invitation.email + '</div>' +
'<div class="member-email">Invited ' + invitation.created_at.slice(0, 10) + ' · pending</div>' +
'<div class="member-email">' + tfKey('workspace_setup.members.invited_pending', { date: invitation.created_at.slice(0, 10) }) + '</div>' +
'</div>' +
'<div class="member-last-active">—</div>' +
'<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);">' + roleLabel(invitation.role) + '</span>' +
@@ -213,19 +222,19 @@ function updatePageMode() {
var submit = document.getElementById('submit-btn');
if (workspaceFormState.isCreateMode) {
document.title = 'Crank — Create workspace';
document.getElementById('page-title').textContent = 'Create a new workspace';
document.getElementById('page-subtitle').textContent = 'A workspace is a shared environment for your team\'s MCP operations and agents. Each workspace gets its own MCP endpoint namespace.';
document.title = 'Crank — ' + tKey('workspace_setup.create.title');
document.getElementById('page-title').textContent = tKey('workspace_setup.create.title');
document.getElementById('page-subtitle').textContent = tKey('workspace_setup.create.subtitle');
document.getElementById('section-invite').style.display = '';
document.getElementById('footer-note').style.display = '';
submit.textContent = 'Create workspace';
submit.textContent = tKey('workspace_setup.actions.create');
submit.disabled = true;
submit.style.opacity = '0.5';
submit.style.cursor = 'not-allowed';
} else {
document.getElementById('section-members').style.display = '';
document.getElementById('section-danger').style.display = '';
submit.textContent = 'Save changes';
submit.textContent = tKey('workspace_setup.actions.save');
}
}
@@ -289,7 +298,7 @@ async function submitForm() {
var submit = document.getElementById('submit-btn');
var originalLabel = submit.textContent;
submit.disabled = true;
submit.textContent = workspaceFormState.isCreateMode ? 'Creating…' : 'Saving…';
submit.textContent = workspaceFormState.isCreateMode ? tKey('workspace_setup.saving_create') : tKey('workspace_setup.saving_update');
try {
var response;
@@ -317,7 +326,7 @@ async function submitForm() {
id: workspace.id,
slug: workspace.slug,
name: workspace.display_name,
role: 'Owner',
role: tKey('workspace_setup.role.owner'),
letter: (workspace.display_name || workspace.slug).charAt(0).toUpperCase(),
color: payload.settings.color || '#0d9488',
status: workspace.status,
@@ -336,14 +345,14 @@ async function submitForm() {
applyWorkspaceRecord({ workspace: workspace });
await loadWorkspaceAccessData();
submit.textContent = 'Saved ✓';
submit.textContent = tKey('workspace_setup.saved');
setTimeout(function () {
submit.textContent = originalLabel;
submit.disabled = false;
}, 1400);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to save workspace', 'Save failed');
window.CrankUi.error(error.message || tKey('workspace_setup.save_error'), tKey('workspace_setup.save_error_title'));
}
submit.disabled = false;
submit.textContent = originalLabel;
@@ -357,11 +366,11 @@ function addInviteRow() {
node.innerHTML =
'<input class="form-input" type="email" placeholder="colleague@company.com" autocomplete="off" style="flex:1;margin-bottom:0;">' +
'<select class="form-input" style="width:130px;margin-bottom:0;">' +
'<option value="admin">Admin</option>' +
'<option value="developer" selected>Developer</option>' +
'<option value="viewer">Viewer</option>' +
'<option value="admin">' + tKey('workspace_setup.role.admin') + '</option>' +
'<option value="developer" selected>' + tKey('workspace_setup.role.operator') + '</option>' +
'<option value="viewer">' + tKey('workspace_setup.role.viewer') + '</option>' +
'</select>' +
'<button class="invite-row-remove" type="button" onclick="removeInviteRow(this)" title="Remove">' +
'<button class="invite-row-remove" type="button" onclick="removeInviteRow(this)" title="' + tKey('workspace_setup.members.remove') + '">' +
'<svg width="12" height="12"><use href="' + (window.APP_BASE || '') + 'icons/general/close.svg#icon"/></svg>' +
'</button>';
rows.appendChild(node);
@@ -407,7 +416,7 @@ async function sendInvite() {
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to send invite', 'Invite failed');
window.CrankUi.error(error.message || tKey('workspace_setup.invite_error'), tKey('workspace_setup.invite_error_title'));
}
}
}
@@ -429,7 +438,7 @@ async function updateRole(select, userId) {
} catch (error) {
select.value = previous;
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to update role', 'Role update failed');
window.CrankUi.error(error.message || tKey('workspace_setup.role_error'), tKey('workspace_setup.role_error_title'));
}
} finally {
select.disabled = false;
@@ -440,7 +449,7 @@ async function removeMember(userId, name) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
if (!confirm('Remove ' + name + ' from this workspace?')) {
if (!confirm(tfKey('workspace_setup.remove_confirm', { name: name }))) {
return;
}
@@ -449,7 +458,7 @@ async function removeMember(userId, name) {
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to remove member', 'Member removal failed');
window.CrankUi.error(error.message || tKey('workspace_setup.remove_error'), tKey('workspace_setup.remove_error_title'));
}
}
}
@@ -464,7 +473,7 @@ async function revokeInviteById(invitationId) {
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to revoke invite', 'Invite revoke failed');
window.CrankUi.error(error.message || tKey('workspace_setup.revoke_error'), tKey('workspace_setup.revoke_error_title'));
}
}
}
@@ -496,11 +505,11 @@ async function exportWorkspaceSnapshot() {
var snapshot = await window.CrankApi.exportWorkspace(workspaceFormState.workspaceId);
var slug = workspaceFormState.workspaceRecord && workspaceFormState.workspaceRecord.workspace
? workspaceFormState.workspaceRecord.workspace.slug
: 'workspace';
: tKey('settings.nav.workspace');
downloadJsonFile(slug + '-snapshot.json', snapshot);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to export workspace', 'Export failed');
window.CrankUi.error(error.message || tKey('workspace_setup.export_error'), tKey('workspace_setup.export_error_title'));
}
}
}
@@ -509,7 +518,7 @@ async function deleteWorkspaceAction() {
if (!workspaceFormState.workspaceId || !window.CrankApi || !window.CrankAuth) {
return;
}
if (!confirm('Delete this workspace? This cannot be undone.')) {
if (!confirm(tKey('workspace_setup.delete_confirm'))) {
return;
}
@@ -518,7 +527,7 @@ async function deleteWorkspaceAction() {
await window.CrankAuth.logout();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to delete workspace', 'Delete failed');
window.CrankUi.error(error.message || tKey('workspace_setup.delete_error'), tKey('workspace_setup.delete_error_title'));
}
}
}