Add i18n support for Settings, Smart Plugs, Notifications, and Backup/Restore

Replace all hardcoded English strings with t() translation calls across
  9 components: SettingsPage, SmartPlugCard, AddSmartPlugModal,
  SwitchbarPopover, NotificationProviderCard, AddNotificationModal,
  NotificationTemplateEditor, NotificationLogViewer, GitHubBackupSettings,
  and RestoreModal. Add ~600 new translation keys to all 7 locales
  (en, de, ja, fr, it, pt-BR, zh-CN).
This commit is contained in:
maziggy
2026-03-06 13:23:55 +01:00
parent b16b12bf8d
commit 8c308487c7
20 changed files with 5195 additions and 1017 deletions
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { History, CheckCircle, XCircle, Loader2, Trash2, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
import { api } from '../api/client';
import { parseUTCDate, formatTimeOnly, formatDateTime, type TimeFormat } from '../utils/date';
@@ -7,19 +8,6 @@ import type { NotificationLogEntry } from '../api/client';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
const EVENT_LABELS: Record<string, string> = {
print_start: 'Print Started',
print_complete: 'Print Complete',
print_failed: 'Print Failed',
print_stopped: 'Print Stopped',
print_progress: 'Progress',
printer_offline: 'Printer Offline',
printer_error: 'Printer Error',
filament_low: 'Low Filament',
maintenance_due: 'Maintenance Due',
test: 'Test',
};
const EVENT_COLORS: Record<string, string> = {
print_start: 'text-blue-400',
print_complete: 'text-bambu-green',
@@ -38,6 +26,7 @@ interface NotificationLogViewerProps {
}
export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { showToast } = useToast();
const [days, setDays] = useState(7);
@@ -83,7 +72,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) return 'Just now';
if (diff < 60000) return t('notifications.justNow');
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
@@ -97,7 +86,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
<div className="p-4 border-b border-bambu-dark-tertiary flex items-center justify-between">
<div className="flex items-center gap-3">
<History className="w-5 h-5 text-bambu-green" />
<h2 className="text-lg font-semibold text-white">Notification Log</h2>
<h2 className="text-lg font-semibold text-white">{t('notifications.notificationLog')}</h2>
</div>
<button
onClick={onClose}
@@ -112,16 +101,16 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
<div className="px-4 py-3 border-b border-bambu-dark-tertiary bg-bambu-dark/50">
<div className="flex items-center gap-6 text-sm">
<span className="text-bambu-gray">
Last {days} days: <span className="text-white font-medium">{stats.total}</span> notifications
{t('notifications.statsSummary', { days })} <span className="text-white font-medium">{stats.total}</span> {t('notifications.statsNotifications')}
</span>
<span className="flex items-center gap-1 text-bambu-green">
<CheckCircle className="w-4 h-4" />
{stats.success_count} sent
{t('notifications.statsSent', { count: stats.success_count })}
</span>
{stats.failure_count > 0 && (
<span className="flex items-center gap-1 text-red-400">
<XCircle className="w-4 h-4" />
{stats.failure_count} failed
{t('notifications.statsFailed', { count: stats.failure_count })}
</span>
)}
</div>
@@ -135,10 +124,10 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
onChange={(e) => setDays(Number(e.target.value))}
className="px-3 py-1.5 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm focus:outline-none focus:ring-1 focus:ring-bambu-green"
>
<option value={1}>Last 24 hours</option>
<option value={7}>Last 7 days</option>
<option value={30}>Last 30 days</option>
<option value={90}>Last 90 days</option>
<option value={1}>{t('notifications.last24Hours')}</option>
<option value={7}>{t('notifications.last7Days')}</option>
<option value={30}>{t('notifications.last30Days')}</option>
<option value={90}>{t('notifications.last90Days')}</option>
</select>
<label className="flex items-center gap-2 text-sm text-bambu-gray cursor-pointer">
@@ -148,7 +137,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
onChange={(e) => setShowFailedOnly(e.target.checked)}
className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
/>
Show failed only
{t('notifications.showFailedOnly')}
</label>
<div className="flex-1" />
@@ -164,7 +153,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
) : (
<RefreshCw className="w-4 h-4" />
)}
Refresh
{t('notifications.refresh')}
</Button>
<Button
@@ -179,7 +168,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
) : (
<Trash2 className="w-4 h-4" />
)}
Clear Old
{t('notifications.clearOld')}
</Button>
</div>
@@ -206,7 +195,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
<div className="text-center py-12 text-bambu-gray">
<History className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p className="text-sm">
{showFailedOnly ? 'No failed notifications' : 'No notifications logged'}
{showFailedOnly ? t('notifications.noFailedNotifications') : t('notifications.noNotificationsLogged')}
</p>
</div>
)}
@@ -229,6 +218,7 @@ function LogEntry({
formatDate: (date: string) => string;
formatFullDate: (date: string) => string;
}) {
const { t } = useTranslation();
return (
<div
className={`border rounded-lg overflow-hidden transition-colors ${
@@ -248,11 +238,11 @@ function LogEntry({
)}
<span className={`text-xs font-medium ${EVENT_COLORS[log.event_type] || 'text-bambu-gray'}`}>
{EVENT_LABELS[log.event_type] || log.event_type}
{t(`notifications.eventTypes.${log.event_type}`, log.event_type)}
</span>
<span className="text-sm text-white truncate flex-1">
{log.provider_name || 'Unknown Provider'}
{log.provider_name || t('notifications.unknownProvider')}
</span>
{log.printer_name && (
@@ -275,22 +265,22 @@ function LogEntry({
{isExpanded && (
<div className="px-3 py-2 border-t border-bambu-dark-tertiary bg-bambu-dark/20 space-y-2">
<div>
<p className="text-xs text-bambu-gray mb-1">Title</p>
<p className="text-xs text-bambu-gray mb-1">{t('notifications.logTitle')}</p>
<p className="text-sm text-white">{log.title}</p>
</div>
<div>
<p className="text-xs text-bambu-gray mb-1">Message</p>
<p className="text-xs text-bambu-gray mb-1">{t('notifications.logMessage')}</p>
<p className="text-sm text-white whitespace-pre-wrap">{log.message}</p>
</div>
{!log.success && log.error_message && (
<div>
<p className="text-xs text-red-400 mb-1">Error</p>
<p className="text-xs text-red-400 mb-1">{t('notifications.logError')}</p>
<p className="text-sm text-red-300">{log.error_message}</p>
</div>
)}
<div className="flex gap-4 text-xs text-bambu-gray pt-1">
<span>Provider: {log.provider_type}</span>
<span>Time: {formatFullDate(log.created_at)}</span>
<span>{t('notifications.logProvider', { type: log.provider_type })}</span>
<span>{t('notifications.logTime', { time: formatFullDate(log.created_at) })}</span>
</div>
</div>
)}