mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
i18n(sync-audit): localize the memory sync-audit panel (#3706)
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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': 'رجوع',
|
||||
|
||||
@@ -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': 'পেছনে',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'वापस',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '뒤로',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Назад',
|
||||
|
||||
@@ -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': '返回',
|
||||
|
||||
Reference in New Issue
Block a user