i18n(sync-audit): localize the memory sync-audit panel (#3706)

This commit is contained in:
obchain
2026-06-22 19:15:57 +05:30
committed by GitHub
parent 62fb5beab1
commit 44ff81642b
16 changed files with 237 additions and 9 deletions
@@ -17,7 +17,7 @@ import { render, screen, waitFor, within } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { SyncAuditEntry } from '../../utils/tauriCommands';
import { SyncAuditPanel } from './SyncAuditPanel';
import { SyncAuditPanel, timeAgo } from './SyncAuditPanel';
const mockAuditLog = vi.fn();
@@ -166,3 +166,16 @@ describe('<SyncAuditPanel />', () => {
expect(within(table).getByText('Cost')).toBeInTheDocument();
});
});
describe('timeAgo', () => {
// Resolve to the fallback English so the `{n}` interpolation is exercised.
const t = (_key: string, fallback?: string) => fallback ?? _key;
const isoAgo = (ms: number) => new Date(Date.now() - ms).toISOString();
it('covers every relative-time bucket and substitutes the {n} placeholder', () => {
expect(timeAgo(isoAgo(0), t)).toBe('just now');
expect(timeAgo(isoAgo(5 * 60_000), t)).toBe('5m ago');
expect(timeAgo(isoAgo(3 * 60 * 60_000), t)).toBe('3h ago');
expect(timeAgo(isoAgo(2 * 24 * 60 * 60_000), t)).toBe('2d ago');
});
});
@@ -35,15 +35,18 @@ function scopeLabel(scope: string): string {
return scope;
}
function timeAgo(iso: string): string {
// `t` is threaded in because this is a module-level helper with no hook scope.
// The `{n}` placeholder follows the codebase's interpolation convention
// (t(...).replace('{n}', value)) — `t()` itself does not interpolate params.
export function timeAgo(iso: string, t: (key: string, fallback?: string) => string): string {
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff / 60_000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
if (mins < 1) return t('sync.timeAgo.justNow', 'just now');
if (mins < 60) return t('sync.timeAgo.minutes', '{n}m ago').replace('{n}', String(mins));
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
if (hours < 24) return t('sync.timeAgo.hours', '{n}h ago').replace('{n}', String(hours));
const days = Math.floor(hours / 24);
return `${days}d ago`;
return t('sync.timeAgo.days', '{n}d ago').replace('{n}', String(days));
}
export function SyncAuditPanel() {
@@ -126,7 +129,7 @@ export function SyncAuditPanel() {
<td
className="px-3 py-1.5 text-stone-600 dark:text-neutral-300 whitespace-nowrap"
title={e.timestamp}>
{timeAgo(e.timestamp)}
{timeAgo(e.timestamp, t)}
</td>
<td
className="px-3 py-1.5 text-stone-700 dark:text-neutral-200 truncate max-w-[180px]"
@@ -149,11 +152,13 @@ export function SyncAuditPanel() {
</td>
<td className="px-3 py-1.5 text-center">
{e.success ? (
<span className="text-green-500" title="Success">
<span className="text-green-500" title={t('sync.status.success', 'Success')}>
</span>
) : (
<span className="text-red-500" title={e.error ?? 'Failed'}>
<span
className="text-red-500"
title={e.error ?? t('sync.status.failed', 'Failed')}>
</span>
)}
+15
View File
@@ -108,6 +108,21 @@ const messages: TranslationMap = {
'common.create': 'إنشاء',
'common.search': 'بحث',
'common.loading': 'جارٍ التحميل…',
'sync.runs': 'عمليات مزامنة',
'sync.totalCost': 'الإجمالي',
'sync.when': 'الوقت',
'sync.source': 'المصدر',
'sync.items': 'العناصر',
'sync.tokens': 'الرموز',
'sync.cost': 'التكلفة',
'sync.duration': 'المدة',
'sync.noAuditEntries': 'لم تُسجَّل أي عمليات مزامنة بعد.',
'sync.timeAgo.justNow': 'الآن',
'sync.timeAgo.minutes': 'قبل {n} د',
'sync.timeAgo.hours': 'قبل {n} س',
'sync.timeAgo.days': 'قبل {n} ي',
'sync.status.success': 'نجاح',
'sync.status.failed': 'فشل',
'common.error': 'خطأ',
'common.success': 'نجاح',
'common.back': 'رجوع',
+15
View File
@@ -109,6 +109,21 @@ const messages: TranslationMap = {
'common.create': 'তৈরি করুন',
'common.search': 'খুঁজুন',
'common.loading': 'লোড হচ্ছে…',
'sync.runs': 'সিঙ্ক রান',
'sync.totalCost': 'মোট',
'sync.when': 'কখন',
'sync.source': 'উৎস',
'sync.items': 'আইটেম',
'sync.tokens': 'টোকেন',
'sync.cost': 'খরচ',
'sync.duration': 'সময়কাল',
'sync.noAuditEntries': 'এখনও কোনো সিঙ্ক রান রেকর্ড করা হয়নি।',
'sync.timeAgo.justNow': 'এইমাত্র',
'sync.timeAgo.minutes': '{n} মি. আগে',
'sync.timeAgo.hours': '{n} ঘ. আগে',
'sync.timeAgo.days': '{n} দি. আগে',
'sync.status.success': 'সফল',
'sync.status.failed': 'ব্যর্থ',
'common.error': 'ত্রুটি',
'common.success': 'সফল',
'common.back': 'পেছনে',
+15
View File
@@ -109,6 +109,21 @@ const messages: TranslationMap = {
'common.create': 'Erstellen',
'common.search': 'Suchen',
'common.loading': 'Laden…',
'sync.runs': 'Synchronisierungen',
'sync.totalCost': 'Gesamt',
'sync.when': 'Wann',
'sync.source': 'Quelle',
'sync.items': 'Elemente',
'sync.tokens': 'Tokens',
'sync.cost': 'Kosten',
'sync.duration': 'Dauer',
'sync.noAuditEntries': 'Noch keine Synchronisierungen aufgezeichnet.',
'sync.timeAgo.justNow': 'gerade eben',
'sync.timeAgo.minutes': 'vor {n} Min.',
'sync.timeAgo.hours': 'vor {n} Std.',
'sync.timeAgo.days': 'vor {n} T.',
'sync.status.success': 'Erfolg',
'sync.status.failed': 'Fehlgeschlagen',
'common.error': 'Fehler',
'common.success': 'Erfolg',
'common.back': 'Zurück',
+15
View File
@@ -97,6 +97,21 @@ const en: TranslationMap = {
'common.create': 'Create',
'common.search': 'Search',
'common.loading': 'Loading…',
'sync.runs': 'sync runs',
'sync.totalCost': 'total',
'sync.when': 'When',
'sync.source': 'Source',
'sync.items': 'Items',
'sync.tokens': 'Tokens',
'sync.cost': 'Cost',
'sync.duration': 'Duration',
'sync.noAuditEntries': 'No sync runs recorded yet.',
'sync.timeAgo.justNow': 'just now',
'sync.timeAgo.minutes': '{n}m ago',
'sync.timeAgo.hours': '{n}h ago',
'sync.timeAgo.days': '{n}d ago',
'sync.status.success': 'Success',
'sync.status.failed': 'Failed',
'common.error': 'Error',
'common.success': 'Success',
'common.back': 'Back',
+15
View File
@@ -109,6 +109,21 @@ const messages: TranslationMap = {
'common.create': 'Crear',
'common.search': 'Buscar',
'common.loading': 'cargando…',
'sync.runs': 'sincronizaciones',
'sync.totalCost': 'total',
'sync.when': 'Cuándo',
'sync.source': 'Origen',
'sync.items': 'Elementos',
'sync.tokens': 'Tokens',
'sync.cost': 'Costo',
'sync.duration': 'Duración',
'sync.noAuditEntries': 'Aún no hay sincronizaciones registradas.',
'sync.timeAgo.justNow': 'ahora mismo',
'sync.timeAgo.minutes': 'hace {n} min',
'sync.timeAgo.hours': 'hace {n} h',
'sync.timeAgo.days': 'hace {n} d',
'sync.status.success': 'Correcto',
'sync.status.failed': 'Fallido',
'common.error': 'error',
'common.success': 'Éxito',
'common.back': 'Atrás',
+15
View File
@@ -110,6 +110,21 @@ const messages: TranslationMap = {
'common.create': 'Créer',
'common.search': 'Rechercher',
'common.loading': 'chargement…',
'sync.runs': 'synchronisations',
'sync.totalCost': 'total',
'sync.when': 'Quand',
'sync.source': 'Source',
'sync.items': 'Éléments',
'sync.tokens': 'Jetons',
'sync.cost': 'Coût',
'sync.duration': 'Durée',
'sync.noAuditEntries': "Aucune synchronisation enregistrée pour l'instant.",
'sync.timeAgo.justNow': "à l'instant",
'sync.timeAgo.minutes': 'il y a {n} min',
'sync.timeAgo.hours': 'il y a {n} h',
'sync.timeAgo.days': 'il y a {n} j',
'sync.status.success': 'Réussi',
'sync.status.failed': 'Échec',
'common.error': 'Erreur',
'common.success': 'Succès',
'common.back': 'Retour',
+15
View File
@@ -108,6 +108,21 @@ const messages: TranslationMap = {
'common.create': 'बनाएं',
'common.search': 'सर्च करें',
'common.loading': 'लोड हो रहा है…',
'sync.runs': 'सिंक रन',
'sync.totalCost': 'कुल',
'sync.when': 'कब',
'sync.source': 'स्रोत',
'sync.items': 'आइटम',
'sync.tokens': 'टोकन',
'sync.cost': 'लागत',
'sync.duration': 'अवधि',
'sync.noAuditEntries': 'अभी तक कोई सिंक रन दर्ज नहीं हुआ।',
'sync.timeAgo.justNow': 'अभी',
'sync.timeAgo.minutes': '{n} मि. पहले',
'sync.timeAgo.hours': '{n} घं. पहले',
'sync.timeAgo.days': '{n} दि. पहले',
'sync.status.success': 'सफल',
'sync.status.failed': 'विफल',
'common.error': 'एरर',
'common.success': 'सफल',
'common.back': 'वापस',
+15
View File
@@ -108,6 +108,21 @@ const messages: TranslationMap = {
'common.create': 'Buat',
'common.search': 'Cari',
'common.loading': 'memuat…',
'sync.runs': 'sinkronisasi',
'sync.totalCost': 'total',
'sync.when': 'Kapan',
'sync.source': 'Sumber',
'sync.items': 'Item',
'sync.tokens': 'Token',
'sync.cost': 'Biaya',
'sync.duration': 'Durasi',
'sync.noAuditEntries': 'Belum ada sinkronisasi yang tercatat.',
'sync.timeAgo.justNow': 'baru saja',
'sync.timeAgo.minutes': '{n} mnt lalu',
'sync.timeAgo.hours': '{n} jam lalu',
'sync.timeAgo.days': '{n} hr lalu',
'sync.status.success': 'Berhasil',
'sync.status.failed': 'Gagal',
'common.error': 'Kesalahan',
'common.success': 'Berhasil',
'common.back': 'Kembali',
+15
View File
@@ -110,6 +110,21 @@ const messages: TranslationMap = {
'common.create': 'Crea',
'common.search': 'Cerca',
'common.loading': 'caricamento…',
'sync.runs': 'sincronizzazioni',
'sync.totalCost': 'totale',
'sync.when': 'Quando',
'sync.source': 'Origine',
'sync.items': 'Elementi',
'sync.tokens': 'Token',
'sync.cost': 'Costo',
'sync.duration': 'Durata',
'sync.noAuditEntries': 'Nessuna sincronizzazione registrata finora.',
'sync.timeAgo.justNow': 'proprio ora',
'sync.timeAgo.minutes': '{n} min fa',
'sync.timeAgo.hours': '{n} h fa',
'sync.timeAgo.days': '{n} g fa',
'sync.status.success': 'Riuscito',
'sync.status.failed': 'Non riuscito',
'common.error': 'Errore',
'common.success': 'Successo',
'common.back': 'Indietro',
+15
View File
@@ -109,6 +109,21 @@ const messages: TranslationMap = {
'common.create': '생성',
'common.search': '검색',
'common.loading': '로딩 중…',
'sync.runs': '동기화 실행',
'sync.totalCost': '합계',
'sync.when': '시간',
'sync.source': '소스',
'sync.items': '항목',
'sync.tokens': '토큰',
'sync.cost': '비용',
'sync.duration': '기간',
'sync.noAuditEntries': '아직 기록된 동기화 실행이 없습니다.',
'sync.timeAgo.justNow': '방금',
'sync.timeAgo.minutes': '{n}분 전',
'sync.timeAgo.hours': '{n}시간 전',
'sync.timeAgo.days': '{n}일 전',
'sync.status.success': '성공',
'sync.status.failed': '실패',
'common.error': '오류',
'common.success': '성공',
'common.back': '뒤로',
+15
View File
@@ -109,6 +109,21 @@ const messages: TranslationMap = {
'common.create': 'Utwórz',
'common.search': 'Szukaj',
'common.loading': 'ładowanie…',
'sync.runs': 'synchronizacji',
'sync.totalCost': 'razem',
'sync.when': 'Kiedy',
'sync.source': 'Źródło',
'sync.items': 'Elementy',
'sync.tokens': 'Tokeny',
'sync.cost': 'Koszt',
'sync.duration': 'Czas trwania',
'sync.noAuditEntries': 'Brak zarejestrowanych synchronizacji.',
'sync.timeAgo.justNow': 'przed chwilą',
'sync.timeAgo.minutes': '{n} min temu',
'sync.timeAgo.hours': '{n} godz. temu',
'sync.timeAgo.days': '{n} dni temu',
'sync.status.success': 'Sukces',
'sync.status.failed': 'Niepowodzenie',
'common.error': 'Błąd',
'common.success': 'Sukces',
'common.back': 'Wstecz',
+15
View File
@@ -110,6 +110,21 @@ const messages: TranslationMap = {
'common.create': 'Criar',
'common.search': 'Pesquisar',
'common.loading': 'carregando…',
'sync.runs': 'sincronizações',
'sync.totalCost': 'total',
'sync.when': 'Quando',
'sync.source': 'Origem',
'sync.items': 'Itens',
'sync.tokens': 'Tokens',
'sync.cost': 'Custo',
'sync.duration': 'Duração',
'sync.noAuditEntries': 'Nenhuma sincronização registrada ainda.',
'sync.timeAgo.justNow': 'agora mesmo',
'sync.timeAgo.minutes': 'há {n} min',
'sync.timeAgo.hours': 'há {n} h',
'sync.timeAgo.days': 'há {n} d',
'sync.status.success': 'Sucesso',
'sync.status.failed': 'Falhou',
'common.error': 'Erro',
'common.success': 'Sucesso',
'common.back': 'Voltar',
+15
View File
@@ -108,6 +108,21 @@ const messages: TranslationMap = {
'common.create': 'Создать',
'common.search': 'Поиск',
'common.loading': 'загрузка…',
'sync.runs': 'синхронизаций',
'sync.totalCost': 'итого',
'sync.when': 'Когда',
'sync.source': 'Источник',
'sync.items': 'Элементы',
'sync.tokens': 'Токены',
'sync.cost': 'Стоимость',
'sync.duration': 'Длительность',
'sync.noAuditEntries': 'Синхронизаций пока не зафиксировано.',
'sync.timeAgo.justNow': 'только что',
'sync.timeAgo.minutes': '{n} мин назад',
'sync.timeAgo.hours': '{n} ч назад',
'sync.timeAgo.days': '{n} дн назад',
'sync.status.success': 'Успешно',
'sync.status.failed': 'Сбой',
'common.error': 'Ошибка',
'common.success': 'Готово',
'common.back': 'Назад',
+15
View File
@@ -108,6 +108,21 @@ const messages: TranslationMap = {
'common.create': '创建',
'common.search': '搜索',
'common.loading': '加载中…',
'sync.runs': '次同步',
'sync.totalCost': '合计',
'sync.when': '时间',
'sync.source': '来源',
'sync.items': '条目',
'sync.tokens': '令牌',
'sync.cost': '费用',
'sync.duration': '时长',
'sync.noAuditEntries': '尚未记录任何同步。',
'sync.timeAgo.justNow': '刚刚',
'sync.timeAgo.minutes': '{n}分钟前',
'sync.timeAgo.hours': '{n}小时前',
'sync.timeAgo.days': '{n}天前',
'sync.status.success': '成功',
'sync.status.failed': '失败',
'common.error': '错误',
'common.success': '成功',
'common.back': '返回',