feat: localize usage page
This commit is contained in:
+98
-2
@@ -193,7 +193,55 @@ var TRANSLATIONS = {
|
||||
|
||||
// Usage page
|
||||
'usage.title': 'Usage',
|
||||
'usage.subtitle': 'Request volume and performance metrics',
|
||||
'usage.subtitle': 'Invocation metrics for all operations in your workspace.',
|
||||
'usage.export': 'Export CSV',
|
||||
'usage.period.7d': 'Last 7 days',
|
||||
'usage.period.30d': 'Last 30 days',
|
||||
'usage.period.90d': 'Last 90 days',
|
||||
'usage.period.this_month': 'This month',
|
||||
'usage.period.label.7d': 'last 7 days',
|
||||
'usage.period.label.30d': 'last 30 days',
|
||||
'usage.period.label.90d': 'last 90 days',
|
||||
'usage.period.label.this_month': 'this month',
|
||||
'usage.stats.total': 'Total invocations',
|
||||
'usage.stats.success': 'Success rate',
|
||||
'usage.stats.p50': 'Median latency (p50)',
|
||||
'usage.stats.p99': 'p99 latency',
|
||||
'usage.stats.across': 'Across {period}',
|
||||
'usage.stats.success_calls': '{count} successful calls',
|
||||
'usage.stats.median': 'Median latency for selected period',
|
||||
'usage.stats.error_calls': '{count} error calls',
|
||||
'usage.chart.title': 'Invocations over time',
|
||||
'usage.chart.success': 'Success',
|
||||
'usage.chart.error': 'Error',
|
||||
'usage.chart.empty.title': 'No usage data yet',
|
||||
'usage.chart.empty.sub': 'Invocation metrics will appear here after tests or published tool calls in the selected period.',
|
||||
'usage.table.title': 'By operation',
|
||||
'usage.table.subtitle': 'Breakdown for {period}',
|
||||
'usage.table.th.operation': 'Operation',
|
||||
'usage.table.th.protocol': 'Protocol',
|
||||
'usage.table.th.calls': 'Calls',
|
||||
'usage.table.th.errors': 'Errors',
|
||||
'usage.table.th.error_rate': 'Error rate',
|
||||
'usage.table.th.latency': 'Latency (p50 / p95 / p99)',
|
||||
'usage.table.th.share': 'Traffic share',
|
||||
'usage.table.empty.title': 'No operation breakdown yet',
|
||||
'usage.table.empty.sub': 'The selected period has no invocation samples to break down by operation.',
|
||||
'usage.table.share': '{value}% of workspace traffic',
|
||||
'usage.loading.title': 'Loading usage…',
|
||||
'usage.loading.sub': 'Aggregating invocation metrics for the selected workspace.',
|
||||
'usage.error.title': 'Unable to load usage',
|
||||
'usage.error.api': 'Admin API is not available',
|
||||
'usage.error.workspace': 'Workspace is not selected',
|
||||
'usage.error.load': 'Failed to load usage',
|
||||
'usage.export.empty.title': 'No usage data loaded',
|
||||
'usage.export.empty.body': 'Load usage data before exporting the CSV snapshot.',
|
||||
'usage.export.done.title': 'Usage exported',
|
||||
'usage.export.done.body': 'The current usage snapshot was exported as CSV.',
|
||||
'usage.chart.ok': '{count} ok',
|
||||
'usage.chart.errors': '{count} errors',
|
||||
'usage.chart.week': 'Wk {index}',
|
||||
'usage.csv.header': 'Operation,Protocol,Calls,Errors,Error Rate (%),p50 (ms),p95 (ms),p99 (ms)',
|
||||
|
||||
// Settings sidebar
|
||||
'settings.title': 'Settings',
|
||||
@@ -512,7 +560,55 @@ var TRANSLATIONS = {
|
||||
|
||||
// Usage page
|
||||
'usage.title': 'Использование',
|
||||
'usage.subtitle': 'Объём запросов и метрики производительности',
|
||||
'usage.subtitle': 'Метрики вызовов по всем операциям этого воркспейса.',
|
||||
'usage.export': 'Экспорт CSV',
|
||||
'usage.period.7d': 'Последние 7 дней',
|
||||
'usage.period.30d': 'Последние 30 дней',
|
||||
'usage.period.90d': 'Последние 90 дней',
|
||||
'usage.period.this_month': 'Этот месяц',
|
||||
'usage.period.label.7d': 'последние 7 дней',
|
||||
'usage.period.label.30d': 'последние 30 дней',
|
||||
'usage.period.label.90d': 'последние 90 дней',
|
||||
'usage.period.label.this_month': 'этот месяц',
|
||||
'usage.stats.total': 'Всего вызовов',
|
||||
'usage.stats.success': 'Доля успешных',
|
||||
'usage.stats.p50': 'Медианная задержка (p50)',
|
||||
'usage.stats.p99': 'Задержка p99',
|
||||
'usage.stats.across': 'За период: {period}',
|
||||
'usage.stats.success_calls': 'Успешных вызовов: {count}',
|
||||
'usage.stats.median': 'Медианная задержка за выбранный период',
|
||||
'usage.stats.error_calls': 'Ошибочных вызовов: {count}',
|
||||
'usage.chart.title': 'Вызовы во времени',
|
||||
'usage.chart.success': 'Успех',
|
||||
'usage.chart.error': 'Ошибка',
|
||||
'usage.chart.empty.title': 'Данных по использованию пока нет',
|
||||
'usage.chart.empty.sub': 'Метрики появятся здесь после тестов или вызовов опубликованных инструментов за выбранный период.',
|
||||
'usage.table.title': 'По операциям',
|
||||
'usage.table.subtitle': 'Разбивка за период: {period}',
|
||||
'usage.table.th.operation': 'Операция',
|
||||
'usage.table.th.protocol': 'Протокол',
|
||||
'usage.table.th.calls': 'Вызовы',
|
||||
'usage.table.th.errors': 'Ошибки',
|
||||
'usage.table.th.error_rate': 'Доля ошибок',
|
||||
'usage.table.th.latency': 'Задержка (p50 / p95 / p99)',
|
||||
'usage.table.th.share': 'Доля трафика',
|
||||
'usage.table.empty.title': 'Разбивки по операциям пока нет',
|
||||
'usage.table.empty.sub': 'За выбранный период нет вызовов, которые можно разложить по операциям.',
|
||||
'usage.table.share': '{value}% трафика воркспейса',
|
||||
'usage.loading.title': 'Загрузка использования…',
|
||||
'usage.loading.sub': 'Собираем метрики вызовов для выбранного воркспейса.',
|
||||
'usage.error.title': 'Не удалось загрузить использование',
|
||||
'usage.error.api': 'Admin API недоступен',
|
||||
'usage.error.workspace': 'Воркспейс не выбран',
|
||||
'usage.error.load': 'Не удалось загрузить метрики использования',
|
||||
'usage.export.empty.title': 'Данные по использованию не загружены',
|
||||
'usage.export.empty.body': 'Сначала загрузите данные использования, а потом экспортируйте снимок CSV.',
|
||||
'usage.export.done.title': 'Использование экспортировано',
|
||||
'usage.export.done.body': 'Текущий снимок использования экспортирован в CSV.',
|
||||
'usage.chart.ok': 'Успешных: {count}',
|
||||
'usage.chart.errors': 'Ошибок: {count}',
|
||||
'usage.chart.week': 'Нед. {index}',
|
||||
'usage.csv.header': 'Операция,Протокол,Вызовы,Ошибки,Доля ошибок (%),p50 (мс),p95 (мс),p99 (мс)',
|
||||
|
||||
// Settings sidebar
|
||||
'settings.title': 'Настройки',
|
||||
|
||||
+37
-31
@@ -16,13 +16,25 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
var subtitle = document.querySelector('.section-card-subtitle');
|
||||
var statCards = document.querySelectorAll('.stats-grid .stat-card');
|
||||
|
||||
function tKey(key) {
|
||||
return window.t ? t(key) : key;
|
||||
}
|
||||
|
||||
function tfKey(key, vars) {
|
||||
return window.tf ? tf(key, vars) : tKey(key);
|
||||
}
|
||||
|
||||
function currentLocale() {
|
||||
return localStorage.getItem('crank_lang') === 'ru' ? 'ru-RU' : 'en-US';
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
return Number(value || 0).toLocaleString();
|
||||
return Number(value || 0).toLocaleString(currentLocale());
|
||||
}
|
||||
|
||||
function formatMs(value) {
|
||||
@@ -36,13 +48,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
|
||||
function periodLabel(period) {
|
||||
var labels = {
|
||||
'7d': 'last 7 days',
|
||||
'30d': 'last 30 days',
|
||||
'90d': 'last 90 days',
|
||||
'this_month': 'this month',
|
||||
};
|
||||
return labels[period] || period;
|
||||
return tKey('usage.period.label.' + period);
|
||||
}
|
||||
|
||||
function protocolColor(protocol) {
|
||||
@@ -68,12 +74,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
function chartBucketLabel(timestamp, period, index) {
|
||||
var date = new Date(timestamp);
|
||||
if (period === '7d') {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
return date.toLocaleDateString(currentLocale(), { weekday: 'short' });
|
||||
}
|
||||
if (period === '90d') {
|
||||
return date.toLocaleDateString('en-US', { month: 'short' });
|
||||
return date.toLocaleDateString(currentLocale(), { month: 'short' });
|
||||
}
|
||||
return 'Wk ' + (index + 1);
|
||||
return tfKey('usage.chart.week', { index: index + 1 });
|
||||
}
|
||||
|
||||
function toUtcBucketStart(date, period) {
|
||||
@@ -186,19 +192,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
var cards = [
|
||||
{
|
||||
value: formatCount(summary ? summary.rollup.calls_total : 0),
|
||||
text: 'Across ' + periodLabel(state.period),
|
||||
text: tfKey('usage.stats.across', { period: periodLabel(state.period) }),
|
||||
},
|
||||
{
|
||||
value: ((summary ? summary.success_rate : 0) || 0).toFixed(1) + '%',
|
||||
text: formatCount(summary ? summary.rollup.calls_ok : 0) + ' successful calls',
|
||||
text: tfKey('usage.stats.success_calls', { count: formatCount(summary ? summary.rollup.calls_ok : 0) }),
|
||||
},
|
||||
{
|
||||
value: formatMs(summary ? summary.rollup.p50_ms : 0),
|
||||
text: 'Median latency for selected period',
|
||||
text: tKey('usage.stats.median'),
|
||||
},
|
||||
{
|
||||
value: formatMs(summary ? summary.rollup.p99_ms : 0),
|
||||
text: formatCount(summary ? summary.rollup.calls_error : 0) + ' error calls',
|
||||
text: tfKey('usage.stats.error_calls', { count: formatCount(summary ? summary.rollup.calls_error : 0) }),
|
||||
}
|
||||
];
|
||||
|
||||
@@ -221,8 +227,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!timeline.length) {
|
||||
chartWrap.innerHTML =
|
||||
'<div class="empty-state" style="width:100%;">' +
|
||||
'<div class="empty-state-title">No usage data yet</div>' +
|
||||
'<div class="empty-state-text">Invocation metrics will appear here after tests or published tool calls in the selected period.</div>' +
|
||||
'<div class="empty-state-title">' + tKey('usage.chart.empty.title') + '</div>' +
|
||||
'<div class="empty-state-text">' + tKey('usage.chart.empty.sub') + '</div>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
@@ -242,9 +248,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
errorHeight = Math.max(errorHeight, 6);
|
||||
}
|
||||
node.querySelector('.chart-bar.success').style.height = okHeight + 'px';
|
||||
node.querySelector('.chart-bar.success').dataset.tip = formatCount(point.calls_ok) + ' ok';
|
||||
node.querySelector('.chart-bar.success').dataset.tip = tfKey('usage.chart.ok', { count: formatCount(point.calls_ok) });
|
||||
node.querySelector('.chart-bar.error').style.height = errorHeight + 'px';
|
||||
node.querySelector('.chart-bar.error').dataset.tip = formatCount(point.calls_error) + ' errors';
|
||||
node.querySelector('.chart-bar.error').dataset.tip = tfKey('usage.chart.errors', { count: formatCount(point.calls_error) });
|
||||
node.querySelector('.chart-col-label').textContent = chartBucketLabel(point.bucket_start, state.period, index);
|
||||
chartWrap.appendChild(node);
|
||||
});
|
||||
@@ -258,8 +264,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
tableBody.innerHTML =
|
||||
'<tr><td colspan="7" style="padding:24px;">' +
|
||||
'<div class="empty-state" style="padding:0;">' +
|
||||
'<div class="empty-state-title">No operation breakdown yet</div>' +
|
||||
'<div class="empty-state-text">The selected period has no invocation samples to break down by operation.</div>' +
|
||||
'<div class="empty-state-title">' + tKey('usage.table.empty.title') + '</div>' +
|
||||
'<div class="empty-state-text">' + tKey('usage.table.empty.sub') + '</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
return;
|
||||
@@ -301,7 +307,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
node.querySelector('.latency-p95').style.width = Math.max(4, Math.round(((operation.p95_ms - operation.p50_ms) / base) * 64)) + 'px';
|
||||
node.querySelector('.latency-p99').style.width = Math.max(4, Math.round(((operation.p99_ms - operation.p95_ms) / base) * 64)) + 'px';
|
||||
|
||||
node.querySelector('.col-quota-label').textContent = share.toFixed(1) + '% of workspace traffic';
|
||||
node.querySelector('.col-quota-label').textContent = tfKey('usage.table.share', { value: share.toFixed(1) });
|
||||
node.querySelector('.col-quota-fill').style.width = share.toFixed(1) + '%';
|
||||
|
||||
tableBody.appendChild(node);
|
||||
@@ -310,12 +316,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
function renderUsage() {
|
||||
if (state.loading && !state.usage) {
|
||||
renderEmpty('Loading usage…', 'Aggregating invocation metrics for the selected workspace.');
|
||||
renderEmpty(tKey('usage.loading.title'), tKey('usage.loading.sub'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.loadError) {
|
||||
renderEmpty('Unable to load usage', state.loadError);
|
||||
renderEmpty(tKey('usage.error.title'), state.loadError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,20 +329,20 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
renderChart();
|
||||
renderTable();
|
||||
if (subtitle) {
|
||||
subtitle.textContent = 'Breakdown for ' + periodLabel(state.period);
|
||||
subtitle.textContent = tfKey('usage.table.subtitle', { period: periodLabel(state.period) });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsage() {
|
||||
if (!window.CrankApi) {
|
||||
state.loadError = 'Admin API is not available';
|
||||
state.loadError = tKey('usage.error.api');
|
||||
renderUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.loadError = 'Workspace is not selected';
|
||||
state.loadError = tKey('usage.error.workspace');
|
||||
renderUsage();
|
||||
return;
|
||||
}
|
||||
@@ -348,7 +354,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period });
|
||||
} catch (error) {
|
||||
state.loadError = error.message || 'Failed to load usage';
|
||||
state.loadError = error.message || tKey('usage.error.load');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
renderUsage();
|
||||
@@ -358,12 +364,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
function exportCsv() {
|
||||
if (!state.usage) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.info('Load usage data before exporting the CSV snapshot.', 'No usage data loaded');
|
||||
window.CrankUi.info(tKey('usage.export.empty.body'), tKey('usage.export.empty.title'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = ['Operation,Protocol,Calls,Errors,Error Rate (%),p50 (ms),p95 (ms),p99 (ms)'];
|
||||
var rows = [tKey('usage.csv.header')];
|
||||
state.usage.operations.forEach(function (operation) {
|
||||
var errorRate = operation.calls_total === 0
|
||||
? 0
|
||||
@@ -388,7 +394,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success('The current usage snapshot was exported as CSV.', 'Usage exported');
|
||||
window.CrankUi.success(tKey('usage.export.done.body'), tKey('usage.export.done.title'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user