feat: localize operations catalog
This commit is contained in:
+30
-13
@@ -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() {
|
||||
|
||||
@@ -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 A–Z',
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user