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
+73 -65
View File
@@ -10,6 +10,12 @@ var wizardProtoUpload = null;
var wizardDescriptorSetUpload = null;
var wizardTestResponsePreview = null;
var grpcDescriptorServices = [];
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
/* ── Dynamic step loading ── */
function _stepFile(n) {
@@ -24,6 +30,7 @@ function loadStep(n, callback) {
.then(function(html) {
var c = document.getElementById('step-panel-container');
if (c) c.insertAdjacentHTML('beforeend', html);
if (typeof applyLang === 'function') applyLang();
if (callback) callback();
})
.catch(function() { if (callback) callback(); });
@@ -38,9 +45,9 @@ function step3PanelId() {
/* Sidebar step-3 label by protocol */
var STEP3_LABELS = {
rest: 'Request config',
graphql: 'GQL operation',
grpc: 'RPC method',
rest: tKey('wizard.step3.rest.label'),
graphql: tKey('wizard.step3.graphql.label'),
grpc: tKey('wizard.step3.grpc.label'),
};
var CHECK_SVG = '<svg width="10" height="10"><use href="' + (window.APP_BASE||'') + 'icons/general/check.svg#icon"/></svg>';
@@ -68,15 +75,15 @@ function _doGoToStep(n) {
if (num < n) {
item.classList.add('done');
if (ind) ind.innerHTML = CHECK_SVG;
if (statusEl) statusEl.textContent = 'Completed';
if (statusEl) statusEl.textContent = tKey('wizard.status.completed');
} else if (num === n) {
item.classList.add('active');
if (ind) ind.textContent = num;
if (statusEl) statusEl.textContent = 'In progress';
if (statusEl) statusEl.textContent = tKey('wizard.status.in_progress');
} else {
item.classList.add('pending');
if (ind) ind.textContent = num;
if (statusEl) statusEl.textContent = 'Not started';
if (statusEl) statusEl.textContent = tKey('wizard.status.not_started');
}
});
@@ -89,7 +96,7 @@ function _doGoToStep(n) {
// 4. update step counter
var counter = document.querySelector('.step-counter');
if (counter) counter.innerHTML = 'Step <strong>' + n + '</strong> of ' + TOTAL_STEPS;
if (counter) counter.innerHTML = tfKey('wizard.step_of', { step: n, total: TOTAL_STEPS });
// 5. back button
var backBtn = document.querySelector('.btn-back');
@@ -109,11 +116,11 @@ function _doGoToStep(n) {
var contBtn = document.querySelector('.btn-continue');
if (contBtn) {
if (n === TOTAL_STEPS) {
var finalLabel = wizardMode === 'edit' ? 'Save changes' : 'Create operation';
var finalLabel = wizardMode === 'edit' ? tKey('wizard.button.save_changes') : tKey('wizard.button.create');
contBtn.innerHTML = finalLabel + ' ' + ARROW_SVG;
contBtn.style.cssText = 'background:#3fb950;border-color:#2ea043;box-shadow:0 1px 4px rgba(63,185,80,0.4);';
} else {
contBtn.innerHTML = 'Continue ' + ARROW_SVG;
contBtn.innerHTML = tKey('wizard.button.continue') + ' ' + ARROW_SVG;
contBtn.style.cssText = '';
}
}
@@ -153,7 +160,7 @@ function bindProtocolCards() {
wizardProtocol = proto;
var s3name = document.getElementById('sidebar-step-3-name');
if (s3name) s3name.textContent = STEP3_LABELS[proto] || 'Protocol config';
if (s3name) s3name.textContent = STEP3_LABELS[proto] || tKey('wizard.step3.rest.label');
if (proto === 'graphql') {
var pathInput = document.getElementById('endpoint-path');
@@ -219,7 +226,7 @@ document.addEventListener('DOMContentLoaded', async function() {
if (params.get('mode') === 'edit' && params.get('operationId')) {
wizardMode = 'edit';
wizardEditId = params.get('operationId');
document.title = 'Crank — Edit Operation';
document.title = 'Crank — ' + tKey('wizard.progress.edit');
await loadOperationForEdit();
}
@@ -293,7 +300,7 @@ function renderDropdownList(filter) {
});
if (filtered.length === 0) {
list.innerHTML = '<div class="upstream-dropdown-empty">No upstreams match "' + escapeHtml(filter) + '"</div>';
list.innerHTML = '<div class="upstream-dropdown-empty">' + tfKey('agents.drawer.ops_no_match', { query: escapeHtml(filter) }) + '</div>';
return;
}
@@ -413,7 +420,7 @@ function startNewUpstream() {
// Clear combobox trigger & hide preview
var val = document.getElementById('upstream-combobox-value');
if (val) val.innerHTML = '<span class="upstream-combobox-placeholder">Select an upstream…</span>';
if (val) val.innerHTML = '<span class="upstream-combobox-placeholder">' + tKey('wizard.step2.title') + '</span>';
var preview = document.getElementById('upstream-preview');
if (preview) preview.style.display = 'none';
selectedUpstreamId = null;
@@ -589,9 +596,10 @@ function renderServiceResults(parsed, ids) {
var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0);
var summaryEl = document.getElementById(ids.summary);
if (summaryEl) {
summaryEl.textContent =
parsed.services.length + ' service' + (parsed.services.length !== 1 ? 's' : '') +
' · ' + totalMethods + ' unary method' + (totalMethods !== 1 ? 's' : '') + ' found';
summaryEl.textContent = tfKey('wizard.grpc.services_found', {
services: parsed.services.length,
methods: totalMethods,
});
}
var listEl = document.getElementById(ids.list);
@@ -601,7 +609,7 @@ function renderServiceResults(parsed, ids) {
if (parsed.services.length === 0) {
var emptyDiv = document.createElement('div');
emptyDiv.className = 'proto-empty';
emptyDiv.textContent = 'No unary RPC methods found.';
emptyDiv.textContent = tKey('wizard.grpc.no_methods');
listEl.appendChild(emptyDiv);
} else {
var svcTmpl = document.getElementById('tmpl-proto-service-group');
@@ -687,7 +695,7 @@ function renderFieldList(elId, typeName) {
if (!fields || fields.length === 0) {
var emptyEl = document.createElement('div');
emptyEl.className = 'proto-field-empty';
emptyEl.innerHTML = 'Fields not resolved.<br><span style="color:var(--text-muted);font-size:11px;">Import or external types are not expanded client-side.</span>';
emptyEl.innerHTML = tKey('wizard.grpc.fields_unresolved') + '<br><span style="color:var(--text-muted);font-size:11px;">' + tKey('wizard.grpc.fields_unresolved_body') + '</span>';
el.appendChild(emptyEl);
return;
}
@@ -831,8 +839,8 @@ var REFLECTION_MOCK = {
function startReflection() {
goToStep(5);
showWizardLiveStatus(
'gRPC discovery moved to live descriptor flow',
'Server reflection is not wired in this deployment. Save the draft, then use the descriptor-set tools in Step 5 to upload a real descriptor set and discover unary methods from the backend.'
tKey('wizard.grpc.discovery_title'),
tKey('wizard.grpc.discovery_body')
);
}
@@ -892,7 +900,7 @@ function parseStructuredText(text) {
try {
return JSON.parse(text);
} catch (_error) {
if (!window.jsyaml) throw new Error('YAML parser is not available');
if (!window.jsyaml) throw new Error(tKey('wizard.error.parser_unavailable'));
return window.jsyaml.load(text) || {};
}
}
@@ -1063,7 +1071,7 @@ function graphqlOperationName(queryText) {
function buildTarget() {
var upstream = currentUpstream();
if (!upstream || !upstream.url) {
throw new Error('Select or register an upstream first');
throw new Error(tKey('wizard.error.select_upstream'));
}
var pathTemplate = textValue('endpoint-path') || '/';
@@ -1153,7 +1161,7 @@ function buildToolDescription() {
function collectWizardPayload() {
var name = textValue('tool-name');
if (!name) throw new Error('Tool name is required');
if (!name) throw new Error(tKey('wizard.error.tool_name'));
var inputSchemaValue = parseStructuredText(textValue('tool-input-schema'));
var outputSchemaValue = parseStructuredText(textValue('tool-output-schema'));
@@ -1180,7 +1188,7 @@ async function saveOperation(stayOnPage) {
await persistCurrentDraft(stayOnPage);
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to save operation', 'Save failed');
window.CrankUi.error(error.message || tKey('wizard.error.save'), tKey('wizard.error.save_title'));
}
}
}
@@ -1202,9 +1210,9 @@ async function loadOperationForEdit() {
function setEditModePresentation() {
var progressLabel = document.querySelector('.progress-label');
if (progressLabel) progressLabel.textContent = 'Edit operation';
if (progressLabel) progressLabel.textContent = tKey('wizard.progress.edit');
var sidebarBrand = document.querySelector('.step-sidebar-brand');
if (sidebarBrand) sidebarBrand.innerHTML = 'Edit <span>operation</span>';
if (sidebarBrand) sidebarBrand.innerHTML = tKey('wizard.sidebar.brand.edit');
}
function selectProtocol(protocol) {
@@ -1217,7 +1225,7 @@ function selectProtocol(protocol) {
card.setAttribute('aria-checked', selected ? 'true' : 'false');
});
var s3name = document.getElementById('sidebar-step-3-name');
if (s3name) s3name.textContent = STEP3_LABELS[wizardProtocol] || 'Protocol config';
if (s3name) s3name.textContent = STEP3_LABELS[wizardProtocol] || tKey('wizard.step3.rest.label');
updateWizardProtocolVisibility();
}
@@ -1352,19 +1360,19 @@ function prefillWizardFromEdit(detail, versionDocument) {
}
function bindWizardLiveActions() {
bindLiveAction('wizard-upload-input-sample', 'Saving input sample', uploadInputSampleFromWizard);
bindLiveAction('wizard-upload-output-sample', 'Saving output sample', uploadOutputSampleFromWizard);
bindLiveAction('wizard-generate-draft', 'Generating draft…', generateDraftFromWizard);
bindLiveAction('wizard-run-test', 'Running test', runWizardTest);
bindLiveAction('wizard-upload-input-sample', tKey('wizard.busy.input_sample'), uploadInputSampleFromWizard);
bindLiveAction('wizard-upload-output-sample', tKey('wizard.busy.output_sample'), uploadOutputSampleFromWizard);
bindLiveAction('wizard-generate-draft', tKey('wizard.busy.generate'), generateDraftFromWizard);
bindLiveAction('wizard-run-test', tKey('wizard.busy.test'), runWizardTest);
bindClick('wizard-copy-test-response', copyTestResponseToOutputSample);
bindLiveAction('wizard-export-yaml', 'Exporting YAML…', exportWizardYaml);
bindLiveAction('wizard-import-yaml', 'Importing YAML…', importWizardYaml);
bindLiveAction('wizard-publish-operation', 'Publishing operation…', publishWizardOperation);
bindLiveAction('wizard-export-yaml', tKey('wizard.busy.export_yaml'), exportWizardYaml);
bindLiveAction('wizard-import-yaml', tKey('wizard.busy.import_yaml'), importWizardYaml);
bindLiveAction('wizard-publish-operation', tKey('wizard.busy.publish'), publishWizardOperation);
bindClick('wizard-select-descriptor-set', function() {
var input = document.getElementById('wizard-descriptor-set-input');
if (input) input.click();
});
bindLiveAction('wizard-upload-descriptor-set', 'Uploading descriptor set…', uploadDescriptorSetAndDiscover);
bindLiveAction('wizard-upload-descriptor-set', tKey('wizard.busy.descriptor'), uploadDescriptorSetAndDiscover);
bindClick('wizard-import-yaml-file-trigger', function() {
var input = document.getElementById('wizard-import-yaml-file');
if (input) input.click();
@@ -1410,14 +1418,14 @@ async function runWizardLiveAction(button, busyLabel, handler) {
await handler();
} catch (error) {
showWizardLiveStatus(
'Live action failed',
error && error.message ? error.message : 'The backend request failed. Review the draft and try again.',
tKey('wizard.live.failed_title'),
error && error.message ? error.message : tKey('wizard.live.failed_body'),
true
);
if (window.CrankUi) {
window.CrankUi.error(
error && error.message ? error.message : 'The backend request failed. Review the draft and try again.',
'Wizard action failed'
error && error.message ? error.message : tKey('wizard.live.failed_body'),
tKey('wizard.live.failed_title')
);
}
} finally {
@@ -1516,7 +1524,7 @@ async function uploadPendingGrpcArtifacts(payload) {
);
wizardDescriptorSetUpload = null;
var descriptorName = document.getElementById('wizard-descriptor-set-name');
if (descriptorName) descriptorName.textContent = 'Descriptor set uploaded';
if (descriptorName) descriptorName.textContent = tKey('wizard.descriptor.uploaded');
}
if (!descriptorResponse) return;
@@ -1532,7 +1540,7 @@ async function uploadPendingGrpcArtifacts(payload) {
async function persistCurrentDraft(stayOnPage) {
if (!wizardWorkspaceId) {
throw new Error('No workspace selected');
throw new Error(tKey('wizard.error.no_workspace'));
}
var payload = collectWizardPayload();
@@ -1568,7 +1576,7 @@ async function persistCurrentDraft(stayOnPage) {
var saveDraftBtn = document.querySelector('.btn-save-draft');
if (saveDraftBtn) {
var label = saveDraftBtn.textContent;
saveDraftBtn.textContent = 'Saved ✓';
saveDraftBtn.textContent = tKey('wizard.save.saved');
setTimeout(function() {
saveDraftBtn.textContent = label;
}, 1400);
@@ -1576,8 +1584,8 @@ async function persistCurrentDraft(stayOnPage) {
}
showWizardLiveStatus(
createdNow ? 'Draft created' : 'Draft updated',
'The current draft is saved on the backend and ready for live actions in this wizard.'
createdNow ? tKey('wizard.save.created') : tKey('wizard.save.updated'),
tKey('wizard.save.body')
);
return response;
}
@@ -1601,7 +1609,7 @@ async function uploadInputSampleFromWizard() {
wizardEditId,
parseStructuredText(textValue('wizard-input-sample'))
);
showWizardLiveStatus('Input sample saved', 'The input sample is now stored against the current draft version.');
showWizardLiveStatus(tKey('wizard.sample.input_saved'), tKey('wizard.sample.input_saved_body'));
}
async function uploadOutputSampleFromWizard() {
@@ -1611,7 +1619,7 @@ async function uploadOutputSampleFromWizard() {
wizardEditId,
parseStructuredText(textValue('wizard-output-sample'))
);
showWizardLiveStatus('Output sample saved', 'The output sample is now stored against the current draft version.');
showWizardLiveStatus(tKey('wizard.sample.output_saved'), tKey('wizard.sample.output_saved_body'));
}
async function generateDraftFromWizard() {
@@ -1621,7 +1629,7 @@ async function generateDraftFromWizard() {
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(generated.output_schema), null, 2));
setValue('tool-input-mapping', mappingSetToEditorValue(generated.input_mapping, 'input', wizardProtocol));
setValue('tool-output-mapping', mappingSetToEditorValue(generated.output_mapping, 'output', wizardProtocol));
showWizardLiveStatus('Draft generated', 'Schemas and mappings were regenerated from the saved samples.');
showWizardLiveStatus(tKey('wizard.sample.generated'), tKey('wizard.sample.generated_body'));
}
async function runWizardTest() {
@@ -1635,21 +1643,21 @@ async function runWizardTest() {
setTextareaValue('wizard-test-response-preview', result.response_preview);
setTextareaValue('wizard-test-errors', result.errors && result.errors.length ? result.errors : []);
showWizardLiveStatus(
result.ok ? 'Test run completed' : 'Test run returned errors',
result.ok ? tKey('wizard.test.completed') : tKey('wizard.test.failed'),
result.ok
? 'The current draft executed successfully against the upstream.'
: 'The draft executed, but the backend returned validation or runtime errors.',
? tKey('wizard.test.completed_body')
: tKey('wizard.test.failed_body'),
!result.ok
);
}
function copyTestResponseToOutputSample() {
if (!wizardTestResponsePreview) {
showWizardLiveStatus('No test response yet', 'Run a test first, then copy the response preview into the output sample editor.', true);
showWizardLiveStatus(tKey('wizard.test.no_response'), tKey('wizard.test.no_response_body'), true);
return;
}
setTextareaValue('wizard-output-sample', wizardTestResponsePreview);
showWizardLiveStatus('Response copied', 'The latest test response was copied into the output sample editor.');
showWizardLiveStatus(tKey('wizard.test.response_copied'), tKey('wizard.test.response_copied_body'));
}
function downloadTextFile(fileName, content, mimeType) {
@@ -1669,20 +1677,20 @@ async function exportWizardYaml() {
version: currentDraftVersion(),
});
downloadTextFile((textValue('tool-name') || 'operation') + '.yaml', yaml, 'application/yaml');
showWizardLiveStatus('YAML exported', 'The current draft YAML was downloaded from the backend.');
showWizardLiveStatus(tKey('wizard.yaml.exported'), tKey('wizard.yaml.exported_body'));
}
async function importWizardYaml() {
if (!wizardWorkspaceId) {
throw new Error('No workspace selected');
throw new Error(tKey('wizard.error.no_workspace'));
}
var yamlDocument = textValue('wizard-import-yaml-text');
if (!yamlDocument) {
showWizardLiveStatus('No YAML provided', 'Paste a YAML document or load it from a file before importing.', true);
showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true);
return;
}
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
showWizardLiveStatus('YAML imported', 'The imported operation draft was stored. The wizard will reopen it now.');
showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body'));
window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id);
}
@@ -1695,8 +1703,8 @@ async function publishWizardOperation() {
);
await refreshCurrentOperationState();
showWizardLiveStatus(
'Operation published',
'Version ' + published.published_version + ' is now published and can be bound into agents.'
tKey('wizard.publish.done'),
tfKey('wizard.publish.done_body', { version: published.published_version })
);
}
@@ -1706,7 +1714,7 @@ function handleImportYamlFileSelection(event) {
var reader = new FileReader();
reader.onload = function(loadEvent) {
setValue('wizard-import-yaml-text', loadEvent.target.result || '');
showWizardLiveStatus('YAML loaded', 'The YAML document was loaded into the import editor.');
showWizardLiveStatus(tKey('wizard.yaml.loaded'), tKey('wizard.yaml.loaded_body'));
};
reader.readAsText(file);
}
@@ -1722,7 +1730,7 @@ function handleDescriptorSetSelection(event) {
};
var fileNameEl = document.getElementById('wizard-descriptor-set-name');
if (fileNameEl) fileNameEl.textContent = file.name;
showWizardLiveStatus('Descriptor set selected', 'Upload the descriptor set to discover unary gRPC methods from the backend.');
showWizardLiveStatus(tKey('wizard.descriptor.selected'), tKey('wizard.descriptor.selected_body'));
};
reader.readAsArrayBuffer(file);
}
@@ -1737,8 +1745,8 @@ function renderDescriptorServiceList(services) {
return total + ((service.methods || []).length);
}, 0);
summaryEl.textContent = grpcDescriptorServices.length
? (grpcDescriptorServices.length + ' services · ' + methodCount + ' unary methods discovered')
: 'No unary methods discovered yet';
? tfKey('wizard.grpc.services_discovered', { services: grpcDescriptorServices.length, methods: methodCount })
: tKey('wizard.grpc.services_none');
listEl.innerHTML = '';
grpcDescriptorServices.forEach(function(service, serviceIndex) {
@@ -1788,7 +1796,7 @@ function applyDiscoveredGrpcMethod(serviceIndex, methodIndex) {
grpcManualTypeInput();
setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(method.input_schema), null, 2));
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(method.output_schema), null, 2));
showWizardLiveStatus('gRPC method applied', 'The route and schemas were filled from the uploaded descriptor set.');
showWizardLiveStatus(tKey('wizard.descriptor.applied'), tKey('wizard.descriptor.applied_body'));
}
async function uploadDescriptorSetAndDiscover() {
@@ -1800,7 +1808,7 @@ async function uploadDescriptorSetAndDiscover() {
);
if (!wizardDescriptorSetUpload && !hasSavedDescriptor) {
showWizardLiveStatus('No descriptor set selected', 'Choose a descriptor-set file before uploading it, or save a draft that already has one attached.', true);
showWizardLiveStatus(tKey('wizard.descriptor.no_file'), tKey('wizard.descriptor.no_file_body'), true);
return;
}
@@ -1812,5 +1820,5 @@ async function uploadDescriptorSetAndDiscover() {
currentDraftVersion()
);
renderDescriptorServiceList(services.services || []);
showWizardLiveStatus('Descriptor services discovered', 'The backend parsed the descriptor set. Pick a method to apply its route and schemas.');
showWizardLiveStatus(tKey('wizard.descriptor.discovered'), tKey('wizard.descriptor.discovered_body'));
}