feat: localize operations catalog

This commit is contained in:
a.tolmachev
2026-04-02 22:51:25 +03:00
parent 4113515d0f
commit a491c246e0
3 changed files with 117 additions and 35 deletions
+30 -13
View File
@@ -28,6 +28,10 @@ function uiStatus(rawStatus) {
return rawStatus || 'draft';
}
function currentLocale() {
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
}
function mapOperation(item) {
return {
id: item.id,
@@ -239,11 +243,11 @@ document.addEventListener('alpine:init', function() {
get activeChips() {
var chips = [];
if (this.filterProtocol) chips.push({ key: 'protocol', label: 'Protocol: ' + this.filterProtocol.toUpperCase() });
if (this.filterCategory) chips.push({ key: 'category', label: 'Category: ' + this.filterCategory });
if (this.filterProtocol) chips.push({ key: 'protocol', label: this.tfKey('ops.filter.protocol', { value: this.filterProtocol.toUpperCase() }) });
if (this.filterCategory) chips.push({ key: 'category', label: this.tfKey('ops.filter.category', { value: this.filterCategory }) });
if (this.filterAgent) {
var agent = this.activeAgents.find(function(item) { return item.agent_id === this.filterAgent; }, this);
chips.push({ key: 'agent', label: 'Agent: ' + (agent ? agent.display_name : '') });
chips.push({ key: 'agent', label: this.tfKey('ops.filter.agent', { value: agent ? agent.display_name : '' }) });
}
return chips;
},
@@ -361,7 +365,7 @@ document.addEventListener('alpine:init', function() {
},
async deleteOperation(id) {
if (!confirm('Delete this operation? This cannot be undone.')) return;
if (!confirm(this.tKey('ops.delete.confirm'))) return;
try {
var operation = this.operations.find(function(item) { return item.id === id; });
await window.CrankApi.deleteOperation(this.workspaceId, id);
@@ -372,13 +376,18 @@ document.addEventListener('alpine:init', function() {
this.stats = computeStats(this.operations);
if (window.CrankUi) {
window.CrankUi.success(
(operation ? (operation.display_name || operation.name) : 'Operation') + ' was deleted.',
'Operation deleted'
this.tfKey('ops.delete.success.message', {
name: operation ? (operation.display_name || operation.name) : ''
}),
this.tKey('ops.delete.success.title')
);
}
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to delete operation', 'Delete failed');
window.CrankUi.error(
error.message || this.tKey('ops.delete.error.message'),
this.tKey('ops.delete.error.title')
);
}
}
},
@@ -400,15 +409,15 @@ document.addEventListener('alpine:init', function() {
},
statusLabel(operation) {
if (operation.status === 'active') return 'Active';
if (operation.status === 'inactive') return 'Inactive';
if (operation.status === 'error') return 'Error';
return operation.status.charAt(0).toUpperCase() + operation.status.slice(1);
if (operation.status === 'active') return this.tKey('tab.active');
if (operation.status === 'inactive') return this.tKey('tab.inactive');
if (operation.status === 'error') return this.tKey('tab.error');
return this.tKey('tab.draft');
},
formatDate(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
return new Date(dateStr).toLocaleDateString(currentLocale(), { month: 'short', day: 'numeric', year: 'numeric' });
},
truncateUrl(url) {
@@ -417,7 +426,15 @@ document.addEventListener('alpine:init', function() {
},
formatMetricNumber(value) {
return new Intl.NumberFormat('en-US').format(value || 0);
return new Intl.NumberFormat(currentLocale()).format(value || 0);
},
tKey(key) {
return window.t ? t(key) : key;
},
tfKey(key, vars) {
return window.tf ? tf(key, vars) : this.tKey(key);
},
get sortOptions() {
+64
View File
@@ -21,6 +21,30 @@ var TRANSLATIONS = {
'ops.loading': 'Loading operations…',
'ops.empty.title': 'No operations found',
'ops.empty.sub': 'Try adjusting your search or filters.',
'ops.empty.initial.title': 'No operations yet',
'ops.empty.initial.sub': 'Create the first operation in this workspace to expose a live MCP tool.',
'ops.error.title': 'Backend connection failed',
'ops.delete.confirm': 'Delete this operation? This cannot be undone.',
'ops.delete.success.title': 'Operation deleted',
'ops.delete.success.message': '{name} was deleted.',
'ops.delete.error.title': 'Delete failed',
'ops.delete.error.message': 'Failed to delete operation',
'ops.action.edit': 'Edit operation',
'ops.action.delete':'Delete operation',
'ops.stats.synced': 'Catalog synced with backend',
'ops.stats.none': 'No operations yet',
'ops.stats.share': '{value}% of total',
'ops.stats.no_published': 'No published tools yet',
'ops.stats.requests_rollup': 'Summed from operation usage',
'ops.stats.no_traffic': 'No traffic recorded today',
'ops.stats.latency_weighted': "Weighted by today's calls",
'ops.stats.latency_waiting': 'Waiting for runtime samples',
'ops.results': 'Showing {shown} of {total}',
'ops.range': 'Showing {from}{to} of {total} operations',
'ops.filter.protocol': 'Protocol: {value}',
'ops.filter.category': 'Category: {value}',
'ops.filter.agent': 'Agent: {value}',
'ops.no_active_agents': 'No active agents',
// Stats
'stats.total': 'Total operations',
@@ -40,9 +64,12 @@ var TRANSLATIONS = {
'filter.protocol': 'Protocol',
'filter.status': 'Status',
'filter.category': 'Category',
'filter.agent': 'Agent',
'filter.showing': 'Showing',
'filter.of': 'of',
'filter.clear': 'Clear filter',
'filter.clear_all': 'Clear all',
'filter.remove': 'Remove filter',
'sort.created_desc':'Sort: Last created',
'sort.created_asc': 'Sort: Oldest first',
'sort.name_asc': 'Sort: Name AZ',
@@ -147,6 +174,30 @@ var TRANSLATIONS = {
'ops.loading': 'Загрузка операций…',
'ops.empty.title': 'Операции не найдены',
'ops.empty.sub': 'Попробуйте изменить поисковый запрос или фильтры.',
'ops.empty.initial.title': 'Операций пока нет',
'ops.empty.initial.sub': 'Создайте первую операцию в этом воркспейсе, чтобы открыть живой MCP-инструмент.',
'ops.error.title': 'Не удалось подключиться к backend',
'ops.delete.confirm': 'Удалить эту операцию? Действие нельзя отменить.',
'ops.delete.success.title': 'Операция удалена',
'ops.delete.success.message': 'Операция {name} удалена.',
'ops.delete.error.title': 'Не удалось удалить',
'ops.delete.error.message': 'Не удалось удалить операцию',
'ops.action.edit': 'Редактировать операцию',
'ops.action.delete':'Удалить операцию',
'ops.stats.synced': 'Каталог синхронизирован с backend',
'ops.stats.none': 'Операций пока нет',
'ops.stats.share': '{value}% от общего числа',
'ops.stats.no_published': 'Опубликованных инструментов пока нет',
'ops.stats.requests_rollup': 'Сумма по статистике операций',
'ops.stats.no_traffic': 'Трафик сегодня не зафиксирован',
'ops.stats.latency_weighted': 'Взвешено по сегодняшним вызовам',
'ops.stats.latency_waiting': 'Ожидание runtime-метрик',
'ops.results': 'Показано {shown} из {total}',
'ops.range': 'Показано {from}{to} из {total} операций',
'ops.filter.protocol': 'Протокол: {value}',
'ops.filter.category': 'Категория: {value}',
'ops.filter.agent': 'Агент: {value}',
'ops.no_active_agents': 'Нет активных агентов',
// Stats
'stats.total': 'Всего операций',
@@ -166,9 +217,12 @@ var TRANSLATIONS = {
'filter.protocol': 'Протокол',
'filter.status': 'Статус',
'filter.category': 'Категория',
'filter.agent': 'Агент',
'filter.showing': 'Показано',
'filter.of': 'из',
'filter.clear': 'Сбросить',
'filter.clear_all': 'Сбросить все',
'filter.remove': 'Убрать фильтр',
'sort.created_desc':'Сначала новые',
'sort.created_asc': 'Сначала старые',
'sort.name_asc': 'Название А–Я',
@@ -263,6 +317,12 @@ function t(key) {
return tr[key] !== undefined ? tr[key] : (TRANSLATIONS.en[key] !== undefined ? TRANSLATIONS.en[key] : key);
}
function tf(key, vars) {
return Object.keys(vars || {}).reduce(function(result, name) {
return result.replaceAll('{' + name + '}', vars[name]);
}, t(key));
}
function applyLang() {
// text content
document.querySelectorAll('[data-i18n]').forEach(function(el) {
@@ -272,6 +332,10 @@ function applyLang() {
document.querySelectorAll('[data-i18n-ph]').forEach(function(el) {
el.placeholder = t(el.getAttribute('data-i18n-ph'));
});
// title
document.querySelectorAll('[data-i18n-title]').forEach(function(el) {
el.title = t(el.getAttribute('data-i18n-title'));
});
// highlight active language button in settings
var lang = localStorage.getItem('crank_lang') || 'en';
document.querySelectorAll('.lang-btn').forEach(function(btn) {