Przeglądaj źródła

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).
maziggy 6 miesięcy temu
rodzic
commit
8c308487c7

+ 1 - 0
CHANGELOG.md

@@ -8,6 +8,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **AMS Info Card & Custom Labels** ([#570](https://github.com/maziggy/bambuddy/pull/570)) — Hovering an AMS label (e.g. "AMS-A") on the Printers page now shows a popover with serial number, firmware version, and an editable friendly name. Custom labels are stored by AMS serial number so they persist when the unit is moved to a different printer. Slot numbers are now displayed inside each filament color circle with auto-inverted contrast for readability. Labels also appear in the Inventory page's location column. Contributed by @cadtoolbox.
 - **AMS Info Card & Custom Labels** ([#570](https://github.com/maziggy/bambuddy/pull/570)) — Hovering an AMS label (e.g. "AMS-A") on the Printers page now shows a popover with serial number, firmware version, and an editable friendly name. Custom labels are stored by AMS serial number so they persist when the unit is moved to a different printer. Slot numbers are now displayed inside each filament color circle with auto-inverted contrast for readability. Labels also appear in the Inventory page's location column. Contributed by @cadtoolbox.
 
 
 ### Improved
 ### Improved
+- **i18n: Settings, Smart Plugs, Notifications, Backup/Restore** — Replaced all hardcoded English strings with translation keys (`t()` calls) across the Settings page, Smart Plug components (SmartPlugCard, AddSmartPlugModal, SwitchbarPopover), Notification components (NotificationProviderCard, AddNotificationModal, NotificationTemplateEditor, NotificationLogViewer), and Backup/Restore components (GitHubBackupSettings, RestoreModal). Added ~600 new translation keys to all 7 supported locales (en, de, ja, fr, it, pt-BR, zh-CN). Removed hardcoded label maps (`PROVIDER_LABELS`, `EVENT_LABELS`, `CATEGORY_LABELS`) in favor of dynamic translation key lookups with fallbacks.
 - **Install Script: Branch Selection** — The native install script (`install.sh`) now supports a `--branch` option and an interactive branch prompt (defaults to `main`). Previously the script hardcoded `origin/main`, so beta testers told to install from a beta branch would silently get the stable release instead. Fresh installs use `git clone --branch`, existing installs checkout and reset to the selected branch. The install summary highlights non-main branches in yellow with a "(beta)" label. Invalid branch names are caught early with an error message listing available branches.
 - **Install Script: Branch Selection** — The native install script (`install.sh`) now supports a `--branch` option and an interactive branch prompt (defaults to `main`). Previously the script hardcoded `origin/main`, so beta testers told to install from a beta branch would silently get the stable release instead. Fresh installs use `git clone --branch`, existing installs checkout and reset to the selected branch. The install summary highlights non-main branches in yellow with a "(beta)" label. Invalid branch names are caught early with an error message listing available branches.
 - **Print Queue Scheduler Diagnostics** ([#616](https://github.com/maziggy/bambuddy/issues/616)) — Added diagnostic logging to the print queue scheduler to help diagnose why queued prints aren't starting. After each queue check, the scheduler now logs a skip summary (how many items were skipped due to manual_start, scheduled_time, etc.) and for each busy printer, logs the exact state preventing it from being considered idle (connected status, printer state, plate_cleared flag). Previously the scheduler only logged "found N pending items" with no visibility into why items were skipped.
 - **Print Queue Scheduler Diagnostics** ([#616](https://github.com/maziggy/bambuddy/issues/616)) — Added diagnostic logging to the print queue scheduler to help diagnose why queued prints aren't starting. After each queue check, the scheduler now logs a skip summary (how many items were skipped due to manual_start, scheduled_time, etc.) and for each busy printer, logs the exact state preventing it from being considered idle (connected status, printer state, plate_cleared flag). Previously the scheduler only logged "found N pending items" with no visibility into why items were skipped.
 
 

+ 42 - 48
frontend/src/components/AddNotificationModal.tsx

@@ -1,5 +1,6 @@
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
 import { X, Save, Loader2, Send, CheckCircle, XCircle } from 'lucide-react';
 import { X, Save, Loader2, Send, CheckCircle, XCircle } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { NotificationProvider, NotificationProviderCreate, NotificationProviderUpdate, ProviderType } from '../api/client';
 import type { NotificationProvider, NotificationProviderCreate, NotificationProviderUpdate, ProviderType } from '../api/client';
@@ -11,17 +12,10 @@ interface AddNotificationModalProps {
   onClose: () => void;
   onClose: () => void;
 }
 }
 
 
-const PROVIDER_OPTIONS: { value: ProviderType; label: string; description: string }[] = [
-  { value: 'email', label: 'Email', description: 'SMTP email notifications' },
-  { value: 'telegram', label: 'Telegram', description: 'Notifications via Telegram bot' },
-  { value: 'discord', label: 'Discord', description: 'Send to Discord channel via webhook' },
-  { value: 'ntfy', label: 'ntfy', description: 'Free, self-hostable push notifications' },
-  { value: 'pushover', label: 'Pushover', description: 'Simple, reliable push notifications' },
-  { value: 'callmebot', label: 'CallMeBot/WhatsApp', description: 'Free WhatsApp notifications via CallMeBot' },
-  { value: 'webhook', label: 'Webhook', description: 'Generic HTTP POST to any URL' },
-];
+const PROVIDER_VALUES: ProviderType[] = ['email', 'telegram', 'discord', 'ntfy', 'pushover', 'callmebot', 'webhook'];
 
 
 export function AddNotificationModal({ provider, onClose }: AddNotificationModalProps) {
 export function AddNotificationModal({ provider, onClose }: AddNotificationModalProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const isEditing = !!provider;
   const isEditing = !!provider;
 
 
@@ -112,7 +106,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
     setError(null);
     setError(null);
 
 
     if (!name.trim()) {
     if (!name.trim()) {
-      setError('Name is required');
+      setError(t('notifications.nameRequired'));
       return;
       return;
     }
     }
 
 
@@ -120,7 +114,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
     const requiredFields = getRequiredFields(providerType);
     const requiredFields = getRequiredFields(providerType);
     for (const field of requiredFields) {
     for (const field of requiredFields) {
       if (!config[field.key]?.trim()) {
       if (!config[field.key]?.trim()) {
-        setError(`${field.label} is required`);
+        setError(t('notifications.fieldRequired', { field: field.label }));
         return;
         return;
       }
       }
     }
     }
@@ -239,7 +233,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
         {/* Header */}
         {/* Header */}
         <div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary">
         <div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary">
           <h2 className="text-lg font-semibold text-white">
           <h2 className="text-lg font-semibold text-white">
-            {isEditing ? 'Edit Notification Provider' : 'Add Notification Provider'}
+            {isEditing ? t('notifications.editTitle') : t('notifications.addTitle')}
           </h2>
           </h2>
           <button
           <button
             onClick={onClose}
             onClick={onClose}
@@ -259,19 +253,19 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
 
 
           {/* Name */}
           {/* Name */}
           <div>
           <div>
-            <label className="block text-sm text-bambu-gray mb-1">Name *</label>
+            <label className="block text-sm text-bambu-gray mb-1">{t('notifications.nameLabel')}</label>
             <input
             <input
               type="text"
               type="text"
               value={name}
               value={name}
               onChange={(e) => setName(e.target.value)}
               onChange={(e) => setName(e.target.value)}
-              placeholder="My Notifications"
+              placeholder={t('notifications.namePlaceholder')}
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
             />
             />
           </div>
           </div>
 
 
           {/* Provider Type */}
           {/* Provider Type */}
           <div>
           <div>
-            <label className="block text-sm text-bambu-gray mb-1">Provider Type *</label>
+            <label className="block text-sm text-bambu-gray mb-1">{t('notifications.providerTypeLabel')}</label>
             <select
             <select
               value={providerType}
               value={providerType}
               onChange={(e) => {
               onChange={(e) => {
@@ -282,20 +276,20 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
               disabled={isEditing}
               disabled={isEditing}
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none disabled:opacity-50"
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none disabled:opacity-50"
             >
             >
-              {PROVIDER_OPTIONS.map((option) => (
-                <option key={option.value} value={option.value}>
-                  {option.label}
+              {PROVIDER_VALUES.map((value) => (
+                <option key={value} value={value}>
+                  {t(`notifications.providerTypes.${value}`, value)}
                 </option>
                 </option>
               ))}
               ))}
             </select>
             </select>
             <p className="text-xs text-bambu-gray mt-1">
             <p className="text-xs text-bambu-gray mt-1">
-              {PROVIDER_OPTIONS.find(o => o.value === providerType)?.description}
+              {t(`notifications.providerDescriptions.${providerType}`, '')}
             </p>
             </p>
           </div>
           </div>
 
 
           {/* Provider-specific configuration */}
           {/* Provider-specific configuration */}
           <div className="space-y-3">
           <div className="space-y-3">
-            <p className="text-sm text-bambu-gray">Configuration</p>
+            <p className="text-sm text-bambu-gray">{t('notifications.configuration')}</p>
             {configFields
             {configFields
               .filter((field) => !('showIf' in field) || (field as { showIf?: (cfg: Record<string, string>) => boolean }).showIf?.(config) !== false)
               .filter((field) => !('showIf' in field) || (field as { showIf?: (cfg: Record<string, string>) => boolean }).showIf?.(config) !== false)
               .map((field) => (
               .map((field) => (
@@ -351,7 +345,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
               ) : (
               ) : (
                 <Send className="w-4 h-4" />
                 <Send className="w-4 h-4" />
               )}
               )}
-              Test Configuration
+              {t('notifications.testConfiguration')}
             </Button>
             </Button>
           </div>
           </div>
 
 
@@ -378,13 +372,13 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
 
 
           {/* Link to Printer */}
           {/* Link to Printer */}
           <div>
           <div>
-            <label className="block text-sm text-bambu-gray mb-1">Printer Filter</label>
+            <label className="block text-sm text-bambu-gray mb-1">{t('notifications.printerFilter')}</label>
             <select
             <select
               value={printerId ?? ''}
               value={printerId ?? ''}
               onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
               onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
             >
             >
-              <option value="">All printers</option>
+              <option value="">{t('notifications.allPrinters')}</option>
               {printers?.map((p) => (
               {printers?.map((p) => (
                 <option key={p.id} value={p.id}>
                 <option key={p.id} value={p.id}>
                   {p.name}
                   {p.name}
@@ -392,14 +386,14 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
               ))}
               ))}
             </select>
             </select>
             <p className="text-xs text-bambu-gray mt-1">
             <p className="text-xs text-bambu-gray mt-1">
-              Only send notifications for events from this printer
+              {t('notifications.onlyFromPrinter')}
             </p>
             </p>
           </div>
           </div>
 
 
           {/* Quiet Hours */}
           {/* Quiet Hours */}
           <div className="space-y-2">
           <div className="space-y-2">
             <div className="flex items-center justify-between">
             <div className="flex items-center justify-between">
-              <label className="text-sm text-white">Quiet Hours (Do Not Disturb)</label>
+              <label className="text-sm text-white">{t('notifications.quietHoursDnd')}</label>
               <Toggle
               <Toggle
                 checked={quietHoursEnabled}
                 checked={quietHoursEnabled}
                 onChange={setQuietHoursEnabled}
                 onChange={setQuietHoursEnabled}
@@ -408,7 +402,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
             {quietHoursEnabled && (
             {quietHoursEnabled && (
               <div className="grid grid-cols-2 gap-3">
               <div className="grid grid-cols-2 gap-3">
                 <div>
                 <div>
-                  <label className="block text-xs text-bambu-gray mb-1">Start</label>
+                  <label className="block text-xs text-bambu-gray mb-1">{t('notifications.quietStart')}</label>
                   <input
                   <input
                     type="time"
                     type="time"
                     value={quietHoursStart}
                     value={quietHoursStart}
@@ -417,7 +411,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
                   />
                   />
                 </div>
                 </div>
                 <div>
                 <div>
-                  <label className="block text-xs text-bambu-gray mb-1">End</label>
+                  <label className="block text-xs text-bambu-gray mb-1">{t('notifications.quietEnd')}</label>
                   <input
                   <input
                     type="time"
                     type="time"
                     value={quietHoursEnd}
                     value={quietHoursEnd}
@@ -433,8 +427,8 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
           <div className="space-y-2">
           <div className="space-y-2">
             <div className="flex items-center justify-between">
             <div className="flex items-center justify-between">
               <div>
               <div>
-                <label className="text-sm text-white">Daily Digest</label>
-                <p className="text-xs text-bambu-gray">Batch notifications into a single daily summary</p>
+                <label className="text-sm text-white">{t('notifications.dailyDigestLabel')}</label>
+                <p className="text-xs text-bambu-gray">{t('notifications.batchNotifications')}</p>
               </div>
               </div>
               <Toggle
               <Toggle
                 checked={dailyDigestEnabled}
                 checked={dailyDigestEnabled}
@@ -443,7 +437,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
             </div>
             </div>
             {dailyDigestEnabled && (
             {dailyDigestEnabled && (
               <div>
               <div>
-                <label className="block text-xs text-bambu-gray mb-1">Send digest at</label>
+                <label className="block text-xs text-bambu-gray mb-1">{t('notifications.sendDigestAt')}</label>
                 <input
                 <input
                   type="time"
                   type="time"
                   value={dailyDigestTime}
                   value={dailyDigestTime}
@@ -451,7 +445,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
                   className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                   className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 />
                 />
                 <p className="text-xs text-bambu-gray mt-1">
                 <p className="text-xs text-bambu-gray mt-1">
-                  Events will be collected and sent as a single summary at this time
+                  {t('notifications.digestCollected')}
                 </p>
                 </p>
               </div>
               </div>
             )}
             )}
@@ -459,39 +453,39 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
 
 
           {/* Event Toggles */}
           {/* Event Toggles */}
           <div className="space-y-3">
           <div className="space-y-3">
-            <p className="text-sm text-bambu-gray">Notification Events</p>
+            <p className="text-sm text-bambu-gray">{t('notifications.notificationEvents')}</p>
 
 
             {/* Print Events */}
             {/* Print Events */}
             <div className="space-y-2 p-3 bg-bambu-dark rounded-lg">
             <div className="space-y-2 p-3 bg-bambu-dark rounded-lg">
-              <p className="text-xs text-bambu-gray uppercase tracking-wide mb-2">Print Events</p>
+              <p className="text-xs text-bambu-gray uppercase tracking-wide mb-2">{t('notifications.printEvents')}</p>
               <div className="grid grid-cols-2 gap-2">
               <div className="grid grid-cols-2 gap-2">
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Start</span>
+                  <span className="text-sm text-white">{t('notifications.start')}</span>
                   <Toggle checked={onPrintStart} onChange={setOnPrintStart} />
                   <Toggle checked={onPrintStart} onChange={setOnPrintStart} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Complete</span>
+                  <span className="text-sm text-white">{t('notifications.complete')}</span>
                   <Toggle checked={onPrintComplete} onChange={setOnPrintComplete} />
                   <Toggle checked={onPrintComplete} onChange={setOnPrintComplete} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Failed</span>
+                  <span className="text-sm text-white">{t('notifications.failed')}</span>
                   <Toggle checked={onPrintFailed} onChange={setOnPrintFailed} />
                   <Toggle checked={onPrintFailed} onChange={setOnPrintFailed} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Stopped</span>
+                  <span className="text-sm text-white">{t('notifications.stopped')}</span>
                   <Toggle checked={onPrintStopped} onChange={setOnPrintStopped} />
                   <Toggle checked={onPrintStopped} onChange={setOnPrintStopped} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between col-span-2">
                 <div className="flex items-center justify-between col-span-2">
                   <div>
                   <div>
-                    <span className="text-sm text-white">Progress</span>
-                    <span className="text-xs text-bambu-gray ml-1">(25%, 50%, 75%)</span>
+                    <span className="text-sm text-white">{t('notifications.progress')}</span>
+                    <span className="text-xs text-bambu-gray ml-1">{t('notifications.progressPercent')}</span>
                   </div>
                   </div>
                   <Toggle checked={onPrintProgress} onChange={setOnPrintProgress} />
                   <Toggle checked={onPrintProgress} onChange={setOnPrintProgress} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between col-span-2">
                 <div className="flex items-center justify-between col-span-2">
                   <div>
                   <div>
-                    <span className="text-sm text-white">Bed Cooled</span>
-                    <span className="text-xs text-bambu-gray ml-1">(after print completes)</span>
+                    <span className="text-sm text-white">{t('notifications.bedCooled')}</span>
+                    <span className="text-xs text-bambu-gray ml-1">{t('notifications.bedCooledAfterPrint')}</span>
                   </div>
                   </div>
                   <Toggle checked={onBedCooled} onChange={setOnBedCooled} />
                   <Toggle checked={onBedCooled} onChange={setOnBedCooled} />
                 </div>
                 </div>
@@ -500,22 +494,22 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
 
 
             {/* Printer Status Events */}
             {/* Printer Status Events */}
             <div className="space-y-2 p-3 bg-bambu-dark rounded-lg">
             <div className="space-y-2 p-3 bg-bambu-dark rounded-lg">
-              <p className="text-xs text-bambu-gray uppercase tracking-wide mb-2">Printer Status</p>
+              <p className="text-xs text-bambu-gray uppercase tracking-wide mb-2">{t('notifications.printerStatus')}</p>
               <div className="grid grid-cols-2 gap-2">
               <div className="grid grid-cols-2 gap-2">
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Offline</span>
+                  <span className="text-sm text-white">{t('notifications.offline')}</span>
                   <Toggle checked={onPrinterOffline} onChange={setOnPrinterOffline} />
                   <Toggle checked={onPrinterOffline} onChange={setOnPrinterOffline} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Error</span>
+                  <span className="text-sm text-white">{t('notifications.error')}</span>
                   <Toggle checked={onPrinterError} onChange={setOnPrinterError} />
                   <Toggle checked={onPrinterError} onChange={setOnPrinterError} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Low Filament</span>
+                  <span className="text-sm text-white">{t('notifications.lowFilament')}</span>
                   <Toggle checked={onFilamentLow} onChange={setOnFilamentLow} />
                   <Toggle checked={onFilamentLow} onChange={setOnFilamentLow} />
                 </div>
                 </div>
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <span className="text-sm text-white">Maintenance</span>
+                  <span className="text-sm text-white">{t('notifications.maintenance')}</span>
                   <Toggle checked={onMaintenanceDue} onChange={setOnMaintenanceDue} />
                   <Toggle checked={onMaintenanceDue} onChange={setOnMaintenanceDue} />
                 </div>
                 </div>
               </div>
               </div>
@@ -530,7 +524,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
               onClick={onClose}
               onClick={onClose}
               className="flex-1"
               className="flex-1"
             >
             >
-              Cancel
+              {t('notifications.cancel')}
             </Button>
             </Button>
             <Button
             <Button
               type="submit"
               type="submit"
@@ -542,7 +536,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
               ) : (
               ) : (
                 <Save className="w-4 h-4" />
                 <Save className="w-4 h-4" />
               )}
               )}
-              {isEditing ? 'Save' : 'Add'}
+              {isEditing ? t('notifications.save') : t('notifications.add')}
             </Button>
             </Button>
           </div>
           </div>
         </form>
         </form>

+ 80 - 83
frontend/src/components/AddSmartPlugModal.tsx

@@ -207,7 +207,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
     } catch (err) {
     } catch (err) {
       setIsScanning(false);
       setIsScanning(false);
       const errorMsg = err instanceof Error ? err.message : (typeof err === 'string' ? err : JSON.stringify(err));
       const errorMsg = err instanceof Error ? err.message : (typeof err === 'string' ? err : JSON.stringify(err));
-      setError(errorMsg || 'Failed to start scan');
+      setError(errorMsg || t('smartPlugs.failedToStartScan'));
     }
     }
   };
   };
 
 
@@ -295,7 +295,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
     setError(null);
     setError(null);
 
 
     if (!name.trim()) {
     if (!name.trim()) {
-      setError('Name is required');
+      setError(t('smartPlugs.nameRequired'));
       return;
       return;
     }
     }
 
 
@@ -305,7 +305,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
     }
     }
 
 
     if (plugType === 'homeassistant' && !haEntityId) {
     if (plugType === 'homeassistant' && !haEntityId) {
-      setError('Entity is required for Home Assistant plugs');
+      setError(t('smartPlugs.entityRequired'));
       return;
       return;
     }
     }
 
 
@@ -316,7 +316,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
       const hasState = mqttStateTopic.trim();
       const hasState = mqttStateTopic.trim();
 
 
       if (!hasPower && !hasEnergy && !hasState) {
       if (!hasPower && !hasEnergy && !hasState) {
-        setError('At least one MQTT topic must be configured for power, energy, or state monitoring');
+        setError(t('smartPlugs.mqttTopicRequired'));
         return;
         return;
       }
       }
     }
     }
@@ -379,7 +379,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
         {/* Header */}
         {/* Header */}
         <div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary flex-shrink-0">
         <div className="flex items-center justify-between px-6 py-4 border-b border-bambu-dark-tertiary flex-shrink-0">
           <h2 className="text-lg font-semibold text-white">
           <h2 className="text-lg font-semibold text-white">
-            {isEditing ? 'Edit Smart Plug' : 'Add Smart Plug'}
+            {isEditing ? t('smartPlugs.editTitle') : t('smartPlugs.addTitle')}
           </h2>
           </h2>
           <button
           <button
             onClick={onClose}
             onClick={onClose}
@@ -458,12 +458,12 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {isScanning ? (
               {isScanning ? (
                 <Button type="button" variant="secondary" onClick={stopScan} className="w-full">
                 <Button type="button" variant="secondary" onClick={stopScan} className="w-full">
                   <X className="w-4 h-4" />
                   <X className="w-4 h-4" />
-                  Stop Scanning
+                  {t('smartPlugs.stopScanning')}
                 </Button>
                 </Button>
               ) : (
               ) : (
                 <Button type="button" variant="primary" onClick={startScan} className="w-full">
                 <Button type="button" variant="primary" onClick={startScan} className="w-full">
                   <Search className="w-4 h-4" />
                   <Search className="w-4 h-4" />
-                  Discover Tasmota Devices
+                  {t('smartPlugs.discoverTasmota')}
                 </Button>
                 </Button>
               )}
               )}
 
 
@@ -486,7 +486,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {/* Discovered devices */}
               {/* Discovered devices */}
               {discoveredDevices.length > 0 && (
               {discoveredDevices.length > 0 && (
                 <div className="space-y-2">
                 <div className="space-y-2">
-                  <p className="text-xs text-bambu-gray">Found {discoveredDevices.length} device(s) - click to select:</p>
+                  <p className="text-xs text-bambu-gray">{t('smartPlugs.foundDevices', { count: discoveredDevices.length })}</p>
                   <div className="max-h-40 overflow-y-auto space-y-1">
                   <div className="max-h-40 overflow-y-auto space-y-1">
                     {discoveredDevices.map((device) => (
                     {discoveredDevices.map((device) => (
                       <button
                       <button
@@ -518,7 +518,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
               {!isScanning && discoveredDevices.length === 0 && scanProgress.total > 0 && (
               {!isScanning && discoveredDevices.length === 0 && scanProgress.total > 0 && (
                 <p className="text-xs text-bambu-gray text-center py-2">
                 <p className="text-xs text-bambu-gray text-center py-2">
-                  No Tasmota devices found on your network
+                  {t('smartPlugs.noDevicesFound')}
                 </p>
                 </p>
               )}
               )}
             </div>
             </div>
@@ -531,11 +531,11 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {!haConfigured && (
               {!haConfigured && (
                 <div className="space-y-3">
                 <div className="space-y-3">
                   <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
                   <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
-                    Home Assistant is not configured. Set it up in{' '}
-                    <span className="font-medium">Settings → Network → Home Assistant</span>
+                    {t('smartPlugs.haNotConfigured')}{' '}
+                    <span className="font-medium">{t('smartPlugs.haSettingsPath')}</span>
                   </div>
                   </div>
                   <div>
                   <div>
-                    <label className="block text-sm text-bambu-gray mb-1 opacity-50">Select Entity *</label>
+                    <label className="block text-sm text-bambu-gray mb-1 opacity-50">{t('smartPlugs.selectEntity')}</label>
                     <select
                     <select
                       disabled
                       disabled
                       className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray cursor-not-allowed opacity-50"
                       className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-bambu-gray cursor-not-allowed opacity-50"
@@ -552,13 +552,13 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                   {haEntitiesLoading && (
                   {haEntitiesLoading && (
                     <div className="flex items-center justify-center py-4 text-bambu-gray">
                     <div className="flex items-center justify-center py-4 text-bambu-gray">
                       <Loader2 className="w-5 h-5 animate-spin mr-2" />
                       <Loader2 className="w-5 h-5 animate-spin mr-2" />
-                      Loading entities...
+                      {t('smartPlugs.loadingEntities')}
                     </div>
                     </div>
                   )}
                   )}
 
 
                   {haEntitiesError && (
                   {haEntitiesError && (
                     <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
                     <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
-                      Failed to load entities: {(haEntitiesError as Error).message}
+                      {t('smartPlugs.failedToLoadEntities', { error: (haEntitiesError as Error).message })}
                     </div>
                     </div>
                   )}
                   )}
 
 
@@ -573,7 +573,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
                     return (
                     return (
                       <div ref={entityDropdownRef} className="relative">
                       <div ref={entityDropdownRef} className="relative">
-                        <label className="block text-sm text-bambu-gray mb-1">Select Entity *</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.selectEntity')}</label>
                         <div className="relative">
                         <div className="relative">
                           <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                           <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                           <input
                           <input
@@ -613,14 +613,14 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                             {haEntitiesLoading && (
                             {haEntitiesLoading && (
                               <div className="px-3 py-2 text-sm text-bambu-gray flex items-center gap-2">
                               <div className="px-3 py-2 text-sm text-bambu-gray flex items-center gap-2">
                                 <Loader2 className="w-4 h-4 animate-spin" />
                                 <Loader2 className="w-4 h-4 animate-spin" />
-                                Loading...
+                                {t('smartPlugs.loading')}
                               </div>
                               </div>
                             )}
                             )}
                             {!haEntitiesLoading && availableEntities.length === 0 && (
                             {!haEntitiesLoading && availableEntities.length === 0 && (
                               <div className="px-3 py-2 text-sm text-bambu-gray">
                               <div className="px-3 py-2 text-sm text-bambu-gray">
                                 {debouncedSearch
                                 {debouncedSearch
-                                  ? `No entities found matching "${debouncedSearch}"`
-                                  : 'No entities available'}
+                                  ? t('smartPlugs.noEntitiesMatching', { search: debouncedSearch })
+                                  : t('smartPlugs.noEntitiesAvailable')}
                               </div>
                               </div>
                             )}
                             )}
                             {!haEntitiesLoading && availableEntities.map((entity) => (
                             {!haEntitiesLoading && availableEntities.map((entity) => (
@@ -652,8 +652,8 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
                         <p className="text-xs text-bambu-gray mt-1">
                         <p className="text-xs text-bambu-gray mt-1">
                           {debouncedSearch
                           {debouncedSearch
-                            ? `Searching all entities (${availableEntities.length} found)`
-                            : `Showing switch, light, input_boolean (${availableEntities.length} available)`}
+                            ? t('smartPlugs.searchingEntities', { count: availableEntities.length })
+                            : t('smartPlugs.showingEntities', { count: availableEntities.length })}
                         </p>
                         </p>
                       </div>
                       </div>
                     );
                     );
@@ -664,9 +664,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                   {haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
                   {haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
                     <div className="border-t border-bambu-dark-tertiary pt-4 mt-4 space-y-3">
                     <div className="border-t border-bambu-dark-tertiary pt-4 mt-4 space-y-3">
                       <div>
                       <div>
-                        <p className="text-white font-medium mb-1">Energy Monitoring (Optional)</p>
+                        <p className="text-white font-medium mb-1">{t('smartPlugs.energyMonitoringOptional')}</p>
                         <p className="text-xs text-bambu-gray mb-3">
                         <p className="text-xs text-bambu-gray mb-3">
-                          Search and select sensors that provide power/energy data.
+                          {t('smartPlugs.energyMonitoringHint')}
                         </p>
                         </p>
                       </div>
                       </div>
 
 
@@ -685,7 +685,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
                         return (
                         return (
                           <div ref={powerDropdownRef} className="relative">
                           <div ref={powerDropdownRef} className="relative">
-                            <label className="block text-sm text-bambu-gray mb-1">Power Sensor (W)</label>
+                            <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.powerSensorW')}</label>
                             <div className="relative">
                             <div className="relative">
                               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                               <input
                               <input
@@ -726,7 +726,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   }}
                                   }}
                                   className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
                                   className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
                                 >
                                 >
-                                  None
+                                  {t('smartPlugs.none')}
                                 </button>
                                 </button>
                                 {filteredPowerSensors.map((sensor) => (
                                 {filteredPowerSensors.map((sensor) => (
                                   <button
                                   <button
@@ -746,7 +746,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   </button>
                                   </button>
                                 ))}
                                 ))}
                                 {filteredPowerSensors.length === 0 && (
                                 {filteredPowerSensors.length === 0 && (
-                                  <div className="px-3 py-2 text-sm text-bambu-gray">No matching sensors</div>
+                                  <div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
                                 )}
                                 )}
                               </div>
                               </div>
                             )}
                             )}
@@ -769,7 +769,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
                         return (
                         return (
                           <div ref={energyTodayDropdownRef} className="relative">
                           <div ref={energyTodayDropdownRef} className="relative">
-                            <label className="block text-sm text-bambu-gray mb-1">Energy Today (kWh)</label>
+                            <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.energyTodayKwh')}</label>
                             <div className="relative">
                             <div className="relative">
                               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                               <input
                               <input
@@ -810,7 +810,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   }}
                                   }}
                                   className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
                                   className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
                                 >
                                 >
-                                  None
+                                  {t('smartPlugs.none')}
                                 </button>
                                 </button>
                                 {filteredEnergySensors.map((sensor) => (
                                 {filteredEnergySensors.map((sensor) => (
                                   <button
                                   <button
@@ -830,7 +830,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   </button>
                                   </button>
                                 ))}
                                 ))}
                                 {filteredEnergySensors.length === 0 && (
                                 {filteredEnergySensors.length === 0 && (
-                                  <div className="px-3 py-2 text-sm text-bambu-gray">No matching sensors</div>
+                                  <div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
                                 )}
                                 )}
                               </div>
                               </div>
                             )}
                             )}
@@ -853,7 +853,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
                         return (
                         return (
                           <div ref={energyTotalDropdownRef} className="relative">
                           <div ref={energyTotalDropdownRef} className="relative">
-                            <label className="block text-sm text-bambu-gray mb-1">Total Energy (kWh)</label>
+                            <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.totalEnergyKwh')}</label>
                             <div className="relative">
                             <div className="relative">
                               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                               <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                               <input
                               <input
@@ -894,7 +894,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   }}
                                   }}
                                   className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
                                   className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
                                 >
                                 >
-                                  None
+                                  {t('smartPlugs.none')}
                                 </button>
                                 </button>
                                 {filteredEnergySensors.map((sensor) => (
                                 {filteredEnergySensors.map((sensor) => (
                                   <button
                                   <button
@@ -914,7 +914,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   </button>
                                   </button>
                                 ))}
                                 ))}
                                 {filteredEnergySensors.length === 0 && (
                                 {filteredEnergySensors.length === 0 && (
-                                  <div className="px-3 py-2 text-sm text-bambu-gray">No matching sensors</div>
+                                  <div className="px-3 py-2 text-sm text-bambu-gray">{t('smartPlugs.noMatchingSensors')}</div>
                                 )}
                                 )}
                               </div>
                               </div>
                             )}
                             )}
@@ -934,9 +934,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {/* MQTT broker not configured */}
               {/* MQTT broker not configured */}
               {!settings?.mqtt_broker && (
               {!settings?.mqtt_broker && (
                 <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
                 <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
-                  MQTT broker not configured. Set broker address in{' '}
-                  <span className="font-medium">Settings → Network → MQTT Publishing</span>
-                  {' '}(you don't need to enable publishing, just fill in the broker details).
+                  {t('smartPlugs.mqttNotConfigured')}{' '}
+                  <span className="font-medium">{t('smartPlugs.mqttSettingsPath')}</span>
+                  {' '}{t('smartPlugs.mqttNotConfiguredSuffix')}
                 </div>
                 </div>
               )}
               )}
 
 
@@ -944,17 +944,17 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {settings?.mqtt_broker && (
               {settings?.mqtt_broker && (
                 <>
                 <>
                   <div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-300">
                   <div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-300">
-                    <p className="font-medium mb-1">Monitor Only</p>
+                    <p className="font-medium mb-1">{t('smartPlugs.monitorOnly')}</p>
                     <p className="text-xs opacity-80">
                     <p className="text-xs opacity-80">
-                      MQTT plugs receive power/energy data via MQTT subscription. On/off control is not available - use your MQTT broker or home automation system.
+                      {t('smartPlugs.mqttMonitorOnlyDescription')}
                     </p>
                     </p>
                   </div>
                   </div>
 
 
                   {/* Power Section */}
                   {/* Power Section */}
                   <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
                   <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
-                    <p className="text-white font-medium text-sm">Power Monitoring</p>
+                    <p className="text-white font-medium text-sm">{t('smartPlugs.powerMonitoring')}</p>
                     <div>
                     <div>
-                      <label className="block text-sm text-bambu-gray mb-1">Topic</label>
+                      <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
                       <input
                       <input
                         type="text"
                         type="text"
                         value={mqttPowerTopic}
                         value={mqttPowerTopic}
@@ -965,7 +965,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                     </div>
                     </div>
                     <div className="grid grid-cols-2 gap-3">
                     <div className="grid grid-cols-2 gap-3">
                       <div>
                       <div>
-                        <label className="block text-sm text-bambu-gray mb-1">JSON Path</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
                         <input
                         <input
                           type="text"
                           type="text"
                           value={mqttPowerPath}
                           value={mqttPowerPath}
@@ -975,7 +975,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                         />
                         />
                       </div>
                       </div>
                       <div>
                       <div>
-                        <label className="block text-sm text-bambu-gray mb-1">Multiplier</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.multiplier')}</label>
                         <input
                         <input
                           type="text"
                           type="text"
                           value={mqttPowerMultiplier}
                           value={mqttPowerMultiplier}
@@ -985,17 +985,16 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                         />
                         />
                       </div>
                       </div>
                     </div>
                     </div>
-                    <p className="text-xs text-bambu-gray">
-                      JSON path extracts value from JSON payload (e.g., "power_l1"). Leave empty if topic publishes raw numeric values.<br/>
-                      Use multiplier 0.001 for mW→W, 1000 for kW→W.
+                    <p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
+                      {t('smartPlugs.mqttPowerHint')}
                     </p>
                     </p>
                   </div>
                   </div>
 
 
                   {/* Energy Section */}
                   {/* Energy Section */}
                   <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
                   <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
-                    <p className="text-white font-medium text-sm">Energy Monitoring <span className="text-bambu-gray font-normal">(optional)</span></p>
+                    <p className="text-white font-medium text-sm">{t('smartPlugs.energyMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
                     <div>
                     <div>
-                      <label className="block text-sm text-bambu-gray mb-1">Topic</label>
+                      <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
                       <input
                       <input
                         type="text"
                         type="text"
                         value={mqttEnergyTopic}
                         value={mqttEnergyTopic}
@@ -1006,7 +1005,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                     </div>
                     </div>
                     <div className="grid grid-cols-2 gap-3">
                     <div className="grid grid-cols-2 gap-3">
                       <div>
                       <div>
-                        <label className="block text-sm text-bambu-gray mb-1">JSON Path</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
                         <input
                         <input
                           type="text"
                           type="text"
                           value={mqttEnergyPath}
                           value={mqttEnergyPath}
@@ -1016,7 +1015,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                         />
                         />
                       </div>
                       </div>
                       <div>
                       <div>
-                        <label className="block text-sm text-bambu-gray mb-1">Multiplier</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.multiplier')}</label>
                         <input
                         <input
                           type="text"
                           type="text"
                           value={mqttEnergyMultiplier}
                           value={mqttEnergyMultiplier}
@@ -1026,17 +1025,16 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                         />
                         />
                       </div>
                       </div>
                     </div>
                     </div>
-                    <p className="text-xs text-bambu-gray">
-                      JSON path extracts value from JSON payload. Leave empty for raw values.<br/>
-                      Use multiplier 0.001 for Wh→kWh, 1000 for MWh→kWh.
+                    <p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
+                      {t('smartPlugs.mqttEnergyHint')}
                     </p>
                     </p>
                   </div>
                   </div>
 
 
                   {/* State Section */}
                   {/* State Section */}
                   <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
                   <div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
-                    <p className="text-white font-medium text-sm">State Monitoring <span className="text-bambu-gray font-normal">(optional)</span></p>
+                    <p className="text-white font-medium text-sm">{t('smartPlugs.stateMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
                     <div>
                     <div>
-                      <label className="block text-sm text-bambu-gray mb-1">Topic</label>
+                      <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.topic')}</label>
                       <input
                       <input
                         type="text"
                         type="text"
                         value={mqttStateTopic}
                         value={mqttStateTopic}
@@ -1047,7 +1045,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                     </div>
                     </div>
                     <div className="grid grid-cols-2 gap-3">
                     <div className="grid grid-cols-2 gap-3">
                       <div>
                       <div>
-                        <label className="block text-sm text-bambu-gray mb-1">JSON Path</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.jsonPath')}</label>
                         <input
                         <input
                           type="text"
                           type="text"
                           value={mqttStatePath}
                           value={mqttStatePath}
@@ -1057,7 +1055,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                         />
                         />
                       </div>
                       </div>
                       <div>
                       <div>
-                        <label className="block text-sm text-bambu-gray mb-1">ON Value</label>
+                        <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.onValue')}</label>
                         <input
                         <input
                           type="text"
                           type="text"
                           value={mqttStateOnValue}
                           value={mqttStateOnValue}
@@ -1067,9 +1065,8 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                         />
                         />
                       </div>
                       </div>
                     </div>
                     </div>
-                    <p className="text-xs text-bambu-gray">
-                      JSON path extracts value from JSON payload. Leave empty for raw values.<br/>
-                      ON value: the exact string that means "ON". Leave empty for auto-detect (ON, true, 1).
+                    <p className="text-xs text-bambu-gray" style={{ whiteSpace: 'pre-line' }}>
+                      {t('smartPlugs.mqttStateHint')}
                     </p>
                     </p>
                   </div>
                   </div>
                 </>
                 </>
@@ -1080,7 +1077,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
           {/* IP Address - only show for Tasmota */}
           {/* IP Address - only show for Tasmota */}
           {plugType === 'tasmota' && (
           {plugType === 'tasmota' && (
             <div>
             <div>
-              <label className="block text-sm text-bambu-gray mb-1">IP Address *</label>
+              <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.ipAddress')}</label>
               <div className="flex gap-2">
               <div className="flex gap-2">
                 <input
                 <input
                   type="text"
                   type="text"
@@ -1103,7 +1100,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                   ) : (
                   ) : (
                     <Wifi className="w-4 h-4" />
                     <Wifi className="w-4 h-4" />
                   )}
                   )}
-                  Test
+                  {t('smartPlugs.test')}
                 </Button>
                 </Button>
               </div>
               </div>
             </div>
             </div>
@@ -1120,10 +1117,10 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                 <>
                 <>
                   <CheckCircle className="w-5 h-5" />
                   <CheckCircle className="w-5 h-5" />
                   <div>
                   <div>
-                    <p className="font-medium">Connected!</p>
+                    <p className="font-medium">{t('smartPlugs.connectedResult')}</p>
                     <p className="text-sm opacity-80">
                     <p className="text-sm opacity-80">
-                      {testResult.device_name && `Device: ${testResult.device_name} - `}
-                      State: {testResult.state}
+                      {testResult.device_name && t('smartPlugs.deviceLabel', { name: testResult.device_name })}
+                      {t('smartPlugs.stateLabel', { state: testResult.state })}
                     </p>
                     </p>
                   </div>
                   </div>
                 </>
                 </>
@@ -1138,7 +1135,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
 
 
           {/* Name */}
           {/* Name */}
           <div>
           <div>
-            <label className="block text-sm text-bambu-gray mb-1">Name *</label>
+            <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.nameLabel')}</label>
             <input
             <input
               type="text"
               type="text"
               value={name}
               value={name}
@@ -1153,7 +1150,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
             <>
             <>
               <div className="grid grid-cols-2 gap-3">
               <div className="grid grid-cols-2 gap-3">
                 <div>
                 <div>
-                  <label className="block text-sm text-bambu-gray mb-1">Username</label>
+                  <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.username')}</label>
                   <input
                   <input
                     type="text"
                     type="text"
                     value={username}
                     value={username}
@@ -1163,7 +1160,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                   />
                   />
                 </div>
                 </div>
                 <div>
                 <div>
-                  <label className="block text-sm text-bambu-gray mb-1">Password</label>
+                  <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.password')}</label>
                   <input
                   <input
                     type="password"
                     type="password"
                     value={password}
                     value={password}
@@ -1174,7 +1171,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                 </div>
                 </div>
               </div>
               </div>
               <p className="text-xs text-bambu-gray -mt-2">
               <p className="text-xs text-bambu-gray -mt-2">
-                Leave empty if your Tasmota device doesn't require authentication
+                {t('smartPlugs.authHint')}
               </p>
               </p>
             </>
             </>
           )}
           )}
@@ -1182,13 +1179,13 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
           {/* Link to Printer - not shown for MQTT plugs (monitor-only) */}
           {/* Link to Printer - not shown for MQTT plugs (monitor-only) */}
           {plugType !== 'mqtt' && (
           {plugType !== 'mqtt' && (
             <div>
             <div>
-              <label className="block text-sm text-bambu-gray mb-1">Link to Printer</label>
+              <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.linkToPrinter')}</label>
               <select
               <select
                 value={printerId ?? ''}
                 value={printerId ?? ''}
                 onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
                 onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
                 className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
               >
               >
-                <option value="">No printer (manual control only)</option>
+                <option value="">{t('smartPlugs.noPrinter')}</option>
                 {availablePrinters?.map((p) => (
                 {availablePrinters?.map((p) => (
                   <option key={p.id} value={p.id}>
                   <option key={p.id} value={p.id}>
                     {p.name}
                     {p.name}
@@ -1196,7 +1193,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                 ))}
                 ))}
               </select>
               </select>
               <p className="text-xs text-bambu-gray mt-1">
               <p className="text-xs text-bambu-gray mt-1">
-                Linking enables automatic on/off when prints start/complete
+                {t('smartPlugs.linkingDescription')}
               </p>
               </p>
             </div>
             </div>
           )}
           )}
@@ -1206,7 +1203,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
             <div className="flex items-center justify-between mb-3">
             <div className="flex items-center justify-between mb-3">
               <div className="flex items-center gap-2">
               <div className="flex items-center gap-2">
                 <Bell className="w-4 h-4 text-bambu-green" />
                 <Bell className="w-4 h-4 text-bambu-green" />
-                <span className="text-white font-medium">Power Alerts</span>
+                <span className="text-white font-medium">{t('smartPlugs.powerAlerts')}</span>
               </div>
               </div>
               <label className="relative inline-flex items-center cursor-pointer">
               <label className="relative inline-flex items-center cursor-pointer">
                 <input
                 <input
@@ -1222,7 +1219,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               <div className="space-y-3">
               <div className="space-y-3">
                 <div className="grid grid-cols-2 gap-3">
                 <div className="grid grid-cols-2 gap-3">
                   <div>
                   <div>
-                    <label className="block text-sm text-bambu-gray mb-1">Alert if above (W)</label>
+                    <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.alertAbove')}</label>
                     <input
                     <input
                       type="number"
                       type="number"
                       value={powerAlertHigh}
                       value={powerAlertHigh}
@@ -1234,7 +1231,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                     />
                     />
                   </div>
                   </div>
                   <div>
                   <div>
-                    <label className="block text-sm text-bambu-gray mb-1">Alert if below (W)</label>
+                    <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.alertBelow')}</label>
                     <input
                     <input
                       type="number"
                       type="number"
                       value={powerAlertLow}
                       value={powerAlertLow}
@@ -1247,7 +1244,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                   </div>
                   </div>
                 </div>
                 </div>
                 <p className="text-xs text-bambu-gray">
                 <p className="text-xs text-bambu-gray">
-                  Get notified when power consumption crosses these thresholds. Leave empty to disable that direction.
+                  {t('smartPlugs.alertDescription')}
                 </p>
                 </p>
               </div>
               </div>
             )}
             )}
@@ -1259,7 +1256,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               <div className="flex items-center justify-between mb-3">
               <div className="flex items-center justify-between mb-3">
                 <div className="flex items-center gap-2">
                 <div className="flex items-center gap-2">
                   <Clock className="w-4 h-4 text-bambu-green" />
                   <Clock className="w-4 h-4 text-bambu-green" />
-                  <span className="text-white font-medium">Daily Schedule</span>
+                  <span className="text-white font-medium">{t('smartPlugs.dailySchedule')}</span>
                 </div>
                 </div>
                 <label className="relative inline-flex items-center cursor-pointer">
                 <label className="relative inline-flex items-center cursor-pointer">
                   <input
                   <input
@@ -1275,7 +1272,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                 <div className="space-y-3">
                 <div className="space-y-3">
                   <div className="grid grid-cols-2 gap-3">
                   <div className="grid grid-cols-2 gap-3">
                     <div>
                     <div>
-                      <label className="block text-sm text-bambu-gray mb-1">Turn On at</label>
+                      <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.turnOnAt')}</label>
                       <input
                       <input
                         type="time"
                         type="time"
                         value={scheduleOnTime}
                         value={scheduleOnTime}
@@ -1284,7 +1281,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                       />
                       />
                     </div>
                     </div>
                     <div>
                     <div>
-                      <label className="block text-sm text-bambu-gray mb-1">Turn Off at</label>
+                      <label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.turnOffAt')}</label>
                       <input
                       <input
                         type="time"
                         type="time"
                         value={scheduleOffTime}
                         value={scheduleOffTime}
@@ -1294,7 +1291,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                     </div>
                     </div>
                   </div>
                   </div>
                   <p className="text-xs text-bambu-gray">
                   <p className="text-xs text-bambu-gray">
-                    Automatically turn the plug on/off at these times daily. Leave empty to skip that action.
+                    {t('smartPlugs.scheduleDescription')}
                   </p>
                   </p>
                 </div>
                 </div>
               )}
               )}
@@ -1307,8 +1304,8 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               <div className="flex items-center gap-2">
               <div className="flex items-center gap-2">
                 <LayoutGrid className="w-4 h-4 text-bambu-green" />
                 <LayoutGrid className="w-4 h-4 text-bambu-green" />
                 <div>
                 <div>
-                  <span className="text-white font-medium">Show in Switchbar</span>
-                  <p className="text-xs text-bambu-gray">Quick access from sidebar</p>
+                  <span className="text-white font-medium">{t('smartPlugs.showInSwitchbar')}</span>
+                  <p className="text-xs text-bambu-gray">{t('smartPlugs.quickAccessSidebar')}</p>
                 </div>
                 </div>
               </div>
               </div>
               <label className="relative inline-flex items-center cursor-pointer">
               <label className="relative inline-flex items-center cursor-pointer">
@@ -1330,8 +1327,8 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                 <div className="flex items-center gap-2">
                 <div className="flex items-center gap-2">
                   <Eye className="w-4 h-4 text-bambu-green" />
                   <Eye className="w-4 h-4 text-bambu-green" />
                   <div>
                   <div>
-                    <span className="text-white font-medium">Show on Printer Card</span>
-                    <p className="text-xs text-bambu-gray">Display button on printer card</p>
+                    <span className="text-white font-medium">{t('smartPlugs.showOnPrinterCard')}</span>
+                    <p className="text-xs text-bambu-gray">{t('smartPlugs.displayOnPrinterCard')}</p>
                   </div>
                   </div>
                 </div>
                 </div>
                 <label className="relative inline-flex items-center cursor-pointer">
                 <label className="relative inline-flex items-center cursor-pointer">
@@ -1355,7 +1352,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               onClick={onClose}
               onClick={onClose}
               className="flex-1"
               className="flex-1"
             >
             >
-              Cancel
+              {t('smartPlugs.cancel')}
             </Button>
             </Button>
             <Button
             <Button
               type="submit"
               type="submit"
@@ -1367,7 +1364,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               ) : (
               ) : (
                 <Save className="w-4 h-4" />
                 <Save className="w-4 h-4" />
               )}
               )}
-              {isEditing ? 'Save' : 'Add'}
+              {isEditing ? t('smartPlugs.save') : t('smartPlugs.add')}
             </Button>
             </Button>
           </div>
           </div>
         </form>
         </form>

+ 75 - 75
frontend/src/components/GitHubBackupSettings.tsx

@@ -205,7 +205,7 @@ export function GitHubBackupSettings() {
           enabled,
           enabled,
         });
         });
         setAccessToken(''); // Clear after save
         setAccessToken(''); // Clear after save
-        showToast('Token updated');
+        showToast(t('backup.tokenUpdated'));
       } else {
       } else {
         // Update without token
         // Update without token
         await api.updateGitHubBackupConfig({
         await api.updateGitHubBackupConfig({
@@ -218,14 +218,14 @@ export function GitHubBackupSettings() {
           backup_settings: backupSettings,
           backup_settings: backupSettings,
           enabled,
           enabled,
         });
         });
-        showToast('Settings saved');
+        showToast(t('backup.settingsSaved'));
       }
       }
       queryClient.invalidateQueries({ queryKey: ['github-backup-config'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-config'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-status'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-status'] });
     } catch (error) {
     } catch (error) {
-      showToast(`Failed to save: ${(error as Error).message}`, 'error');
+      showToast(t('backup.failedToSave', { message: (error as Error).message }), 'error');
     }
     }
-  }, [config?.has_token, repoUrl, accessToken, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, enabled, queryClient, showToast]);
+  }, [config?.has_token, repoUrl, accessToken, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, enabled, queryClient, showToast, t]);
 
 
   // Auto-save effect for existing configs (debounced)
   // Auto-save effect for existing configs (debounced)
   useEffect(() => {
   useEffect(() => {
@@ -271,12 +271,12 @@ export function GitHubBackupSettings() {
     onSuccess: () => {
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['github-backup-config'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-config'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-status'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-status'] });
-      showToast('GitHub backup enabled');
+      showToast(t('backup.githubBackupEnabled'));
       setAccessToken('');
       setAccessToken('');
       isInitializedRef.current = true;
       isInitializedRef.current = true;
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
-      showToast(`Failed to save: ${error.message}`, 'error');
+      showToast(t('backup.failedToSave', { message: error.message }), 'error');
     },
     },
   });
   });
 
 
@@ -287,16 +287,16 @@ export function GitHubBackupSettings() {
       queryClient.invalidateQueries({ queryKey: ['github-backup-logs'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-logs'] });
       if (result.success) {
       if (result.success) {
         if (result.files_changed > 0) {
         if (result.files_changed > 0) {
-          showToast(`Backup complete - ${result.files_changed} files updated`);
+          showToast(t('backup.backupCompleteFiles', { count: result.files_changed }));
         } else {
         } else {
-          showToast('Backup skipped - no changes');
+          showToast(t('backup.backupSkippedNoChanges'));
         }
         }
       } else {
       } else {
-        showToast(`Backup failed: ${result.message}`, 'error');
+        showToast(t('backup.backupFailed2', { message: result.message }), 'error');
       }
       }
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
-      showToast(`Backup failed: ${error.message}`, 'error');
+      showToast(t('backup.backupFailed2', { message: error.message }), 'error');
     },
     },
   });
   });
 
 
@@ -304,10 +304,10 @@ export function GitHubBackupSettings() {
     mutationFn: () => api.clearGitHubBackupLogs(0),
     mutationFn: () => api.clearGitHubBackupLogs(0),
     onSuccess: (result) => {
     onSuccess: (result) => {
       queryClient.invalidateQueries({ queryKey: ['github-backup-logs'] });
       queryClient.invalidateQueries({ queryKey: ['github-backup-logs'] });
-      showToast(`Cleared ${result.deleted} logs`);
+      showToast(t('backup.clearedLogs', { count: result.deleted }));
     },
     },
     onError: (error: Error) => {
     onError: (error: Error) => {
-      showToast(`Failed to clear logs: ${error.message}`, 'error');
+      showToast(t('backup.failedToClearLogs', { message: error.message }), 'error');
     },
     },
   });
   });
 
 
@@ -319,7 +319,7 @@ export function GitHubBackupSettings() {
       // If user entered a new token, test with those credentials
       // If user entered a new token, test with those credentials
       if (accessToken) {
       if (accessToken) {
         if (!repoUrl) {
         if (!repoUrl) {
-          showToast('Enter repository URL', 'error');
+          showToast(t('backup.enterRepoUrl'), 'error');
           setTestLoading(false);
           setTestLoading(false);
           return;
           return;
         }
         }
@@ -328,7 +328,7 @@ export function GitHubBackupSettings() {
         // Use stored credentials
         // Use stored credentials
         result = await api.testGitHubStoredConnection();
         result = await api.testGitHubStoredConnection();
       } else {
       } else {
-        showToast('Enter repository URL and access token', 'error');
+        showToast(t('backup.enterRepoAndToken'), 'error');
         setTestLoading(false);
         setTestLoading(false);
         return;
         return;
       }
       }
@@ -343,11 +343,11 @@ export function GitHubBackupSettings() {
   // Initial setup save (only for new configs)
   // Initial setup save (only for new configs)
   const handleInitialSetup = () => {
   const handleInitialSetup = () => {
     if (!repoUrl) {
     if (!repoUrl) {
-      showToast('Repository URL is required', 'error');
+      showToast(t('backup.repoRequired'), 'error');
       return;
       return;
     }
     }
     if (!accessToken) {
     if (!accessToken) {
-      showToast('Access token is required', 'error');
+      showToast(t('backup.tokenRequired'), 'error');
       return;
       return;
     }
     }
 
 
@@ -381,11 +381,11 @@ export function GitHubBackupSettings() {
             <div className="flex items-center justify-between">
             <div className="flex items-center justify-between">
               <div className="flex items-center gap-2">
               <div className="flex items-center gap-2">
                 <Github className="w-5 h-5 text-gray-400" />
                 <Github className="w-5 h-5 text-gray-400" />
-                <h2 className="text-lg font-semibold text-white">GitHub Backup</h2>
+                <h2 className="text-lg font-semibold text-white">{t('backup.githubBackup')}</h2>
               </div>
               </div>
               {config && cloudStatus?.is_authenticated && (
               {config && cloudStatus?.is_authenticated && (
                 <div className="flex items-center gap-2">
                 <div className="flex items-center gap-2">
-                  <span className="text-sm text-bambu-gray">Enabled</span>
+                  <span className="text-sm text-bambu-gray">{t('backup.enabled')}</span>
                   <Toggle
                   <Toggle
                     checked={enabled}
                     checked={enabled}
                     onChange={setEnabled}
                     onChange={setEnabled}
@@ -400,19 +400,19 @@ export function GitHubBackupSettings() {
               <div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
               <div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
                 <AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
                 <AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
                 <p className="text-sm text-yellow-400">
                 <p className="text-sm text-yellow-400">
-                  Bambu Cloud login required. Sign in under Profiles → Cloud Profiles to enable GitHub backup.
+                  {t('backup.cloudLoginRequired')}
                 </p>
                 </p>
               </div>
               </div>
             ) : (
             ) : (
               <>
               <>
                 <p className="text-sm text-bambu-gray">
                 <p className="text-sm text-bambu-gray">
-                  Automatically sync your profiles to a private GitHub repository for backup and version history.
+                  {t('backup.githubDescription')}
                 </p>
                 </p>
 
 
                 {/* Repository URL */}
                 {/* Repository URL */}
                 <div>
                 <div>
                   <label className="block text-sm text-bambu-gray mb-1">
                   <label className="block text-sm text-bambu-gray mb-1">
-                    Repository URL
+                    {t('backup.repositoryUrl')}
                   </label>
                   </label>
                   <input
                   <input
                     type="text"
                     type="text"
@@ -426,24 +426,24 @@ export function GitHubBackupSettings() {
                 {/* Access Token */}
                 {/* Access Token */}
                 <div>
                 <div>
                   <label className="block text-sm text-bambu-gray mb-1">
                   <label className="block text-sm text-bambu-gray mb-1">
-                    Personal Access Token {config?.has_token && <span className="text-green-400">(saved)</span>}
+                    {t('backup.personalAccessToken')} {config?.has_token && <span className="text-green-400">{t('backup.tokenSaved')}</span>}
                   </label>
                   </label>
                   <input
                   <input
                     type="password"
                     type="password"
                     value={accessToken}
                     value={accessToken}
                     onChange={(e) => { setAccessToken(e.target.value); setTestResult(null); }}
                     onChange={(e) => { setAccessToken(e.target.value); setTestResult(null); }}
-                    placeholder={config?.has_token ? 'Enter new token to update' : 'ghp_xxxxxxxxxxxx'}
+                    placeholder={config?.has_token ? t('backup.enterNewToken') : 'ghp_xxxxxxxxxxxx'}
                     className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                     className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                   />
                   />
                   <p className="text-xs text-bambu-gray mt-1">
                   <p className="text-xs text-bambu-gray mt-1">
-                    Fine-grained token with Contents read/write permission
+                    {t('backup.tokenHint')}
                   </p>
                   </p>
                 </div>
                 </div>
 
 
             {/* Branch - inline with schedule */}
             {/* Branch - inline with schedule */}
             <div className="grid grid-cols-2 gap-4">
             <div className="grid grid-cols-2 gap-4">
               <div>
               <div>
-                <label className="block text-sm text-bambu-gray mb-1">Branch</label>
+                <label className="block text-sm text-bambu-gray mb-1">{t('backup.branch')}</label>
                 <input
                 <input
                   type="text"
                   type="text"
                   value={branch}
                   value={branch}
@@ -453,7 +453,7 @@ export function GitHubBackupSettings() {
                 />
                 />
               </div>
               </div>
               <div>
               <div>
-                <label className="block text-sm text-bambu-gray mb-1">Auto-backup</label>
+                <label className="block text-sm text-bambu-gray mb-1">{t('backup.autoBackup')}</label>
                 <select
                 <select
                   value={scheduleEnabled ? scheduleType : 'disabled'}
                   value={scheduleEnabled ? scheduleType : 'disabled'}
                   onChange={(e) => {
                   onChange={(e) => {
@@ -466,17 +466,17 @@ export function GitHubBackupSettings() {
                   }}
                   }}
                   className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                   className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
                 >
                 >
-                  <option value="disabled">Manual only</option>
-                  <option value="hourly">Hourly</option>
-                  <option value="daily">Daily</option>
-                  <option value="weekly">Weekly</option>
+                  <option value="disabled">{t('backup.manualOnly')}</option>
+                  <option value="hourly">{t('backup.hourly')}</option>
+                  <option value="daily">{t('backup.daily')}</option>
+                  <option value="weekly">{t('backup.weekly')}</option>
                 </select>
                 </select>
               </div>
               </div>
             </div>
             </div>
 
 
             {/* What to backup */}
             {/* What to backup */}
             <div>
             <div>
-              <label className="block text-sm text-bambu-gray mb-2">Include in backup</label>
+              <label className="block text-sm text-bambu-gray mb-2">{t('backup.includeInBackup')}</label>
               <div className="space-y-2">
               <div className="space-y-2">
                 <label className={`flex items-start gap-2 ${noPrintersConnected ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}>
                 <label className={`flex items-start gap-2 ${noPrintersConnected ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}>
                   <input
                   <input
@@ -488,21 +488,21 @@ export function GitHubBackupSettings() {
                   />
                   />
                   <div className="flex-1">
                   <div className="flex-1">
                     <div className="flex items-center gap-2">
                     <div className="flex items-center gap-2">
-                      <span className={`text-sm ${noPrintersConnected ? 'text-bambu-gray' : 'text-white'}`}>K-Profiles</span>
+                      <span className={`text-sm ${noPrintersConnected ? 'text-bambu-gray' : 'text-white'}`}>{t('backup.kProfiles')}</span>
                       {noPrintersConnected && (
                       {noPrintersConnected && (
                         <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
                         <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
                           <AlertTriangle className="w-3 h-3" />
                           <AlertTriangle className="w-3 h-3" />
-                          No printers connected
+                          {t('backup.noPrintersConnected')}
                         </span>
                         </span>
                       )}
                       )}
                       {somePrintersDisconnected && (
                       {somePrintersDisconnected && (
                         <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
                         <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400">
                           <AlertTriangle className="w-3 h-3" />
                           <AlertTriangle className="w-3 h-3" />
-                          {connectedPrinters}/{totalPrinters} connected
+                          {t('backup.printersConnected', { connected: connectedPrinters, total: totalPrinters })}
                         </span>
                         </span>
                       )}
                       )}
                     </div>
                     </div>
-                    <p className="text-xs text-bambu-gray">Pressure advance calibration from connected printers</p>
+                    <p className="text-xs text-bambu-gray">{t('backup.kProfilesDescription')}</p>
                   </div>
                   </div>
                 </label>
                 </label>
                 <label className="flex items-start gap-2 cursor-pointer">
                 <label className="flex items-start gap-2 cursor-pointer">
@@ -514,8 +514,8 @@ export function GitHubBackupSettings() {
                     disabled={!cloudStatus?.is_authenticated}
                     disabled={!cloudStatus?.is_authenticated}
                   />
                   />
                   <div>
                   <div>
-                    <span className={`text-sm ${cloudStatus?.is_authenticated ? 'text-white' : 'text-bambu-gray'}`}>Cloud Profiles</span>
-                    <p className="text-xs text-bambu-gray">Filament, printer, and process presets from Bambu Cloud</p>
+                    <span className={`text-sm ${cloudStatus?.is_authenticated ? 'text-white' : 'text-bambu-gray'}`}>{t('backup.cloudProfiles')}</span>
+                    <p className="text-xs text-bambu-gray">{t('backup.cloudProfilesDescription')}</p>
                   </div>
                   </div>
                 </label>
                 </label>
                 <label className="flex items-start gap-2 cursor-pointer">
                 <label className="flex items-start gap-2 cursor-pointer">
@@ -526,8 +526,8 @@ export function GitHubBackupSettings() {
                     className="w-4 h-4 mt-0.5 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
                     className="w-4 h-4 mt-0.5 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
                   />
                   />
                   <div>
                   <div>
-                    <span className="text-white text-sm">App Settings</span>
-                    <p className="text-xs text-bambu-gray">Bambuddy configuration (complete database)</p>
+                    <span className="text-white text-sm">{t('backup.appSettings')}</span>
+                    <p className="text-xs text-bambu-gray">{t('backup.appSettingsDescription')}</p>
                   </div>
                   </div>
                 </label>
                 </label>
               </div>
               </div>
@@ -541,17 +541,17 @@ export function GitHubBackupSettings() {
                   <div className="flex items-center gap-2 text-bambu-gray">
                   <div className="flex items-center gap-2 text-bambu-gray">
                     {status.last_backup_at ? (
                     {status.last_backup_at ? (
                       <>
                       <>
-                        <span>Last backup: {formatRelativeTime(status.last_backup_at, 'system', t)}</span>
+                        <span>{t('backup.lastBackupAt')} {formatRelativeTime(status.last_backup_at, 'system', t)}</span>
                         <StatusBadge status={status.last_backup_status} />
                         <StatusBadge status={status.last_backup_status} />
                       </>
                       </>
                     ) : (
                     ) : (
-                      <span>No backups yet</span>
+                      <span>{t('backup.noBackupsYet')}</span>
                     )}
                     )}
                   </div>
                   </div>
                   {status.next_scheduled_run && (
                   {status.next_scheduled_run && (
                     <span className="text-bambu-gray">
                     <span className="text-bambu-gray">
                       <Clock className="w-3 h-3 inline mr-1" />
                       <Clock className="w-3 h-3 inline mr-1" />
-                      Next: {formatRelativeTime(status.next_scheduled_run, 'system', t)}
+                      {t('backup.next')} {formatRelativeTime(status.next_scheduled_run, 'system', t)}
                     </span>
                     </span>
                   )}
                   )}
                 </div>
                 </div>
@@ -572,7 +572,7 @@ export function GitHubBackupSettings() {
                     {(triggerBackupMutation.isPending || status.is_running) ? (
                     {(triggerBackupMutation.isPending || status.is_running) ? (
                       <div className="flex items-center gap-2 text-bambu-green">
                       <div className="flex items-center gap-2 text-bambu-green">
                         <Loader2 className="w-4 h-4 animate-spin" />
                         <Loader2 className="w-4 h-4 animate-spin" />
-                        <span className="text-sm">{status.progress || 'Starting backup...'}</span>
+                        <span className="text-sm">{status.progress || t('backup.startingBackup')}</span>
                       </div>
                       </div>
                     ) : (
                     ) : (
                       <>
                       <>
@@ -583,7 +583,7 @@ export function GitHubBackupSettings() {
                           disabled={!config?.enabled}
                           disabled={!config?.enabled}
                         >
                         >
                           <Play className="w-4 h-4" />
                           <Play className="w-4 h-4" />
-                          Backup Now
+                          {t('backup.backupNow')}
                         </Button>
                         </Button>
                         <Button
                         <Button
                           variant="secondary"
                           variant="secondary"
@@ -592,7 +592,7 @@ export function GitHubBackupSettings() {
                           disabled={testLoading}
                           disabled={testLoading}
                         >
                         >
                           {testLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
                           {testLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
-                          Test
+                          {t('backup.test')}
                         </Button>
                         </Button>
                       </>
                       </>
                     )}
                     )}
@@ -606,7 +606,7 @@ export function GitHubBackupSettings() {
                       disabled={saveConfigMutation.isPending || !repoUrl || !accessToken}
                       disabled={saveConfigMutation.isPending || !repoUrl || !accessToken}
                     >
                     >
                       {saveConfigMutation.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
                       {saveConfigMutation.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
-                      Enable Backup
+                      {t('backup.enableBackup')}
                     </Button>
                     </Button>
                     <Button
                     <Button
                       variant="secondary"
                       variant="secondary"
@@ -615,7 +615,7 @@ export function GitHubBackupSettings() {
                       disabled={testLoading || !repoUrl || !accessToken}
                       disabled={testLoading || !repoUrl || !accessToken}
                     >
                     >
                       {testLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
                       {testLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
-                      Test Connection
+                      {t('backup.testConnection')}
                     </Button>
                     </Button>
                   </>
                   </>
                 )}
                 )}
@@ -633,7 +633,7 @@ export function GitHubBackupSettings() {
               <div className="flex items-center justify-between">
               <div className="flex items-center justify-between">
                 <div className="flex items-center gap-2">
                 <div className="flex items-center gap-2">
                   <History className="w-5 h-5 text-gray-400" />
                   <History className="w-5 h-5 text-gray-400" />
-                  <h2 className="text-lg font-semibold text-white">History</h2>
+                  <h2 className="text-lg font-semibold text-white">{t('backup.history')}</h2>
                 </div>
                 </div>
                 <Button
                 <Button
                   variant="ghost"
                   variant="ghost"
@@ -642,7 +642,7 @@ export function GitHubBackupSettings() {
                   disabled={clearLogsMutation.isPending}
                   disabled={clearLogsMutation.isPending}
                 >
                 >
                   <Trash2 className="w-4 h-4" />
                   <Trash2 className="w-4 h-4" />
-                  Clear
+                  {t('backup.clear')}
                 </Button>
                 </Button>
               </div>
               </div>
             </CardHeader>
             </CardHeader>
@@ -651,9 +651,9 @@ export function GitHubBackupSettings() {
                 <table className="w-full text-sm">
                 <table className="w-full text-sm">
                   <thead>
                   <thead>
                     <tr className="text-bambu-gray border-b border-bambu-dark-tertiary">
                     <tr className="text-bambu-gray border-b border-bambu-dark-tertiary">
-                      <th className="text-left py-2 px-2">Date</th>
-                      <th className="text-left py-2 px-2">Status</th>
-                      <th className="text-left py-2 px-2">Commit</th>
+                      <th className="text-left py-2 px-2">{t('backup.date')}</th>
+                      <th className="text-left py-2 px-2">{t('backup.status')}</th>
+                      <th className="text-left py-2 px-2">{t('backup.commit')}</th>
                     </tr>
                     </tr>
                   </thead>
                   </thead>
                   <tbody>
                   <tbody>
@@ -692,20 +692,20 @@ export function GitHubBackupSettings() {
           <CardHeader>
           <CardHeader>
             <div className="flex items-center gap-2">
             <div className="flex items-center gap-2">
               <Database className="w-5 h-5 text-gray-400" />
               <Database className="w-5 h-5 text-gray-400" />
-              <h2 className="text-lg font-semibold text-white">Local Backup</h2>
+              <h2 className="text-lg font-semibold text-white">{t('backup.localBackup')}</h2>
             </div>
             </div>
           </CardHeader>
           </CardHeader>
           <CardContent className="space-y-4">
           <CardContent className="space-y-4">
             <p className="text-sm text-bambu-gray">
             <p className="text-sm text-bambu-gray">
-              Create a complete backup of your Bambuddy data including the database, archives, uploads, and all files.
+              {t('backup.localBackupDescription')}
             </p>
             </p>
 
 
             {/* Export */}
             {/* Export */}
             <div className="flex items-center justify-between py-3 border-b border-bambu-dark-tertiary">
             <div className="flex items-center justify-between py-3 border-b border-bambu-dark-tertiary">
               <div>
               <div>
-                <p className="text-white">Download Backup</p>
+                <p className="text-white">{t('backup.downloadBackupLabel')}</p>
                 <p className="text-sm text-bambu-gray">
                 <p className="text-sm text-bambu-gray">
-                  Complete backup: database + all files (ZIP)
+                  {t('backup.completeBackupZip')}
                 </p>
                 </p>
               </div>
               </div>
               <Button
               <Button
@@ -714,20 +714,20 @@ export function GitHubBackupSettings() {
                 disabled={isExporting || isRestoring}
                 disabled={isExporting || isRestoring}
                 onClick={async () => {
                 onClick={async () => {
                   setIsExporting(true);
                   setIsExporting(true);
-                  setOperationStatus('Preparing backup...');
+                  setOperationStatus(t('backup.preparingBackup'));
                   try {
                   try {
-                    setOperationStatus('Creating backup archive... This may take a while for large archives.');
+                    setOperationStatus(t('backup.creatingArchive'));
                     const { blob, filename } = await api.exportBackup();
                     const { blob, filename } = await api.exportBackup();
-                    setOperationStatus('Downloading backup file...');
+                    setOperationStatus(t('backup.downloadingFile'));
                     const url = URL.createObjectURL(blob);
                     const url = URL.createObjectURL(blob);
                     const a = document.createElement('a');
                     const a = document.createElement('a');
                     a.href = url;
                     a.href = url;
                     a.download = filename;
                     a.download = filename;
                     a.click();
                     a.click();
                     URL.revokeObjectURL(url);
                     URL.revokeObjectURL(url);
-                    showToast('Backup downloaded successfully');
+                    showToast(t('backup.backupDownloaded'));
                   } catch (e) {
                   } catch (e) {
-                    showToast(`Failed to create backup: ${e instanceof Error ? e.message : 'Unknown error'}`, 'error');
+                    showToast(t('backup.failedToCreateBackup', { message: e instanceof Error ? e.message : 'Unknown error' }), 'error');
                   } finally {
                   } finally {
                     setIsExporting(false);
                     setIsExporting(false);
                     setOperationStatus('');
                     setOperationStatus('');
@@ -735,7 +735,7 @@ export function GitHubBackupSettings() {
                 }}
                 }}
               >
               >
                 <Download className="w-4 h-4" />
                 <Download className="w-4 h-4" />
-                Download
+                {t('backup.download')}
               </Button>
               </Button>
             </div>
             </div>
 
 
@@ -771,7 +771,7 @@ export function GitHubBackupSettings() {
                 onClick={() => fileInputRef.current?.click()}
                 onClick={() => fileInputRef.current?.click()}
               >
               >
                 <Upload className="w-4 h-4" />
                 <Upload className="w-4 h-4" />
-                Restore
+                {t('backup.restore')}
               </Button>
               </Button>
             </div>
             </div>
 
 
@@ -793,7 +793,7 @@ export function GitHubBackupSettings() {
                           onClick={() => window.location.reload()}
                           onClick={() => window.location.reload()}
                         >
                         >
                           <RotateCcw className="w-3 h-3" />
                           <RotateCcw className="w-3 h-3" />
-                          Reload Now
+                          {t('backup.reloadNow')}
                         </Button>
                         </Button>
                       </div>
                       </div>
                     )}
                     )}
@@ -807,8 +807,8 @@ export function GitHubBackupSettings() {
               <div className="flex items-start gap-2 text-sm">
               <div className="flex items-start gap-2 text-sm">
                 <AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
                 <AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
                 <div className="text-yellow-200">
                 <div className="text-yellow-200">
-                  <span className="font-medium">Restore replaces all data.</span>{' '}
-                  <span className="text-yellow-200/70">Your current database and files will be completely replaced. A restart is required after restore.</span>
+                  <span className="font-medium">{t('backup.restoreReplacesAll')}</span>{' '}
+                  <span className="text-yellow-200/70">{t('backup.restoreReplacesAllDetail')}</span>
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
@@ -819,25 +819,25 @@ export function GitHubBackupSettings() {
       {/* Restore Confirmation Modal */}
       {/* Restore Confirmation Modal */}
       {showRestoreConfirm && restoreFile && (
       {showRestoreConfirm && restoreFile && (
         <ConfirmModal
         <ConfirmModal
-          title="Restore Backup"
-          message={`Are you sure you want to restore from "${restoreFile.name}"? This will completely replace your current database and all files. The application will need to be restarted after restore.`}
-          confirmText="Restore Backup"
+          title={t('backup.restoreConfirmTitle')}
+          message={t('backup.restoreConfirmMessage', { filename: restoreFile.name })}
+          confirmText={t('backup.restoreConfirmButton')}
           variant="danger"
           variant="danger"
           onConfirm={async () => {
           onConfirm={async () => {
             setShowRestoreConfirm(false);
             setShowRestoreConfirm(false);
             setIsRestoring(true);
             setIsRestoring(true);
             setRestoreResult(null);
             setRestoreResult(null);
             try {
             try {
-              setOperationStatus('Uploading backup file...');
+              setOperationStatus(t('backup.uploadingFile'));
               const result = await api.importBackup(restoreFile);
               const result = await api.importBackup(restoreFile);
               setRestoreResult(result);
               setRestoreResult(result);
               if (result.success) {
               if (result.success) {
-                showToast('Backup restored. Please restart Bambuddy.', 'success');
+                showToast(t('backup.backupRestoredRestart'), 'success');
               } else {
               } else {
                 showToast(result.message, 'error');
                 showToast(result.message, 'error');
               }
               }
             } catch (e) {
             } catch (e) {
-              const message = e instanceof Error ? e.message : 'Failed to restore backup';
+              const message = e instanceof Error ? e.message : t('backup.failedToRestore');
               setRestoreResult({ success: false, message });
               setRestoreResult({ success: false, message });
               showToast(message, 'error');
               showToast(message, 'error');
             } finally {
             } finally {
@@ -864,16 +864,16 @@ export function GitHubBackupSettings() {
               </div>
               </div>
             </div>
             </div>
             <h3 className="text-xl font-semibold text-white mb-2">
             <h3 className="text-xl font-semibold text-white mb-2">
-              {isExporting ? 'Creating Backup' : 'Restoring Backup'}
+              {isExporting ? t('backup.creatingBackup') : t('backup.restoringBackup')}
             </h3>
             </h3>
             <p className="text-bambu-gray mb-4">
             <p className="text-bambu-gray mb-4">
-              {operationStatus || (isExporting ? 'Preparing...' : 'Processing...')}
+              {operationStatus || (isExporting ? t('backup.preparing') : t('backup.processing'))}
             </p>
             </p>
             <div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
             <div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
               <div className="flex items-start gap-2 text-sm">
               <div className="flex items-start gap-2 text-sm">
                 <AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
                 <AlertTriangle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
                 <p className="text-yellow-200 text-left">
                 <p className="text-yellow-200 text-left">
-                  Please do not close this page or navigate away. This operation may take several minutes for large backups.
+                  {t('backup.doNotClosePage')}
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>

+ 23 - 33
frontend/src/components/NotificationLogViewer.tsx

@@ -1,5 +1,6 @@
 import { useState } from 'react';
 import { useState } from 'react';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 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 { History, CheckCircle, XCircle, Loader2, Trash2, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import { parseUTCDate, formatTimeOnly, formatDateTime, type TimeFormat } from '../utils/date';
 import { parseUTCDate, formatTimeOnly, formatDateTime, type TimeFormat } from '../utils/date';
@@ -7,19 +8,6 @@ import type { NotificationLogEntry } from '../api/client';
 import { Button } from './Button';
 import { Button } from './Button';
 import { useToast } from '../contexts/ToastContext';
 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> = {
 const EVENT_COLORS: Record<string, string> = {
   print_start: 'text-blue-400',
   print_start: 'text-blue-400',
   print_complete: 'text-bambu-green',
   print_complete: 'text-bambu-green',
@@ -38,6 +26,7 @@ interface NotificationLogViewerProps {
 }
 }
 
 
 export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
 export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { showToast } = useToast();
   const [days, setDays] = useState(7);
   const [days, setDays] = useState(7);
@@ -83,7 +72,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
     const now = new Date();
     const now = new Date();
     const diff = now.getTime() - date.getTime();
     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 < 3600000) return `${Math.floor(diff / 60000)}m ago`;
     if (diff < 86400000) return `${Math.floor(diff / 3600000)}h 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="p-4 border-b border-bambu-dark-tertiary flex items-center justify-between">
           <div className="flex items-center gap-3">
           <div className="flex items-center gap-3">
             <History className="w-5 h-5 text-bambu-green" />
             <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>
           </div>
           <button
           <button
             onClick={onClose}
             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="px-4 py-3 border-b border-bambu-dark-tertiary bg-bambu-dark/50">
             <div className="flex items-center gap-6 text-sm">
             <div className="flex items-center gap-6 text-sm">
               <span className="text-bambu-gray">
               <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>
               <span className="flex items-center gap-1 text-bambu-green">
               <span className="flex items-center gap-1 text-bambu-green">
                 <CheckCircle className="w-4 h-4" />
                 <CheckCircle className="w-4 h-4" />
-                {stats.success_count} sent
+                {t('notifications.statsSent', { count: stats.success_count })}
               </span>
               </span>
               {stats.failure_count > 0 && (
               {stats.failure_count > 0 && (
                 <span className="flex items-center gap-1 text-red-400">
                 <span className="flex items-center gap-1 text-red-400">
                   <XCircle className="w-4 h-4" />
                   <XCircle className="w-4 h-4" />
-                  {stats.failure_count} failed
+                  {t('notifications.statsFailed', { count: stats.failure_count })}
                 </span>
                 </span>
               )}
               )}
             </div>
             </div>
@@ -135,10 +124,10 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
             onChange={(e) => setDays(Number(e.target.value))}
             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"
             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>
           </select>
 
 
           <label className="flex items-center gap-2 text-sm text-bambu-gray cursor-pointer">
           <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)}
               onChange={(e) => setShowFailedOnly(e.target.checked)}
               className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
               className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
             />
             />
-            Show failed only
+            {t('notifications.showFailedOnly')}
           </label>
           </label>
 
 
           <div className="flex-1" />
           <div className="flex-1" />
@@ -164,7 +153,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
             ) : (
             ) : (
               <RefreshCw className="w-4 h-4" />
               <RefreshCw className="w-4 h-4" />
             )}
             )}
-            Refresh
+            {t('notifications.refresh')}
           </Button>
           </Button>
 
 
           <Button
           <Button
@@ -179,7 +168,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
             ) : (
             ) : (
               <Trash2 className="w-4 h-4" />
               <Trash2 className="w-4 h-4" />
             )}
             )}
-            Clear Old
+            {t('notifications.clearOld')}
           </Button>
           </Button>
         </div>
         </div>
 
 
@@ -206,7 +195,7 @@ export function NotificationLogViewer({ onClose }: NotificationLogViewerProps) {
             <div className="text-center py-12 text-bambu-gray">
             <div className="text-center py-12 text-bambu-gray">
               <History className="w-12 h-12 mx-auto mb-3 opacity-30" />
               <History className="w-12 h-12 mx-auto mb-3 opacity-30" />
               <p className="text-sm">
               <p className="text-sm">
-                {showFailedOnly ? 'No failed notifications' : 'No notifications logged'}
+                {showFailedOnly ? t('notifications.noFailedNotifications') : t('notifications.noNotificationsLogged')}
               </p>
               </p>
             </div>
             </div>
           )}
           )}
@@ -229,6 +218,7 @@ function LogEntry({
   formatDate: (date: string) => string;
   formatDate: (date: string) => string;
   formatFullDate: (date: string) => string;
   formatFullDate: (date: string) => string;
 }) {
 }) {
+  const { t } = useTranslation();
   return (
   return (
     <div
     <div
       className={`border rounded-lg overflow-hidden transition-colors ${
       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'}`}>
         <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>
 
 
         <span className="text-sm text-white truncate flex-1">
         <span className="text-sm text-white truncate flex-1">
-          {log.provider_name || 'Unknown Provider'}
+          {log.provider_name || t('notifications.unknownProvider')}
         </span>
         </span>
 
 
         {log.printer_name && (
         {log.printer_name && (
@@ -275,22 +265,22 @@ function LogEntry({
       {isExpanded && (
       {isExpanded && (
         <div className="px-3 py-2 border-t border-bambu-dark-tertiary bg-bambu-dark/20 space-y-2">
         <div className="px-3 py-2 border-t border-bambu-dark-tertiary bg-bambu-dark/20 space-y-2">
           <div>
           <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>
             <p className="text-sm text-white">{log.title}</p>
           </div>
           </div>
           <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>
             <p className="text-sm text-white whitespace-pre-wrap">{log.message}</p>
           </div>
           </div>
           {!log.success && log.error_message && (
           {!log.success && log.error_message && (
             <div>
             <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>
               <p className="text-sm text-red-300">{log.error_message}</p>
             </div>
             </div>
           )}
           )}
           <div className="flex gap-4 text-xs text-bambu-gray pt-1">
           <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>
         </div>
         </div>
       )}
       )}

+ 82 - 90
frontend/src/components/NotificationProviderCard.tsx

@@ -1,5 +1,6 @@
 import { useState } from 'react';
 import { useState } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
 import { Bell, Trash2, Settings2, Edit2, Send, Loader2, CheckCircle, XCircle, Moon, Clock, ChevronDown, ChevronUp, Calendar } from 'lucide-react';
 import { Bell, Trash2, Settings2, Edit2, Send, Loader2, CheckCircle, XCircle, Moon, Clock, ChevronDown, ChevronUp, Calendar } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import { formatDateOnly, parseUTCDate } from '../utils/date';
 import { formatDateOnly, parseUTCDate } from '../utils/date';
@@ -14,17 +15,8 @@ interface NotificationProviderCardProps {
   onEdit: (provider: NotificationProvider) => void;
   onEdit: (provider: NotificationProvider) => void;
 }
 }
 
 
-const PROVIDER_LABELS: Record<string, string> = {
-  callmebot: 'CallMeBot/WhatsApp',
-  ntfy: 'ntfy',
-  pushover: 'Pushover',
-  telegram: 'Telegram',
-  email: 'Email',
-  discord: 'Discord',
-  webhook: 'Webhook',
-};
-
 export function NotificationProviderCard({ provider, onEdit }: NotificationProviderCardProps) {
 export function NotificationProviderCard({ provider, onEdit }: NotificationProviderCardProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
   const [isExpanded, setIsExpanded] = useState(false);
   const [isExpanded, setIsExpanded] = useState(false);
@@ -84,20 +76,20 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
               </div>
               </div>
               <div>
               <div>
                 <h3 className="font-medium text-white">{provider.name}</h3>
                 <h3 className="font-medium text-white">{provider.name}</h3>
-                <p className="text-sm text-bambu-gray">{PROVIDER_LABELS[provider.provider_type] || provider.provider_type}</p>
+                <p className="text-sm text-bambu-gray">{t(`notifications.providerTypes.${provider.provider_type}`, provider.provider_type)}</p>
               </div>
               </div>
             </div>
             </div>
 
 
             {/* Quick enable/disable toggle + Status indicator */}
             {/* Quick enable/disable toggle + Status indicator */}
             <div className="flex items-center gap-3">
             <div className="flex items-center gap-3">
               {provider.last_success && (
               {provider.last_success && (
-                <span className="text-xs text-status-ok hidden sm:inline">Last: {formatDateOnly(provider.last_success)}</span>
+                <span className="text-xs text-status-ok hidden sm:inline">{t('notifications.lastSuccess', { date: formatDateOnly(provider.last_success) })}</span>
               )}
               )}
               {/* Only show error if it's more recent than last success */}
               {/* Only show error if it's more recent than last success */}
               {provider.last_error && provider.last_error_at && (
               {provider.last_error && provider.last_error_at && (
                 !provider.last_success || (parseUTCDate(provider.last_error_at)?.getTime() || 0) > (parseUTCDate(provider.last_success)?.getTime() || 0)
                 !provider.last_success || (parseUTCDate(provider.last_error_at)?.getTime() || 0) > (parseUTCDate(provider.last_success)?.getTime() || 0)
               ) && (
               ) && (
-                <span className="text-xs text-status-error" title={provider.last_error}>Error</span>
+                <span className="text-xs text-status-error" title={provider.last_error}>{t('notifications.error')}</span>
               )}
               )}
               <Toggle
               <Toggle
                 checked={provider.enabled}
                 checked={provider.enabled}
@@ -109,73 +101,73 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
           {/* Linked Printer */}
           {/* Linked Printer */}
           {linkedPrinter && (
           {linkedPrinter && (
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
-              <span className="text-xs text-bambu-gray">Printer: </span>
+              <span className="text-xs text-bambu-gray">{t('notifications.printer')} </span>
               <span className="text-sm text-white">{linkedPrinter.name}</span>
               <span className="text-sm text-white">{linkedPrinter.name}</span>
             </div>
             </div>
           )}
           )}
           {!linkedPrinter && !provider.printer_id && (
           {!linkedPrinter && !provider.printer_id && (
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
-              <span className="text-xs text-bambu-gray">All printers</span>
+              <span className="text-xs text-bambu-gray">{t('notifications.allPrinters')}</span>
             </div>
             </div>
           )}
           )}
 
 
           {/* Event summary - show all event tags */}
           {/* Event summary - show all event tags */}
           <div className="mb-3 flex flex-wrap gap-1">
           <div className="mb-3 flex flex-wrap gap-1">
             {provider.on_print_start && (
             {provider.on_print_start && (
-              <span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded">Start</span>
+              <span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded">{t('notifications.start')}</span>
             )}
             )}
             {provider.on_plate_not_empty && (
             {provider.on_plate_not_empty && (
-              <span className="px-2 py-0.5 bg-rose-600/20 text-rose-300 text-xs rounded">Plate Check</span>
+              <span className="px-2 py-0.5 bg-rose-600/20 text-rose-300 text-xs rounded">{t('notifications.plateCheck')}</span>
             )}
             )}
             {provider.on_print_complete && (
             {provider.on_print_complete && (
-              <span className="px-2 py-0.5 bg-bambu-green/20 text-bambu-green text-xs rounded">Complete</span>
+              <span className="px-2 py-0.5 bg-bambu-green/20 text-bambu-green text-xs rounded">{t('notifications.complete')}</span>
             )}
             )}
             {provider.on_print_failed && (
             {provider.on_print_failed && (
-              <span className="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded">Failed</span>
+              <span className="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded">{t('notifications.failed')}</span>
             )}
             )}
             {provider.on_print_stopped && (
             {provider.on_print_stopped && (
-              <span className="px-2 py-0.5 bg-orange-500/20 text-orange-400 text-xs rounded">Stopped</span>
+              <span className="px-2 py-0.5 bg-orange-500/20 text-orange-400 text-xs rounded">{t('notifications.stopped')}</span>
             )}
             )}
             {provider.on_print_progress && (
             {provider.on_print_progress && (
-              <span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded">Progress</span>
+              <span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded">{t('notifications.progress')}</span>
             )}
             )}
             {provider.on_printer_offline && (
             {provider.on_printer_offline && (
-              <span className="px-2 py-0.5 bg-gray-500/20 text-gray-400 text-xs rounded">Offline</span>
+              <span className="px-2 py-0.5 bg-gray-500/20 text-gray-400 text-xs rounded">{t('notifications.offline')}</span>
             )}
             )}
             {provider.on_printer_error && (
             {provider.on_printer_error && (
-              <span className="px-2 py-0.5 bg-rose-500/20 text-rose-400 text-xs rounded">Error</span>
+              <span className="px-2 py-0.5 bg-rose-500/20 text-rose-400 text-xs rounded">{t('notifications.error')}</span>
             )}
             )}
             {provider.on_filament_low && (
             {provider.on_filament_low && (
-              <span className="px-2 py-0.5 bg-cyan-500/20 text-cyan-400 text-xs rounded">Low Filament</span>
+              <span className="px-2 py-0.5 bg-cyan-500/20 text-cyan-400 text-xs rounded">{t('notifications.lowFilament')}</span>
             )}
             )}
             {provider.on_maintenance_due && (
             {provider.on_maintenance_due && (
-              <span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">Maintenance</span>
+              <span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">{t('notifications.maintenance')}</span>
             )}
             )}
             {provider.on_ams_humidity_high && (
             {provider.on_ams_humidity_high && (
-              <span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 text-xs rounded">AMS Humidity</span>
+              <span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 text-xs rounded">{t('notifications.amsHumidity')}</span>
             )}
             )}
             {provider.on_ams_temperature_high && (
             {provider.on_ams_temperature_high && (
-              <span className="px-2 py-0.5 bg-orange-600/20 text-orange-300 text-xs rounded">AMS Temp</span>
+              <span className="px-2 py-0.5 bg-orange-600/20 text-orange-300 text-xs rounded">{t('notifications.amsTemp')}</span>
             )}
             )}
             {provider.on_ams_ht_humidity_high && (
             {provider.on_ams_ht_humidity_high && (
-              <span className="px-2 py-0.5 bg-cyan-600/20 text-cyan-300 text-xs rounded">AMS-HT Humidity</span>
+              <span className="px-2 py-0.5 bg-cyan-600/20 text-cyan-300 text-xs rounded">{t('notifications.amsHtHumidity')}</span>
             )}
             )}
             {provider.on_ams_ht_temperature_high && (
             {provider.on_ams_ht_temperature_high && (
-              <span className="px-2 py-0.5 bg-amber-600/20 text-amber-300 text-xs rounded">AMS-HT Temp</span>
+              <span className="px-2 py-0.5 bg-amber-600/20 text-amber-300 text-xs rounded">{t('notifications.amsHtTemp')}</span>
             )}
             )}
             {provider.on_bed_cooled && (
             {provider.on_bed_cooled && (
-              <span className="px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded">Bed Cooled</span>
+              <span className="px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded">{t('notifications.bedCooled')}</span>
             )}
             )}
             {provider.quiet_hours_enabled && (
             {provider.quiet_hours_enabled && (
               <span className="px-2 py-0.5 bg-indigo-500/20 text-indigo-400 text-xs rounded flex items-center gap-1">
               <span className="px-2 py-0.5 bg-indigo-500/20 text-indigo-400 text-xs rounded flex items-center gap-1">
                 <Moon className="w-3 h-3" />
                 <Moon className="w-3 h-3" />
-                Quiet
+                {t('notifications.quiet')}
               </span>
               </span>
             )}
             )}
             {provider.daily_digest_enabled && (
             {provider.daily_digest_enabled && (
               <span className="px-2 py-0.5 bg-emerald-500/20 text-emerald-400 text-xs rounded flex items-center gap-1">
               <span className="px-2 py-0.5 bg-emerald-500/20 text-emerald-400 text-xs rounded flex items-center gap-1">
                 <Calendar className="w-3 h-3" />
                 <Calendar className="w-3 h-3" />
-                Digest {provider.daily_digest_time}
+                {t('notifications.digest', { time: provider.daily_digest_time })}
               </span>
               </span>
             )}
             )}
           </div>
           </div>
@@ -197,7 +189,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
               ) : (
               ) : (
                 <Send className="w-4 h-4" />
                 <Send className="w-4 h-4" />
               )}
               )}
-              Send Test Notification
+              {t('notifications.sendTestNotification')}
             </Button>
             </Button>
           </div>
           </div>
 
 
@@ -224,7 +216,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
           >
           >
             <span className="flex items-center gap-2">
             <span className="flex items-center gap-2">
               <Settings2 className="w-4 h-4" />
               <Settings2 className="w-4 h-4" />
-              Event Settings
+              {t('notifications.eventSettings')}
             </span>
             </span>
             {isExpanded ? (
             {isExpanded ? (
               <ChevronUp className="w-4 h-4" />
               <ChevronUp className="w-4 h-4" />
@@ -239,8 +231,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
               {/* Enabled Toggle */}
               {/* Enabled Toggle */}
               <div className="flex items-center justify-between">
               <div className="flex items-center justify-between">
                 <div>
                 <div>
-                  <p className="text-sm text-white">Enabled</p>
-                  <p className="text-xs text-bambu-gray">Send notifications from this provider</p>
+                  <p className="text-sm text-white">{t('notifications.enabled')}</p>
+                  <p className="text-xs text-bambu-gray">{t('notifications.sendFromProvider')}</p>
                 </div>
                 </div>
                 <Toggle
                 <Toggle
                   checked={provider.enabled}
                   checked={provider.enabled}
@@ -250,10 +242,10 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
               {/* Print Lifecycle Events */}
               {/* Print Lifecycle Events */}
               <div className="space-y-2">
               <div className="space-y-2">
-                <p className="text-xs text-bambu-gray uppercase tracking-wide">Print Events</p>
+                <p className="text-xs text-bambu-gray uppercase tracking-wide">{t('notifications.printEvents')}</p>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Print Started</p>
+                  <p className="text-sm text-white">{t('notifications.printStarted')}</p>
                   <Toggle
                   <Toggle
                     checked={provider.on_print_start}
                     checked={provider.on_print_start}
                     onChange={(checked) => updateMutation.mutate({ on_print_start: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_print_start: checked })}
@@ -262,8 +254,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Plate Not Empty</p>
-                    <p className="text-xs text-bambu-gray">Objects detected before print</p>
+                    <p className="text-sm text-white">{t('notifications.plateNotEmpty')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.plateNotEmptyDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_plate_not_empty ?? true}
                     checked={provider.on_plate_not_empty ?? true}
@@ -272,7 +264,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 </div>
                 </div>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Print Completed</p>
+                  <p className="text-sm text-white">{t('notifications.printCompleted')}</p>
                   <Toggle
                   <Toggle
                     checked={provider.on_print_complete}
                     checked={provider.on_print_complete}
                     onChange={(checked) => updateMutation.mutate({ on_print_complete: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_print_complete: checked })}
@@ -281,8 +273,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Bed Cooled</p>
-                    <p className="text-xs text-bambu-gray">Bed cooled below threshold after print</p>
+                    <p className="text-sm text-white">{t('notifications.bedCooledLabel')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.bedCooledDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_bed_cooled ?? false}
                     checked={provider.on_bed_cooled ?? false}
@@ -291,7 +283,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 </div>
                 </div>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Print Failed</p>
+                  <p className="text-sm text-white">{t('notifications.printFailed')}</p>
                   <Toggle
                   <Toggle
                     checked={provider.on_print_failed}
                     checked={provider.on_print_failed}
                     onChange={(checked) => updateMutation.mutate({ on_print_failed: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_print_failed: checked })}
@@ -299,7 +291,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 </div>
                 </div>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Print Stopped</p>
+                  <p className="text-sm text-white">{t('notifications.printStopped')}</p>
                   <Toggle
                   <Toggle
                     checked={provider.on_print_stopped}
                     checked={provider.on_print_stopped}
                     onChange={(checked) => updateMutation.mutate({ on_print_stopped: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_print_stopped: checked })}
@@ -308,8 +300,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Progress Milestones</p>
-                    <p className="text-xs text-bambu-gray">Notify at 25%, 50%, 75%</p>
+                    <p className="text-sm text-white">{t('notifications.progressMilestones')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.progressMilestonesDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_print_progress}
                     checked={provider.on_print_progress}
@@ -320,10 +312,10 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
               {/* Printer Status Events */}
               {/* Printer Status Events */}
               <div className="space-y-2">
               <div className="space-y-2">
-                <p className="text-xs text-bambu-gray uppercase tracking-wide">Printer Status</p>
+                <p className="text-xs text-bambu-gray uppercase tracking-wide">{t('notifications.printerStatus')}</p>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Printer Offline</p>
+                  <p className="text-sm text-white">{t('notifications.printerOffline')}</p>
                   <Toggle
                   <Toggle
                     checked={provider.on_printer_offline}
                     checked={provider.on_printer_offline}
                     onChange={(checked) => updateMutation.mutate({ on_printer_offline: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_printer_offline: checked })}
@@ -331,7 +323,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 </div>
                 </div>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Printer Error</p>
+                  <p className="text-sm text-white">{t('notifications.printerError')}</p>
                   <Toggle
                   <Toggle
                     checked={provider.on_printer_error}
                     checked={provider.on_printer_error}
                     onChange={(checked) => updateMutation.mutate({ on_printer_error: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_printer_error: checked })}
@@ -339,8 +331,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 </div>
                 </div>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
-                  <p className="text-sm text-white">Low Filament</p>
-                  <Toggle
+                  <p className="text-sm text-white">{t('notifications.lowFilamentLabel')}</p>
+<Toggle
                     checked={provider.on_filament_low}
                     checked={provider.on_filament_low}
                     onChange={(checked) => updateMutation.mutate({ on_filament_low: checked })}
                     onChange={(checked) => updateMutation.mutate({ on_filament_low: checked })}
                   />
                   />
@@ -348,8 +340,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Maintenance Due</p>
-                    <p className="text-xs text-bambu-gray">Notify when maintenance is needed</p>
+                    <p className="text-sm text-white">{t('notifications.maintenanceDue')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.maintenanceDueDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_maintenance_due ?? false}
                     checked={provider.on_maintenance_due ?? false}
@@ -360,12 +352,12 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
               {/* AMS Environmental Alarms (regular AMS) */}
               {/* AMS Environmental Alarms (regular AMS) */}
               <div className="space-y-2">
               <div className="space-y-2">
-                <p className="text-xs text-bambu-gray uppercase tracking-wide">AMS Alarms</p>
+                <p className="text-xs text-bambu-gray uppercase tracking-wide">{t('notifications.amsAlarms')}</p>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">AMS Humidity High</p>
-                    <p className="text-xs text-bambu-gray">Regular AMS humidity exceeds threshold</p>
+                    <p className="text-sm text-white">{t('notifications.amsHumidityHigh')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.amsHumidityHighDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_ams_humidity_high ?? false}
                     checked={provider.on_ams_humidity_high ?? false}
@@ -375,8 +367,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">AMS Temperature High</p>
-                    <p className="text-xs text-bambu-gray">Regular AMS temperature exceeds threshold</p>
+                    <p className="text-sm text-white">{t('notifications.amsTemperatureHigh')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.amsTemperatureHighDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_ams_temperature_high ?? false}
                     checked={provider.on_ams_temperature_high ?? false}
@@ -387,12 +379,12 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
               {/* AMS-HT Environmental Alarms */}
               {/* AMS-HT Environmental Alarms */}
               <div className="space-y-2">
               <div className="space-y-2">
-                <p className="text-xs text-bambu-gray uppercase tracking-wide">AMS-HT Alarms</p>
+                <p className="text-xs text-bambu-gray uppercase tracking-wide">{t('notifications.amsHtAlarms')}</p>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">AMS-HT Humidity High</p>
-                    <p className="text-xs text-bambu-gray">AMS-HT humidity exceeds threshold</p>
+                    <p className="text-sm text-white">{t('notifications.amsHtHumidityHigh')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.amsHtHumidityHighDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_ams_ht_humidity_high ?? false}
                     checked={provider.on_ams_ht_humidity_high ?? false}
@@ -402,8 +394,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">AMS-HT Temperature High</p>
-                    <p className="text-xs text-bambu-gray">AMS-HT temperature exceeds threshold</p>
+                    <p className="text-sm text-white">{t('notifications.amsHtTemperatureHigh')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.amsHtTemperatureHighDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_ams_ht_temperature_high ?? false}
                     checked={provider.on_ams_ht_temperature_high ?? false}
@@ -414,12 +406,12 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
               {/* Print Queue Events */}
               {/* Print Queue Events */}
               <div className="space-y-2">
               <div className="space-y-2">
-                <p className="text-xs text-bambu-gray uppercase tracking-wide">Print Queue</p>
+                <p className="text-xs text-bambu-gray uppercase tracking-wide">{t('notifications.printQueue')}</p>
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Job Added</p>
-                    <p className="text-xs text-bambu-gray">Job added to queue</p>
+                    <p className="text-sm text-white">{t('notifications.jobAdded')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.jobAddedDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_job_added ?? false}
                     checked={provider.on_queue_job_added ?? false}
@@ -429,8 +421,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Job Assigned</p>
-                    <p className="text-xs text-bambu-gray">Model-based job assigned to printer</p>
+                    <p className="text-sm text-white">{t('notifications.jobAssigned')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.jobAssignedDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_job_assigned ?? false}
                     checked={provider.on_queue_job_assigned ?? false}
@@ -440,8 +432,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Job Started</p>
-                    <p className="text-xs text-bambu-gray">Queue job started printing</p>
+                    <p className="text-sm text-white">{t('notifications.jobStarted')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.jobStartedDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_job_started ?? false}
                     checked={provider.on_queue_job_started ?? false}
@@ -451,8 +443,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Job Waiting</p>
-                    <p className="text-xs text-bambu-gray">Job waiting for filament</p>
+                    <p className="text-sm text-white">{t('notifications.jobWaiting')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.jobWaitingDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_job_waiting ?? true}
                     checked={provider.on_queue_job_waiting ?? true}
@@ -462,8 +454,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Job Skipped</p>
-                    <p className="text-xs text-bambu-gray">Job skipped (previous failed)</p>
+                    <p className="text-sm text-white">{t('notifications.jobSkipped')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.jobSkippedDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_job_skipped ?? true}
                     checked={provider.on_queue_job_skipped ?? true}
@@ -473,8 +465,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Job Failed</p>
-                    <p className="text-xs text-bambu-gray">Job failed to start</p>
+                    <p className="text-sm text-white">{t('notifications.jobFailed')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.jobFailedDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_job_failed ?? true}
                     checked={provider.on_queue_job_failed ?? true}
@@ -484,8 +476,8 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div>
                   <div>
-                    <p className="text-sm text-white">Queue Complete</p>
-                    <p className="text-xs text-bambu-gray">All queue jobs finished</p>
+                    <p className="text-sm text-white">{t('notifications.queueComplete')}</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.queueCompleteDescription')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.on_queue_completed ?? false}
                     checked={provider.on_queue_completed ?? false}
@@ -499,7 +491,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div className="flex items-center gap-2">
                   <div className="flex items-center gap-2">
                     <Moon className="w-4 h-4 text-purple-400" />
                     <Moon className="w-4 h-4 text-purple-400" />
-                    <p className="text-sm text-white">Quiet Hours</p>
+                    <p className="text-sm text-white">{t('notifications.quietHours')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.quiet_hours_enabled}
                     checked={provider.quiet_hours_enabled}
@@ -509,14 +501,14 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 {provider.quiet_hours_enabled && (
                 {provider.quiet_hours_enabled && (
                   <div className="pl-4 border-l-2 border-bambu-dark-tertiary space-y-2">
                   <div className="pl-4 border-l-2 border-bambu-dark-tertiary space-y-2">
-                    <p className="text-xs text-bambu-gray">No notifications during these hours</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.noNotificationsDuring')}</p>
                     <div className="flex items-center gap-2">
                     <div className="flex items-center gap-2">
                       <Clock className="w-4 h-4 text-bambu-gray" />
                       <Clock className="w-4 h-4 text-bambu-gray" />
                       <span className="text-sm text-white">
                       <span className="text-sm text-white">
                         {formatTime(provider.quiet_hours_start) || '22:00'} - {formatTime(provider.quiet_hours_end) || '07:00'}
                         {formatTime(provider.quiet_hours_start) || '22:00'} - {formatTime(provider.quiet_hours_end) || '07:00'}
                       </span>
                       </span>
                     </div>
                     </div>
-                    <p className="text-xs text-bambu-gray">Edit provider to change quiet hours</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.editProviderToChangeQuietHours')}</p>
                   </div>
                   </div>
                 )}
                 )}
               </div>
               </div>
@@ -526,7 +518,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">
                   <div className="flex items-center gap-2">
                   <div className="flex items-center gap-2">
                     <Calendar className="w-4 h-4 text-emerald-400" />
                     <Calendar className="w-4 h-4 text-emerald-400" />
-                    <p className="text-sm text-white">Daily Digest</p>
+                    <p className="text-sm text-white">{t('notifications.dailyDigest')}</p>
                   </div>
                   </div>
                   <Toggle
                   <Toggle
                     checked={provider.daily_digest_enabled}
                     checked={provider.daily_digest_enabled}
@@ -536,14 +528,14 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
 
 
                 {provider.daily_digest_enabled && (
                 {provider.daily_digest_enabled && (
                   <div className="pl-4 border-l-2 border-bambu-dark-tertiary space-y-2">
                   <div className="pl-4 border-l-2 border-bambu-dark-tertiary space-y-2">
-                    <p className="text-xs text-bambu-gray">Batch notifications into a single daily summary</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.batchNotifications')}</p>
                     <div className="flex items-center gap-2">
                     <div className="flex items-center gap-2">
                       <Clock className="w-4 h-4 text-bambu-gray" />
                       <Clock className="w-4 h-4 text-bambu-gray" />
                       <span className="text-sm text-white">
                       <span className="text-sm text-white">
-                        Send at {formatTime(provider.daily_digest_time) || '08:00'}
+                        {t('notifications.sendAt', { time: formatTime(provider.daily_digest_time) || '08:00' })}
                       </span>
                       </span>
                     </div>
                     </div>
-                    <p className="text-xs text-bambu-gray">Edit provider to change digest time</p>
+                    <p className="text-xs text-bambu-gray">{t('notifications.editProviderToChangeDigestTime')}</p>
                   </div>
                   </div>
                 )}
                 )}
               </div>
               </div>
@@ -557,7 +549,7 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                   className="flex-1"
                   className="flex-1"
                 >
                 >
                   <Edit2 className="w-4 h-4" />
                   <Edit2 className="w-4 h-4" />
-                  Edit
+                  {t('notifications.edit')}
                 </Button>
                 </Button>
                 <Button
                 <Button
                   size="sm"
                   size="sm"
@@ -576,9 +568,9 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
       {/* Delete Confirmation */}
       {/* Delete Confirmation */}
       {showDeleteConfirm && (
       {showDeleteConfirm && (
         <ConfirmModal
         <ConfirmModal
-          title="Delete Notification Provider"
-          message={`Are you sure you want to delete "${provider.name}"? This cannot be undone.`}
-          confirmText="Delete"
+          title={t('notifications.deleteProvider')}
+          message={t('notifications.deleteConfirm', { name: provider.name })}
+          confirmText={t('notifications.delete')}
           variant="danger"
           variant="danger"
           onConfirm={() => {
           onConfirm={() => {
             deleteMutation.mutate();
             deleteMutation.mutate();

+ 20 - 18
frontend/src/components/NotificationTemplateEditor.tsx

@@ -1,5 +1,6 @@
 import { useState, useEffect, useRef } from 'react';
 import { useState, useEffect, useRef } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
 import { X, Save, Loader2, RotateCcw, Plus, Eye } from 'lucide-react';
 import { X, Save, Loader2, RotateCcw, Plus, Eye } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { NotificationTemplate, NotificationTemplateUpdate } from '../api/client';
 import type { NotificationTemplate, NotificationTemplateUpdate } from '../api/client';
@@ -11,6 +12,7 @@ interface NotificationTemplateEditorProps {
 }
 }
 
 
 export function NotificationTemplateEditor({ template, onClose }: NotificationTemplateEditorProps) {
 export function NotificationTemplateEditor({ template, onClose }: NotificationTemplateEditorProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const bodyRef = useRef<HTMLTextAreaElement>(null);
   const bodyRef = useRef<HTMLTextAreaElement>(null);
 
 
@@ -78,11 +80,11 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
     setError(null);
     setError(null);
 
 
     if (!titleTemplate.trim()) {
     if (!titleTemplate.trim()) {
-      setError('Title is required');
+      setError(t('notifications.titleRequired'));
       return;
       return;
     }
     }
     if (!bodyTemplate.trim()) {
     if (!bodyTemplate.trim()) {
-      setError('Body is required');
+      setError(t('notifications.bodyRequired'));
       return;
       return;
     }
     }
 
 
@@ -121,7 +123,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
         {/* Header */}
         {/* Header */}
         <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary shrink-0">
         <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary shrink-0">
           <h2 className="text-lg font-semibold text-white">
           <h2 className="text-lg font-semibold text-white">
-            Edit Template: {template.name}
+            {t('notifications.editTemplate', { name: template.name })}
           </h2>
           </h2>
           <button
           <button
             onClick={onClose}
             onClick={onClose}
@@ -142,21 +144,21 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
           {/* Title */}
           {/* Title */}
           <div>
           <div>
             <label className="block text-sm font-medium text-bambu-gray mb-1">
             <label className="block text-sm font-medium text-bambu-gray mb-1">
-              Title
+              {t('notifications.titleLabel')}
             </label>
             </label>
             <input
             <input
               type="text"
               type="text"
               value={titleTemplate}
               value={titleTemplate}
               onChange={(e) => setTitleTemplate(e.target.value)}
               onChange={(e) => setTitleTemplate(e.target.value)}
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white focus:outline-none focus:ring-1 focus:ring-bambu-green"
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white focus:outline-none focus:ring-1 focus:ring-bambu-green"
-              placeholder="Notification title..."
+              placeholder={t('notifications.titlePlaceholder')}
             />
             />
           </div>
           </div>
 
 
           {/* Body */}
           {/* Body */}
           <div>
           <div>
             <label className="block text-sm font-medium text-bambu-gray mb-1">
             <label className="block text-sm font-medium text-bambu-gray mb-1">
-              Body
+              {t('notifications.bodyLabel')}
             </label>
             </label>
             <textarea
             <textarea
               ref={bodyRef}
               ref={bodyRef}
@@ -164,7 +166,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
               onChange={(e) => setBodyTemplate(e.target.value)}
               onChange={(e) => setBodyTemplate(e.target.value)}
               rows={4}
               rows={4}
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white focus:outline-none focus:ring-1 focus:ring-bambu-green font-mono text-sm resize-none"
               className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white focus:outline-none focus:ring-1 focus:ring-bambu-green font-mono text-sm resize-none"
-              placeholder="Notification body..."
+              placeholder={t('notifications.bodyPlaceholder')}
             />
             />
           </div>
           </div>
 
 
@@ -172,7 +174,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
           {eventVariables && (
           {eventVariables && (
             <div>
             <div>
               <label className="block text-sm font-medium text-bambu-gray mb-2">
               <label className="block text-sm font-medium text-bambu-gray mb-2">
-                Available Variables
+                {t('notifications.availableVariables')}
               </label>
               </label>
               <div className="flex flex-wrap gap-2">
               <div className="flex flex-wrap gap-2">
                 {eventVariables.variables.map((variable) => (
                 {eventVariables.variables.map((variable) => (
@@ -188,7 +190,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
                 ))}
                 ))}
               </div>
               </div>
               <p className="text-xs text-bambu-gray/60 mt-1">
               <p className="text-xs text-bambu-gray/60 mt-1">
-                Click to insert at cursor position in body
+                {t('notifications.clickToInsert')}
               </p>
               </p>
             </div>
             </div>
           )}
           )}
@@ -198,14 +200,14 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
             <div className="flex items-center justify-between mb-2">
             <div className="flex items-center justify-between mb-2">
               <label className="text-sm font-medium text-bambu-gray flex items-center gap-2">
               <label className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                 <Eye className="w-4 h-4" />
                 <Eye className="w-4 h-4" />
-                Live Preview
+                {t('notifications.livePreview')}
               </label>
               </label>
               <button
               <button
                 type="button"
                 type="button"
                 onClick={() => setShowPreview(!showPreview)}
                 onClick={() => setShowPreview(!showPreview)}
                 className="text-xs text-bambu-green hover:text-bambu-green-light"
                 className="text-xs text-bambu-green hover:text-bambu-green-light"
               >
               >
-                {showPreview ? 'Hide' : 'Show'}
+                {showPreview ? t('notifications.hide') : t('notifications.show')}
               </button>
               </button>
             </div>
             </div>
             {showPreview && (
             {showPreview && (
@@ -213,22 +215,22 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
                 {previewLoading ? (
                 {previewLoading ? (
                   <div className="flex items-center gap-2 text-bambu-gray text-sm">
                   <div className="flex items-center gap-2 text-bambu-gray text-sm">
                     <Loader2 className="w-4 h-4 animate-spin" />
                     <Loader2 className="w-4 h-4 animate-spin" />
-                    Loading preview...
+                    {t('notifications.loadingPreview')}
                   </div>
                   </div>
                 ) : preview ? (
                 ) : preview ? (
                   <>
                   <>
                     <div>
                     <div>
-                      <span className="text-xs text-bambu-gray">Title:</span>
+                      <span className="text-xs text-bambu-gray">{t('notifications.titlePreview')}</span>
                       <div className="text-white font-medium">{preview.title}</div>
                       <div className="text-white font-medium">{preview.title}</div>
                     </div>
                     </div>
                     <div>
                     <div>
-                      <span className="text-xs text-bambu-gray">Body:</span>
+                      <span className="text-xs text-bambu-gray">{t('notifications.bodyPreview')}</span>
                       <div className="text-white whitespace-pre-wrap text-sm">{preview.body}</div>
                       <div className="text-white whitespace-pre-wrap text-sm">{preview.body}</div>
                     </div>
                     </div>
                   </>
                   </>
                 ) : (
                 ) : (
                   <div className="text-bambu-gray text-sm">
                   <div className="text-bambu-gray text-sm">
-                    Enter template content to see preview
+                    {t('notifications.enterTemplateContent')}
                   </div>
                   </div>
                 )}
                 )}
               </div>
               </div>
@@ -250,12 +252,12 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
             ) : (
             ) : (
               <RotateCcw className="w-4 h-4 mr-2" />
               <RotateCcw className="w-4 h-4 mr-2" />
             )}
             )}
-            Reset to Default
+            {t('notifications.resetToDefault')}
           </Button>
           </Button>
 
 
           <div className="flex gap-2">
           <div className="flex gap-2">
             <Button type="button" variant="secondary" onClick={onClose}>
             <Button type="button" variant="secondary" onClick={onClose}>
-              Cancel
+              {t('notifications.cancel')}
             </Button>
             </Button>
             <Button
             <Button
               onClick={handleSubmit}
               onClick={handleSubmit}
@@ -266,7 +268,7 @@ export function NotificationTemplateEditor({ template, onClose }: NotificationTe
               ) : (
               ) : (
                 <Save className="w-4 h-4 mr-2" />
                 <Save className="w-4 h-4 mr-2" />
               )}
               )}
-              Save
+              {t('notifications.save')}
             </Button>
             </Button>
           </div>
           </div>
         </div>
         </div>

+ 37 - 50
frontend/src/components/RestoreModal.tsx

@@ -1,4 +1,5 @@
 import { useState, useRef, useEffect } from 'react';
 import { useState, useRef, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
 import { Upload, X, AlertTriangle, CheckCircle, SkipForward, RefreshCw, Loader2, ChevronDown, ChevronUp } from 'lucide-react';
 import { Upload, X, AlertTriangle, CheckCircle, SkipForward, RefreshCw, Loader2, ChevronDown, ChevronUp } from 'lucide-react';
 import { Card, CardContent } from './Card';
 import { Card, CardContent } from './Card';
 import { Button } from './Button';
 import { Button } from './Button';
@@ -23,22 +24,8 @@ interface RestoreModalProps {
 
 
 type ModalState = 'options' | 'restoring' | 'result';
 type ModalState = 'options' | 'restoring' | 'result';
 
 
-const CATEGORY_LABELS: Record<string, string> = {
-  settings: 'Settings',
-  notification_providers: 'Notification Providers',
-  notification_templates: 'Notification Templates',
-  smart_plugs: 'Smart Plugs',
-  printers: 'Printers',
-  filaments: 'Filaments',
-  maintenance_types: 'Maintenance Types',
-  archives: 'Archives',
-  projects: 'Projects',
-  pending_uploads: 'Pending Uploads',
-  external_links: 'External Links',
-  api_keys: 'API Keys',
-};
-
 export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProps) {
 export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProps) {
+  const { t } = useTranslation();
   const [state, setState] = useState<ModalState>('options');
   const [state, setState] = useState<ModalState>('options');
   const [overwrite, setOverwrite] = useState(false);
   const [overwrite, setOverwrite] = useState(false);
   const [selectedFile, setSelectedFile] = useState<File | null>(null);
   const [selectedFile, setSelectedFile] = useState<File | null>(null);
@@ -80,7 +67,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
     } catch {
     } catch {
       setResult({
       setResult({
         success: false,
         success: false,
-        message: 'Failed to restore backup. Please check the file format.',
+        message: t('backup.failedToRestore'),
       });
       });
       setState('result');
       setState('result');
     }
     }
@@ -143,13 +130,13 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
               </div>
               </div>
               <div>
               <div>
                 <h3 className="text-lg font-semibold text-white">
                 <h3 className="text-lg font-semibold text-white">
-                  {state === 'options' && 'Restore Backup'}
-                  {state === 'restoring' && 'Restoring...'}
-                  {state === 'result' && (result?.success ? 'Restore Complete' : 'Restore Failed')}
+                  {state === 'options' && t('backup.restoreBackup')}
+                  {state === 'restoring' && t('backup.restoring')}
+                  {state === 'result' && (result?.success ? t('backup.restoreComplete') : t('backup.restoreFailed2'))}
                 </h3>
                 </h3>
                 <p className="text-sm text-bambu-gray">
                 <p className="text-sm text-bambu-gray">
-                  {state === 'options' && 'Import settings from a backup file'}
-                  {state === 'restoring' && 'Please wait while your data is being restored'}
+                  {state === 'options' && t('backup.importSettings')}
+                  {state === 'restoring' && t('backup.pleaseWaitRestoring')}
                   {state === 'result' && result?.message}
                   {state === 'result' && result?.message}
                 </p>
                 </p>
               </div>
               </div>
@@ -194,7 +181,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                     ) : (
                     ) : (
                       <div className="flex flex-col items-center gap-2 text-bambu-gray">
                       <div className="flex flex-col items-center gap-2 text-bambu-gray">
                         <Upload className="w-8 h-8" />
                         <Upload className="w-8 h-8" />
-                        <span>Click to select backup file (.json or .zip)</span>
+                        <span>{t('backup.selectBackupFile')}</span>
                       </div>
                       </div>
                     )}
                     )}
                   </button>
                   </button>
@@ -205,15 +192,15 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                   <div className="flex items-start gap-2 text-sm">
                   <div className="flex items-start gap-2 text-sm">
                     <AlertTriangle className="w-4 h-4 text-blue-500 dark:text-blue-400 mt-0.5 flex-shrink-0" />
                     <AlertTriangle className="w-4 h-4 text-blue-500 dark:text-blue-400 mt-0.5 flex-shrink-0" />
                     <div className="text-blue-700 dark:text-blue-200">
                     <div className="text-blue-700 dark:text-blue-200">
-                      <p className="font-medium mb-1">How duplicate handling works:</p>
+                      <p className="font-medium mb-1">{t('backup.duplicateHandling')}</p>
                       <ul className="text-blue-600 dark:text-blue-200/80 space-y-1 text-xs">
                       <ul className="text-blue-600 dark:text-blue-200/80 space-y-1 text-xs">
-                        <li><strong>Printers</strong> - matched by serial number</li>
-                        <li><strong>Smart Plugs</strong> - matched by IP address</li>
-                        <li><strong>Notification Providers</strong> - matched by name</li>
-                        <li><strong>Filaments</strong> - matched by name + type + brand</li>
-                        <li><strong>Archives</strong> - matched by content hash (always skipped)</li>
-                        <li><strong>Pending Uploads</strong> - matched by filename</li>
-                        <li><strong>Settings & Templates</strong> - always overwritten</li>
+                        <li><strong>{t('backup.matchPrinters')}</strong> - {t('backup.matchPrintersBy')}</li>
+                        <li><strong>{t('backup.matchSmartPlugs')}</strong> - {t('backup.matchSmartPlugsBy')}</li>
+                        <li><strong>{t('backup.matchNotificationProviders')}</strong> - {t('backup.matchNotificationProvidersBy')}</li>
+                        <li><strong>{t('backup.matchFilaments')}</strong> - {t('backup.matchFilamentsBy')}</li>
+                        <li><strong>{t('backup.matchArchives')}</strong> - {t('backup.matchArchivesBy')}</li>
+                        <li><strong>{t('backup.matchPendingUploads')}</strong> - {t('backup.matchPendingUploadsBy')}</li>
+                        <li><strong>{t('backup.matchSettingsTemplates')}</strong> - {t('backup.matchSettingsTemplatesBy')}</li>
                       </ul>
                       </ul>
                     </div>
                     </div>
                   </div>
                   </div>
@@ -229,12 +216,12 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                         ) : (
                         ) : (
                           <SkipForward className="w-4 h-4 text-bambu-gray" />
                           <SkipForward className="w-4 h-4 text-bambu-gray" />
                         )}
                         )}
-                        {overwrite ? 'Replace existing data' : 'Keep existing data'}
+                        {overwrite ? t('backup.replaceExisting') : t('backup.keepExisting')}
                       </p>
                       </p>
                       <p className="text-sm text-bambu-gray mt-1">
                       <p className="text-sm text-bambu-gray mt-1">
                         {overwrite
                         {overwrite
-                          ? 'Overwrite items that already exist with backup data'
-                          : 'Only restore items that don\'t already exist'}
+                          ? t('backup.overwriteDescription')
+                          : t('backup.keepDescription')}
                       </p>
                       </p>
                     </div>
                     </div>
                     <Toggle checked={overwrite} onChange={setOverwrite} />
                     <Toggle checked={overwrite} onChange={setOverwrite} />
@@ -246,7 +233,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                     <div className="flex items-start gap-2 text-sm">
                     <div className="flex items-start gap-2 text-sm">
                       <AlertTriangle className="w-4 h-4 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
                       <AlertTriangle className="w-4 h-4 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
                       <div className="text-orange-700 dark:text-orange-200">
                       <div className="text-orange-700 dark:text-orange-200">
-                        <span className="font-medium">Caution:</span> Overwriting will replace your current configurations with data from the backup. Printer access codes are never overwritten for security.
+                        <span className="font-medium">{t('backup.overwriteCaution')}</span> {t('backup.overwriteWarning')}
                       </div>
                       </div>
                     </div>
                     </div>
                   </div>
                   </div>
@@ -256,7 +243,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
               {/* Footer */}
               {/* Footer */}
               <div className="flex items-center justify-end gap-3 p-4 border-t border-bambu-dark-tertiary">
               <div className="flex items-center justify-end gap-3 p-4 border-t border-bambu-dark-tertiary">
                 <Button type="button" variant="secondary" onClick={onClose}>
                 <Button type="button" variant="secondary" onClick={onClose}>
-                  Cancel
+                  {t('backup.cancel')}
                 </Button>
                 </Button>
                 <Button
                 <Button
                   type="button"
                   type="button"
@@ -265,7 +252,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                   className="bg-bambu-green hover:bg-bambu-green-dark disabled:opacity-50"
                   className="bg-bambu-green hover:bg-bambu-green-dark disabled:opacity-50"
                 >
                 >
                   <Upload className="w-4 h-4 mr-2" />
                   <Upload className="w-4 h-4 mr-2" />
-                  Restore
+                  {t('backup.restore')}
                 </Button>
                 </Button>
               </div>
               </div>
             </>
             </>
@@ -275,7 +262,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
           {state === 'restoring' && (
           {state === 'restoring' && (
             <div className="p-8 flex flex-col items-center gap-4">
             <div className="p-8 flex flex-col items-center gap-4">
               <Loader2 className="w-12 h-12 text-bambu-green animate-spin" />
               <Loader2 className="w-12 h-12 text-bambu-green animate-spin" />
-              <p className="text-bambu-gray">Processing backup file...</p>
+              <p className="text-bambu-gray">{t('backup.processingBackup')}</p>
             </div>
             </div>
           )}
           )}
 
 
@@ -287,11 +274,11 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                 <div className="grid grid-cols-2 gap-3">
                 <div className="grid grid-cols-2 gap-3">
                   <div className="p-3 rounded-lg bg-bambu-green/10 border border-bambu-green/30">
                   <div className="p-3 rounded-lg bg-bambu-green/10 border border-bambu-green/30">
                     <div className="text-2xl font-bold text-bambu-green">{totalRestored}</div>
                     <div className="text-2xl font-bold text-bambu-green">{totalRestored}</div>
-                    <div className="text-sm text-bambu-gray">Items Restored</div>
+                    <div className="text-sm text-bambu-gray">{t('backup.itemsRestored')}</div>
                   </div>
                   </div>
                   <div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
                   <div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
                     <div className="text-2xl font-bold text-yellow-500">{totalSkipped}</div>
                     <div className="text-2xl font-bold text-yellow-500">{totalSkipped}</div>
-                    <div className="text-sm text-bambu-gray">Items Skipped</div>
+                    <div className="text-sm text-bambu-gray">{t('backup.itemsSkipped')}</div>
                   </div>
                   </div>
                 </div>
                 </div>
 
 
@@ -300,20 +287,20 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                   <div className="space-y-2">
                   <div className="space-y-2">
                     <h4 className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                     <h4 className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                       <CheckCircle className="w-4 h-4 text-bambu-green" />
                       <CheckCircle className="w-4 h-4 text-bambu-green" />
-                      Restored
+                      {t('backup.restored')}
                     </h4>
                     </h4>
                     <div className="space-y-1">
                     <div className="space-y-1">
                       {Object.entries(result.restored)
                       {Object.entries(result.restored)
                         .filter(([, count]) => count > 0)
                         .filter(([, count]) => count > 0)
                         .map(([key, count]) => (
                         .map(([key, count]) => (
                           <div key={key} className="flex items-center justify-between text-sm p-2 rounded bg-bambu-dark">
                           <div key={key} className="flex items-center justify-between text-sm p-2 rounded bg-bambu-dark">
-                            <span className="text-white">{CATEGORY_LABELS[key] || key}</span>
+                            <span className="text-white">{t(`backup.categories.${key}`, key)}</span>
                             <span className="text-bambu-green font-medium">{count}</span>
                             <span className="text-bambu-green font-medium">{count}</span>
                           </div>
                           </div>
                         ))}
                         ))}
                       {(result.files_restored || 0) > 0 && (
                       {(result.files_restored || 0) > 0 && (
                         <div className="flex items-center justify-between text-sm p-2 rounded bg-bambu-dark">
                         <div className="flex items-center justify-between text-sm p-2 rounded bg-bambu-dark">
-                          <span className="text-white">Files (3MF, thumbnails, etc.)</span>
+                          <span className="text-white">{t('backup.filesCategory')}</span>
                           <span className="text-bambu-green font-medium">{result.files_restored}</span>
                           <span className="text-bambu-green font-medium">{result.files_restored}</span>
                         </div>
                         </div>
                       )}
                       )}
@@ -326,7 +313,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                   <div className="space-y-2">
                   <div className="space-y-2">
                     <h4 className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                     <h4 className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                       <SkipForward className="w-4 h-4 text-yellow-500" />
                       <SkipForward className="w-4 h-4 text-yellow-500" />
-                      Skipped (already exist)
+                      {t('backup.skippedAlreadyExist')}
                     </h4>
                     </h4>
                     <div className="space-y-1">
                     <div className="space-y-1">
                       {Object.entries(result.skipped)
                       {Object.entries(result.skipped)
@@ -343,7 +330,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                                 }`}
                                 }`}
                               >
                               >
                                 <span className="text-white flex items-center gap-2">
                                 <span className="text-white flex items-center gap-2">
-                                  {CATEGORY_LABELS[key] || key}
+                                  {t(`backup.categories.${key}`, key)}
                                   {details.length > 0 && (
                                   {details.length > 0 && (
                                     isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />
                                     isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />
                                   )}
                                   )}
@@ -356,7 +343,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                                     <div key={i}>{item}</div>
                                     <div key={i}>{item}</div>
                                   ))}
                                   ))}
                                   {details.length > 10 && (
                                   {details.length > 10 && (
-                                    <div className="text-bambu-gray/60">...and {details.length - 10} more</div>
+                                    <div className="text-bambu-gray/60">{t('backup.andMore', { count: details.length - 10 })}</div>
                                   )}
                                   )}
                                 </div>
                                 </div>
                               )}
                               )}
@@ -372,11 +359,11 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                   <div className="space-y-2">
                   <div className="space-y-2">
                     <h4 className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                     <h4 className="text-sm font-medium text-bambu-gray flex items-center gap-2">
                       <AlertTriangle className="w-4 h-4 text-orange-500" />
                       <AlertTriangle className="w-4 h-4 text-orange-500" />
-                      New API Keys Generated
+                      {t('backup.newApiKeysGenerated')}
                     </h4>
                     </h4>
                     <div className="p-3 rounded bg-orange-500/10 border border-orange-500/30">
                     <div className="p-3 rounded bg-orange-500/10 border border-orange-500/30">
                       <p className="text-xs text-orange-200 mb-2">
                       <p className="text-xs text-orange-200 mb-2">
-                        These keys are only shown once. Copy them now!
+                        {t('backup.keysShownOnce')}
                       </p>
                       </p>
                       <div className="space-y-2">
                       <div className="space-y-2">
                         {result.new_api_keys.map((apiKey: { name: string; key: string; key_prefix: string }, i: number) => (
                         {result.new_api_keys.map((apiKey: { name: string; key: string; key_prefix: string }, i: number) => (
@@ -390,7 +377,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
                                 onClick={() => navigator.clipboard.writeText(apiKey.key)}
                                 onClick={() => navigator.clipboard.writeText(apiKey.key)}
                                 className="text-xs text-bambu-gray hover:text-white px-2 py-1 rounded bg-bambu-dark-tertiary"
                                 className="text-xs text-bambu-gray hover:text-white px-2 py-1 rounded bg-bambu-dark-tertiary"
                               >
                               >
-                                Copy
+                                {t('backup.copy')}
                               </button>
                               </button>
                             </div>
                             </div>
                           </div>
                           </div>
@@ -402,7 +389,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
 
 
                 {totalRestored === 0 && totalSkipped === 0 && (
                 {totalRestored === 0 && totalSkipped === 0 && (
                   <div className="p-4 text-center text-bambu-gray">
                   <div className="p-4 text-center text-bambu-gray">
-                    No data was found to restore in the backup file.
+                    {t('backup.noDataFound')}
                   </div>
                   </div>
                 )}
                 )}
               </div>
               </div>
@@ -410,7 +397,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
               {/* Footer */}
               {/* Footer */}
               <div className="flex items-center justify-end gap-3 p-4 border-t border-bambu-dark-tertiary">
               <div className="flex items-center justify-end gap-3 p-4 border-t border-bambu-dark-tertiary">
                 <Button onClick={handleClose}>
                 <Button onClick={handleClose}>
-                  Close
+                  {t('backup.close')}
                 </Button>
                 </Button>
               </div>
               </div>
             </>
             </>

+ 32 - 32
frontend/src/components/SmartPlugCard.tsx

@@ -62,7 +62,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       if (context?.previousStatus) {
       if (context?.previousStatus) {
         queryClient.setQueryData(['smart-plug-status', plug.id], context.previousStatus);
         queryClient.setQueryData(['smart-plug-status', plug.id], context.previousStatus);
       }
       }
-      showToast(`Failed to turn ${action} "${plug.name}"`, 'error');
+      showToast(t('smartPlugs.failedToTurn', { action, name: plug.name }), 'error');
     },
     },
     onSettled: () => {
     onSettled: () => {
       // Refetch after a short delay to get actual state
       // Refetch after a short delay to get actual state
@@ -162,13 +162,13 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                 <div className="flex items-center gap-1 text-sm">
                 <div className="flex items-center gap-1 text-sm">
                   <span className="px-1 py-0.5 bg-blue-500/20 text-blue-400 text-[10px] font-medium rounded">HA</span>
                   <span className="px-1 py-0.5 bg-blue-500/20 text-blue-400 text-[10px] font-medium rounded">HA</span>
                   <span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
                   <span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
-                    {isReachable ? (status?.state || '?') : 'Offline'}
+                    {isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
                   </span>
                   </span>
                 </div>
                 </div>
               ) : isReachable ? (
               ) : isReachable ? (
                 <div className="flex items-center gap-1 text-sm">
                 <div className="flex items-center gap-1 text-sm">
                   <Wifi className="w-4 h-4 text-status-ok" />
                   <Wifi className="w-4 h-4 text-status-ok" />
-                  <span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
+                  <span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || t('smartPlugs.unknown')}</span>
                 </div>
                 </div>
               ) : (
               ) : (
                 <div className="flex items-center gap-1 text-sm text-status-error">
                 <div className="flex items-center gap-1 text-sm text-status-error">
@@ -195,7 +195,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
           {/* Linked Printer */}
           {/* Linked Printer */}
           {linkedPrinter && (
           {linkedPrinter && (
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
             <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
-              <span className="text-xs text-bambu-gray">Linked to: </span>
+              <span className="text-xs text-bambu-gray">{t('smartPlugs.linkedTo')} </span>
               <span className="text-sm text-white">{linkedPrinter.name}</span>
               <span className="text-sm text-white">{linkedPrinter.name}</span>
             </div>
             </div>
           )}
           )}
@@ -206,13 +206,13 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
               {plug.plug_type === 'mqtt' && (
               {plug.plug_type === 'mqtt' && (
                 <span className="flex items-center gap-1 px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded-full">
                 <span className="flex items-center gap-1 px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded-full">
                   <Eye className="w-3 h-3" />
                   <Eye className="w-3 h-3" />
-                  Monitor Only
+                  {t('smartPlugs.monitorOnly')}
                 </span>
                 </span>
               )}
               )}
               {plug.power_alert_enabled && (
               {plug.power_alert_enabled && (
                 <span className="flex items-center gap-1 px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded-full">
                 <span className="flex items-center gap-1 px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded-full">
                   <Bell className="w-3 h-3" />
                   <Bell className="w-3 h-3" />
-                  Alerts
+                  {t('smartPlugs.alerts')}
                 </span>
                 </span>
               )}
               )}
               {plug.schedule_enabled && (
               {plug.schedule_enabled && (
@@ -221,8 +221,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   {plug.schedule_on_time && plug.schedule_off_time
                   {plug.schedule_on_time && plug.schedule_off_time
                     ? `${plug.schedule_on_time} - ${plug.schedule_off_time}`
                     ? `${plug.schedule_on_time} - ${plug.schedule_off_time}`
                     : plug.schedule_on_time
                     : plug.schedule_on_time
-                      ? `On ${plug.schedule_on_time}`
-                      : `Off ${plug.schedule_off_time}`}
+                      ? t('smartPlugs.scheduleOn', { time: plug.schedule_on_time })
+                      : t('smartPlugs.scheduleOff', { time: plug.schedule_off_time })}
                 </span>
                 </span>
               )}
               )}
             </div>
             </div>
@@ -239,7 +239,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                 className="flex-1"
                 className="flex-1"
               >
               >
                 {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
                 {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
-                On
+                {t('smartPlugs.on')}
               </Button>
               </Button>
               <Button
               <Button
                 size="sm"
                 size="sm"
@@ -249,7 +249,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                 className="flex-1"
                 className="flex-1"
               >
               >
                 {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
                 {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
-                Off
+                {t('smartPlugs.off')}
               </Button>
               </Button>
             </div>
             </div>
           )}
           )}
@@ -260,13 +260,13 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
               {status.energy.power !== null && status.energy.power !== undefined && (
               {status.energy.power !== null && status.energy.power !== undefined && (
                 <div className="flex-1 text-center">
                 <div className="flex-1 text-center">
                   <p className="text-lg font-semibold text-white">{Math.round(status.energy.power)}W</p>
                   <p className="text-lg font-semibold text-white">{Math.round(status.energy.power)}W</p>
-                  <p className="text-xs text-bambu-gray">Power</p>
+                  <p className="text-xs text-bambu-gray">{t('smartPlugs.power')}</p>
                 </div>
                 </div>
               )}
               )}
               {status.energy.today !== null && status.energy.today !== undefined && (
               {status.energy.today !== null && status.energy.today !== undefined && (
                 <div className="flex-1 text-center border-l border-bambu-dark-tertiary">
                 <div className="flex-1 text-center border-l border-bambu-dark-tertiary">
                   <p className="text-lg font-semibold text-white">{status.energy.today.toFixed(3)}</p>
                   <p className="text-lg font-semibold text-white">{status.energy.today.toFixed(3)}</p>
-                  <p className="text-xs text-bambu-gray">kWh Today</p>
+                  <p className="text-xs text-bambu-gray">{t('smartPlugs.kwhToday')}</p>
                 </div>
                 </div>
               )}
               )}
             </div>
             </div>
@@ -279,7 +279,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
           >
           >
             <span className="flex items-center gap-2">
             <span className="flex items-center gap-2">
               <Settings2 className="w-4 h-4" />
               <Settings2 className="w-4 h-4" />
-              {plug.plug_type === 'mqtt' ? 'Settings' : 'Automation Settings'}
+              {plug.plug_type === 'mqtt' ? t('smartPlugs.settings') : t('smartPlugs.automationSettings')}
             </span>
             </span>
             <span>{isExpanded ? '-' : '+'}</span>
             <span>{isExpanded ? '-' : '+'}</span>
           </button>
           </button>
@@ -292,8 +292,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                 <div className="flex items-center gap-2">
                 <div className="flex items-center gap-2">
                   <LayoutGrid className="w-4 h-4 text-bambu-green" />
                   <LayoutGrid className="w-4 h-4 text-bambu-green" />
                   <div>
                   <div>
-                    <p className="text-sm text-white">Show in Switchbar</p>
-                    <p className="text-xs text-bambu-gray">Quick access from sidebar</p>
+                    <p className="text-sm text-white">{t('smartPlugs.showInSwitchbar')}</p>
+                    <p className="text-xs text-bambu-gray">{t('smartPlugs.quickAccessSidebar')}</p>
                   </div>
                   </div>
                 </div>
                 </div>
                 <label className="relative inline-flex items-center cursor-pointer">
                 <label className="relative inline-flex items-center cursor-pointer">
@@ -313,8 +313,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   {/* Enabled Toggle */}
                   {/* Enabled Toggle */}
                   <div className="flex items-center justify-between">
                   <div className="flex items-center justify-between">
                     <div>
                     <div>
-                      <p className="text-sm text-white">Enabled</p>
-                      <p className="text-xs text-bambu-gray">Enable automation for this plug</p>
+                      <p className="text-sm text-white">{t('smartPlugs.enabled')}</p>
+                      <p className="text-xs text-bambu-gray">{t('smartPlugs.enableAutomation')}</p>
                     </div>
                     </div>
                     <label className="relative inline-flex items-center cursor-pointer">
                     <label className="relative inline-flex items-center cursor-pointer">
                       <input
                       <input
@@ -330,8 +330,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   {/* Auto On */}
                   {/* Auto On */}
                   <div className="flex items-center justify-between">
                   <div className="flex items-center justify-between">
                     <div>
                     <div>
-                      <p className="text-sm text-white">Auto On</p>
-                      <p className="text-xs text-bambu-gray">Turn on when print starts</p>
+                      <p className="text-sm text-white">{t('smartPlugs.autoOn')}</p>
+                      <p className="text-xs text-bambu-gray">{t('smartPlugs.autoOnDescription')}</p>
                     </div>
                     </div>
                     <label className="relative inline-flex items-center cursor-pointer">
                     <label className="relative inline-flex items-center cursor-pointer">
                       <input
                       <input
@@ -347,8 +347,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   {/* Auto Off */}
                   {/* Auto Off */}
                   <div className="flex items-center justify-between">
                   <div className="flex items-center justify-between">
                     <div>
                     <div>
-                      <p className="text-sm text-white">Auto Off</p>
-                      <p className="text-xs text-bambu-gray">Turn off when print completes (one-shot)</p>
+                      <p className="text-sm text-white">{t('smartPlugs.autoOff')}</p>
+                      <p className="text-xs text-bambu-gray">{t('smartPlugs.autoOffDescription')}</p>
                 </div>
                 </div>
                 <label className="relative inline-flex items-center cursor-pointer">
                 <label className="relative inline-flex items-center cursor-pointer">
                   <input
                   <input
@@ -365,7 +365,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
               {plug.auto_off && (
               {plug.auto_off && (
                 <div className="space-y-3 pl-4 border-l-2 border-bambu-dark-tertiary">
                 <div className="space-y-3 pl-4 border-l-2 border-bambu-dark-tertiary">
                   <div>
                   <div>
-                    <p className="text-sm text-white mb-2">Turn Off Delay Mode</p>
+                    <p className="text-sm text-white mb-2">{t('smartPlugs.turnOffDelayMode')}</p>
                     <div className="flex gap-2">
                     <div className="flex gap-2">
                       <button
                       <button
                         onClick={() => updateMutation.mutate({ off_delay_mode: 'time' })}
                         onClick={() => updateMutation.mutate({ off_delay_mode: 'time' })}
@@ -376,7 +376,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                         }`}
                         }`}
                       >
                       >
                         <Clock className="w-4 h-4" />
                         <Clock className="w-4 h-4" />
-                        Time
+                        {t('smartPlugs.time')}
                       </button>
                       </button>
                       <button
                       <button
                         onClick={() => updateMutation.mutate({ off_delay_mode: 'temperature' })}
                         onClick={() => updateMutation.mutate({ off_delay_mode: 'temperature' })}
@@ -387,14 +387,14 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                         }`}
                         }`}
                       >
                       >
                         <Thermometer className="w-4 h-4" />
                         <Thermometer className="w-4 h-4" />
-                        Temp
+                        {t('smartPlugs.temp')}
                       </button>
                       </button>
                     </div>
                     </div>
                   </div>
                   </div>
 
 
                   {plug.off_delay_mode === 'time' ? (
                   {plug.off_delay_mode === 'time' ? (
                     <div>
                     <div>
-                      <label className="block text-xs text-bambu-gray mb-1">Delay (minutes)</label>
+                      <label className="block text-xs text-bambu-gray mb-1">{t('smartPlugs.delayMinutes')}</label>
                       <input
                       <input
                         type="number"
                         type="number"
                         min="1"
                         min="1"
@@ -406,7 +406,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                     </div>
                     </div>
                   ) : (
                   ) : (
                     <div>
                     <div>
-                      <label className="block text-xs text-bambu-gray mb-1">Temperature threshold (C)</label>
+                      <label className="block text-xs text-bambu-gray mb-1">{t('smartPlugs.tempThreshold')}</label>
                       <input
                       <input
                         type="number"
                         type="number"
                         min="30"
                         min="30"
@@ -415,7 +415,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                         onChange={(e) => updateMutation.mutate({ off_temp_threshold: parseInt(e.target.value) || 70 })}
                         onChange={(e) => updateMutation.mutate({ off_temp_threshold: parseInt(e.target.value) || 70 })}
                         className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none"
                         className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none"
                       />
                       />
-                      <p className="text-xs text-bambu-gray mt-1">Turns off when nozzle cools below this temperature</p>
+                      <p className="text-xs text-bambu-gray mt-1">{t('smartPlugs.tempThresholdDescription')}</p>
                     </div>
                     </div>
                   )}
                   )}
                 </div>
                 </div>
@@ -432,7 +432,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   className="flex-1"
                   className="flex-1"
                 >
                 >
                   <Edit2 className="w-4 h-4" />
                   <Edit2 className="w-4 h-4" />
-                  Edit
+                  {t('smartPlugs.edit')}
                 </Button>
                 </Button>
                 <Button
                 <Button
                   size="sm"
                   size="sm"
@@ -452,8 +452,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {showDeleteConfirm && (
       {showDeleteConfirm && (
         <ConfirmModal
         <ConfirmModal
           title={t('smartPlugs.deleteSmartPlug')}
           title={t('smartPlugs.deleteSmartPlug')}
-          message={`Are you sure you want to delete "${plug.name}"? This cannot be undone.`}
-          confirmText="Delete"
+          message={t('smartPlugs.deleteConfirm', { name: plug.name })}
+          confirmText={t('smartPlugs.delete')}
           variant="danger"
           variant="danger"
           onConfirm={() => {
           onConfirm={() => {
             deleteMutation.mutate();
             deleteMutation.mutate();
@@ -467,7 +467,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {showPowerOnConfirm && (
       {showPowerOnConfirm && (
         <ConfirmModal
         <ConfirmModal
           title={t('smartPlugs.turnOnSmartPlug')}
           title={t('smartPlugs.turnOnSmartPlug')}
-          message={`Are you sure you want to turn on "${plug.name}"?`}
+          message={t('smartPlugs.turnOnConfirm', { name: plug.name })}
           confirmText={t('smartPlugs.turnOn')}
           confirmText={t('smartPlugs.turnOn')}
           variant="default"
           variant="default"
           onConfirm={() => {
           onConfirm={() => {
@@ -482,7 +482,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {showPowerOffConfirm && (
       {showPowerOffConfirm && (
         <ConfirmModal
         <ConfirmModal
           title={t('smartPlugs.turnOffSmartPlug')}
           title={t('smartPlugs.turnOffSmartPlug')}
-          message={`Are you sure you want to turn off "${plug.name}"? This will cut power to the connected device.`}
+          message={t('smartPlugs.turnOffConfirm', { name: plug.name })}
           confirmText={t('smartPlugs.turnOff')}
           confirmText={t('smartPlugs.turnOff')}
           variant="danger"
           variant="danger"
           onConfirm={() => {
           onConfirm={() => {

+ 4 - 2
frontend/src/components/SwitchbarPopover.tsx

@@ -1,5 +1,6 @@
 import { useState } from 'react';
 import { useState } from 'react';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
 import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap, Radio, Eye } from 'lucide-react';
 import { Plug, Power, PowerOff, Loader2, Wifi, WifiOff, Zap, Radio, Eye } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { SmartPlug } from '../api/client';
 import type { SmartPlug } from '../api/client';
@@ -146,6 +147,7 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
 }
 }
 
 
 export function SwitchbarPopover({ onClose }: SwitchbarPopoverProps) {
 export function SwitchbarPopover({ onClose }: SwitchbarPopoverProps) {
+  const { t } = useTranslation();
   // Fetch all smart plugs
   // Fetch all smart plugs
   const { data: plugs, isLoading } = useQuery({
   const { data: plugs, isLoading } = useQuery({
     queryKey: ['smart-plugs'],
     queryKey: ['smart-plugs'],
@@ -177,9 +179,9 @@ export function SwitchbarPopover({ onClose }: SwitchbarPopoverProps) {
         ) : switchbarPlugs.length === 0 ? (
         ) : switchbarPlugs.length === 0 ? (
           <div className="text-center py-6 px-4">
           <div className="text-center py-6 px-4">
             <Plug className="w-8 h-8 text-bambu-gray mx-auto mb-2" />
             <Plug className="w-8 h-8 text-bambu-gray mx-auto mb-2" />
-            <p className="text-sm text-bambu-gray">No switches in switchbar</p>
+            <p className="text-sm text-bambu-gray">{t('smartPlugs.noSwitchesInSwitchbar')}</p>
             <p className="text-xs text-bambu-gray mt-1">
             <p className="text-xs text-bambu-gray mt-1">
-              Enable "Show in Switchbar" in Settings &gt; Smart Plugs
+              {t('smartPlugs.enableSwitchbarHint')}
             </p>
             </p>
           </div>
           </div>
         ) : (
         ) : (

+ 607 - 0
frontend/src/i18n/locales/de.ts

@@ -1654,6 +1654,163 @@ export default {
       imported: '{{added}} Farben importiert ({{skipped}} übersprungen)',
       imported: '{{added}} Farben importiert ({{skipped}} übersprungen)',
       importFailed: 'Import fehlgeschlagen: ungültiges JSON-Format',
       importFailed: 'Import fehlgeschlagen: ungültiges JSON-Format',
     },
     },
+    // General tab
+    dateFormat: 'Datumsformat',
+    dateFormatUs: 'US (MM/TT/JJJJ)',
+    dateFormatEu: 'EU (TT/MM/JJJJ)',
+    dateFormatIso: 'ISO (JJJJ-MM-TT)',
+    timeFormat: 'Zeitformat',
+    timeFormat12: '12-Stunden (3:30 PM)',
+    timeFormat24: '24-Stunden (15:30)',
+    defaultPrinter: 'Standarddrucker',
+    defaultPrinterDescription: 'Diesen Drucker für Uploads, Nachdrucke und andere Vorgänge vorauswählen.',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'Elemente in der Seitenleiste per Drag & Drop neu anordnen. Hier auf Standardreihenfolge zurücksetzen.',
+    reset: 'Zurücksetzen',
+    // Appearance
+    darkMode: 'Dunkelmodus',
+    lightMode: 'Hellmodus',
+    active: '(aktiv)',
+    background: 'Hintergrund',
+    accent: 'Akzent',
+    style: 'Stil',
+    bgNeutral: 'Neutral',
+    bgWarm: 'Warm',
+    bgCool: 'Kühl',
+    bgOled: 'OLED Schwarz',
+    bgSlate: 'Schieferblau',
+    bgForest: 'Waldgrün',
+    accentGreen: 'Grün',
+    accentTeal: 'Türkis',
+    accentBlue: 'Blau',
+    accentOrange: 'Orange',
+    accentPurple: 'Lila',
+    accentRed: 'Rot',
+    styleClassic: 'Klassisch',
+    styleGlow: 'Leuchtend',
+    styleVibrant: 'Lebendig',
+    themeToggleHint: 'Zwischen Hell- und Dunkelmodus mit dem Sonnen-/Mondsymbol in der Seitenleiste wechseln.',
+    // Archive
+    autoArchivePrints: 'Drucke automatisch archivieren',
+    autoArchiveDescription: '3MF-Dateien automatisch speichern, wenn Drucke abgeschlossen sind',
+    saveThumbnailsDescription: 'Vorschaubilder aus 3MF-Dateien extrahieren und speichern',
+    captureFinishPhotoDescription: 'Foto von der Druckerkamera aufnehmen, wenn der Druck abgeschlossen ist',
+    ffmpegNotInstalled: 'ffmpeg nicht installiert',
+    ffmpegRequired: 'Kameraaufnahme benötigt ffmpeg. Installieren über <brew>brew install ffmpeg</brew> (macOS) oder <apt>apt install ffmpeg</apt> (Linux).',
+    // Camera
+    camera: 'Kamera',
+    cameraViewMode: 'Kamera-Ansichtsmodus',
+    cameraOverlayDescription: 'Kamera öffnet sich als größenveränderbares Overlay auf dem Hauptbildschirm',
+    cameraWindowDescription: 'Kamera öffnet sich in einem separaten Browserfenster',
+    externalCamerasDescription: 'Externe Kameras konfigurieren, um die eingebaute Druckerkamera zu ersetzen. Unterstützt MJPEG-Streams, RTSP, HTTP-Snapshots und USB-Kameras (V4L2). Wenn aktiviert, wird die externe Kamera für Live-Ansicht und Abschlussfotos verwendet.',
+    cameraPlaceholderUsb: 'Gerätepfad (/dev/video0)',
+    cameraPlaceholderUrl: 'Kamera-URL (rtsp://... oder http://...)',
+    cameraTypeMjpeg: 'MJPEG-Stream',
+    cameraTypeRtsp: 'RTSP-Stream',
+    cameraTypeSnapshot: 'HTTP-Snapshot',
+    cameraTypeUsb: 'USB-Kamera (V4L2)',
+    test: 'Testen',
+    connected: 'Verbunden',
+    disconnected: 'Getrennt',
+    // Cost tracking
+    currency: 'Währung',
+    defaultFilamentCost: 'Standard-Filamentkosten (pro kg)',
+    electricityCost: 'Stromkosten pro kWh',
+    energyDisplayMode: 'Energieanzeige-Modus',
+    energyModePrintDescription: 'Dashboard zeigt Summe der während Drucken verbrauchten Energie',
+    energyModeTotalDescription: 'Dashboard zeigt Gesamtenergie der Smart Plugs',
+    // File Manager
+    fileManager: 'Dateimanager',
+    createArchiveEntry: 'Archiveintrag beim Drucken erstellen',
+    createArchiveEntryDescription: 'Beim Drucken aus dem Dateimanager optional einen Archiveintrag erstellen',
+    lowDiskSpaceWarning: 'Warnung bei wenig Speicherplatz',
+    lowDiskSpaceDescription: 'Warnung anzeigen, wenn freier Speicherplatz unter diesen Schwellenwert fällt',
+    // Updates
+    printerFirmware: 'Drucker-Firmware',
+    checkFirmwareDescription: 'Nach Firmware-Updates von Bambu Lab suchen',
+    bambuddySoftware: 'Bambuddy Software',
+    autoCheckDescription: 'Automatisch beim Start nach neuen Versionen suchen',
+    checkNow: 'Jetzt prüfen',
+    updateAvailableVersion: 'Update verfügbar: v{{version}}',
+    releaseNotes: 'Versionshinweise',
+    updateViaDocker: 'Update über Docker Compose:',
+    installUpdate: 'Update installieren',
+    latestVersionRunning: 'Sie verwenden die neueste Version',
+    failedToCheckUpdates: 'Update-Prüfung fehlgeschlagen: {{error}}',
+    // Data Management
+    backupRestore: 'Sicherung & Wiederherstellung',
+    backupRestoreDescription: 'Einstellungen exportieren/importieren und GitHub-Backup konfigurieren',
+    goToBackup: 'Zur Sicherung',
+    // Network tab
+    externalUrl: 'Externe URL',
+    externalUrlDescription: 'Die externe URL, unter der Bambuddy erreichbar ist. Wird für Benachrichtigungsbilder und externe Integrationen verwendet.',
+    bambuddyUrl: 'Bambuddy-URL',
+    externalUrlHint: 'Protokoll und Port angeben (z.B. http://192.168.1.100:8000)',
+    ftpRetry: 'FTP-Wiederholung',
+    ftpRetryDescription: 'FTP-Operationen bei unzuverlässigem Drucker-WLAN wiederholen. Gilt für 3MF-Downloads, Druck-Uploads, Zeitraffer-Downloads und Firmware-Updates.',
+    autoRetryDescription: 'Fehlgeschlagene FTP-Operationen automatisch wiederholen',
+    retryAttempts: 'Wiederholungsversuche',
+    retryDelay: 'Wiederholungsverzögerung',
+    connectionTimeout: 'Verbindungs-Timeout',
+    time_one: '{{count}} Mal',
+    time_other: '{{count}} Mal',
+    second_one: '{{count}} Sekunde',
+    second_other: '{{count}} Sekunden',
+    nSeconds: '{{count}} Sekunden',
+    increaseForWeakWifi: 'Erhöhen für Drucker mit schwachem WLAN',
+    // Home Assistant
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Mit Home Assistant verbinden, um Smart Plugs über die HA REST-API zu steuern. Unterstützt Switch-, Light-, Input_Boolean- und Script-Entitäten.',
+    homeAssistantUrl: 'Home Assistant URL',
+    longLivedAccessToken: 'Langlebiges Zugriffstoken',
+    haTokenHint: 'Token in HA erstellen: Profil → Langlebige Zugriffstoken → Token erstellen',
+    connectionSuccessful: 'Verbindung erfolgreich',
+    connectionFailed: 'Verbindung fehlgeschlagen',
+    haConnectionSuccess: 'Erfolgreich mit Home Assistant verbunden.',
+    haConnectionFailed: 'Verbindung zu Home Assistant fehlgeschlagen.',
+    // MQTT
+    mqttPublishing: 'MQTT-Veröffentlichung',
+    mqttDescription: 'BamBuddy-Ereignisse an einen externen MQTT-Broker zur Integration mit Node-RED, Home Assistant und anderen Automatisierungssystemen veröffentlichen.',
+    mqttEnableDescription: 'Ereignisse an externen MQTT-Broker veröffentlichen',
+    brokerHostname: 'Broker-Hostname',
+    port: 'Port',
+    usernameOptional: 'Benutzername (optional)',
+    passwordOptional: 'Passwort (optional)',
+    topicPrefix: 'Topic-Präfix',
+    topicPrefixHint: 'Topics werden sein: {{prefix}}/printers/<serial>/status, etc.',
+    // Prometheus
+    prometheusMetrics: 'Prometheus-Metriken',
+    prometheusEndpointDescription: 'Druckermetriken unter <code>/api/v1/metrics</code> für Prometheus/Grafana-Überwachung bereitstellen.',
+    bearerTokenOptional: 'Bearer-Token (optional)',
+    bearerTokenHint: 'Wenn gesetzt, müssen Anfragen <code>Authorization: Bearer <token></code> enthalten',
+    metricsConnectionStatus: 'Verbindungsstatus',
+    metricsPrinterState: 'Druckerstatus (idle/printing/etc)',
+    metricsPrintProgress: 'Druckfortschritt 0-100%',
+    metricsBedTemp: 'Betttemperatur',
+    metricsNozzleTemp: 'Düsentemperatur',
+    metricsPrintsTotal: 'Gesamtdrucke nach Ergebnis',
+    metricsMore: '...und mehr (Schichten, Lüfter, Warteschlange, Filamentverbrauch)',
+    // Smart Plugs
+    smartPlugsDescription: 'Smart Plugs (Tasmota oder Home Assistant) verbinden, um Stromsteuerung zu automatisieren und Energieverbrauch für Ihre Drucker zu verfolgen.',
+    allOn: 'Alle Ein',
+    allOff: 'Alle Aus',
+    addSmartPlug: 'Smart Plug hinzufügen',
+    energySummary: 'Energieübersicht',
+    currentPower: 'Aktuelle Leistung',
+    plugsOnline: '{{reachable}}/{{total}} Plugs online',
+    today: 'Heute',
+    yesterday: 'Gestern',
+    total: 'Gesamt',
+    enablePlugsForSummary: 'Plugs aktivieren, um Energieübersicht zu sehen',
+    addNotificationProvider: 'Hinzufügen',
+    // Users
+    systemBadge: '(System)',
+    creating: 'Erstellen...',
+    changing: 'Ändern...',
+    deleteUserAndItems: 'Benutzer UND dessen Elemente löschen',
+    deleteUserKeepItems: 'Benutzer löschen, Elemente behalten (werden herrenlos)',
+    ok: 'OK',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2868,6 +3025,143 @@ export default {
     backupFailed: 'Sicherung fehlgeschlagen',
     backupFailed: 'Sicherung fehlgeschlagen',
     restoreFailed: 'Wiederherstellung fehlgeschlagen',
     restoreFailed: 'Wiederherstellung fehlgeschlagen',
     restoreNote: 'Virtueller Drucker wird während der Wiederherstellung gestoppt',
     restoreNote: 'Virtueller Drucker wird während der Wiederherstellung gestoppt',
+
+    // GitHub Backup
+    githubBackup: 'GitHub Backup',
+    enabled: 'Aktiviert',
+    cloudLoginRequired: 'Bambu Cloud Login erforderlich. Melden Sie sich unter Profile → Cloud-Profile an, um GitHub-Backup zu aktivieren.',
+    githubDescription: 'Synchronisieren Sie Ihre Profile automatisch mit einem privaten GitHub-Repository für Backup und Versionsverlauf.',
+    repositoryUrl: 'Repository-URL',
+    personalAccessToken: 'Persönlicher Zugriffstoken',
+    tokenSaved: '(gespeichert)',
+    enterNewToken: 'Neuen Token eingeben zum Aktualisieren',
+    tokenHint: 'Feingranularer Token mit Lese-/Schreibberechtigung für Inhalte',
+    branch: 'Branch',
+    manualOnly: 'Nur manuell',
+    hourly: 'Stündlich',
+    daily: 'Täglich',
+    weekly: 'Wöchentlich',
+    includeInBackup: 'In Sicherung einschließen',
+    kProfiles: 'K-Profile',
+    kProfilesDescription: 'Druckvorschub-Kalibrierung von verbundenen Druckern',
+    noPrintersConnected: 'Keine Drucker verbunden',
+    printersConnected: '{{connected}}/{{total}} verbunden',
+    cloudProfiles: 'Cloud-Profile',
+    cloudProfilesDescription: 'Filament-, Drucker- und Prozessprofile aus der Bambu Cloud',
+    appSettings: 'App-Einstellungen',
+    appSettingsDescription: 'Bambuddy-Konfiguration (komplette Datenbank)',
+    lastBackupAt: 'Letzte Sicherung:',
+    noBackupsYet: 'Noch keine Sicherungen',
+    next: 'Nächste:',
+    startingBackup: 'Sicherung wird gestartet...',
+    test: 'Test',
+    enableBackup: 'Sicherung aktivieren',
+    testConnection: 'Verbindung testen',
+    enterRepoUrl: 'Repository-URL eingeben',
+    enterRepoAndToken: 'Repository-URL und Zugriffstoken eingeben',
+    repoRequired: 'Repository-URL ist erforderlich',
+    tokenRequired: 'Zugriffstoken ist erforderlich',
+    githubBackupEnabled: 'GitHub-Backup aktiviert',
+    tokenUpdated: 'Token aktualisiert',
+    settingsSaved: 'Einstellungen gespeichert',
+    failedToSave: 'Speichern fehlgeschlagen: {{message}}',
+    backupCompleteFiles: 'Sicherung abgeschlossen - {{count}} Dateien aktualisiert',
+    backupSkippedNoChanges: 'Sicherung übersprungen - keine Änderungen',
+    backupFailed2: 'Sicherung fehlgeschlagen: {{message}}',
+    clearedLogs: '{{count}} Protokolle gelöscht',
+    failedToClearLogs: 'Protokolle löschen fehlgeschlagen: {{message}}',
+
+    // History
+    history: 'Verlauf',
+    clear: 'Löschen',
+    date: 'Datum',
+    status: 'Status',
+    commit: 'Commit',
+
+    // Local Backup
+    localBackup: 'Lokale Sicherung',
+    localBackupDescription: 'Erstellen Sie eine vollständige Sicherung Ihrer Bambuddy-Daten einschließlich Datenbank, Archive, Uploads und aller Dateien.',
+    downloadBackupLabel: 'Sicherung herunterladen',
+    completeBackupZip: 'Vollständige Sicherung: Datenbank + alle Dateien (ZIP)',
+    download: 'Herunterladen',
+    preparingBackup: 'Sicherung wird vorbereitet...',
+    creatingArchive: 'Sicherungsarchiv wird erstellt... Dies kann bei großen Archiven eine Weile dauern.',
+    downloadingFile: 'Sicherungsdatei wird heruntergeladen...',
+    backupDownloaded: 'Sicherung erfolgreich heruntergeladen',
+    failedToCreateBackup: 'Sicherung erstellen fehlgeschlagen: {{message}}',
+    restore: 'Wiederherstellen',
+    restoreReplacesAll: 'Wiederherstellung ersetzt alle Daten.',
+    restoreReplacesAllDetail: 'Ihre aktuelle Datenbank und Dateien werden vollständig ersetzt. Nach der Wiederherstellung ist ein Neustart erforderlich.',
+    restoreConfirmTitle: 'Sicherung wiederherstellen',
+    restoreConfirmMessage: 'Sind Sie sicher, dass Sie von "{{filename}}" wiederherstellen möchten? Dies ersetzt Ihre aktuelle Datenbank und alle Dateien vollständig. Die Anwendung muss nach der Wiederherstellung neu gestartet werden.',
+    restoreConfirmButton: 'Sicherung wiederherstellen',
+    uploadingFile: 'Sicherungsdatei wird hochgeladen...',
+    backupRestoredRestart: 'Sicherung wiederhergestellt. Bitte starten Sie Bambuddy neu.',
+    failedToRestore: 'Sicherung wiederherstellen fehlgeschlagen. Bitte überprüfen Sie das Dateiformat.',
+    reloadNow: 'Jetzt neu laden',
+    creatingBackup: 'Sicherung erstellen',
+    restoringBackup: 'Sicherung wiederherstellen',
+    preparing: 'Vorbereiten...',
+    processing: 'Verarbeiten...',
+    doNotClosePage: 'Bitte schließen Sie diese Seite nicht und navigieren Sie nicht weg. Dieser Vorgang kann bei großen Sicherungen mehrere Minuten dauern.',
+
+    // RestoreModal
+    restoring: 'Wiederherstellen...',
+    restoreComplete: 'Wiederherstellung abgeschlossen',
+    restoreFailed2: 'Wiederherstellung fehlgeschlagen',
+    importSettings: 'Einstellungen aus einer Sicherungsdatei importieren',
+    pleaseWaitRestoring: 'Bitte warten Sie, während Ihre Daten wiederhergestellt werden',
+    selectBackupFile: 'Klicken Sie, um eine Sicherungsdatei auszuwählen (.json oder .zip)',
+    duplicateHandling: 'So funktioniert die Duplikatbehandlung:',
+    matchPrinters: 'Drucker',
+    matchPrintersBy: 'abgeglichen nach Seriennummer',
+    matchSmartPlugs: 'Smart Plugs',
+    matchSmartPlugsBy: 'abgeglichen nach IP-Adresse',
+    matchNotificationProviders: 'Benachrichtigungsanbieter',
+    matchNotificationProvidersBy: 'abgeglichen nach Name',
+    matchFilaments: 'Filamente',
+    matchFilamentsBy: 'abgeglichen nach Name + Typ + Marke',
+    matchArchives: 'Archive',
+    matchArchivesBy: 'abgeglichen nach Inhaltshash (immer übersprungen)',
+    matchPendingUploads: 'Ausstehende Uploads',
+    matchPendingUploadsBy: 'abgeglichen nach Dateiname',
+    matchSettingsTemplates: 'Einstellungen & Vorlagen',
+    matchSettingsTemplatesBy: 'immer überschrieben',
+    replaceExisting: 'Vorhandene Daten ersetzen',
+    keepExisting: 'Vorhandene Daten behalten',
+    overwriteDescription: 'Bereits vorhandene Elemente mit Sicherungsdaten überschreiben',
+    keepDescription: 'Nur Elemente wiederherstellen, die noch nicht vorhanden sind',
+    overwriteCaution: 'Achtung:',
+    overwriteWarning: 'Das Überschreiben ersetzt Ihre aktuellen Konfigurationen durch Daten aus der Sicherung. Drucker-Zugangscodes werden aus Sicherheitsgründen nie überschrieben.',
+    cancel: 'Abbrechen',
+    processingBackup: 'Sicherungsdatei wird verarbeitet...',
+    itemsRestored: 'Wiederhergestellt',
+    itemsSkipped: 'Übersprungen',
+    restored: 'Wiederhergestellt',
+    skippedAlreadyExist: 'Übersprungen (bereits vorhanden)',
+    filesCategory: 'Dateien (3MF, Thumbnails, etc.)',
+    andMore: '...und {{count}} weitere',
+    newApiKeysGenerated: 'Neue API-Schlüssel generiert',
+    keysShownOnce: 'Diese Schlüssel werden nur einmal angezeigt. Kopieren Sie sie jetzt!',
+    copy: 'Kopieren',
+    noDataFound: 'In der Sicherungsdatei wurden keine Daten zur Wiederherstellung gefunden.',
+    close: 'Schließen',
+
+    // Category labels
+    categories: {
+      settings: 'Einstellungen',
+      notification_providers: 'Benachrichtigungsanbieter',
+      notification_templates: 'Benachrichtigungsvorlagen',
+      smart_plugs: 'Smart Plugs',
+      printers: 'Drucker',
+      filaments: 'Filamente',
+      maintenance_types: 'Wartungstypen',
+      archives: 'Archive',
+      projects: 'Projekte',
+      pending_uploads: 'Ausstehende Uploads',
+      external_links: 'Externe Links',
+      api_keys: 'API-Schlüssel',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3245,6 +3539,319 @@ export default {
         mqttSameAsPower: 'Gleich wie Leistungs-Topic oder anders',
         mqttSameAsPower: 'Gleich wie Leistungs-Topic oder anders',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: 'Verbunden mit:',
+    monitorOnly: 'Nur Überwachung',
+    alerts: 'Alarme',
+    scheduleOn: 'Ein {{time}}',
+    scheduleOff: 'Aus {{time}}',
+    on: 'Ein',
+    off: 'Aus',
+    power: 'Leistung',
+    kwhToday: 'kWh Heute',
+    settings: 'Einstellungen',
+    automationSettings: 'Automatisierungseinstellungen',
+    showInSwitchbar: 'In Schaltleiste anzeigen',
+    quickAccessSidebar: 'Schnellzugriff über Seitenleiste',
+    enabled: 'Aktiviert',
+    enableAutomation: 'Automatisierung für diesen Stecker aktivieren',
+    autoOn: 'Auto Ein',
+    autoOnDescription: 'Einschalten wenn Druck startet',
+    autoOff: 'Auto Aus',
+    autoOffDescription: 'Ausschalten wenn Druck abgeschlossen (einmalig)',
+    turnOffDelayMode: 'Ausschaltverzögerungsmodus',
+    time: 'Zeit',
+    temp: 'Temp',
+    delayMinutes: 'Verzögerung (Minuten)',
+    tempThreshold: 'Temperaturschwelle (°C)',
+    tempThresholdDescription: 'Schaltet aus wenn die Düse unter diese Temperatur abkühlt',
+    edit: 'Bearbeiten',
+    deleteConfirm: 'Möchten Sie "{{name}}" wirklich löschen? Dies kann nicht rückgängig gemacht werden.',
+    turnOnConfirm: 'Möchten Sie "{{name}}" wirklich einschalten?',
+    turnOffConfirm: 'Möchten Sie "{{name}}" wirklich ausschalten? Dies unterbricht die Stromversorgung des angeschlossenen Geräts.',
+    failedToTurn: '{{name}}" konnte nicht {{action}} werden',
+    unknown: 'Unbekannt',
+    // AddSmartPlugModal
+    addTitle: 'Smart Plug hinzufügen',
+    editTitle: 'Smart Plug bearbeiten',
+    stopScanning: 'Suche beenden',
+    discoverTasmota: 'Tasmota Geräte suchen',
+    foundDevices: '{{count}} Gerät(e) gefunden - zum Auswählen klicken:',
+    noDevicesFound: 'Keine Tasmota Geräte in Ihrem Netzwerk gefunden',
+    haNotConfigured: 'Home Assistant ist nicht konfiguriert. Einrichtung unter',
+    haSettingsPath: 'Einstellungen → Netzwerk → Home Assistant',
+    selectEntity: 'Entität auswählen *',
+    ipAddress: 'IP-Adresse *',
+    nameLabel: 'Name *',
+    username: 'Benutzername',
+    password: 'Passwort',
+    authHint: 'Leer lassen, wenn Ihr Tasmota-Gerät keine Authentifizierung benötigt',
+    linkToPrinter: 'Mit Drucker verbinden',
+    noPrinter: 'Kein Drucker (nur manuelle Steuerung)',
+    linkingDescription: 'Verknüpfung ermöglicht automatisches Ein-/Ausschalten bei Druckstart/-ende',
+    powerAlerts: 'Leistungsalarme',
+    alertAbove: 'Alarm wenn über (W)',
+    alertBelow: 'Alarm wenn unter (W)',
+    alertDescription: 'Benachrichtigung wenn der Stromverbrauch diese Schwellenwerte überschreitet. Leer lassen um diese Richtung zu deaktivieren.',
+    dailySchedule: 'Tagesplan',
+    turnOnAt: 'Einschalten um',
+    turnOffAt: 'Ausschalten um',
+    scheduleDescription: 'Den Stecker automatisch täglich zu diesen Zeiten ein-/ausschalten. Leer lassen um diese Aktion zu überspringen.',
+    showOnPrinterCard: 'Auf Druckerkarte anzeigen',
+    displayOnPrinterCard: 'Schaltfläche auf Druckerkarte anzeigen',
+    connectedResult: 'Verbunden!',
+    deviceLabel: 'Gerät: {{name}} - ',
+    stateLabel: 'Status: {{state}}',
+    test: 'Test',
+    delete: 'Löschen',
+    save: 'Speichern',
+    add: 'Hinzufügen',
+    cancel: 'Abbrechen',
+    failedToStartScan: 'Suche konnte nicht gestartet werden',
+    nameRequired: 'Name ist erforderlich',
+    entityRequired: 'Entität ist für Home Assistant Stecker erforderlich',
+    mqttTopicRequired: 'Mindestens ein MQTT-Topic muss für Leistung, Energie oder Statusüberwachung konfiguriert sein',
+    loadingEntities: 'Entitäten werden geladen...',
+    loading: 'Laden...',
+    failedToLoadEntities: 'Entitäten konnten nicht geladen werden: {{error}}',
+    noEntitiesMatching: 'Keine Entitäten gefunden die "{{search}}" entsprechen',
+    noEntitiesAvailable: 'Keine Entitäten verfügbar',
+    searchingEntities: 'Alle Entitäten durchsuchen ({{count}} gefunden)',
+    showingEntities: 'Zeige switch, light, input_boolean ({{count}} verfügbar)',
+    energyMonitoringOptional: 'Energieüberwachung (Optional)',
+    energyMonitoringHint: 'Sensoren suchen und auswählen, die Leistungs-/Energiedaten liefern.',
+    powerSensorW: 'Leistungssensor (W)',
+    energyTodayKwh: 'Energie Heute (kWh)',
+    totalEnergyKwh: 'Gesamtenergie (kWh)',
+    noMatchingSensors: 'Keine passenden Sensoren',
+    none: 'Keine',
+    mqttNotConfigured: 'MQTT-Broker nicht konfiguriert. Broker-Adresse einstellen unter',
+    mqttSettingsPath: 'Einstellungen → Netzwerk → MQTT-Veröffentlichung',
+    mqttNotConfiguredSuffix: '(Sie müssen die Veröffentlichung nicht aktivieren, nur die Broker-Details ausfüllen).',
+    mqttMonitorOnlyDescription: 'MQTT-Stecker empfangen Leistungs-/Energiedaten über MQTT-Abonnement. Ein-/Ausschalten ist nicht verfügbar - verwenden Sie Ihren MQTT-Broker oder Ihr Home-Automation-System.',
+    powerMonitoring: 'Leistungsüberwachung',
+    energyMonitoring: 'Energieüberwachung',
+    stateMonitoring: 'Statusüberwachung',
+    optional: 'optional',
+    topic: 'Topic',
+    jsonPath: 'JSON-Pfad',
+    multiplier: 'Multiplikator',
+    onValue: 'EIN-Wert',
+    mqttPowerHint: 'JSON-Pfad extrahiert Wert aus JSON-Payload (z.B. "power_l1"). Leer lassen wenn Topic rohe numerische Werte sendet.\nMultiplikator 0.001 für mW→W, 1000 für kW→W verwenden.',
+    mqttEnergyHint: 'JSON-Pfad extrahiert Wert aus JSON-Payload. Leer lassen für rohe Werte.\nMultiplikator 0.001 für Wh→kWh, 1000 für MWh→kWh verwenden.',
+    mqttStateHint: 'JSON-Pfad extrahiert Wert aus JSON-Payload. Leer lassen für rohe Werte.\nEIN-Wert: der genaue String der "EIN" bedeutet. Leer lassen für Auto-Erkennung (ON, true, 1).',
+    noSwitchesInSwitchbar: 'Keine Schalter in der Schaltleiste',
+    enableSwitchbarHint: '"In Schaltleiste anzeigen" unter Einstellungen > Smart Plugs aktivieren',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'E-Mail',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'SMTP-E-Mail-Benachrichtigungen',
+      telegram: 'Benachrichtigungen über Telegram-Bot',
+      discord: 'An Discord-Kanal per Webhook senden',
+      ntfy: 'Kostenlose, selbst-hostbare Push-Benachrichtigungen',
+      pushover: 'Einfache, zuverlässige Push-Benachrichtigungen',
+      callmebot: 'Kostenlose WhatsApp-Benachrichtigungen über CallMeBot',
+      webhook: 'Generischer HTTP-POST an beliebige URL',
+    },
+    // NotificationProviderCard
+    lastSuccess: 'Zuletzt: {{date}}',
+    error: 'Fehler',
+    printer: 'Drucker:',
+    allPrinters: 'Alle Drucker',
+    sendTestNotification: 'Testbenachrichtigung senden',
+    eventSettings: 'Ereigniseinstellungen',
+    enabled: 'Aktiviert',
+    sendFromProvider: 'Benachrichtigungen von diesem Anbieter senden',
+    // Event categories
+    printEvents: 'Druckereignisse',
+    printerStatus: 'Druckerstatus',
+    amsAlarms: 'AMS-Alarme',
+    amsHtAlarms: 'AMS-HT-Alarme',
+    printQueue: 'Druckwarteschlange',
+    // Event tags (badges)
+    start: 'Start',
+    plateCheck: 'Plattenkontrolle',
+    complete: 'Abgeschlossen',
+    failed: 'Fehlgeschlagen',
+    stopped: 'Gestoppt',
+    progress: 'Fortschritt',
+    offline: 'Offline',
+    lowFilament: 'Filament niedrig',
+    maintenance: 'Wartung',
+    amsHumidity: 'AMS-Feuchtigkeit',
+    amsTemp: 'AMS-Temperatur',
+    amsHtHumidity: 'AMS-HT-Feuchtigkeit',
+    amsHtTemp: 'AMS-HT-Temperatur',
+    bedCooled: 'Bett abgekühlt',
+    quiet: 'Ruhe',
+    digest: 'Zusammenfassung {{time}}',
+    // Event labels (expanded settings)
+    printStarted: 'Druck gestartet',
+    plateNotEmpty: 'Platte nicht leer',
+    plateNotEmptyDescription: 'Objekte vor dem Druck erkannt',
+    printCompleted: 'Druck abgeschlossen',
+    bedCooledLabel: 'Bett abgekühlt',
+    bedCooledDescription: 'Bett nach dem Druck unter Schwellenwert abgekühlt',
+    printFailed: 'Druck fehlgeschlagen',
+    printStopped: 'Druck gestoppt',
+    progressMilestones: 'Fortschrittsmeilensteine',
+    progressMilestonesDescription: 'Benachrichtigung bei 25%, 50%, 75%',
+    printerOffline: 'Drucker offline',
+    printerError: 'Druckerfehler',
+    lowFilamentLabel: 'Filament niedrig',
+    maintenanceDue: 'Wartung fällig',
+    maintenanceDueDescription: 'Benachrichtigen, wenn Wartung erforderlich ist',
+    amsHumidityHigh: 'AMS-Feuchtigkeit hoch',
+    amsHumidityHighDescription: 'Normale AMS-Feuchtigkeit überschreitet Schwellenwert',
+    amsTemperatureHigh: 'AMS-Temperatur hoch',
+    amsTemperatureHighDescription: 'Normale AMS-Temperatur überschreitet Schwellenwert',
+    amsHtHumidityHigh: 'AMS-HT-Feuchtigkeit hoch',
+    amsHtHumidityHighDescription: 'AMS-HT-Feuchtigkeit überschreitet Schwellenwert',
+    amsHtTemperatureHigh: 'AMS-HT-Temperatur hoch',
+    amsHtTemperatureHighDescription: 'AMS-HT-Temperatur überschreitet Schwellenwert',
+    // Queue events
+    jobAdded: 'Auftrag hinzugefügt',
+    jobAddedDescription: 'Auftrag zur Warteschlange hinzugefügt',
+    jobAssigned: 'Auftrag zugewiesen',
+    jobAssignedDescription: 'Modellbasierter Auftrag einem Drucker zugewiesen',
+    jobStarted: 'Auftrag gestartet',
+    jobStartedDescription: 'Warteschlangenauftrag hat Druck begonnen',
+    jobWaiting: 'Auftrag wartet',
+    jobWaitingDescription: 'Auftrag wartet auf Filament',
+    jobSkipped: 'Auftrag übersprungen',
+    jobSkippedDescription: 'Auftrag übersprungen (vorheriger fehlgeschlagen)',
+    jobFailed: 'Auftrag fehlgeschlagen',
+    jobFailedDescription: 'Auftrag konnte nicht gestartet werden',
+    queueComplete: 'Warteschlange abgeschlossen',
+    queueCompleteDescription: 'Alle Warteschlangenaufträge beendet',
+    // Quiet hours
+    quietHours: 'Ruhezeiten',
+    noNotificationsDuring: 'Keine Benachrichtigungen während dieser Zeiten',
+    editProviderToChangeQuietHours: 'Anbieter bearbeiten, um Ruhezeiten zu ändern',
+    // Daily digest
+    dailyDigest: 'Tägliche Zusammenfassung',
+    batchNotifications: 'Benachrichtigungen zu einer täglichen Zusammenfassung bündeln',
+    sendAt: 'Senden um {{time}}',
+    editProviderToChangeDigestTime: 'Anbieter bearbeiten, um Zusammenfassungszeit zu ändern',
+    // Actions
+    edit: 'Bearbeiten',
+    deleteProvider: 'Benachrichtigungsanbieter löschen',
+    deleteConfirm: 'Sind Sie sicher, dass Sie "{{name}}" löschen möchten? Dies kann nicht rückgängig gemacht werden.',
+    delete: 'Löschen',
+    // AddNotificationModal
+    addTitle: 'Benachrichtigungsanbieter hinzufügen',
+    editTitle: 'Benachrichtigungsanbieter bearbeiten',
+    nameLabel: 'Name *',
+    namePlaceholder: 'Meine Benachrichtigungen',
+    providerTypeLabel: 'Anbietertyp *',
+    configuration: 'Konfiguration',
+    testConfiguration: 'Konfiguration testen',
+    printerFilter: 'Druckerfilter',
+    onlyFromPrinter: 'Nur Benachrichtigungen für Ereignisse von diesem Drucker senden',
+    quietHoursDnd: 'Ruhezeiten (Nicht stören)',
+    quietStart: 'Start',
+    quietEnd: 'Ende',
+    dailyDigestLabel: 'Tägliche Zusammenfassung',
+    sendDigestAt: 'Zusammenfassung senden um',
+    digestCollected: 'Ereignisse werden gesammelt und als einzelne Zusammenfassung zu dieser Zeit gesendet',
+    notificationEvents: 'Benachrichtigungsereignisse',
+    progressPercent: '(25%, 50%, 75%)',
+    bedCooledAfterPrint: '(nach Druckabschluss)',
+    cancel: 'Abbrechen',
+    save: 'Speichern',
+    add: 'Hinzufügen',
+    nameRequired: 'Name ist erforderlich',
+    fieldRequired: '{{field}} ist erforderlich',
+    // Config field labels
+    phoneNumber: 'Telefonnummer',
+    apiKey: 'API-Schlüssel',
+    serverUrl: 'Server-URL',
+    topic: 'Thema',
+    authToken: 'Auth-Token',
+    userKey: 'Benutzerschlüssel',
+    appToken: 'App-Token',
+    priority: 'Priorität',
+    botToken: 'Bot-Token',
+    chatId: 'Chat-ID',
+    smtpServer: 'SMTP-Server',
+    smtpPort: 'SMTP-Port',
+    security: 'Sicherheit',
+    authentication: 'Authentifizierung',
+    username: 'Benutzername',
+    password: 'Passwort',
+    fromEmail: 'Absender-E-Mail',
+    toEmail: 'Empfänger-E-Mail',
+    webhookUrl: 'Webhook-URL',
+    payloadFormat: 'Payload-Format',
+    authorization: 'Autorisierung',
+    titleFieldName: 'Titel-Feldname',
+    messageFieldName: 'Nachrichten-Feldname',
+    // NotificationTemplateEditor
+    editTemplate: 'Vorlage bearbeiten: {{name}}',
+    titleLabel: 'Titel',
+    bodyLabel: 'Inhalt',
+    titlePlaceholder: 'Benachrichtigungstitel...',
+    bodyPlaceholder: 'Benachrichtigungsinhalt...',
+    availableVariables: 'Verfügbare Variablen',
+    clickToInsert: 'Klicken, um an Cursorposition im Inhalt einzufügen',
+    livePreview: 'Live-Vorschau',
+    hide: 'Ausblenden',
+    show: 'Anzeigen',
+    loadingPreview: 'Vorschau wird geladen...',
+    enterTemplateContent: 'Vorlageninhalt eingeben, um Vorschau zu sehen',
+    titlePreview: 'Titel:',
+    bodyPreview: 'Inhalt:',
+    resetToDefault: 'Auf Standard zurücksetzen',
+    titleRequired: 'Titel ist erforderlich',
+    bodyRequired: 'Inhalt ist erforderlich',
+    // NotificationLogViewer
+    notificationLog: 'Benachrichtigungsprotokoll',
+    showFailedOnly: 'Nur fehlgeschlagene',
+    last24Hours: 'Letzte 24 Stunden',
+    last7Days: 'Letzte 7 Tage',
+    last30Days: 'Letzte 30 Tage',
+    last90Days: 'Letzte 90 Tage',
+    justNow: 'Gerade eben',
+    noFailedNotifications: 'Keine fehlgeschlagenen Benachrichtigungen',
+    noNotificationsLogged: 'Keine Benachrichtigungen protokolliert',
+    unknownProvider: 'Unbekannter Anbieter',
+    logTitle: 'Titel',
+    logMessage: 'Nachricht',
+    logError: 'Fehler',
+    logProvider: 'Anbieter: {{type}}',
+    logTime: 'Zeit: {{time}}',
+    refresh: 'Aktualisieren',
+    clearOld: 'Alte löschen',
+    statsSummary: 'Letzte {{days}} Tage:',
+    statsNotifications: 'Benachrichtigungen',
+    statsSent: '{{count}} gesendet',
+    statsFailed: '{{count}} fehlgeschlagen',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: 'Druck gestartet',
+      print_complete: 'Druck abgeschlossen',
+      print_failed: 'Druck fehlgeschlagen',
+      print_stopped: 'Druck gestoppt',
+      print_progress: 'Fortschritt',
+      printer_offline: 'Drucker offline',
+      printer_error: 'Druckerfehler',
+      filament_low: 'Filament niedrig',
+      maintenance_due: 'Wartung fällig',
+      test: 'Test',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

+ 607 - 0
frontend/src/i18n/locales/en.ts

@@ -1654,6 +1654,163 @@ export default {
       imported: 'Imported {{added}} colors ({{skipped}} skipped)',
       imported: 'Imported {{added}} colors ({{skipped}} skipped)',
       importFailed: 'Failed to import: invalid JSON format',
       importFailed: 'Failed to import: invalid JSON format',
     },
     },
+    // General tab
+    dateFormat: 'Date Format',
+    dateFormatUs: 'US (MM/DD/YYYY)',
+    dateFormatEu: 'EU (DD/MM/YYYY)',
+    dateFormatIso: 'ISO (YYYY-MM-DD)',
+    timeFormat: 'Time Format',
+    timeFormat12: '12-hour (3:30 PM)',
+    timeFormat24: '24-hour (15:30)',
+    defaultPrinter: 'Default Printer',
+    defaultPrinterDescription: 'Pre-select this printer for uploads, reprints, and other operations.',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'Drag items in the sidebar to reorder. Reset to default order here.',
+    reset: 'Reset',
+    // Appearance
+    darkMode: 'Dark Mode',
+    lightMode: 'Light Mode',
+    active: '(active)',
+    background: 'Background',
+    accent: 'Accent',
+    style: 'Style',
+    bgNeutral: 'Neutral',
+    bgWarm: 'Warm',
+    bgCool: 'Cool',
+    bgOled: 'OLED Black',
+    bgSlate: 'Slate Blue',
+    bgForest: 'Forest Green',
+    accentGreen: 'Green',
+    accentTeal: 'Teal',
+    accentBlue: 'Blue',
+    accentOrange: 'Orange',
+    accentPurple: 'Purple',
+    accentRed: 'Red',
+    styleClassic: 'Classic',
+    styleGlow: 'Glow',
+    styleVibrant: 'Vibrant',
+    themeToggleHint: 'Toggle between dark and light mode using the sun/moon icon in the sidebar.',
+    // Archive
+    autoArchivePrints: 'Auto-archive prints',
+    autoArchiveDescription: 'Automatically save 3MF files when prints complete',
+    saveThumbnailsDescription: 'Extract and save preview images from 3MF files',
+    captureFinishPhotoDescription: 'Take a photo from printer camera when print completes',
+    ffmpegNotInstalled: 'ffmpeg not installed',
+    ffmpegRequired: 'Camera capture requires ffmpeg. Install it via <brew>brew install ffmpeg</brew> (macOS) or <apt>apt install ffmpeg</apt> (Linux).',
+    // Camera
+    camera: 'Camera',
+    cameraViewMode: 'Camera View Mode',
+    cameraOverlayDescription: 'Camera opens in a resizable overlay on the main screen',
+    cameraWindowDescription: 'Camera opens in a separate browser window',
+    externalCamerasDescription: 'Configure external cameras to replace the built-in printer camera. Supports MJPEG streams, RTSP, HTTP snapshots, and USB cameras (V4L2). When enabled, the external camera is used for live view and finish photos.',
+    cameraPlaceholderUsb: 'Device path (/dev/video0)',
+    cameraPlaceholderUrl: 'Camera URL (rtsp://... or http://...)',
+    cameraTypeMjpeg: 'MJPEG Stream',
+    cameraTypeRtsp: 'RTSP Stream',
+    cameraTypeSnapshot: 'HTTP Snapshot',
+    cameraTypeUsb: 'USB Camera (V4L2)',
+    test: 'Test',
+    connected: 'Connected',
+    disconnected: 'Disconnected',
+    // Cost tracking
+    currency: 'Currency',
+    defaultFilamentCost: 'Default filament cost (per kg)',
+    electricityCost: 'Electricity cost per kWh',
+    energyDisplayMode: 'Energy display mode',
+    energyModePrintDescription: 'Dashboard shows sum of energy used during prints',
+    energyModeTotalDescription: 'Dashboard shows lifetime energy from smart plugs',
+    // File Manager
+    fileManager: 'File Manager',
+    createArchiveEntry: 'Create Archive Entry When Printing',
+    createArchiveEntryDescription: 'When printing from File Manager, optionally create an archive entry',
+    lowDiskSpaceWarning: 'Low Disk Space Warning',
+    lowDiskSpaceDescription: 'Show warning when free disk space falls below this threshold',
+    // Updates
+    printerFirmware: 'Printer Firmware',
+    checkFirmwareDescription: 'Check for printer firmware updates from Bambu Lab',
+    bambuddySoftware: 'Bambuddy Software',
+    autoCheckDescription: 'Automatically check for new versions on startup',
+    checkNow: 'Check now',
+    updateAvailableVersion: 'Update available: v{{version}}',
+    releaseNotes: 'Release Notes',
+    updateViaDocker: 'Update via Docker Compose:',
+    installUpdate: 'Install Update',
+    latestVersionRunning: "You're running the latest version",
+    failedToCheckUpdates: 'Failed to check for updates: {{error}}',
+    // Data Management
+    backupRestore: 'Backup & Restore',
+    backupRestoreDescription: 'Export/import settings and configure GitHub backup',
+    goToBackup: 'Go to Backup',
+    // Network tab
+    externalUrl: 'External URL',
+    externalUrlDescription: 'The external URL where Bambuddy is accessible. Used for notification images and external integrations.',
+    bambuddyUrl: 'Bambuddy URL',
+    externalUrlHint: 'Include protocol and port (e.g., http://192.168.1.100:8000)',
+    ftpRetry: 'FTP Retry',
+    ftpRetryDescription: 'Retry FTP operations when printer WiFi is unreliable. Applies to 3MF downloads, print uploads, timelapse downloads, and firmware updates.',
+    autoRetryDescription: 'Automatically retry failed FTP operations',
+    retryAttempts: 'Retry attempts',
+    retryDelay: 'Retry delay',
+    connectionTimeout: 'Connection timeout',
+    time_one: '{{count}} time',
+    time_other: '{{count}} times',
+    second_one: '{{count}} second',
+    second_other: '{{count}} seconds',
+    nSeconds: '{{count}} seconds',
+    increaseForWeakWifi: 'Increase for printers with weak WiFi',
+    // Home Assistant
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Connect to Home Assistant to control smart plugs via HA\'s REST API. Supports switch, light, input_boolean, and script entities.',
+    homeAssistantUrl: 'Home Assistant URL',
+    longLivedAccessToken: 'Long-Lived Access Token',
+    haTokenHint: 'Create a token in HA: Profile → Long-Lived Access Tokens → Create Token',
+    connectionSuccessful: 'Connection Successful',
+    connectionFailed: 'Connection Failed',
+    haConnectionSuccess: 'Successfully connected to Home Assistant.',
+    haConnectionFailed: 'Failed to connect to Home Assistant.',
+    // MQTT
+    mqttPublishing: 'MQTT Publishing',
+    mqttDescription: 'Publish BamBuddy events to an external MQTT broker for integration with Node-RED, Home Assistant, and other automation systems.',
+    mqttEnableDescription: 'Publish events to external MQTT broker',
+    brokerHostname: 'Broker hostname',
+    port: 'Port',
+    usernameOptional: 'Username (optional)',
+    passwordOptional: 'Password (optional)',
+    topicPrefix: 'Topic prefix',
+    topicPrefixHint: 'Topics will be: {{prefix}}/printers/<serial>/status, etc.',
+    // Prometheus
+    prometheusMetrics: 'Prometheus Metrics',
+    prometheusEndpointDescription: 'Expose printer metrics at <code>/api/v1/metrics</code> for Prometheus/Grafana monitoring.',
+    bearerTokenOptional: 'Bearer Token (optional)',
+    bearerTokenHint: 'If set, requests must include <code>Authorization: Bearer <token></code>',
+    metricsConnectionStatus: 'Connection status',
+    metricsPrinterState: 'Printer state (idle/printing/etc)',
+    metricsPrintProgress: 'Print progress 0-100%',
+    metricsBedTemp: 'Bed temperature',
+    metricsNozzleTemp: 'Nozzle temperature',
+    metricsPrintsTotal: 'Total prints by result',
+    metricsMore: '...and more (layers, fans, queue, filament usage)',
+    // Smart Plugs
+    smartPlugsDescription: 'Connect smart plugs (Tasmota or Home Assistant) to automate power control and track energy usage for your printers.',
+    allOn: 'All On',
+    allOff: 'All Off',
+    addSmartPlug: 'Add Smart Plug',
+    energySummary: 'Energy Summary',
+    currentPower: 'Current Power',
+    plugsOnline: '{{reachable}}/{{total}} plugs online',
+    today: 'Today',
+    yesterday: 'Yesterday',
+    total: 'Total',
+    enablePlugsForSummary: 'Enable plugs to see energy summary',
+    addNotificationProvider: 'Add',
+    // Users
+    systemBadge: '(System)',
+    creating: 'Creating...',
+    changing: 'Changing...',
+    deleteUserAndItems: 'Delete user AND their items',
+    deleteUserKeepItems: 'Delete user, keep items (become ownerless)',
+    ok: 'OK',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2872,6 +3029,143 @@ export default {
     backupFailed: 'Backup failed',
     backupFailed: 'Backup failed',
     restoreFailed: 'Restore failed',
     restoreFailed: 'Restore failed',
     restoreNote: 'Virtual Printer will be stopped during restore',
     restoreNote: 'Virtual Printer will be stopped during restore',
+
+    // GitHub Backup
+    githubBackup: 'GitHub Backup',
+    enabled: 'Enabled',
+    cloudLoginRequired: 'Bambu Cloud login required. Sign in under Profiles → Cloud Profiles to enable GitHub backup.',
+    githubDescription: 'Automatically sync your profiles to a private GitHub repository for backup and version history.',
+    repositoryUrl: 'Repository URL',
+    personalAccessToken: 'Personal Access Token',
+    tokenSaved: '(saved)',
+    enterNewToken: 'Enter new token to update',
+    tokenHint: 'Fine-grained token with Contents read/write permission',
+    branch: 'Branch',
+    manualOnly: 'Manual only',
+    hourly: 'Hourly',
+    daily: 'Daily',
+    weekly: 'Weekly',
+    includeInBackup: 'Include in backup',
+    kProfiles: 'K-Profiles',
+    kProfilesDescription: 'Pressure advance calibration from connected printers',
+    noPrintersConnected: 'No printers connected',
+    printersConnected: '{{connected}}/{{total}} connected',
+    cloudProfiles: 'Cloud Profiles',
+    cloudProfilesDescription: 'Filament, printer, and process presets from Bambu Cloud',
+    appSettings: 'App Settings',
+    appSettingsDescription: 'Bambuddy configuration (complete database)',
+    lastBackupAt: 'Last backup:',
+    noBackupsYet: 'No backups yet',
+    next: 'Next:',
+    startingBackup: 'Starting backup...',
+    test: 'Test',
+    enableBackup: 'Enable Backup',
+    testConnection: 'Test Connection',
+    enterRepoUrl: 'Enter repository URL',
+    enterRepoAndToken: 'Enter repository URL and access token',
+    repoRequired: 'Repository URL is required',
+    tokenRequired: 'Access token is required',
+    githubBackupEnabled: 'GitHub backup enabled',
+    tokenUpdated: 'Token updated',
+    settingsSaved: 'Settings saved',
+    failedToSave: 'Failed to save: {{message}}',
+    backupCompleteFiles: 'Backup complete - {{count}} files updated',
+    backupSkippedNoChanges: 'Backup skipped - no changes',
+    backupFailed2: 'Backup failed: {{message}}',
+    clearedLogs: 'Cleared {{count}} logs',
+    failedToClearLogs: 'Failed to clear logs: {{message}}',
+
+    // History
+    history: 'History',
+    clear: 'Clear',
+    date: 'Date',
+    status: 'Status',
+    commit: 'Commit',
+
+    // Local Backup
+    localBackup: 'Local Backup',
+    localBackupDescription: 'Create a complete backup of your Bambuddy data including the database, archives, uploads, and all files.',
+    downloadBackupLabel: 'Download Backup',
+    completeBackupZip: 'Complete backup: database + all files (ZIP)',
+    download: 'Download',
+    preparingBackup: 'Preparing backup...',
+    creatingArchive: 'Creating backup archive... This may take a while for large archives.',
+    downloadingFile: 'Downloading backup file...',
+    backupDownloaded: 'Backup downloaded successfully',
+    failedToCreateBackup: 'Failed to create backup: {{message}}',
+    restore: 'Restore',
+    restoreReplacesAll: 'Restore replaces all data.',
+    restoreReplacesAllDetail: 'Your current database and files will be completely replaced. A restart is required after restore.',
+    restoreConfirmTitle: 'Restore Backup',
+    restoreConfirmMessage: 'Are you sure you want to restore from "{{filename}}"? This will completely replace your current database and all files. The application will need to be restarted after restore.',
+    restoreConfirmButton: 'Restore Backup',
+    uploadingFile: 'Uploading backup file...',
+    backupRestoredRestart: 'Backup restored. Please restart Bambuddy.',
+    failedToRestore: 'Failed to restore backup. Please check the file format.',
+    reloadNow: 'Reload Now',
+    creatingBackup: 'Creating Backup',
+    restoringBackup: 'Restoring Backup',
+    preparing: 'Preparing...',
+    processing: 'Processing...',
+    doNotClosePage: 'Please do not close this page or navigate away. This operation may take several minutes for large backups.',
+
+    // RestoreModal
+    restoring: 'Restoring...',
+    restoreComplete: 'Restore Complete',
+    restoreFailed2: 'Restore Failed',
+    importSettings: 'Import settings from a backup file',
+    pleaseWaitRestoring: 'Please wait while your data is being restored',
+    selectBackupFile: 'Click to select backup file (.json or .zip)',
+    duplicateHandling: 'How duplicate handling works:',
+    matchPrinters: 'Printers',
+    matchPrintersBy: 'matched by serial number',
+    matchSmartPlugs: 'Smart Plugs',
+    matchSmartPlugsBy: 'matched by IP address',
+    matchNotificationProviders: 'Notification Providers',
+    matchNotificationProvidersBy: 'matched by name',
+    matchFilaments: 'Filaments',
+    matchFilamentsBy: 'matched by name + type + brand',
+    matchArchives: 'Archives',
+    matchArchivesBy: 'matched by content hash (always skipped)',
+    matchPendingUploads: 'Pending Uploads',
+    matchPendingUploadsBy: 'matched by filename',
+    matchSettingsTemplates: 'Settings & Templates',
+    matchSettingsTemplatesBy: 'always overwritten',
+    replaceExisting: 'Replace existing data',
+    keepExisting: 'Keep existing data',
+    overwriteDescription: 'Overwrite items that already exist with backup data',
+    keepDescription: "Only restore items that don't already exist",
+    overwriteCaution: 'Caution:',
+    overwriteWarning: 'Overwriting will replace your current configurations with data from the backup. Printer access codes are never overwritten for security.',
+    cancel: 'Cancel',
+    processingBackup: 'Processing backup file...',
+    itemsRestored: 'Items Restored',
+    itemsSkipped: 'Items Skipped',
+    restored: 'Restored',
+    skippedAlreadyExist: 'Skipped (already exist)',
+    filesCategory: 'Files (3MF, thumbnails, etc.)',
+    andMore: '...and {{count}} more',
+    newApiKeysGenerated: 'New API Keys Generated',
+    keysShownOnce: 'These keys are only shown once. Copy them now!',
+    copy: 'Copy',
+    noDataFound: 'No data was found to restore in the backup file.',
+    close: 'Close',
+
+    // Category labels
+    categories: {
+      settings: 'Settings',
+      notification_providers: 'Notification Providers',
+      notification_templates: 'Notification Templates',
+      smart_plugs: 'Smart Plugs',
+      printers: 'Printers',
+      filaments: 'Filaments',
+      maintenance_types: 'Maintenance Types',
+      archives: 'Archives',
+      projects: 'Projects',
+      pending_uploads: 'Pending Uploads',
+      external_links: 'External Links',
+      api_keys: 'API Keys',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3250,6 +3544,319 @@ export default {
         mqttSameAsPower: 'Same as power topic, or different',
         mqttSameAsPower: 'Same as power topic, or different',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: 'Linked to:',
+    monitorOnly: 'Monitor Only',
+    alerts: 'Alerts',
+    scheduleOn: 'On {{time}}',
+    scheduleOff: 'Off {{time}}',
+    on: 'On',
+    off: 'Off',
+    power: 'Power',
+    kwhToday: 'kWh Today',
+    settings: 'Settings',
+    automationSettings: 'Automation Settings',
+    showInSwitchbar: 'Show in Switchbar',
+    quickAccessSidebar: 'Quick access from sidebar',
+    enabled: 'Enabled',
+    enableAutomation: 'Enable automation for this plug',
+    autoOn: 'Auto On',
+    autoOnDescription: 'Turn on when print starts',
+    autoOff: 'Auto Off',
+    autoOffDescription: 'Turn off when print completes (one-shot)',
+    turnOffDelayMode: 'Turn Off Delay Mode',
+    time: 'Time',
+    temp: 'Temp',
+    delayMinutes: 'Delay (minutes)',
+    tempThreshold: 'Temperature threshold (°C)',
+    tempThresholdDescription: 'Turns off when nozzle cools below this temperature',
+    edit: 'Edit',
+    deleteConfirm: 'Are you sure you want to delete "{{name}}"? This cannot be undone.',
+    turnOnConfirm: 'Are you sure you want to turn on "{{name}}"?',
+    turnOffConfirm: 'Are you sure you want to turn off "{{name}}"? This will cut power to the connected device.',
+    failedToTurn: 'Failed to turn {{action}} "{{name}}"',
+    unknown: 'Unknown',
+    // AddSmartPlugModal
+    addTitle: 'Add Smart Plug',
+    editTitle: 'Edit Smart Plug',
+    stopScanning: 'Stop Scanning',
+    discoverTasmota: 'Discover Tasmota Devices',
+    foundDevices: 'Found {{count}} device(s) - click to select:',
+    noDevicesFound: 'No Tasmota devices found on your network',
+    haNotConfigured: 'Home Assistant is not configured. Set it up in',
+    haSettingsPath: 'Settings → Network → Home Assistant',
+    selectEntity: 'Select Entity *',
+    ipAddress: 'IP Address *',
+    nameLabel: 'Name *',
+    username: 'Username',
+    password: 'Password',
+    authHint: "Leave empty if your Tasmota device doesn't require authentication",
+    linkToPrinter: 'Link to Printer',
+    noPrinter: 'No printer (manual control only)',
+    linkingDescription: 'Linking enables automatic on/off when prints start/complete',
+    powerAlerts: 'Power Alerts',
+    alertAbove: 'Alert if above (W)',
+    alertBelow: 'Alert if below (W)',
+    alertDescription: 'Get notified when power consumption crosses these thresholds. Leave empty to disable that direction.',
+    dailySchedule: 'Daily Schedule',
+    turnOnAt: 'Turn On at',
+    turnOffAt: 'Turn Off at',
+    scheduleDescription: 'Automatically turn the plug on/off at these times daily. Leave empty to skip that action.',
+    showOnPrinterCard: 'Show on Printer Card',
+    displayOnPrinterCard: 'Display button on printer card',
+    connectedResult: 'Connected!',
+    deviceLabel: 'Device: {{name}} - ',
+    stateLabel: 'State: {{state}}',
+    test: 'Test',
+    delete: 'Delete',
+    save: 'Save',
+    add: 'Add',
+    cancel: 'Cancel',
+    failedToStartScan: 'Failed to start scan',
+    nameRequired: 'Name is required',
+    entityRequired: 'Entity is required for Home Assistant plugs',
+    mqttTopicRequired: 'At least one MQTT topic must be configured for power, energy, or state monitoring',
+    loadingEntities: 'Loading entities...',
+    loading: 'Loading...',
+    failedToLoadEntities: 'Failed to load entities: {{error}}',
+    noEntitiesMatching: 'No entities found matching "{{search}}"',
+    noEntitiesAvailable: 'No entities available',
+    searchingEntities: 'Searching all entities ({{count}} found)',
+    showingEntities: 'Showing switch, light, input_boolean ({{count}} available)',
+    energyMonitoringOptional: 'Energy Monitoring (Optional)',
+    energyMonitoringHint: 'Search and select sensors that provide power/energy data.',
+    powerSensorW: 'Power Sensor (W)',
+    energyTodayKwh: 'Energy Today (kWh)',
+    totalEnergyKwh: 'Total Energy (kWh)',
+    noMatchingSensors: 'No matching sensors',
+    none: 'None',
+    mqttNotConfigured: 'MQTT broker not configured. Set broker address in',
+    mqttSettingsPath: 'Settings → Network → MQTT Publishing',
+    mqttNotConfiguredSuffix: "(you don't need to enable publishing, just fill in the broker details).",
+    mqttMonitorOnlyDescription: 'MQTT plugs receive power/energy data via MQTT subscription. On/off control is not available - use your MQTT broker or home automation system.',
+    powerMonitoring: 'Power Monitoring',
+    energyMonitoring: 'Energy Monitoring',
+    stateMonitoring: 'State Monitoring',
+    optional: 'optional',
+    topic: 'Topic',
+    jsonPath: 'JSON Path',
+    multiplier: 'Multiplier',
+    onValue: 'ON Value',
+    mqttPowerHint: 'JSON path extracts value from JSON payload (e.g., "power_l1"). Leave empty if topic publishes raw numeric values.\nUse multiplier 0.001 for mW→W, 1000 for kW→W.',
+    mqttEnergyHint: 'JSON path extracts value from JSON payload. Leave empty for raw values.\nUse multiplier 0.001 for Wh→kWh, 1000 for MWh→kWh.',
+    mqttStateHint: 'JSON path extracts value from JSON payload. Leave empty for raw values.\nON value: the exact string that means "ON". Leave empty for auto-detect (ON, true, 1).',
+    noSwitchesInSwitchbar: 'No switches in switchbar',
+    enableSwitchbarHint: 'Enable "Show in Switchbar" in Settings > Smart Plugs',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'Email',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'SMTP email notifications',
+      telegram: 'Notifications via Telegram bot',
+      discord: 'Send to Discord channel via webhook',
+      ntfy: 'Free, self-hostable push notifications',
+      pushover: 'Simple, reliable push notifications',
+      callmebot: 'Free WhatsApp notifications via CallMeBot',
+      webhook: 'Generic HTTP POST to any URL',
+    },
+    // NotificationProviderCard
+    lastSuccess: 'Last: {{date}}',
+    error: 'Error',
+    printer: 'Printer:',
+    allPrinters: 'All printers',
+    sendTestNotification: 'Send Test Notification',
+    eventSettings: 'Event Settings',
+    enabled: 'Enabled',
+    sendFromProvider: 'Send notifications from this provider',
+    // Event categories
+    printEvents: 'Print Events',
+    printerStatus: 'Printer Status',
+    amsAlarms: 'AMS Alarms',
+    amsHtAlarms: 'AMS-HT Alarms',
+    printQueue: 'Print Queue',
+    // Event tags (badges)
+    start: 'Start',
+    plateCheck: 'Plate Check',
+    complete: 'Complete',
+    failed: 'Failed',
+    stopped: 'Stopped',
+    progress: 'Progress',
+    offline: 'Offline',
+    lowFilament: 'Low Filament',
+    maintenance: 'Maintenance',
+    amsHumidity: 'AMS Humidity',
+    amsTemp: 'AMS Temp',
+    amsHtHumidity: 'AMS-HT Humidity',
+    amsHtTemp: 'AMS-HT Temp',
+    bedCooled: 'Bed Cooled',
+    quiet: 'Quiet',
+    digest: 'Digest {{time}}',
+    // Event labels (expanded settings)
+    printStarted: 'Print Started',
+    plateNotEmpty: 'Plate Not Empty',
+    plateNotEmptyDescription: 'Objects detected before print',
+    printCompleted: 'Print Completed',
+    bedCooledLabel: 'Bed Cooled',
+    bedCooledDescription: 'Bed cooled below threshold after print',
+    printFailed: 'Print Failed',
+    printStopped: 'Print Stopped',
+    progressMilestones: 'Progress Milestones',
+    progressMilestonesDescription: 'Notify at 25%, 50%, 75%',
+    printerOffline: 'Printer Offline',
+    printerError: 'Printer Error',
+    lowFilamentLabel: 'Low Filament',
+    maintenanceDue: 'Maintenance Due',
+    maintenanceDueDescription: 'Notify when maintenance is needed',
+    amsHumidityHigh: 'AMS Humidity High',
+    amsHumidityHighDescription: 'Regular AMS humidity exceeds threshold',
+    amsTemperatureHigh: 'AMS Temperature High',
+    amsTemperatureHighDescription: 'Regular AMS temperature exceeds threshold',
+    amsHtHumidityHigh: 'AMS-HT Humidity High',
+    amsHtHumidityHighDescription: 'AMS-HT humidity exceeds threshold',
+    amsHtTemperatureHigh: 'AMS-HT Temperature High',
+    amsHtTemperatureHighDescription: 'AMS-HT temperature exceeds threshold',
+    // Queue events
+    jobAdded: 'Job Added',
+    jobAddedDescription: 'Job added to queue',
+    jobAssigned: 'Job Assigned',
+    jobAssignedDescription: 'Model-based job assigned to printer',
+    jobStarted: 'Job Started',
+    jobStartedDescription: 'Queue job started printing',
+    jobWaiting: 'Job Waiting',
+    jobWaitingDescription: 'Job waiting for filament',
+    jobSkipped: 'Job Skipped',
+    jobSkippedDescription: 'Job skipped (previous failed)',
+    jobFailed: 'Job Failed',
+    jobFailedDescription: 'Job failed to start',
+    queueComplete: 'Queue Complete',
+    queueCompleteDescription: 'All queue jobs finished',
+    // Quiet hours
+    quietHours: 'Quiet Hours',
+    noNotificationsDuring: 'No notifications during these hours',
+    editProviderToChangeQuietHours: 'Edit provider to change quiet hours',
+    // Daily digest
+    dailyDigest: 'Daily Digest',
+    batchNotifications: 'Batch notifications into a single daily summary',
+    sendAt: 'Send at {{time}}',
+    editProviderToChangeDigestTime: 'Edit provider to change digest time',
+    // Actions
+    edit: 'Edit',
+    deleteProvider: 'Delete Notification Provider',
+    deleteConfirm: 'Are you sure you want to delete "{{name}}"? This cannot be undone.',
+    delete: 'Delete',
+    // AddNotificationModal
+    addTitle: 'Add Notification Provider',
+    editTitle: 'Edit Notification Provider',
+    nameLabel: 'Name *',
+    namePlaceholder: 'My Notifications',
+    providerTypeLabel: 'Provider Type *',
+    configuration: 'Configuration',
+    testConfiguration: 'Test Configuration',
+    printerFilter: 'Printer Filter',
+    onlyFromPrinter: 'Only send notifications for events from this printer',
+    quietHoursDnd: 'Quiet Hours (Do Not Disturb)',
+    quietStart: 'Start',
+    quietEnd: 'End',
+    dailyDigestLabel: 'Daily Digest',
+    sendDigestAt: 'Send digest at',
+    digestCollected: 'Events will be collected and sent as a single summary at this time',
+    notificationEvents: 'Notification Events',
+    progressPercent: '(25%, 50%, 75%)',
+    bedCooledAfterPrint: '(after print completes)',
+    cancel: 'Cancel',
+    save: 'Save',
+    add: 'Add',
+    nameRequired: 'Name is required',
+    fieldRequired: '{{field}} is required',
+    // Config field labels
+    phoneNumber: 'Phone Number',
+    apiKey: 'API Key',
+    serverUrl: 'Server URL',
+    topic: 'Topic',
+    authToken: 'Auth Token',
+    userKey: 'User Key',
+    appToken: 'App Token',
+    priority: 'Priority',
+    botToken: 'Bot Token',
+    chatId: 'Chat ID',
+    smtpServer: 'SMTP Server',
+    smtpPort: 'SMTP Port',
+    security: 'Security',
+    authentication: 'Authentication',
+    username: 'Username',
+    password: 'Password',
+    fromEmail: 'From Email',
+    toEmail: 'To Email',
+    webhookUrl: 'Webhook URL',
+    payloadFormat: 'Payload Format',
+    authorization: 'Authorization',
+    titleFieldName: 'Title Field Name',
+    messageFieldName: 'Message Field Name',
+    // NotificationTemplateEditor
+    editTemplate: 'Edit Template: {{name}}',
+    titleLabel: 'Title',
+    bodyLabel: 'Body',
+    titlePlaceholder: 'Notification title...',
+    bodyPlaceholder: 'Notification body...',
+    availableVariables: 'Available Variables',
+    clickToInsert: 'Click to insert at cursor position in body',
+    livePreview: 'Live Preview',
+    hide: 'Hide',
+    show: 'Show',
+    loadingPreview: 'Loading preview...',
+    enterTemplateContent: 'Enter template content to see preview',
+    titlePreview: 'Title:',
+    bodyPreview: 'Body:',
+    resetToDefault: 'Reset to Default',
+    titleRequired: 'Title is required',
+    bodyRequired: 'Body is required',
+    // NotificationLogViewer
+    notificationLog: 'Notification Log',
+    showFailedOnly: 'Failed only',
+    last24Hours: 'Last 24 hours',
+    last7Days: 'Last 7 days',
+    last30Days: 'Last 30 days',
+    last90Days: 'Last 90 days',
+    justNow: 'Just now',
+    noFailedNotifications: 'No failed notifications',
+    noNotificationsLogged: 'No notifications logged',
+    unknownProvider: 'Unknown Provider',
+    logTitle: 'Title',
+    logMessage: 'Message',
+    logError: 'Error',
+    logProvider: 'Provider: {{type}}',
+    logTime: 'Time: {{time}}',
+    refresh: 'Refresh',
+    clearOld: 'Clear Old',
+    statsSummary: 'Last {{days}} days:',
+    statsNotifications: 'notifications',
+    statsSent: '{{count}} sent',
+    statsFailed: '{{count}} failed',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      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',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

+ 594 - 0
frontend/src/i18n/locales/fr.ts

@@ -1654,6 +1654,150 @@ export default {
       imported: '{{added}} couleurs importées ({{skipped}} ignorées)',
       imported: '{{added}} couleurs importées ({{skipped}} ignorées)',
       importFailed: 'Échec import : format JSON invalide',
       importFailed: 'Échec import : format JSON invalide',
     },
     },
+    // General tab
+    dateFormat: 'Format de date',
+    dateFormatUs: 'US (MM/JJ/AAAA)',
+    dateFormatEu: 'EU (JJ/MM/AAAA)',
+    dateFormatIso: 'ISO (AAAA-MM-JJ)',
+    timeFormat: 'Format horaire',
+    timeFormat12: '12 heures (3:30 PM)',
+    timeFormat24: '24 heures (15:30)',
+    defaultPrinter: 'Imprimante par défaut',
+    defaultPrinterDescription: 'Présélectionner cette imprimante pour les téléversements, réimpressions et autres opérations.',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'Glissez les éléments dans la barre latérale pour réorganiser. Réinitialiser l\'ordre par défaut ici.',
+    reset: 'Réinitialiser',
+    darkMode: 'Mode sombre',
+    lightMode: 'Mode clair',
+    active: '(actif)',
+    background: 'Arrière-plan',
+    accent: 'Accent',
+    style: 'Style',
+    bgNeutral: 'Neutre',
+    bgWarm: 'Chaud',
+    bgCool: 'Froid',
+    bgOled: 'OLED Noir',
+    bgSlate: 'Bleu ardoise',
+    bgForest: 'Vert forêt',
+    accentGreen: 'Vert',
+    accentTeal: 'Sarcelle',
+    accentBlue: 'Bleu',
+    accentOrange: 'Orange',
+    accentPurple: 'Violet',
+    accentRed: 'Rouge',
+    styleClassic: 'Classique',
+    styleGlow: 'Lumineux',
+    styleVibrant: 'Vibrant',
+    themeToggleHint: 'Basculer entre le mode sombre et clair avec l\'icône soleil/lune dans la barre latérale.',
+    autoArchivePrints: 'Archiver automatiquement les impressions',
+    autoArchiveDescription: 'Sauvegarder automatiquement les fichiers 3MF à la fin des impressions',
+    saveThumbnailsDescription: 'Extraire et sauvegarder les images d\'aperçu des fichiers 3MF',
+    captureFinishPhotoDescription: 'Prendre une photo avec la caméra de l\'imprimante à la fin de l\'impression',
+    ffmpegNotInstalled: 'ffmpeg non installé',
+    ffmpegRequired: 'La capture caméra nécessite ffmpeg. Installez-le via <brew>brew install ffmpeg</brew> (macOS) ou <apt>apt install ffmpeg</apt> (Linux).',
+    camera: 'Caméra',
+    cameraViewMode: 'Mode d\'affichage caméra',
+    cameraOverlayDescription: 'La caméra s\'ouvre dans un overlay redimensionnable sur l\'écran principal',
+    cameraWindowDescription: 'La caméra s\'ouvre dans une fenêtre de navigateur séparée',
+    externalCamerasDescription: 'Configurer des caméras externes pour remplacer la caméra intégrée. Supporte les flux MJPEG, RTSP, snapshots HTTP et caméras USB (V4L2). Lorsqu\'activée, la caméra externe est utilisée pour la vue en direct et les photos de fin.',
+    cameraPlaceholderUsb: 'Chemin du périphérique (/dev/video0)',
+    cameraPlaceholderUrl: 'URL caméra (rtsp://... ou http://...)',
+    cameraTypeMjpeg: 'Flux MJPEG',
+    cameraTypeRtsp: 'Flux RTSP',
+    cameraTypeSnapshot: 'Snapshot HTTP',
+    cameraTypeUsb: 'Caméra USB (V4L2)',
+    test: 'Tester',
+    connected: 'Connecté',
+    disconnected: 'Déconnecté',
+    currency: 'Devise',
+    defaultFilamentCost: 'Coût filament par défaut (par kg)',
+    electricityCost: 'Coût électricité par kWh',
+    energyDisplayMode: 'Mode d\'affichage énergie',
+    energyModePrintDescription: 'Le tableau de bord affiche la somme de l\'énergie utilisée pendant les impressions',
+    energyModeTotalDescription: 'Le tableau de bord affiche l\'énergie totale des prises connectées',
+    fileManager: 'Gestionnaire de fichiers',
+    createArchiveEntry: 'Créer une entrée d\'archive lors de l\'impression',
+    createArchiveEntryDescription: 'Lors de l\'impression depuis le gestionnaire de fichiers, créer optionnellement une entrée d\'archive',
+    lowDiskSpaceWarning: 'Avertissement espace disque faible',
+    lowDiskSpaceDescription: 'Afficher un avertissement lorsque l\'espace disque libre descend sous ce seuil',
+    printerFirmware: 'Firmware imprimante',
+    checkFirmwareDescription: 'Vérifier les mises à jour firmware de Bambu Lab',
+    bambuddySoftware: 'Logiciel Bambuddy',
+    autoCheckDescription: 'Vérifier automatiquement les nouvelles versions au démarrage',
+    checkNow: 'Vérifier maintenant',
+    updateAvailableVersion: 'Mise à jour disponible : v{{version}}',
+    releaseNotes: 'Notes de version',
+    updateViaDocker: 'Mettre à jour via Docker Compose :',
+    installUpdate: 'Installer la mise à jour',
+    latestVersionRunning: 'Vous utilisez la dernière version',
+    failedToCheckUpdates: 'Échec de la vérification des mises à jour : {{error}}',
+    backupRestore: 'Sauvegarde & Restauration',
+    backupRestoreDescription: 'Exporter/importer les paramètres et configurer la sauvegarde GitHub',
+    goToBackup: 'Aller à la sauvegarde',
+    externalUrl: 'URL externe',
+    externalUrlDescription: 'L\'URL externe où Bambuddy est accessible. Utilisée pour les images de notification et les intégrations externes.',
+    bambuddyUrl: 'URL Bambuddy',
+    externalUrlHint: 'Inclure le protocole et le port (ex : http://192.168.1.100:8000)',
+    ftpRetry: 'Réessai FTP',
+    ftpRetryDescription: 'Réessayer les opérations FTP lorsque le WiFi de l\'imprimante est instable. S\'applique aux téléchargements 3MF, uploads d\'impression, téléchargements timelapse et mises à jour firmware.',
+    autoRetryDescription: 'Réessayer automatiquement les opérations FTP échouées',
+    retryAttempts: 'Tentatives de réessai',
+    retryDelay: 'Délai de réessai',
+    connectionTimeout: 'Délai de connexion',
+    time_one: '{{count}} fois',
+    time_other: '{{count}} fois',
+    second_one: '{{count}} seconde',
+    second_other: '{{count}} secondes',
+    nSeconds: '{{count}} secondes',
+    increaseForWeakWifi: 'Augmenter pour les imprimantes avec un WiFi faible',
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Se connecter à Home Assistant pour contrôler les prises connectées via l\'API REST HA. Supporte les entités switch, light, input_boolean et script.',
+    homeAssistantUrl: 'URL Home Assistant',
+    longLivedAccessToken: 'Token d\'accès longue durée',
+    haTokenHint: 'Créer un token dans HA : Profil → Tokens d\'accès longue durée → Créer un token',
+    connectionSuccessful: 'Connexion réussie',
+    connectionFailed: 'Connexion échouée',
+    haConnectionSuccess: 'Connexion à Home Assistant réussie.',
+    haConnectionFailed: 'Échec de la connexion à Home Assistant.',
+    mqttPublishing: 'Publication MQTT',
+    mqttDescription: 'Publier les événements BamBuddy vers un broker MQTT externe pour l\'intégration avec Node-RED, Home Assistant et d\'autres systèmes d\'automatisation.',
+    mqttEnableDescription: 'Publier les événements vers un broker MQTT externe',
+    brokerHostname: 'Nom d\'hôte du broker',
+    port: 'Port',
+    usernameOptional: 'Nom d\'utilisateur (optionnel)',
+    passwordOptional: 'Mot de passe (optionnel)',
+    topicPrefix: 'Préfixe de topic',
+    topicPrefixHint: 'Les topics seront : {{prefix}}/printers/<serial>/status, etc.',
+    prometheusMetrics: 'Métriques Prometheus',
+    prometheusEndpointDescription: 'Exposer les métriques imprimante sur <code>/api/v1/metrics</code> pour la surveillance Prometheus/Grafana.',
+    bearerTokenOptional: 'Token Bearer (optionnel)',
+    bearerTokenHint: 'Si défini, les requêtes doivent inclure <code>Authorization: Bearer <token></code>',
+    metricsConnectionStatus: 'État de connexion',
+    metricsPrinterState: 'État imprimante (idle/printing/etc)',
+    metricsPrintProgress: 'Progression impression 0-100%',
+    metricsBedTemp: 'Température du plateau',
+    metricsNozzleTemp: 'Température de la buse',
+    metricsPrintsTotal: 'Total impressions par résultat',
+    metricsMore: '...et plus (couches, ventilateurs, file d\'attente, utilisation filament)',
+    smartPlugsDescription: 'Connecter des prises connectées (Tasmota ou Home Assistant) pour automatiser le contrôle de l\'alimentation et suivre la consommation d\'énergie de vos imprimantes.',
+    allOn: 'Tout allumer',
+    allOff: 'Tout éteindre',
+    addSmartPlug: 'Ajouter une prise',
+    energySummary: 'Résumé énergétique',
+    currentPower: 'Puissance actuelle',
+    plugsOnline: '{{reachable}}/{{total}} prises en ligne',
+    today: 'Aujourd\'hui',
+    yesterday: 'Hier',
+    total: 'Total',
+    enablePlugsForSummary: 'Activer les prises pour voir le résumé énergétique',
+    addNotificationProvider: 'Ajouter',
+    systemBadge: '(Système)',
+    creating: 'Création...',
+    changing: 'Modification...',
+    deleteUserAndItems: 'Supprimer l\'utilisateur ET ses éléments',
+    deleteUserKeepItems: 'Supprimer l\'utilisateur, garder les éléments (deviennent sans propriétaire)',
+    ok: 'OK',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2872,6 +3016,143 @@ export default {
     backupFailed: 'Échec sauvegarde',
     backupFailed: 'Échec sauvegarde',
     restoreFailed: 'Échec restauration',
     restoreFailed: 'Échec restauration',
     restoreNote: 'L\'imprimante virtuelle sera arrêtée pendant la restauration',
     restoreNote: 'L\'imprimante virtuelle sera arrêtée pendant la restauration',
+
+    // GitHub Backup
+    githubBackup: 'Sauvegarde GitHub',
+    enabled: 'Activé',
+    cloudLoginRequired: 'Connexion Bambu Cloud requise. Connectez-vous sous Profils → Profils Cloud pour activer la sauvegarde GitHub.',
+    githubDescription: 'Synchronisez automatiquement vos profils vers un dépôt GitHub privé pour la sauvegarde et l\'historique des versions.',
+    repositoryUrl: 'URL du dépôt',
+    personalAccessToken: 'Jeton d\'accès personnel',
+    tokenSaved: '(enregistré)',
+    enterNewToken: 'Entrez un nouveau jeton pour mettre à jour',
+    tokenHint: 'Jeton à granularité fine avec permission de lecture/écriture du contenu',
+    branch: 'Branche',
+    manualOnly: 'Manuel uniquement',
+    hourly: 'Toutes les heures',
+    daily: 'Quotidien',
+    weekly: 'Hebdomadaire',
+    includeInBackup: 'Inclure dans la sauvegarde',
+    kProfiles: 'K-Profils',
+    kProfilesDescription: 'Calibration de l\'avance de pression des imprimantes connectées',
+    noPrintersConnected: 'Aucune imprimante connectée',
+    printersConnected: '{{connected}}/{{total}} connectées',
+    cloudProfiles: 'Profils Cloud',
+    cloudProfilesDescription: 'Préréglages de filament, imprimante et processus depuis Bambu Cloud',
+    appSettings: 'Paramètres de l\'application',
+    appSettingsDescription: 'Configuration Bambuddy (base de données complète)',
+    lastBackupAt: 'Dernière sauvegarde :',
+    noBackupsYet: 'Aucune sauvegarde pour l\'instant',
+    next: 'Prochaine :',
+    startingBackup: 'Démarrage de la sauvegarde...',
+    test: 'Tester',
+    enableBackup: 'Activer la sauvegarde',
+    testConnection: 'Tester la connexion',
+    enterRepoUrl: 'Entrez l\'URL du dépôt',
+    enterRepoAndToken: 'Entrez l\'URL du dépôt et le jeton d\'accès',
+    repoRequired: 'L\'URL du dépôt est requise',
+    tokenRequired: 'Le jeton d\'accès est requis',
+    githubBackupEnabled: 'Sauvegarde GitHub activée',
+    tokenUpdated: 'Jeton mis à jour',
+    settingsSaved: 'Paramètres enregistrés',
+    failedToSave: 'Échec de l\'enregistrement : {{message}}',
+    backupCompleteFiles: 'Sauvegarde terminée - {{count}} fichiers mis à jour',
+    backupSkippedNoChanges: 'Sauvegarde ignorée - aucun changement',
+    backupFailed2: 'Échec de la sauvegarde : {{message}}',
+    clearedLogs: '{{count}} journaux supprimés',
+    failedToClearLogs: 'Échec de la suppression des journaux : {{message}}',
+
+    // History
+    history: 'Historique',
+    clear: 'Effacer',
+    date: 'Date',
+    status: 'Statut',
+    commit: 'Commit',
+
+    // Local Backup
+    localBackup: 'Sauvegarde locale',
+    localBackupDescription: 'Créez une sauvegarde complète de vos données Bambuddy incluant la base de données, les archives, les téléchargements et tous les fichiers.',
+    downloadBackupLabel: 'Télécharger la sauvegarde',
+    completeBackupZip: 'Sauvegarde complète : base de données + tous les fichiers (ZIP)',
+    download: 'Télécharger',
+    preparingBackup: 'Préparation de la sauvegarde...',
+    creatingArchive: 'Création de l\'archive de sauvegarde... Cela peut prendre un moment pour les archives volumineuses.',
+    downloadingFile: 'Téléchargement du fichier de sauvegarde...',
+    backupDownloaded: 'Sauvegarde téléchargée avec succès',
+    failedToCreateBackup: 'Échec de la création de la sauvegarde : {{message}}',
+    restore: 'Restaurer',
+    restoreReplacesAll: 'La restauration remplace toutes les données.',
+    restoreReplacesAllDetail: 'Votre base de données et vos fichiers actuels seront complètement remplacés. Un redémarrage est nécessaire après la restauration.',
+    restoreConfirmTitle: 'Restaurer la sauvegarde',
+    restoreConfirmMessage: 'Êtes-vous sûr de vouloir restaurer depuis "{{filename}}" ? Cela remplacera complètement votre base de données et tous vos fichiers. L\'application devra être redémarrée après la restauration.',
+    restoreConfirmButton: 'Restaurer la sauvegarde',
+    uploadingFile: 'Téléchargement du fichier de sauvegarde...',
+    backupRestoredRestart: 'Sauvegarde restaurée. Veuillez redémarrer Bambuddy.',
+    failedToRestore: 'Échec de la restauration. Veuillez vérifier le format du fichier.',
+    reloadNow: 'Recharger maintenant',
+    creatingBackup: 'Création de la sauvegarde',
+    restoringBackup: 'Restauration de la sauvegarde',
+    preparing: 'Préparation...',
+    processing: 'Traitement...',
+    doNotClosePage: 'Veuillez ne pas fermer cette page ni naviguer ailleurs. Cette opération peut prendre plusieurs minutes pour les sauvegardes volumineuses.',
+
+    // RestoreModal
+    restoring: 'Restauration...',
+    restoreComplete: 'Restauration terminée',
+    restoreFailed2: 'Échec de la restauration',
+    importSettings: 'Importer les paramètres depuis un fichier de sauvegarde',
+    pleaseWaitRestoring: 'Veuillez patienter pendant la restauration de vos données',
+    selectBackupFile: 'Cliquez pour sélectionner un fichier de sauvegarde (.json ou .zip)',
+    duplicateHandling: 'Comment fonctionne la gestion des doublons :',
+    matchPrinters: 'Imprimantes',
+    matchPrintersBy: 'correspondance par numéro de série',
+    matchSmartPlugs: 'Smart Plugs',
+    matchSmartPlugsBy: 'correspondance par adresse IP',
+    matchNotificationProviders: 'Fournisseurs de notifications',
+    matchNotificationProvidersBy: 'correspondance par nom',
+    matchFilaments: 'Filaments',
+    matchFilamentsBy: 'correspondance par nom + type + marque',
+    matchArchives: 'Archives',
+    matchArchivesBy: 'correspondance par hash de contenu (toujours ignoré)',
+    matchPendingUploads: 'Téléchargements en attente',
+    matchPendingUploadsBy: 'correspondance par nom de fichier',
+    matchSettingsTemplates: 'Paramètres et modèles',
+    matchSettingsTemplatesBy: 'toujours écrasés',
+    replaceExisting: 'Remplacer les données existantes',
+    keepExisting: 'Conserver les données existantes',
+    overwriteDescription: 'Écraser les éléments qui existent déjà avec les données de sauvegarde',
+    keepDescription: 'Restaurer uniquement les éléments qui n\'existent pas encore',
+    overwriteCaution: 'Attention :',
+    overwriteWarning: 'L\'écrasement remplacera vos configurations actuelles par les données de la sauvegarde. Les codes d\'accès des imprimantes ne sont jamais écrasés pour des raisons de sécurité.',
+    cancel: 'Annuler',
+    processingBackup: 'Traitement du fichier de sauvegarde...',
+    itemsRestored: 'Éléments restaurés',
+    itemsSkipped: 'Éléments ignorés',
+    restored: 'Restaurés',
+    skippedAlreadyExist: 'Ignorés (existent déjà)',
+    filesCategory: 'Fichiers (3MF, miniatures, etc.)',
+    andMore: '...et {{count}} de plus',
+    newApiKeysGenerated: 'Nouvelles clés API générées',
+    keysShownOnce: 'Ces clés ne sont affichées qu\'une seule fois. Copiez-les maintenant !',
+    copy: 'Copier',
+    noDataFound: 'Aucune donnée à restaurer n\'a été trouvée dans le fichier de sauvegarde.',
+    close: 'Fermer',
+
+    // Category labels
+    categories: {
+      settings: 'Paramètres',
+      notification_providers: 'Fournisseurs de notifications',
+      notification_templates: 'Modèles de notifications',
+      smart_plugs: 'Smart Plugs',
+      printers: 'Imprimantes',
+      filaments: 'Filaments',
+      maintenance_types: 'Types de maintenance',
+      archives: 'Archives',
+      projects: 'Projets',
+      pending_uploads: 'Téléchargements en attente',
+      external_links: 'Liens externes',
+      api_keys: 'Clés API',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3250,6 +3531,319 @@ export default {
         mqttSameAsPower: 'Identique au topic puissance, ou différent',
         mqttSameAsPower: 'Identique au topic puissance, ou différent',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: 'Lié à :',
+    monitorOnly: 'Surveillance uniquement',
+    alerts: 'Alertes',
+    scheduleOn: 'On {{time}}',
+    scheduleOff: 'Off {{time}}',
+    on: 'On',
+    off: 'Off',
+    power: 'Puissance',
+    kwhToday: 'kWh Aujourd\'hui',
+    settings: 'Paramètres',
+    automationSettings: 'Paramètres d\'automatisation',
+    showInSwitchbar: 'Afficher dans la barre de commutateurs',
+    quickAccessSidebar: 'Accès rapide depuis la barre latérale',
+    enabled: 'Activé',
+    enableAutomation: 'Activer l\'automatisation pour cette prise',
+    autoOn: 'Auto On',
+    autoOnDescription: 'Allumer au démarrage de l\'impression',
+    autoOff: 'Auto Off',
+    autoOffDescription: 'Éteindre à la fin de l\'impression (unique)',
+    turnOffDelayMode: 'Mode de délai d\'extinction',
+    time: 'Temps',
+    temp: 'Temp',
+    delayMinutes: 'Délai (minutes)',
+    tempThreshold: 'Seuil de température (°C)',
+    tempThresholdDescription: 'S\'éteint lorsque la buse refroidit en dessous de cette température',
+    edit: 'Modifier',
+    deleteConfirm: 'Êtes-vous sûr de vouloir supprimer "{{name}}" ? Cette action est irréversible.',
+    turnOnConfirm: 'Êtes-vous sûr de vouloir allumer "{{name}}" ?',
+    turnOffConfirm: 'Êtes-vous sûr de vouloir éteindre "{{name}}" ? Cela coupera l\'alimentation de l\'appareil connecté.',
+    failedToTurn: 'Impossible de {{action}} "{{name}}"',
+    unknown: 'Inconnu',
+    // AddSmartPlugModal
+    addTitle: 'Ajouter une prise connectée',
+    editTitle: 'Modifier la prise connectée',
+    stopScanning: 'Arrêter le scan',
+    discoverTasmota: 'Découvrir les appareils Tasmota',
+    foundDevices: '{{count}} appareil(s) trouvé(s) - cliquez pour sélectionner :',
+    noDevicesFound: 'Aucun appareil Tasmota trouvé sur votre réseau',
+    haNotConfigured: 'Home Assistant n\'est pas configuré. Configurez-le dans',
+    haSettingsPath: 'Paramètres → Réseau → Home Assistant',
+    selectEntity: 'Sélectionner l\'entité *',
+    ipAddress: 'Adresse IP *',
+    nameLabel: 'Nom *',
+    username: 'Nom d\'utilisateur',
+    password: 'Mot de passe',
+    authHint: 'Laissez vide si votre appareil Tasmota ne nécessite pas d\'authentification',
+    linkToPrinter: 'Lier à l\'imprimante',
+    noPrinter: 'Pas d\'imprimante (contrôle manuel uniquement)',
+    linkingDescription: 'La liaison permet l\'allumage/extinction automatique au début/fin de l\'impression',
+    powerAlerts: 'Alertes de puissance',
+    alertAbove: 'Alerte si au-dessus (W)',
+    alertBelow: 'Alerte si en dessous (W)',
+    alertDescription: 'Recevoir une notification lorsque la consommation dépasse ces seuils. Laisser vide pour désactiver cette direction.',
+    dailySchedule: 'Planification quotidienne',
+    turnOnAt: 'Allumer à',
+    turnOffAt: 'Éteindre à',
+    scheduleDescription: 'Allumer/éteindre automatiquement la prise à ces heures chaque jour. Laisser vide pour ignorer cette action.',
+    showOnPrinterCard: 'Afficher sur la carte imprimante',
+    displayOnPrinterCard: 'Afficher le bouton sur la carte imprimante',
+    connectedResult: 'Connecté !',
+    deviceLabel: 'Appareil : {{name}} - ',
+    stateLabel: 'État : {{state}}',
+    test: 'Tester',
+    delete: 'Supprimer',
+    save: 'Enregistrer',
+    add: 'Ajouter',
+    cancel: 'Annuler',
+    failedToStartScan: 'Impossible de démarrer le scan',
+    nameRequired: 'Le nom est requis',
+    entityRequired: 'L\'entité est requise pour les prises Home Assistant',
+    mqttTopicRequired: 'Au moins un topic MQTT doit être configuré pour la puissance, l\'énergie ou la surveillance d\'état',
+    loadingEntities: 'Chargement des entités...',
+    loading: 'Chargement...',
+    failedToLoadEntities: 'Échec du chargement des entités : {{error}}',
+    noEntitiesMatching: 'Aucune entité trouvée correspondant à "{{search}}"',
+    noEntitiesAvailable: 'Aucune entité disponible',
+    searchingEntities: 'Recherche de toutes les entités ({{count}} trouvées)',
+    showingEntities: 'Affichage switch, light, input_boolean ({{count}} disponibles)',
+    energyMonitoringOptional: 'Surveillance énergétique (Optionnel)',
+    energyMonitoringHint: 'Recherchez et sélectionnez les capteurs fournissant des données de puissance/énergie.',
+    powerSensorW: 'Capteur de puissance (W)',
+    energyTodayKwh: 'Énergie aujourd\'hui (kWh)',
+    totalEnergyKwh: 'Énergie totale (kWh)',
+    noMatchingSensors: 'Aucun capteur correspondant',
+    none: 'Aucun',
+    mqttNotConfigured: 'Broker MQTT non configuré. Définissez l\'adresse du broker dans',
+    mqttSettingsPath: 'Paramètres → Réseau → Publication MQTT',
+    mqttNotConfiguredSuffix: '(vous n\'avez pas besoin d\'activer la publication, remplissez simplement les détails du broker).',
+    mqttMonitorOnlyDescription: 'Les prises MQTT reçoivent les données de puissance/énergie via un abonnement MQTT. Le contrôle on/off n\'est pas disponible - utilisez votre broker MQTT ou système domotique.',
+    powerMonitoring: 'Surveillance de puissance',
+    energyMonitoring: 'Surveillance énergétique',
+    stateMonitoring: 'Surveillance d\'état',
+    optional: 'optionnel',
+    topic: 'Topic',
+    jsonPath: 'Chemin JSON',
+    multiplier: 'Multiplicateur',
+    onValue: 'Valeur ON',
+    mqttPowerHint: 'Le chemin JSON extrait la valeur du payload JSON (ex: "power_l1"). Laisser vide si le topic publie des valeurs numériques brutes.\nUtiliser le multiplicateur 0.001 pour mW→W, 1000 pour kW→W.',
+    mqttEnergyHint: 'Le chemin JSON extrait la valeur du payload JSON. Laisser vide pour les valeurs brutes.\nUtiliser le multiplicateur 0.001 pour Wh→kWh, 1000 pour MWh→kWh.',
+    mqttStateHint: 'Le chemin JSON extrait la valeur du payload JSON. Laisser vide pour les valeurs brutes.\nValeur ON : la chaîne exacte signifiant "ON". Laisser vide pour la détection auto (ON, true, 1).',
+    noSwitchesInSwitchbar: 'Aucun commutateur dans la barre',
+    enableSwitchbarHint: 'Activez "Afficher dans la barre de commutateurs" dans Paramètres > Smart Plugs',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'E-mail',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'Notifications par e-mail SMTP',
+      telegram: 'Notifications via un bot Telegram',
+      discord: 'Envoyer vers un canal Discord via webhook',
+      ntfy: 'Notifications push gratuites, auto-hébergeables',
+      pushover: 'Notifications push simples et fiables',
+      callmebot: 'Notifications WhatsApp gratuites via CallMeBot',
+      webhook: 'POST HTTP générique vers n\'importe quelle URL',
+    },
+    // NotificationProviderCard
+    lastSuccess: 'Dernier : {{date}}',
+    error: 'Erreur',
+    printer: 'Imprimante :',
+    allPrinters: 'Toutes les imprimantes',
+    sendTestNotification: 'Envoyer une notification de test',
+    eventSettings: 'Paramètres des événements',
+    enabled: 'Activé',
+    sendFromProvider: 'Envoyer des notifications depuis ce fournisseur',
+    // Event categories
+    printEvents: 'Événements d\'impression',
+    printerStatus: 'État de l\'imprimante',
+    amsAlarms: 'Alarmes AMS',
+    amsHtAlarms: 'Alarmes AMS-HT',
+    printQueue: 'File d\'attente d\'impression',
+    // Event tags (badges)
+    start: 'Début',
+    plateCheck: 'Vérification du plateau',
+    complete: 'Terminé',
+    failed: 'Échoué',
+    stopped: 'Arrêté',
+    progress: 'Progression',
+    offline: 'Hors ligne',
+    lowFilament: 'Filament bas',
+    maintenance: 'Maintenance',
+    amsHumidity: 'Humidité AMS',
+    amsTemp: 'Temp. AMS',
+    amsHtHumidity: 'Humidité AMS-HT',
+    amsHtTemp: 'Temp. AMS-HT',
+    bedCooled: 'Plateau refroidi',
+    quiet: 'Silencieux',
+    digest: 'Résumé {{time}}',
+    // Event labels (expanded settings)
+    printStarted: 'Impression démarrée',
+    plateNotEmpty: 'Plateau non vide',
+    plateNotEmptyDescription: 'Objets détectés avant l\'impression',
+    printCompleted: 'Impression terminée',
+    bedCooledLabel: 'Plateau refroidi',
+    bedCooledDescription: 'Plateau refroidi sous le seuil après l\'impression',
+    printFailed: 'Impression échouée',
+    printStopped: 'Impression arrêtée',
+    progressMilestones: 'Jalons de progression',
+    progressMilestonesDescription: 'Notifier à 25 %, 50 %, 75 %',
+    printerOffline: 'Imprimante hors ligne',
+    printerError: 'Erreur de l\'imprimante',
+    lowFilamentLabel: 'Filament bas',
+    maintenanceDue: 'Maintenance requise',
+    maintenanceDueDescription: 'Notifier lorsqu\'une maintenance est nécessaire',
+    amsHumidityHigh: 'Humidité AMS élevée',
+    amsHumidityHighDescription: 'L\'humidité de l\'AMS standard dépasse le seuil',
+    amsTemperatureHigh: 'Température AMS élevée',
+    amsTemperatureHighDescription: 'La température de l\'AMS standard dépasse le seuil',
+    amsHtHumidityHigh: 'Humidité AMS-HT élevée',
+    amsHtHumidityHighDescription: 'L\'humidité de l\'AMS-HT dépasse le seuil',
+    amsHtTemperatureHigh: 'Température AMS-HT élevée',
+    amsHtTemperatureHighDescription: 'La température de l\'AMS-HT dépasse le seuil',
+    // Queue events
+    jobAdded: 'Tâche ajoutée',
+    jobAddedDescription: 'Tâche ajoutée à la file d\'attente',
+    jobAssigned: 'Tâche assignée',
+    jobAssignedDescription: 'Tâche basée sur le modèle assignée à l\'imprimante',
+    jobStarted: 'Tâche démarrée',
+    jobStartedDescription: 'La tâche de la file a commencé l\'impression',
+    jobWaiting: 'Tâche en attente',
+    jobWaitingDescription: 'Tâche en attente de filament',
+    jobSkipped: 'Tâche ignorée',
+    jobSkippedDescription: 'Tâche ignorée (échec précédent)',
+    jobFailed: 'Tâche échouée',
+    jobFailedDescription: 'La tâche n\'a pas pu démarrer',
+    queueComplete: 'File d\'attente terminée',
+    queueCompleteDescription: 'Toutes les tâches de la file sont terminées',
+    // Quiet hours
+    quietHours: 'Heures silencieuses',
+    noNotificationsDuring: 'Aucune notification pendant ces heures',
+    editProviderToChangeQuietHours: 'Modifier le fournisseur pour changer les heures silencieuses',
+    // Daily digest
+    dailyDigest: 'Résumé quotidien',
+    batchNotifications: 'Regrouper les notifications en un seul résumé quotidien',
+    sendAt: 'Envoyer à {{time}}',
+    editProviderToChangeDigestTime: 'Modifier le fournisseur pour changer l\'heure du résumé',
+    // Actions
+    edit: 'Modifier',
+    deleteProvider: 'Supprimer le fournisseur de notifications',
+    deleteConfirm: 'Êtes-vous sûr de vouloir supprimer « {{name}} » ? Cette action est irréversible.',
+    delete: 'Supprimer',
+    // AddNotificationModal
+    addTitle: 'Ajouter un fournisseur de notifications',
+    editTitle: 'Modifier le fournisseur de notifications',
+    nameLabel: 'Nom *',
+    namePlaceholder: 'Mes notifications',
+    providerTypeLabel: 'Type de fournisseur *',
+    configuration: 'Configuration',
+    testConfiguration: 'Tester la configuration',
+    printerFilter: 'Filtre d\'imprimante',
+    onlyFromPrinter: 'Envoyer uniquement les notifications pour les événements de cette imprimante',
+    quietHoursDnd: 'Heures silencieuses (Ne pas déranger)',
+    quietStart: 'Début',
+    quietEnd: 'Fin',
+    dailyDigestLabel: 'Résumé quotidien',
+    sendDigestAt: 'Envoyer le résumé à',
+    digestCollected: 'Les événements seront collectés et envoyés en un seul résumé à cette heure',
+    notificationEvents: 'Événements de notification',
+    progressPercent: '(25 %, 50 %, 75 %)',
+    bedCooledAfterPrint: '(après la fin de l\'impression)',
+    cancel: 'Annuler',
+    save: 'Enregistrer',
+    add: 'Ajouter',
+    nameRequired: 'Le nom est requis',
+    fieldRequired: '{{field}} est requis',
+    // Config field labels
+    phoneNumber: 'Numéro de téléphone',
+    apiKey: 'Clé API',
+    serverUrl: 'URL du serveur',
+    topic: 'Sujet',
+    authToken: 'Jeton d\'authentification',
+    userKey: 'Clé utilisateur',
+    appToken: 'Jeton d\'application',
+    priority: 'Priorité',
+    botToken: 'Jeton du bot',
+    chatId: 'ID du chat',
+    smtpServer: 'Serveur SMTP',
+    smtpPort: 'Port SMTP',
+    security: 'Sécurité',
+    authentication: 'Authentification',
+    username: 'Nom d\'utilisateur',
+    password: 'Mot de passe',
+    fromEmail: 'E-mail expéditeur',
+    toEmail: 'E-mail destinataire',
+    webhookUrl: 'URL du webhook',
+    payloadFormat: 'Format du payload',
+    authorization: 'Autorisation',
+    titleFieldName: 'Nom du champ titre',
+    messageFieldName: 'Nom du champ message',
+    // NotificationTemplateEditor
+    editTemplate: 'Modifier le modèle : {{name}}',
+    titleLabel: 'Titre',
+    bodyLabel: 'Corps',
+    titlePlaceholder: 'Titre de la notification...',
+    bodyPlaceholder: 'Corps de la notification...',
+    availableVariables: 'Variables disponibles',
+    clickToInsert: 'Cliquer pour insérer à la position du curseur dans le corps',
+    livePreview: 'Aperçu en direct',
+    hide: 'Masquer',
+    show: 'Afficher',
+    loadingPreview: 'Chargement de l\'aperçu...',
+    enterTemplateContent: 'Saisir le contenu du modèle pour voir l\'aperçu',
+    titlePreview: 'Titre :',
+    bodyPreview: 'Corps :',
+    resetToDefault: 'Réinitialiser par défaut',
+    titleRequired: 'Le titre est requis',
+    bodyRequired: 'Le corps est requis',
+    // NotificationLogViewer
+    notificationLog: 'Journal des notifications',
+    showFailedOnly: 'Échecs uniquement',
+    last24Hours: 'Dernières 24 heures',
+    last7Days: '7 derniers jours',
+    last30Days: '30 derniers jours',
+    last90Days: '90 derniers jours',
+    justNow: 'À l\'instant',
+    noFailedNotifications: 'Aucune notification échouée',
+    noNotificationsLogged: 'Aucune notification enregistrée',
+    unknownProvider: 'Fournisseur inconnu',
+    logTitle: 'Titre',
+    logMessage: 'Message',
+    logError: 'Erreur',
+    logProvider: 'Fournisseur : {{type}}',
+    logTime: 'Heure : {{time}}',
+    refresh: 'Actualiser',
+    clearOld: 'Purger les anciens',
+    statsSummary: '{{days}} derniers jours :',
+    statsNotifications: 'notifications',
+    statsSent: '{{count}} envoyées',
+    statsFailed: '{{count}} échouées',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: 'Impression démarrée',
+      print_complete: 'Impression terminée',
+      print_failed: 'Impression échouée',
+      print_stopped: 'Impression arrêtée',
+      print_progress: 'Progression',
+      printer_offline: 'Imprimante hors ligne',
+      printer_error: 'Erreur de l\'imprimante',
+      filament_low: 'Filament bas',
+      maintenance_due: 'Maintenance requise',
+      test: 'Test',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

+ 593 - 0
frontend/src/i18n/locales/it.ts

@@ -1654,6 +1654,149 @@ export default {
       imported: '{{added}} colori importati ({{skipped}} saltati)',
       imported: '{{added}} colori importati ({{skipped}} saltati)',
       importFailed: 'Impossibile importare: formato JSON non valido',
       importFailed: 'Impossibile importare: formato JSON non valido',
     },
     },
+    dateFormat: 'Formato data',
+    dateFormatUs: 'US (MM/GG/AAAA)',
+    dateFormatEu: 'EU (GG/MM/AAAA)',
+    dateFormatIso: 'ISO (AAAA-MM-GG)',
+    timeFormat: 'Formato ora',
+    timeFormat12: '12 ore (3:30 PM)',
+    timeFormat24: '24 ore (15:30)',
+    defaultPrinter: 'Stampante predefinita',
+    defaultPrinterDescription: 'Preseleziona questa stampante per upload, ristampe e altre operazioni.',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'Trascina gli elementi nella barra laterale per riordinare. Ripristina l\'ordine predefinito qui.',
+    reset: 'Ripristina',
+    darkMode: 'Modalità scura',
+    lightMode: 'Modalità chiara',
+    active: '(attivo)',
+    background: 'Sfondo',
+    accent: 'Accento',
+    style: 'Stile',
+    bgNeutral: 'Neutro',
+    bgWarm: 'Caldo',
+    bgCool: 'Freddo',
+    bgOled: 'OLED Nero',
+    bgSlate: 'Blu ardesia',
+    bgForest: 'Verde foresta',
+    accentGreen: 'Verde',
+    accentTeal: 'Verde acqua',
+    accentBlue: 'Blu',
+    accentOrange: 'Arancione',
+    accentPurple: 'Viola',
+    accentRed: 'Rosso',
+    styleClassic: 'Classico',
+    styleGlow: 'Luminoso',
+    styleVibrant: 'Vibrante',
+    themeToggleHint: 'Passa tra modalità scura e chiara con l\'icona sole/luna nella barra laterale.',
+    autoArchivePrints: 'Archiviazione automatica stampe',
+    autoArchiveDescription: 'Salva automaticamente i file 3MF al completamento delle stampe',
+    saveThumbnailsDescription: 'Estrai e salva le immagini di anteprima dai file 3MF',
+    captureFinishPhotoDescription: 'Scatta una foto dalla fotocamera della stampante al completamento della stampa',
+    ffmpegNotInstalled: 'ffmpeg non installato',
+    ffmpegRequired: 'L\'acquisizione dalla fotocamera richiede ffmpeg. Installalo tramite <brew>brew install ffmpeg</brew> (macOS) o <apt>apt install ffmpeg</apt> (Linux).',
+    camera: 'Fotocamera',
+    cameraViewMode: 'Modalità visualizzazione fotocamera',
+    cameraOverlayDescription: 'La fotocamera si apre in un overlay ridimensionabile sulla schermata principale',
+    cameraWindowDescription: 'La fotocamera si apre in una finestra separata del browser',
+    externalCamerasDescription: 'Configura fotocamere esterne per sostituire la fotocamera integrata della stampante. Supporta stream MJPEG, RTSP, snapshot HTTP e fotocamere USB (V4L2). Quando abilitata, la fotocamera esterna viene usata per la vista in diretta e le foto di completamento.',
+    cameraPlaceholderUsb: 'Percorso dispositivo (/dev/video0)',
+    cameraPlaceholderUrl: 'URL fotocamera (rtsp://... o http://...)',
+    cameraTypeMjpeg: 'Stream MJPEG',
+    cameraTypeRtsp: 'Stream RTSP',
+    cameraTypeSnapshot: 'Snapshot HTTP',
+    cameraTypeUsb: 'Fotocamera USB (V4L2)',
+    test: 'Test',
+    connected: 'Connesso',
+    disconnected: 'Disconnesso',
+    currency: 'Valuta',
+    defaultFilamentCost: 'Costo filamento predefinito (per kg)',
+    electricityCost: 'Costo elettricità per kWh',
+    energyDisplayMode: 'Modalità visualizzazione energia',
+    energyModePrintDescription: 'La dashboard mostra la somma dell\'energia usata durante le stampe',
+    energyModeTotalDescription: 'La dashboard mostra l\'energia totale dalle prese smart',
+    fileManager: 'Gestore file',
+    createArchiveEntry: 'Crea voce archivio durante la stampa',
+    createArchiveEntryDescription: 'Quando si stampa dal gestore file, crea opzionalmente una voce di archivio',
+    lowDiskSpaceWarning: 'Avviso spazio disco insufficiente',
+    lowDiskSpaceDescription: 'Mostra avviso quando lo spazio disco scende sotto questa soglia',
+    printerFirmware: 'Firmware stampante',
+    checkFirmwareDescription: 'Controlla aggiornamenti firmware da Bambu Lab',
+    bambuddySoftware: 'Software Bambuddy',
+    autoCheckDescription: 'Controlla automaticamente nuove versioni all\'avvio',
+    checkNow: 'Controlla ora',
+    updateAvailableVersion: 'Aggiornamento disponibile: v{{version}}',
+    releaseNotes: 'Note di rilascio',
+    updateViaDocker: 'Aggiorna tramite Docker Compose:',
+    installUpdate: 'Installa aggiornamento',
+    latestVersionRunning: 'Stai usando l\'ultima versione',
+    failedToCheckUpdates: 'Controllo aggiornamenti fallito: {{error}}',
+    backupRestore: 'Backup e ripristino',
+    backupRestoreDescription: 'Esporta/importa impostazioni e configura backup GitHub',
+    goToBackup: 'Vai al backup',
+    externalUrl: 'URL esterno',
+    externalUrlDescription: 'L\'URL esterno dove Bambuddy è accessibile. Usato per immagini di notifica e integrazioni esterne.',
+    bambuddyUrl: 'URL Bambuddy',
+    externalUrlHint: 'Includi protocollo e porta (es. http://192.168.1.100:8000)',
+    ftpRetry: 'Riprova FTP',
+    ftpRetryDescription: 'Riprova le operazioni FTP quando il WiFi della stampante è instabile. Si applica a download 3MF, upload stampe, download timelapse e aggiornamenti firmware.',
+    autoRetryDescription: 'Riprova automaticamente le operazioni FTP fallite',
+    retryAttempts: 'Tentativi di ripetizione',
+    retryDelay: 'Ritardo ripetizione',
+    connectionTimeout: 'Timeout connessione',
+    time_one: '{{count}} volta',
+    time_other: '{{count}} volte',
+    second_one: '{{count}} secondo',
+    second_other: '{{count}} secondi',
+    nSeconds: '{{count}} secondi',
+    increaseForWeakWifi: 'Aumenta per stampanti con WiFi debole',
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Connetti a Home Assistant per controllare le prese smart tramite l\'API REST HA. Supporta entità switch, light, input_boolean e script.',
+    homeAssistantUrl: 'URL Home Assistant',
+    longLivedAccessToken: 'Token di accesso a lunga durata',
+    haTokenHint: 'Crea un token in HA: Profilo → Token di accesso a lunga durata → Crea token',
+    connectionSuccessful: 'Connessione riuscita',
+    connectionFailed: 'Connessione fallita',
+    haConnectionSuccess: 'Connesso con successo a Home Assistant.',
+    haConnectionFailed: 'Connessione a Home Assistant fallita.',
+    mqttPublishing: 'Pubblicazione MQTT',
+    mqttDescription: 'Pubblica eventi BamBuddy su un broker MQTT esterno per l\'integrazione con Node-RED, Home Assistant e altri sistemi di automazione.',
+    mqttEnableDescription: 'Pubblica eventi su broker MQTT esterno',
+    brokerHostname: 'Hostname broker',
+    port: 'Porta',
+    usernameOptional: 'Nome utente (opzionale)',
+    passwordOptional: 'Password (opzionale)',
+    topicPrefix: 'Prefisso topic',
+    topicPrefixHint: 'I topic saranno: {{prefix}}/printers/<serial>/status, ecc.',
+    prometheusMetrics: 'Metriche Prometheus',
+    prometheusEndpointDescription: 'Esponi metriche stampante su <code>/api/v1/metrics</code> per monitoraggio Prometheus/Grafana.',
+    bearerTokenOptional: 'Token Bearer (opzionale)',
+    bearerTokenHint: 'Se impostato, le richieste devono includere <code>Authorization: Bearer <token></code>',
+    metricsConnectionStatus: 'Stato connessione',
+    metricsPrinterState: 'Stato stampante (idle/printing/ecc)',
+    metricsPrintProgress: 'Progresso stampa 0-100%',
+    metricsBedTemp: 'Temperatura piatto',
+    metricsNozzleTemp: 'Temperatura ugello',
+    metricsPrintsTotal: 'Stampe totali per risultato',
+    metricsMore: '...e altro (strati, ventole, coda, consumo filamento)',
+    smartPlugsDescription: 'Connetti prese smart (Tasmota o Home Assistant) per automatizzare il controllo dell\'alimentazione e monitorare il consumo energetico delle stampanti.',
+    allOn: 'Tutte accese',
+    allOff: 'Tutte spente',
+    addSmartPlug: 'Aggiungi presa smart',
+    energySummary: 'Riepilogo energia',
+    currentPower: 'Potenza attuale',
+    plugsOnline: '{{reachable}}/{{total}} prese online',
+    today: 'Oggi',
+    yesterday: 'Ieri',
+    total: 'Totale',
+    enablePlugsForSummary: 'Abilita le prese per vedere il riepilogo energia',
+    addNotificationProvider: 'Aggiungi',
+    systemBadge: '(Sistema)',
+    creating: 'Creazione...',
+    changing: 'Modifica...',
+    deleteUserAndItems: 'Elimina utente E i suoi elementi',
+    deleteUserKeepItems: 'Elimina utente, mantieni elementi (diventeranno senza proprietario)',
+    ok: 'OK',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2872,6 +3015,143 @@ export default {
     backupFailed: 'Backup fallito',
     backupFailed: 'Backup fallito',
     restoreFailed: 'Ripristino fallito',
     restoreFailed: 'Ripristino fallito',
     restoreNote: 'La stampante virtuale verrà fermata durante il ripristino',
     restoreNote: 'La stampante virtuale verrà fermata durante il ripristino',
+
+    // GitHub Backup
+    githubBackup: 'Backup GitHub',
+    enabled: 'Abilitato',
+    cloudLoginRequired: 'Accesso Bambu Cloud richiesto. Accedi in Profili → Profili Cloud per abilitare il backup GitHub.',
+    githubDescription: 'Sincronizza automaticamente i tuoi profili con un repository GitHub privato per backup e cronologia delle versioni.',
+    repositoryUrl: 'URL del repository',
+    personalAccessToken: 'Token di accesso personale',
+    tokenSaved: '(salvato)',
+    enterNewToken: 'Inserisci un nuovo token per aggiornare',
+    tokenHint: 'Token a grana fine con permesso di lettura/scrittura dei contenuti',
+    branch: 'Branch',
+    manualOnly: 'Solo manuale',
+    hourly: 'Ogni ora',
+    daily: 'Giornaliero',
+    weekly: 'Settimanale',
+    includeInBackup: 'Includi nel backup',
+    kProfiles: 'K-Profili',
+    kProfilesDescription: 'Calibrazione dell\'avanzamento pressione dalle stampanti connesse',
+    noPrintersConnected: 'Nessuna stampante connessa',
+    printersConnected: '{{connected}}/{{total}} connesse',
+    cloudProfiles: 'Profili Cloud',
+    cloudProfilesDescription: 'Preset di filamento, stampante e processo da Bambu Cloud',
+    appSettings: 'Impostazioni App',
+    appSettingsDescription: 'Configurazione Bambuddy (database completo)',
+    lastBackupAt: 'Ultimo backup:',
+    noBackupsYet: 'Nessun backup ancora',
+    next: 'Prossimo:',
+    startingBackup: 'Avvio del backup...',
+    test: 'Test',
+    enableBackup: 'Abilita backup',
+    testConnection: 'Testa connessione',
+    enterRepoUrl: 'Inserisci l\'URL del repository',
+    enterRepoAndToken: 'Inserisci l\'URL del repository e il token di accesso',
+    repoRequired: 'L\'URL del repository è obbligatorio',
+    tokenRequired: 'Il token di accesso è obbligatorio',
+    githubBackupEnabled: 'Backup GitHub abilitato',
+    tokenUpdated: 'Token aggiornato',
+    settingsSaved: 'Impostazioni salvate',
+    failedToSave: 'Salvataggio fallito: {{message}}',
+    backupCompleteFiles: 'Backup completato - {{count}} file aggiornati',
+    backupSkippedNoChanges: 'Backup saltato - nessuna modifica',
+    backupFailed2: 'Backup fallito: {{message}}',
+    clearedLogs: '{{count}} log eliminati',
+    failedToClearLogs: 'Eliminazione log fallita: {{message}}',
+
+    // History
+    history: 'Cronologia',
+    clear: 'Cancella',
+    date: 'Data',
+    status: 'Stato',
+    commit: 'Commit',
+
+    // Local Backup
+    localBackup: 'Backup locale',
+    localBackupDescription: 'Crea un backup completo dei tuoi dati Bambuddy includendo database, archivi, upload e tutti i file.',
+    downloadBackupLabel: 'Scarica backup',
+    completeBackupZip: 'Backup completo: database + tutti i file (ZIP)',
+    download: 'Scarica',
+    preparingBackup: 'Preparazione del backup...',
+    creatingArchive: 'Creazione dell\'archivio di backup... Potrebbe richiedere del tempo per archivi di grandi dimensioni.',
+    downloadingFile: 'Download del file di backup...',
+    backupDownloaded: 'Backup scaricato con successo',
+    failedToCreateBackup: 'Creazione del backup fallita: {{message}}',
+    restore: 'Ripristina',
+    restoreReplacesAll: 'Il ripristino sostituisce tutti i dati.',
+    restoreReplacesAllDetail: 'Il database e i file attuali verranno completamente sostituiti. È necessario un riavvio dopo il ripristino.',
+    restoreConfirmTitle: 'Ripristina backup',
+    restoreConfirmMessage: 'Sei sicuro di voler ripristinare da "{{filename}}"? Questo sostituirà completamente il tuo database e tutti i file. L\'applicazione dovrà essere riavviata dopo il ripristino.',
+    restoreConfirmButton: 'Ripristina backup',
+    uploadingFile: 'Caricamento del file di backup...',
+    backupRestoredRestart: 'Backup ripristinato. Riavvia Bambuddy.',
+    failedToRestore: 'Ripristino del backup fallito. Controlla il formato del file.',
+    reloadNow: 'Ricarica ora',
+    creatingBackup: 'Creazione del backup',
+    restoringBackup: 'Ripristino del backup',
+    preparing: 'Preparazione...',
+    processing: 'Elaborazione...',
+    doNotClosePage: 'Non chiudere questa pagina e non navigare altrove. Questa operazione potrebbe richiedere diversi minuti per backup di grandi dimensioni.',
+
+    // RestoreModal
+    restoring: 'Ripristino in corso...',
+    restoreComplete: 'Ripristino completato',
+    restoreFailed2: 'Ripristino fallito',
+    importSettings: 'Importa impostazioni da un file di backup',
+    pleaseWaitRestoring: 'Attendere durante il ripristino dei dati',
+    selectBackupFile: 'Clicca per selezionare un file di backup (.json o .zip)',
+    duplicateHandling: 'Come funziona la gestione dei duplicati:',
+    matchPrinters: 'Stampanti',
+    matchPrintersBy: 'corrispondenza per numero di serie',
+    matchSmartPlugs: 'Smart Plug',
+    matchSmartPlugsBy: 'corrispondenza per indirizzo IP',
+    matchNotificationProviders: 'Provider di notifica',
+    matchNotificationProvidersBy: 'corrispondenza per nome',
+    matchFilaments: 'Filamenti',
+    matchFilamentsBy: 'corrispondenza per nome + tipo + marca',
+    matchArchives: 'Archivi',
+    matchArchivesBy: 'corrispondenza per hash del contenuto (sempre saltato)',
+    matchPendingUploads: 'Upload in sospeso',
+    matchPendingUploadsBy: 'corrispondenza per nome file',
+    matchSettingsTemplates: 'Impostazioni e modelli',
+    matchSettingsTemplatesBy: 'sempre sovrascritti',
+    replaceExisting: 'Sostituisci dati esistenti',
+    keepExisting: 'Mantieni dati esistenti',
+    overwriteDescription: 'Sovrascrivi gli elementi già esistenti con i dati del backup',
+    keepDescription: 'Ripristina solo gli elementi che non esistono ancora',
+    overwriteCaution: 'Attenzione:',
+    overwriteWarning: 'La sovrascrittura sostituirà le configurazioni attuali con i dati del backup. I codici di accesso delle stampanti non vengono mai sovrascritti per sicurezza.',
+    cancel: 'Annulla',
+    processingBackup: 'Elaborazione del file di backup...',
+    itemsRestored: 'Elementi ripristinati',
+    itemsSkipped: 'Elementi saltati',
+    restored: 'Ripristinati',
+    skippedAlreadyExist: 'Saltati (già esistenti)',
+    filesCategory: 'File (3MF, miniature, ecc.)',
+    andMore: '...e altri {{count}}',
+    newApiKeysGenerated: 'Nuove chiavi API generate',
+    keysShownOnce: 'Queste chiavi vengono mostrate solo una volta. Copiale ora!',
+    copy: 'Copia',
+    noDataFound: 'Nessun dato da ripristinare trovato nel file di backup.',
+    close: 'Chiudi',
+
+    // Category labels
+    categories: {
+      settings: 'Impostazioni',
+      notification_providers: 'Provider di notifica',
+      notification_templates: 'Modelli di notifica',
+      smart_plugs: 'Smart Plug',
+      printers: 'Stampanti',
+      filaments: 'Filamenti',
+      maintenance_types: 'Tipi di manutenzione',
+      archives: 'Archivi',
+      projects: 'Progetti',
+      pending_uploads: 'Upload in sospeso',
+      external_links: 'Link esterni',
+      api_keys: 'Chiavi API',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3250,6 +3530,319 @@ export default {
         mqttSameAsPower: 'Stesso del topic potenza, o diverso',
         mqttSameAsPower: 'Stesso del topic potenza, o diverso',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: 'Collegato a:',
+    monitorOnly: 'Solo monitoraggio',
+    alerts: 'Avvisi',
+    scheduleOn: 'On {{time}}',
+    scheduleOff: 'Off {{time}}',
+    on: 'On',
+    off: 'Off',
+    power: 'Potenza',
+    kwhToday: 'kWh Oggi',
+    settings: 'Impostazioni',
+    automationSettings: 'Impostazioni automazione',
+    showInSwitchbar: 'Mostra nella barra interruttori',
+    quickAccessSidebar: 'Accesso rapido dalla barra laterale',
+    enabled: 'Abilitato',
+    enableAutomation: 'Abilita automazione per questa presa',
+    autoOn: 'Auto On',
+    autoOnDescription: 'Accendi quando inizia la stampa',
+    autoOff: 'Auto Off',
+    autoOffDescription: 'Spegni quando la stampa è completata (una tantum)',
+    turnOffDelayMode: 'Modalità ritardo spegnimento',
+    time: 'Tempo',
+    temp: 'Temp',
+    delayMinutes: 'Ritardo (minuti)',
+    tempThreshold: 'Soglia temperatura (°C)',
+    tempThresholdDescription: 'Si spegne quando l\'ugello si raffredda sotto questa temperatura',
+    edit: 'Modifica',
+    deleteConfirm: 'Sei sicuro di voler eliminare "{{name}}"? Questa azione non può essere annullata.',
+    turnOnConfirm: 'Sei sicuro di voler accendere "{{name}}"?',
+    turnOffConfirm: 'Sei sicuro di voler spegnere "{{name}}"? Questo interromperà l\'alimentazione del dispositivo collegato.',
+    failedToTurn: 'Impossibile {{action}} "{{name}}"',
+    unknown: 'Sconosciuto',
+    // AddSmartPlugModal
+    addTitle: 'Aggiungi presa smart',
+    editTitle: 'Modifica presa smart',
+    stopScanning: 'Interrompi scansione',
+    discoverTasmota: 'Scopri dispositivi Tasmota',
+    foundDevices: '{{count}} dispositivo/i trovato/i - clicca per selezionare:',
+    noDevicesFound: 'Nessun dispositivo Tasmota trovato nella rete',
+    haNotConfigured: 'Home Assistant non è configurato. Configuralo in',
+    haSettingsPath: 'Impostazioni → Rete → Home Assistant',
+    selectEntity: 'Seleziona entità *',
+    ipAddress: 'Indirizzo IP *',
+    nameLabel: 'Nome *',
+    username: 'Nome utente',
+    password: 'Password',
+    authHint: 'Lascia vuoto se il tuo dispositivo Tasmota non richiede autenticazione',
+    linkToPrinter: 'Collega alla stampante',
+    noPrinter: 'Nessuna stampante (solo controllo manuale)',
+    linkingDescription: 'Il collegamento abilita accensione/spegnimento automatico all\'inizio/fine stampa',
+    powerAlerts: 'Avvisi potenza',
+    alertAbove: 'Avviso se sopra (W)',
+    alertBelow: 'Avviso se sotto (W)',
+    alertDescription: 'Ricevi notifiche quando il consumo supera queste soglie. Lascia vuoto per disabilitare quella direzione.',
+    dailySchedule: 'Programma giornaliero',
+    turnOnAt: 'Accendi alle',
+    turnOffAt: 'Spegni alle',
+    scheduleDescription: 'Accendi/spegni automaticamente la presa a questi orari ogni giorno. Lascia vuoto per saltare quell\'azione.',
+    showOnPrinterCard: 'Mostra sulla scheda stampante',
+    displayOnPrinterCard: 'Mostra pulsante sulla scheda stampante',
+    connectedResult: 'Connesso!',
+    deviceLabel: 'Dispositivo: {{name}} - ',
+    stateLabel: 'Stato: {{state}}',
+    test: 'Test',
+    delete: 'Elimina',
+    save: 'Salva',
+    add: 'Aggiungi',
+    cancel: 'Annulla',
+    failedToStartScan: 'Impossibile avviare la scansione',
+    nameRequired: 'Il nome è obbligatorio',
+    entityRequired: 'L\'entità è obbligatoria per le prese Home Assistant',
+    mqttTopicRequired: 'Almeno un topic MQTT deve essere configurato per potenza, energia o monitoraggio stato',
+    loadingEntities: 'Caricamento entità...',
+    loading: 'Caricamento...',
+    failedToLoadEntities: 'Impossibile caricare le entità: {{error}}',
+    noEntitiesMatching: 'Nessuna entità trovata corrispondente a "{{search}}"',
+    noEntitiesAvailable: 'Nessuna entità disponibile',
+    searchingEntities: 'Ricerca in tutte le entità ({{count}} trovate)',
+    showingEntities: 'Mostrando switch, light, input_boolean ({{count}} disponibili)',
+    energyMonitoringOptional: 'Monitoraggio energia (Opzionale)',
+    energyMonitoringHint: 'Cerca e seleziona i sensori che forniscono dati di potenza/energia.',
+    powerSensorW: 'Sensore potenza (W)',
+    energyTodayKwh: 'Energia oggi (kWh)',
+    totalEnergyKwh: 'Energia totale (kWh)',
+    noMatchingSensors: 'Nessun sensore corrispondente',
+    none: 'Nessuno',
+    mqttNotConfigured: 'Broker MQTT non configurato. Imposta l\'indirizzo del broker in',
+    mqttSettingsPath: 'Impostazioni → Rete → Pubblicazione MQTT',
+    mqttNotConfiguredSuffix: '(non è necessario abilitare la pubblicazione, basta inserire i dettagli del broker).',
+    mqttMonitorOnlyDescription: 'Le prese MQTT ricevono dati di potenza/energia tramite sottoscrizione MQTT. Il controllo on/off non è disponibile - usa il tuo broker MQTT o sistema domotico.',
+    powerMonitoring: 'Monitoraggio potenza',
+    energyMonitoring: 'Monitoraggio energia',
+    stateMonitoring: 'Monitoraggio stato',
+    optional: 'opzionale',
+    topic: 'Topic',
+    jsonPath: 'Percorso JSON',
+    multiplier: 'Moltiplicatore',
+    onValue: 'Valore ON',
+    mqttPowerHint: 'Il percorso JSON estrae il valore dal payload JSON (es. "power_l1"). Lascia vuoto se il topic pubblica valori numerici grezzi.\nUsa moltiplicatore 0.001 per mW→W, 1000 per kW→W.',
+    mqttEnergyHint: 'Il percorso JSON estrae il valore dal payload JSON. Lascia vuoto per valori grezzi.\nUsa moltiplicatore 0.001 per Wh→kWh, 1000 per MWh→kWh.',
+    mqttStateHint: 'Il percorso JSON estrae il valore dal payload JSON. Lascia vuoto per valori grezzi.\nValore ON: la stringa esatta che significa "ON". Lascia vuoto per rilevamento auto (ON, true, 1).',
+    noSwitchesInSwitchbar: 'Nessun interruttore nella barra',
+    enableSwitchbarHint: 'Abilita "Mostra nella barra interruttori" in Impostazioni > Smart Plugs',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'Email',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'Notifiche email tramite SMTP',
+      telegram: 'Notifiche tramite bot Telegram',
+      discord: 'Invia a un canale Discord tramite webhook',
+      ntfy: 'Notifiche push gratuite e self-hostabili',
+      pushover: 'Notifiche push semplici e affidabili',
+      callmebot: 'Notifiche WhatsApp gratuite tramite CallMeBot',
+      webhook: 'POST HTTP generico verso qualsiasi URL',
+    },
+    // NotificationProviderCard
+    lastSuccess: 'Ultimo: {{date}}',
+    error: 'Errore',
+    printer: 'Stampante:',
+    allPrinters: 'Tutte le stampanti',
+    sendTestNotification: 'Invia notifica di prova',
+    eventSettings: 'Impostazioni eventi',
+    enabled: 'Abilitato',
+    sendFromProvider: 'Invia notifiche da questo provider',
+    // Event categories
+    printEvents: 'Eventi di stampa',
+    printerStatus: 'Stato stampante',
+    amsAlarms: 'Allarmi AMS',
+    amsHtAlarms: 'Allarmi AMS-HT',
+    printQueue: 'Coda di stampa',
+    // Event tags (badges)
+    start: 'Avvio',
+    plateCheck: 'Controllo piatto',
+    complete: 'Completato',
+    failed: 'Fallito',
+    stopped: 'Interrotto',
+    progress: 'Avanzamento',
+    offline: 'Offline',
+    lowFilament: 'Filamento scarso',
+    maintenance: 'Manutenzione',
+    amsHumidity: 'Umidità AMS',
+    amsTemp: 'Temp AMS',
+    amsHtHumidity: 'Umidità AMS-HT',
+    amsHtTemp: 'Temp AMS-HT',
+    bedCooled: 'Piatto raffreddato',
+    quiet: 'Silenzioso',
+    digest: 'Riepilogo {{time}}',
+    // Event labels (expanded settings)
+    printStarted: 'Stampa avviata',
+    plateNotEmpty: 'Piatto non vuoto',
+    plateNotEmptyDescription: 'Oggetti rilevati prima della stampa',
+    printCompleted: 'Stampa completata',
+    bedCooledLabel: 'Piatto raffreddato',
+    bedCooledDescription: 'Piatto raffreddato sotto la soglia dopo la stampa',
+    printFailed: 'Stampa fallita',
+    printStopped: 'Stampa interrotta',
+    progressMilestones: 'Traguardi di avanzamento',
+    progressMilestonesDescription: 'Notifica al 25%, 50%, 75%',
+    printerOffline: 'Stampante offline',
+    printerError: 'Errore stampante',
+    lowFilamentLabel: 'Filamento scarso',
+    maintenanceDue: 'Manutenzione necessaria',
+    maintenanceDueDescription: 'Notifica quando è necessaria la manutenzione',
+    amsHumidityHigh: 'Umidità AMS elevata',
+    amsHumidityHighDescription: 'L\'umidità dell\'AMS standard supera la soglia',
+    amsTemperatureHigh: 'Temperatura AMS elevata',
+    amsTemperatureHighDescription: 'La temperatura dell\'AMS standard supera la soglia',
+    amsHtHumidityHigh: 'Umidità AMS-HT elevata',
+    amsHtHumidityHighDescription: 'L\'umidità dell\'AMS-HT supera la soglia',
+    amsHtTemperatureHigh: 'Temperatura AMS-HT elevata',
+    amsHtTemperatureHighDescription: 'La temperatura dell\'AMS-HT supera la soglia',
+    // Queue events
+    jobAdded: 'Lavoro aggiunto',
+    jobAddedDescription: 'Lavoro aggiunto alla coda',
+    jobAssigned: 'Lavoro assegnato',
+    jobAssignedDescription: 'Lavoro basato su modello assegnato alla stampante',
+    jobStarted: 'Lavoro avviato',
+    jobStartedDescription: 'Lavoro in coda avviato per la stampa',
+    jobWaiting: 'Lavoro in attesa',
+    jobWaitingDescription: 'Lavoro in attesa di filamento',
+    jobSkipped: 'Lavoro saltato',
+    jobSkippedDescription: 'Lavoro saltato (precedente fallito)',
+    jobFailed: 'Lavoro fallito',
+    jobFailedDescription: 'Avvio del lavoro fallito',
+    queueComplete: 'Coda completata',
+    queueCompleteDescription: 'Tutti i lavori in coda completati',
+    // Quiet hours
+    quietHours: 'Ore silenziose',
+    noNotificationsDuring: 'Nessuna notifica durante queste ore',
+    editProviderToChangeQuietHours: 'Modifica il provider per cambiare le ore silenziose',
+    // Daily digest
+    dailyDigest: 'Riepilogo giornaliero',
+    batchNotifications: 'Raggruppa le notifiche in un unico riepilogo giornaliero',
+    sendAt: 'Invia alle {{time}}',
+    editProviderToChangeDigestTime: 'Modifica il provider per cambiare l\'orario del riepilogo',
+    // Actions
+    edit: 'Modifica',
+    deleteProvider: 'Elimina provider di notifica',
+    deleteConfirm: 'Sei sicuro di voler eliminare "{{name}}"? Questa azione non può essere annullata.',
+    delete: 'Elimina',
+    // AddNotificationModal
+    addTitle: 'Aggiungi provider di notifica',
+    editTitle: 'Modifica provider di notifica',
+    nameLabel: 'Nome *',
+    namePlaceholder: 'Le mie notifiche',
+    providerTypeLabel: 'Tipo di provider *',
+    configuration: 'Configurazione',
+    testConfiguration: 'Testa configurazione',
+    printerFilter: 'Filtro stampante',
+    onlyFromPrinter: 'Invia notifiche solo per eventi da questa stampante',
+    quietHoursDnd: 'Ore silenziose (Non disturbare)',
+    quietStart: 'Inizio',
+    quietEnd: 'Fine',
+    dailyDigestLabel: 'Riepilogo giornaliero',
+    sendDigestAt: 'Invia riepilogo alle',
+    digestCollected: 'Gli eventi verranno raccolti e inviati come riepilogo unico a quest\'ora',
+    notificationEvents: 'Eventi di notifica',
+    progressPercent: '(25%, 50%, 75%)',
+    bedCooledAfterPrint: '(dopo il completamento della stampa)',
+    cancel: 'Annulla',
+    save: 'Salva',
+    add: 'Aggiungi',
+    nameRequired: 'Il nome è obbligatorio',
+    fieldRequired: '{{field}} è obbligatorio',
+    // Config field labels
+    phoneNumber: 'Numero di telefono',
+    apiKey: 'Chiave API',
+    serverUrl: 'URL del server',
+    topic: 'Argomento',
+    authToken: 'Token di autenticazione',
+    userKey: 'Chiave utente',
+    appToken: 'Token applicazione',
+    priority: 'Priorità',
+    botToken: 'Token del bot',
+    chatId: 'ID chat',
+    smtpServer: 'Server SMTP',
+    smtpPort: 'Porta SMTP',
+    security: 'Sicurezza',
+    authentication: 'Autenticazione',
+    username: 'Nome utente',
+    password: 'Password',
+    fromEmail: 'Email mittente',
+    toEmail: 'Email destinatario',
+    webhookUrl: 'URL webhook',
+    payloadFormat: 'Formato payload',
+    authorization: 'Autorizzazione',
+    titleFieldName: 'Nome campo titolo',
+    messageFieldName: 'Nome campo messaggio',
+    // NotificationTemplateEditor
+    editTemplate: 'Modifica modello: {{name}}',
+    titleLabel: 'Titolo',
+    bodyLabel: 'Corpo',
+    titlePlaceholder: 'Titolo della notifica...',
+    bodyPlaceholder: 'Corpo della notifica...',
+    availableVariables: 'Variabili disponibili',
+    clickToInsert: 'Clicca per inserire alla posizione del cursore nel corpo',
+    livePreview: 'Anteprima live',
+    hide: 'Nascondi',
+    show: 'Mostra',
+    loadingPreview: 'Caricamento anteprima...',
+    enterTemplateContent: 'Inserisci il contenuto del modello per vedere l\'anteprima',
+    titlePreview: 'Titolo:',
+    bodyPreview: 'Corpo:',
+    resetToDefault: 'Ripristina predefinito',
+    titleRequired: 'Il titolo è obbligatorio',
+    bodyRequired: 'Il corpo è obbligatorio',
+    // NotificationLogViewer
+    notificationLog: 'Registro notifiche',
+    showFailedOnly: 'Solo fallite',
+    last24Hours: 'Ultime 24 ore',
+    last7Days: 'Ultimi 7 giorni',
+    last30Days: 'Ultimi 30 giorni',
+    last90Days: 'Ultimi 90 giorni',
+    justNow: 'Proprio ora',
+    noFailedNotifications: 'Nessuna notifica fallita',
+    noNotificationsLogged: 'Nessuna notifica registrata',
+    unknownProvider: 'Provider sconosciuto',
+    logTitle: 'Titolo',
+    logMessage: 'Messaggio',
+    logError: 'Errore',
+    logProvider: 'Provider: {{type}}',
+    logTime: 'Ora: {{time}}',
+    refresh: 'Aggiorna',
+    clearOld: 'Cancella vecchie',
+    statsSummary: 'Ultimi {{days}} giorni:',
+    statsNotifications: 'notifiche',
+    statsSent: '{{count}} inviate',
+    statsFailed: '{{count}} fallite',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: 'Stampa avviata',
+      print_complete: 'Stampa completata',
+      print_failed: 'Stampa fallita',
+      print_stopped: 'Stampa interrotta',
+      print_progress: 'Avanzamento',
+      printer_offline: 'Stampante offline',
+      printer_error: 'Errore stampante',
+      filament_low: 'Filamento scarso',
+      maintenance_due: 'Manutenzione necessaria',
+      test: 'Prova',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

+ 607 - 0
frontend/src/i18n/locales/ja.ts

@@ -1654,6 +1654,163 @@ export default {
       imported: '{{added}}件のカラーをインポートしました({{skipped}}件スキップ)',
       imported: '{{added}}件のカラーをインポートしました({{skipped}}件スキップ)',
       importFailed: 'インポートに失敗しました:無効なJSON形式',
       importFailed: 'インポートに失敗しました:無効なJSON形式',
     },
     },
+    // General tab
+    dateFormat: '日付形式',
+    dateFormatUs: 'US (MM/DD/YYYY)',
+    dateFormatEu: 'EU (DD/MM/YYYY)',
+    dateFormatIso: 'ISO (YYYY-MM-DD)',
+    timeFormat: '時刻形式',
+    timeFormat12: '12時間制 (3:30 PM)',
+    timeFormat24: '24時間制 (15:30)',
+    defaultPrinter: 'デフォルトプリンター',
+    defaultPrinterDescription: 'アップロード、再印刷、その他の操作でこのプリンターを事前選択します。',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'サイドバーの項目をドラッグして並べ替え。ここでデフォルトの順序にリセット。',
+    reset: 'リセット',
+    // Appearance
+    darkMode: 'ダークモード',
+    lightMode: 'ライトモード',
+    active: '(アクティブ)',
+    background: '背景',
+    accent: 'アクセント',
+    style: 'スタイル',
+    bgNeutral: 'ニュートラル',
+    bgWarm: 'ウォーム',
+    bgCool: 'クール',
+    bgOled: 'OLEDブラック',
+    bgSlate: 'スレートブルー',
+    bgForest: 'フォレストグリーン',
+    accentGreen: 'グリーン',
+    accentTeal: 'ティール',
+    accentBlue: 'ブルー',
+    accentOrange: 'オレンジ',
+    accentPurple: 'パープル',
+    accentRed: 'レッド',
+    styleClassic: 'クラシック',
+    styleGlow: 'グロー',
+    styleVibrant: 'ビビッド',
+    themeToggleHint: 'サイドバーの太陽/月アイコンでダークモードとライトモードを切り替えます。',
+    // Archive
+    autoArchivePrints: '印刷を自動アーカイブ',
+    autoArchiveDescription: '印刷完了時に3MFファイルを自動保存',
+    saveThumbnailsDescription: '3MFファイルからプレビュー画像を抽出して保存',
+    captureFinishPhotoDescription: '印刷完了時にプリンターカメラから写真を撮影',
+    ffmpegNotInstalled: 'ffmpegがインストールされていません',
+    ffmpegRequired: 'カメラ撮影にはffmpegが必要です。<brew>brew install ffmpeg</brew>(macOS)または<apt>apt install ffmpeg</apt>(Linux)でインストールしてください。',
+    // Camera
+    camera: 'カメラ',
+    cameraViewMode: 'カメラ表示モード',
+    cameraOverlayDescription: 'メイン画面上にリサイズ可能なオーバーレイでカメラを表示',
+    cameraWindowDescription: '別のブラウザウィンドウでカメラを表示',
+    externalCamerasDescription: '内蔵プリンターカメラの代わりに外部カメラを設定。MJPEGストリーム、RTSP、HTTPスナップショット、USBカメラ(V4L2)をサポート。有効にすると、ライブビューと完了写真に外部カメラが使用されます。',
+    cameraPlaceholderUsb: 'デバイスパス (/dev/video0)',
+    cameraPlaceholderUrl: 'カメラURL (rtsp://... または http://...)',
+    cameraTypeMjpeg: 'MJPEGストリーム',
+    cameraTypeRtsp: 'RTSPストリーム',
+    cameraTypeSnapshot: 'HTTPスナップショット',
+    cameraTypeUsb: 'USBカメラ (V4L2)',
+    test: 'テスト',
+    connected: '接続済み',
+    disconnected: '未接続',
+    // Cost tracking
+    currency: '通貨',
+    defaultFilamentCost: 'デフォルトフィラメントコスト(kg単価)',
+    electricityCost: '電気料金(kWh単価)',
+    energyDisplayMode: 'エネルギー表示モード',
+    energyModePrintDescription: 'ダッシュボードに印刷中の消費エネルギーの合計を表示',
+    energyModeTotalDescription: 'ダッシュボードにスマートプラグの累計エネルギーを表示',
+    // File Manager
+    fileManager: 'ファイルマネージャー',
+    createArchiveEntry: '印刷時にアーカイブエントリを作成',
+    createArchiveEntryDescription: 'ファイルマネージャーから印刷時に、オプションでアーカイブエントリを作成',
+    lowDiskSpaceWarning: 'ディスク容量不足の警告',
+    lowDiskSpaceDescription: '空きディスク容量がこのしきい値を下回った場合に警告を表示',
+    // Updates
+    printerFirmware: 'プリンターファームウェア',
+    checkFirmwareDescription: 'Bambu Labのプリンターファームウェア更新を確認',
+    bambuddySoftware: 'Bambuddyソフトウェア',
+    autoCheckDescription: '起動時に自動的に新しいバージョンを確認',
+    checkNow: '今すぐ確認',
+    updateAvailableVersion: 'アップデート利用可能: v{{version}}',
+    releaseNotes: 'リリースノート',
+    updateViaDocker: 'Docker Composeでアップデート:',
+    installUpdate: 'アップデートをインストール',
+    latestVersionRunning: '最新バージョンを使用しています',
+    failedToCheckUpdates: 'アップデートの確認に失敗しました: {{error}}',
+    // Data Management
+    backupRestore: 'バックアップと復元',
+    backupRestoreDescription: '設定のエクスポート/インポートとGitHubバックアップの設定',
+    goToBackup: 'バックアップへ',
+    // Network tab
+    externalUrl: '外部URL',
+    externalUrlDescription: 'Bambuddyがアクセス可能な外部URL。通知画像や外部連携に使用されます。',
+    bambuddyUrl: 'Bambuddy URL',
+    externalUrlHint: 'プロトコルとポートを含めてください(例: http://192.168.1.100:8000)',
+    ftpRetry: 'FTPリトライ',
+    ftpRetryDescription: 'プリンターのWi-Fiが不安定な場合にFTP操作をリトライ。3MFダウンロード、印刷アップロード、タイムラプスダウンロード、ファームウェア更新に適用。',
+    autoRetryDescription: '失敗したFTP操作を自動的にリトライ',
+    retryAttempts: 'リトライ回数',
+    retryDelay: 'リトライ遅延',
+    connectionTimeout: '接続タイムアウト',
+    time_one: '{{count}}回',
+    time_other: '{{count}}回',
+    second_one: '{{count}}秒',
+    second_other: '{{count}}秒',
+    nSeconds: '{{count}}秒',
+    increaseForWeakWifi: 'Wi-Fiが弱いプリンター用に増やしてください',
+    // Home Assistant
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Home Assistantに接続してHA REST API経由でスマートプラグを制御。switch、light、input_boolean、scriptエンティティをサポート。',
+    homeAssistantUrl: 'Home Assistant URL',
+    longLivedAccessToken: '長期アクセストークン',
+    haTokenHint: 'HAでトークンを作成: プロフィール → 長期アクセストークン → トークンを作成',
+    connectionSuccessful: '接続成功',
+    connectionFailed: '接続失敗',
+    haConnectionSuccess: 'Home Assistantへの接続に成功しました。',
+    haConnectionFailed: 'Home Assistantへの接続に失敗しました。',
+    // MQTT
+    mqttPublishing: 'MQTTパブリッシュ',
+    mqttDescription: 'Node-RED、Home Assistant、その他の自動化システムとの統合のため、外部MQTTブローカーにBamBuddyイベントをパブリッシュ。',
+    mqttEnableDescription: '外部MQTTブローカーにイベントをパブリッシュ',
+    brokerHostname: 'ブローカーホスト名',
+    port: 'ポート',
+    usernameOptional: 'ユーザー名(オプション)',
+    passwordOptional: 'パスワード(オプション)',
+    topicPrefix: 'トピックプレフィックス',
+    topicPrefixHint: 'トピック形式: {{prefix}}/printers/<serial>/status 等',
+    // Prometheus
+    prometheusMetrics: 'Prometheusメトリクス',
+    prometheusEndpointDescription: 'Prometheus/Grafanaモニタリング用に<code>/api/v1/metrics</code>でプリンターメトリクスを公開。',
+    bearerTokenOptional: 'Bearerトークン(オプション)',
+    bearerTokenHint: '設定時、リクエストに<code>Authorization: Bearer <token></code>が必要',
+    metricsConnectionStatus: '接続状態',
+    metricsPrinterState: 'プリンター状態(idle/printing等)',
+    metricsPrintProgress: '印刷進捗 0-100%',
+    metricsBedTemp: 'ベッド温度',
+    metricsNozzleTemp: 'ノズル温度',
+    metricsPrintsTotal: '結果別の総印刷数',
+    metricsMore: '...その他(レイヤー、ファン、キュー、フィラメント使用量)',
+    // Smart Plugs
+    smartPlugsDescription: 'スマートプラグ(TasmotaまたはHome Assistant)を接続して、電源制御の自動化とプリンターのエネルギー使用量を追跡。',
+    allOn: 'すべてオン',
+    allOff: 'すべてオフ',
+    addSmartPlug: 'スマートプラグを追加',
+    energySummary: 'エネルギー概要',
+    currentPower: '現在の消費電力',
+    plugsOnline: '{{reachable}}/{{total}}プラグオンライン',
+    today: '今日',
+    yesterday: '昨日',
+    total: '合計',
+    enablePlugsForSummary: 'プラグを有効にしてエネルギー概要を表示',
+    addNotificationProvider: '追加',
+    // Users
+    systemBadge: '(システム)',
+    creating: '作成中...',
+    changing: '変更中...',
+    deleteUserAndItems: 'ユーザーとそのアイテムを削除',
+    deleteUserKeepItems: 'ユーザーを削除、アイテムは保持(オーナーなしになります)',
+    ok: 'OK',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2872,6 +3029,143 @@ export default {
     backupFailed: 'バックアップに失敗しました: {{error}}',
     backupFailed: 'バックアップに失敗しました: {{error}}',
     restoreFailed: '復元に失敗しました',
     restoreFailed: '復元に失敗しました',
     restoreNote: '復元中、仮想プリンターは停止されます',
     restoreNote: '復元中、仮想プリンターは停止されます',
+
+    // GitHub Backup
+    githubBackup: 'GitHubバックアップ',
+    enabled: '有効',
+    cloudLoginRequired: 'Bambu Cloudログインが必要です。GitHubバックアップを有効にするには、プロファイル → クラウドプロファイルからサインインしてください。',
+    githubDescription: 'プロファイルをプライベートGitHubリポジトリに自動的に同期し、バックアップとバージョン履歴を保持します。',
+    repositoryUrl: 'リポジトリURL',
+    personalAccessToken: '個人アクセストークン',
+    tokenSaved: '(保存済み)',
+    enterNewToken: '新しいトークンを入力して更新',
+    tokenHint: 'Contents読み書き権限を持つきめ細かいトークン',
+    branch: 'ブランチ',
+    manualOnly: '手動のみ',
+    hourly: '毎時',
+    daily: '毎日',
+    weekly: '毎週',
+    includeInBackup: 'バックアップに含める',
+    kProfiles: 'Kプロファイル',
+    kProfilesDescription: '接続されたプリンターからの圧力キャリブレーション',
+    noPrintersConnected: 'プリンターが接続されていません',
+    printersConnected: '{{connected}}/{{total}} 接続済み',
+    cloudProfiles: 'クラウドプロファイル',
+    cloudProfilesDescription: 'Bambu Cloudからのフィラメント、プリンター、プロセスプリセット',
+    appSettings: 'アプリ設定',
+    appSettingsDescription: 'Bambuddy設定(データベース全体)',
+    lastBackupAt: '最終バックアップ:',
+    noBackupsYet: 'バックアップはまだありません',
+    next: '次回:',
+    startingBackup: 'バックアップを開始しています...',
+    test: 'テスト',
+    enableBackup: 'バックアップを有効化',
+    testConnection: '接続テスト',
+    enterRepoUrl: 'リポジトリURLを入力してください',
+    enterRepoAndToken: 'リポジトリURLとアクセストークンを入力してください',
+    repoRequired: 'リポジトリURLは必須です',
+    tokenRequired: 'アクセストークンは必須です',
+    githubBackupEnabled: 'GitHubバックアップが有効になりました',
+    tokenUpdated: 'トークンが更新されました',
+    settingsSaved: '設定が保存されました',
+    failedToSave: '保存に失敗しました: {{message}}',
+    backupCompleteFiles: 'バックアップ完了 - {{count}}ファイルが更新されました',
+    backupSkippedNoChanges: 'バックアップをスキップ - 変更なし',
+    backupFailed2: 'バックアップに失敗しました: {{message}}',
+    clearedLogs: '{{count}}件のログを削除しました',
+    failedToClearLogs: 'ログの削除に失敗しました: {{message}}',
+
+    // History
+    history: '履歴',
+    clear: 'クリア',
+    date: '日付',
+    status: 'ステータス',
+    commit: 'コミット',
+
+    // Local Backup
+    localBackup: 'ローカルバックアップ',
+    localBackupDescription: 'データベース、アーカイブ、アップロード、すべてのファイルを含むBambuddyデータの完全なバックアップを作成します。',
+    downloadBackupLabel: 'バックアップをダウンロード',
+    completeBackupZip: '完全バックアップ: データベース + 全ファイル (ZIP)',
+    download: 'ダウンロード',
+    preparingBackup: 'バックアップを準備しています...',
+    creatingArchive: 'バックアップアーカイブを作成しています...大きなアーカイブの場合、時間がかかることがあります。',
+    downloadingFile: 'バックアップファイルをダウンロードしています...',
+    backupDownloaded: 'バックアップのダウンロードが成功しました',
+    failedToCreateBackup: 'バックアップの作成に失敗しました: {{message}}',
+    restore: '復元',
+    restoreReplacesAll: '復元はすべてのデータを置き換えます。',
+    restoreReplacesAllDetail: '現在のデータベースとファイルは完全に置き換えられます。復元後に再起動が必要です。',
+    restoreConfirmTitle: 'バックアップを復元',
+    restoreConfirmMessage: '"{{filename}}"から復元してもよろしいですか?現在のデータベースとすべてのファイルが完全に置き換えられます。復元後にアプリケーションの再起動が必要です。',
+    restoreConfirmButton: 'バックアップを復元',
+    uploadingFile: 'バックアップファイルをアップロードしています...',
+    backupRestoredRestart: 'バックアップが復元されました。Bambuddyを再起動してください。',
+    failedToRestore: 'バックアップの復元に失敗しました。ファイル形式を確認してください。',
+    reloadNow: '今すぐリロード',
+    creatingBackup: 'バックアップを作成中',
+    restoringBackup: 'バックアップを復元中',
+    preparing: '準備中...',
+    processing: '処理中...',
+    doNotClosePage: 'このページを閉じたり、移動しないでください。大きなバックアップの場合、この操作には数分かかることがあります。',
+
+    // RestoreModal
+    restoring: '復元中...',
+    restoreComplete: '復元完了',
+    restoreFailed2: '復元失敗',
+    importSettings: 'バックアップファイルから設定をインポート',
+    pleaseWaitRestoring: 'データの復元中です。お待ちください',
+    selectBackupFile: 'クリックしてバックアップファイルを選択 (.jsonまたは.zip)',
+    duplicateHandling: '重複処理の仕組み:',
+    matchPrinters: 'プリンター',
+    matchPrintersBy: 'シリアル番号で照合',
+    matchSmartPlugs: 'スマートプラグ',
+    matchSmartPlugsBy: 'IPアドレスで照合',
+    matchNotificationProviders: '通知プロバイダー',
+    matchNotificationProvidersBy: '名前で照合',
+    matchFilaments: 'フィラメント',
+    matchFilamentsBy: '名前 + タイプ + ブランドで照合',
+    matchArchives: 'アーカイブ',
+    matchArchivesBy: 'コンテンツハッシュで照合(常にスキップ)',
+    matchPendingUploads: '保留中のアップロード',
+    matchPendingUploadsBy: 'ファイル名で照合',
+    matchSettingsTemplates: '設定とテンプレート',
+    matchSettingsTemplatesBy: '常に上書き',
+    replaceExisting: '既存のデータを置き換え',
+    keepExisting: '既存のデータを保持',
+    overwriteDescription: '既に存在する項目をバックアップデータで上書き',
+    keepDescription: 'まだ存在しない項目のみ復元',
+    overwriteCaution: '注意:',
+    overwriteWarning: '上書きすると、現在の設定がバックアップのデータで置き換えられます。プリンターのアクセスコードはセキュリティのため上書きされません。',
+    cancel: 'キャンセル',
+    processingBackup: 'バックアップファイルを処理しています...',
+    itemsRestored: '復元済み',
+    itemsSkipped: 'スキップ済み',
+    restored: '復元済み',
+    skippedAlreadyExist: 'スキップ(既に存在)',
+    filesCategory: 'ファイル(3MF、サムネイルなど)',
+    andMore: '...他{{count}}件',
+    newApiKeysGenerated: '新しいAPIキーが生成されました',
+    keysShownOnce: 'これらのキーは一度だけ表示されます。今すぐコピーしてください!',
+    copy: 'コピー',
+    noDataFound: 'バックアップファイルに復元するデータが見つかりませんでした。',
+    close: '閉じる',
+
+    // Category labels
+    categories: {
+      settings: '設定',
+      notification_providers: '通知プロバイダー',
+      notification_templates: '通知テンプレート',
+      smart_plugs: 'スマートプラグ',
+      printers: 'プリンター',
+      filaments: 'フィラメント',
+      maintenance_types: 'メンテナンスタイプ',
+      archives: 'アーカイブ',
+      projects: 'プロジェクト',
+      pending_uploads: '保留中のアップロード',
+      external_links: '外部リンク',
+      api_keys: 'APIキー',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3250,6 +3544,319 @@ export default {
         mqttSameAsPower: '電力トピックと同じ、または異なる',
         mqttSameAsPower: '電力トピックと同じ、または異なる',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: 'リンク先:',
+    monitorOnly: '監視のみ',
+    alerts: 'アラート',
+    scheduleOn: 'オン {{time}}',
+    scheduleOff: 'オフ {{time}}',
+    on: 'オン',
+    off: 'オフ',
+    power: '電力',
+    kwhToday: '本日のkWh',
+    settings: '設定',
+    automationSettings: '自動化設定',
+    showInSwitchbar: 'スイッチバーに表示',
+    quickAccessSidebar: 'サイドバーからクイックアクセス',
+    enabled: '有効',
+    enableAutomation: 'このプラグの自動化を有効にする',
+    autoOn: '自動オン',
+    autoOnDescription: '印刷開始時にオンにする',
+    autoOff: '自動オフ',
+    autoOffDescription: '印刷完了時にオフにする(ワンショット)',
+    turnOffDelayMode: 'オフ遅延モード',
+    time: '時間',
+    temp: '温度',
+    delayMinutes: '遅延(分)',
+    tempThreshold: '温度しきい値(°C)',
+    tempThresholdDescription: 'ノズルがこの温度以下に冷却されるとオフになります',
+    edit: '編集',
+    deleteConfirm: '"{{name}}"を削除してもよろしいですか?この操作は取り消せません。',
+    turnOnConfirm: '"{{name}}"をオンにしてもよろしいですか?',
+    turnOffConfirm: '"{{name}}"をオフにしてもよろしいですか?接続されたデバイスの電源が切れます。',
+    failedToTurn: '"{{name}}"を{{action}}できませんでした',
+    unknown: '不明',
+    // AddSmartPlugModal
+    addTitle: 'スマートプラグを追加',
+    editTitle: 'スマートプラグを編集',
+    stopScanning: 'スキャン停止',
+    discoverTasmota: 'Tasmotaデバイスを検出',
+    foundDevices: '{{count}}台のデバイスが見つかりました - クリックして選択:',
+    noDevicesFound: 'ネットワーク上にTasmotaデバイスが見つかりません',
+    haNotConfigured: 'Home Assistantが設定されていません。設定場所:',
+    haSettingsPath: '設定 → ネットワーク → Home Assistant',
+    selectEntity: 'エンティティを選択 *',
+    ipAddress: 'IPアドレス *',
+    nameLabel: '名前 *',
+    username: 'ユーザー名',
+    password: 'パスワード',
+    authHint: 'Tasmotaデバイスが認証を必要としない場合は空のままにしてください',
+    linkToPrinter: 'プリンターにリンク',
+    noPrinter: 'プリンターなし(手動制御のみ)',
+    linkingDescription: 'リンクすると印刷開始/完了時に自動でオン/オフできます',
+    powerAlerts: '電力アラート',
+    alertAbove: '上限アラート(W)',
+    alertBelow: '下限アラート(W)',
+    alertDescription: '電力消費がこれらのしきい値を超えた場合に通知します。無効にするには空のままにしてください。',
+    dailySchedule: 'デイリースケジュール',
+    turnOnAt: 'オンにする時刻',
+    turnOffAt: 'オフにする時刻',
+    scheduleDescription: '毎日これらの時刻にプラグを自動的にオン/オフします。スキップするには空のままにしてください。',
+    showOnPrinterCard: 'プリンターカードに表示',
+    displayOnPrinterCard: 'プリンターカードにボタンを表示',
+    connectedResult: '接続成功!',
+    deviceLabel: 'デバイス: {{name}} - ',
+    stateLabel: '状態: {{state}}',
+    test: 'テスト',
+    delete: '削除',
+    save: '保存',
+    add: '追加',
+    cancel: 'キャンセル',
+    failedToStartScan: 'スキャンを開始できませんでした',
+    nameRequired: '名前は必須です',
+    entityRequired: 'Home AssistantプラグにはエンティティIDが必要です',
+    mqttTopicRequired: '電力、エネルギー、または状態監視用に少なくとも1つのMQTTトピックを設定する必要があります',
+    loadingEntities: 'エンティティを読み込み中...',
+    loading: '読み込み中...',
+    failedToLoadEntities: 'エンティティの読み込みに失敗しました: {{error}}',
+    noEntitiesMatching: '"{{search}}"に一致するエンティティが見つかりません',
+    noEntitiesAvailable: '利用可能なエンティティがありません',
+    searchingEntities: 'すべてのエンティティを検索中({{count}}件見つかりました)',
+    showingEntities: 'switch、light、input_booleanを表示({{count}}件利用可能)',
+    energyMonitoringOptional: 'エネルギー監視(オプション)',
+    energyMonitoringHint: '電力/エネルギーデータを提供するセンサーを検索して選択します。',
+    powerSensorW: '電力センサー(W)',
+    energyTodayKwh: '本日のエネルギー(kWh)',
+    totalEnergyKwh: '総エネルギー(kWh)',
+    noMatchingSensors: '一致するセンサーがありません',
+    none: 'なし',
+    mqttNotConfigured: 'MQTTブローカーが設定されていません。ブローカーアドレスを設定してください:',
+    mqttSettingsPath: '設定 → ネットワーク → MQTT配信',
+    mqttNotConfiguredSuffix: '(配信を有効にする必要はありません。ブローカーの詳細を入力するだけです)。',
+    mqttMonitorOnlyDescription: 'MQTTプラグはMQTTサブスクリプション経由で電力/エネルギーデータを受信します。オン/オフ制御は利用できません - MQTTブローカーまたはホームオートメーションシステムを使用してください。',
+    powerMonitoring: '電力監視',
+    energyMonitoring: 'エネルギー監視',
+    stateMonitoring: '状態監視',
+    optional: 'オプション',
+    topic: 'トピック',
+    jsonPath: 'JSONパス',
+    multiplier: '乗数',
+    onValue: 'ON値',
+    mqttPowerHint: 'JSONパスはJSONペイロードから値を抽出します(例: "power_l1")。トピックが生の数値を送信する場合は空のままにしてください。\n乗数: mW→Wは0.001、kW→Wは1000を使用。',
+    mqttEnergyHint: 'JSONパスはJSONペイロードから値を抽出します。生の値の場合は空のままにしてください。\n乗数: Wh→kWhは0.001、MWh→kWhは1000を使用。',
+    mqttStateHint: 'JSONパスはJSONペイロードから値を抽出します。生の値の場合は空のままにしてください。\nON値: "ON"を意味する正確な文字列。自動検出(ON、true、1)の場合は空のままにしてください。',
+    noSwitchesInSwitchbar: 'スイッチバーにスイッチがありません',
+    enableSwitchbarHint: '設定 > スマートプラグで「スイッチバーに表示」を有効にしてください',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'メール',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'SMTPメール通知',
+      telegram: 'Telegramボット経由の通知',
+      discord: 'Webhook経由でDiscordチャンネルに送信',
+      ntfy: '無料のセルフホスト可能なプッシュ通知',
+      pushover: 'シンプルで信頼性の高いプッシュ通知',
+      callmebot: 'CallMeBot経由の無料WhatsApp通知',
+      webhook: '任意のURLへの汎用HTTP POST',
+    },
+    // NotificationProviderCard
+    lastSuccess: '最終: {{date}}',
+    error: 'エラー',
+    printer: 'プリンター:',
+    allPrinters: 'すべてのプリンター',
+    sendTestNotification: 'テスト通知を送信',
+    eventSettings: 'イベント設定',
+    enabled: '有効',
+    sendFromProvider: 'このプロバイダーから通知を送信',
+    // Event categories
+    printEvents: '印刷イベント',
+    printerStatus: 'プリンターステータス',
+    amsAlarms: 'AMSアラーム',
+    amsHtAlarms: 'AMS-HTアラーム',
+    printQueue: '印刷キュー',
+    // Event tags (badges)
+    start: '開始',
+    plateCheck: 'プレートチェック',
+    complete: '完了',
+    failed: '失敗',
+    stopped: '停止',
+    progress: '進捗',
+    offline: 'オフライン',
+    lowFilament: 'フィラメント残量低下',
+    maintenance: 'メンテナンス',
+    amsHumidity: 'AMS湿度',
+    amsTemp: 'AMS温度',
+    amsHtHumidity: 'AMS-HT湿度',
+    amsHtTemp: 'AMS-HT温度',
+    bedCooled: 'ベッド冷却済み',
+    quiet: '静音',
+    digest: 'ダイジェスト {{time}}',
+    // Event labels (expanded settings)
+    printStarted: '印刷開始',
+    plateNotEmpty: 'プレートが空でない',
+    plateNotEmptyDescription: '印刷前にオブジェクトが検出されました',
+    printCompleted: '印刷完了',
+    bedCooledLabel: 'ベッド冷却済み',
+    bedCooledDescription: '印刷後にベッドがしきい値以下に冷却',
+    printFailed: '印刷失敗',
+    printStopped: '印刷停止',
+    progressMilestones: '進捗マイルストーン',
+    progressMilestonesDescription: '25%、50%、75%で通知',
+    printerOffline: 'プリンターオフライン',
+    printerError: 'プリンターエラー',
+    lowFilamentLabel: 'フィラメント残量低下',
+    maintenanceDue: 'メンテナンス期限',
+    maintenanceDueDescription: 'メンテナンスが必要な場合に通知',
+    amsHumidityHigh: 'AMS湿度高',
+    amsHumidityHighDescription: '通常AMSの湿度がしきい値を超過',
+    amsTemperatureHigh: 'AMS温度高',
+    amsTemperatureHighDescription: '通常AMSの温度がしきい値を超過',
+    amsHtHumidityHigh: 'AMS-HT湿度高',
+    amsHtHumidityHighDescription: 'AMS-HTの湿度がしきい値を超過',
+    amsHtTemperatureHigh: 'AMS-HT温度高',
+    amsHtTemperatureHighDescription: 'AMS-HTの温度がしきい値を超過',
+    // Queue events
+    jobAdded: 'ジョブ追加',
+    jobAddedDescription: 'キューにジョブが追加されました',
+    jobAssigned: 'ジョブ割り当て',
+    jobAssignedDescription: 'モデルベースのジョブがプリンターに割り当てられました',
+    jobStarted: 'ジョブ開始',
+    jobStartedDescription: 'キュージョブの印刷が開始されました',
+    jobWaiting: 'ジョブ待機中',
+    jobWaitingDescription: 'フィラメント待ちのジョブ',
+    jobSkipped: 'ジョブスキップ',
+    jobSkippedDescription: 'ジョブがスキップされました(前のジョブが失敗)',
+    jobFailed: 'ジョブ失敗',
+    jobFailedDescription: 'ジョブの開始に失敗しました',
+    queueComplete: 'キュー完了',
+    queueCompleteDescription: 'すべてのキュージョブが完了しました',
+    // Quiet hours
+    quietHours: '静音時間',
+    noNotificationsDuring: 'この時間帯は通知を送信しません',
+    editProviderToChangeQuietHours: 'プロバイダーを編集して静音時間を変更',
+    // Daily digest
+    dailyDigest: 'デイリーダイジェスト',
+    batchNotifications: '通知をまとめて1日のサマリーとして送信',
+    sendAt: '{{time}}に送信',
+    editProviderToChangeDigestTime: 'プロバイダーを編集してダイジェスト時刻を変更',
+    // Actions
+    edit: '編集',
+    deleteProvider: '通知プロバイダーを削除',
+    deleteConfirm: '"{{name}}"を削除してもよろしいですか?この操作は取り消せません。',
+    delete: '削除',
+    // AddNotificationModal
+    addTitle: '通知プロバイダーを追加',
+    editTitle: '通知プロバイダーを編集',
+    nameLabel: '名前 *',
+    namePlaceholder: 'マイ通知',
+    providerTypeLabel: 'プロバイダータイプ *',
+    configuration: '設定',
+    testConfiguration: '設定をテスト',
+    printerFilter: 'プリンターフィルター',
+    onlyFromPrinter: 'このプリンターからのイベントのみ通知を送信',
+    quietHoursDnd: '静音時間(おやすみモード)',
+    quietStart: '開始',
+    quietEnd: '終了',
+    dailyDigestLabel: 'デイリーダイジェスト',
+    sendDigestAt: 'ダイジェスト送信時刻',
+    digestCollected: 'イベントが収集され、この時刻にまとめて送信されます',
+    notificationEvents: '通知イベント',
+    progressPercent: '(25%、50%、75%)',
+    bedCooledAfterPrint: '(印刷完了後)',
+    cancel: 'キャンセル',
+    save: '保存',
+    add: '追加',
+    nameRequired: '名前は必須です',
+    fieldRequired: '{{field}}は必須です',
+    // Config field labels
+    phoneNumber: '電話番号',
+    apiKey: 'APIキー',
+    serverUrl: 'サーバーURL',
+    topic: 'トピック',
+    authToken: '認証トークン',
+    userKey: 'ユーザーキー',
+    appToken: 'アプリトークン',
+    priority: '優先度',
+    botToken: 'ボットトークン',
+    chatId: 'チャットID',
+    smtpServer: 'SMTPサーバー',
+    smtpPort: 'SMTPポート',
+    security: 'セキュリティ',
+    authentication: '認証',
+    username: 'ユーザー名',
+    password: 'パスワード',
+    fromEmail: '送信元メール',
+    toEmail: '宛先メール',
+    webhookUrl: 'Webhook URL',
+    payloadFormat: 'ペイロード形式',
+    authorization: '認可',
+    titleFieldName: 'タイトルフィールド名',
+    messageFieldName: 'メッセージフィールド名',
+    // NotificationTemplateEditor
+    editTemplate: 'テンプレートを編集: {{name}}',
+    titleLabel: 'タイトル',
+    bodyLabel: '本文',
+    titlePlaceholder: '通知タイトル...',
+    bodyPlaceholder: '通知本文...',
+    availableVariables: '利用可能な変数',
+    clickToInsert: 'クリックして本文のカーソル位置に挿入',
+    livePreview: 'ライブプレビュー',
+    hide: '非表示',
+    show: '表示',
+    loadingPreview: 'プレビューを読み込み中...',
+    enterTemplateContent: 'テンプレートの内容を入力するとプレビューが表示されます',
+    titlePreview: 'タイトル:',
+    bodyPreview: '本文:',
+    resetToDefault: 'デフォルトにリセット',
+    titleRequired: 'タイトルは必須です',
+    bodyRequired: '本文は必須です',
+    // NotificationLogViewer
+    notificationLog: '通知ログ',
+    showFailedOnly: '失敗のみ',
+    last24Hours: '過去24時間',
+    last7Days: '過去7日間',
+    last30Days: '過去30日間',
+    last90Days: '過去90日間',
+    justNow: 'たった今',
+    noFailedNotifications: '失敗した通知はありません',
+    noNotificationsLogged: '記録された通知はありません',
+    unknownProvider: '不明なプロバイダー',
+    logTitle: 'タイトル',
+    logMessage: 'メッセージ',
+    logError: 'エラー',
+    logProvider: 'プロバイダー: {{type}}',
+    logTime: '時刻: {{time}}',
+    refresh: '更新',
+    clearOld: '古いものを削除',
+    statsSummary: '過去{{days}}日間:',
+    statsNotifications: '通知',
+    statsSent: '{{count}}件送信',
+    statsFailed: '{{count}}件失敗',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: '印刷開始',
+      print_complete: '印刷完了',
+      print_failed: '印刷失敗',
+      print_stopped: '印刷停止',
+      print_progress: '進捗',
+      printer_offline: 'プリンターオフライン',
+      printer_error: 'プリンターエラー',
+      filament_low: 'フィラメント残量低下',
+      maintenance_due: 'メンテナンス期限',
+      test: 'テスト',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

+ 593 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -1654,6 +1654,149 @@ export default {
       imported: 'Importadas {{added}} cores ({{skipped}} ignoradas)',
       imported: 'Importadas {{added}} cores ({{skipped}} ignoradas)',
       importFailed: 'Falha ao importar: formato JSON inválido',
       importFailed: 'Falha ao importar: formato JSON inválido',
     },
     },
+    dateFormat: 'Formato de data',
+    dateFormatUs: 'US (MM/DD/AAAA)',
+    dateFormatEu: 'EU (DD/MM/AAAA)',
+    dateFormatIso: 'ISO (AAAA-MM-DD)',
+    timeFormat: 'Formato de hora',
+    timeFormat12: '12 horas (3:30 PM)',
+    timeFormat24: '24 horas (15:30)',
+    defaultPrinter: 'Impressora padrão',
+    defaultPrinterDescription: 'Pré-selecionar esta impressora para uploads, reimpressões e outras operações.',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: 'Arraste itens na barra lateral para reordenar. Restaurar ordem padrão aqui.',
+    reset: 'Redefinir',
+    darkMode: 'Modo escuro',
+    lightMode: 'Modo claro',
+    active: '(ativo)',
+    background: 'Fundo',
+    accent: 'Destaque',
+    style: 'Estilo',
+    bgNeutral: 'Neutro',
+    bgWarm: 'Quente',
+    bgCool: 'Frio',
+    bgOled: 'OLED Preto',
+    bgSlate: 'Azul ardósia',
+    bgForest: 'Verde floresta',
+    accentGreen: 'Verde',
+    accentTeal: 'Azul-petróleo',
+    accentBlue: 'Azul',
+    accentOrange: 'Laranja',
+    accentPurple: 'Roxo',
+    accentRed: 'Vermelho',
+    styleClassic: 'Clássico',
+    styleGlow: 'Brilhante',
+    styleVibrant: 'Vibrante',
+    themeToggleHint: 'Alternar entre modo escuro e claro usando o ícone de sol/lua na barra lateral.',
+    autoArchivePrints: 'Arquivar impressões automaticamente',
+    autoArchiveDescription: 'Salvar automaticamente arquivos 3MF quando impressões forem concluídas',
+    saveThumbnailsDescription: 'Extrair e salvar imagens de pré-visualização dos arquivos 3MF',
+    captureFinishPhotoDescription: 'Tirar foto da câmera da impressora quando a impressão for concluída',
+    ffmpegNotInstalled: 'ffmpeg não instalado',
+    ffmpegRequired: 'A captura de câmera requer ffmpeg. Instale via <brew>brew install ffmpeg</brew> (macOS) ou <apt>apt install ffmpeg</apt> (Linux).',
+    camera: 'Câmera',
+    cameraViewMode: 'Modo de visualização da câmera',
+    cameraOverlayDescription: 'A câmera abre em uma sobreposição redimensionável na tela principal',
+    cameraWindowDescription: 'A câmera abre em uma janela separada do navegador',
+    externalCamerasDescription: 'Configure câmeras externas para substituir a câmera integrada da impressora. Suporta streams MJPEG, RTSP, snapshots HTTP e câmeras USB (V4L2). Quando habilitada, a câmera externa é usada para visualização ao vivo e fotos de conclusão.',
+    cameraPlaceholderUsb: 'Caminho do dispositivo (/dev/video0)',
+    cameraPlaceholderUrl: 'URL da câmera (rtsp://... ou http://...)',
+    cameraTypeMjpeg: 'Stream MJPEG',
+    cameraTypeRtsp: 'Stream RTSP',
+    cameraTypeSnapshot: 'Snapshot HTTP',
+    cameraTypeUsb: 'Câmera USB (V4L2)',
+    test: 'Testar',
+    connected: 'Conectado',
+    disconnected: 'Desconectado',
+    currency: 'Moeda',
+    defaultFilamentCost: 'Custo padrão do filamento (por kg)',
+    electricityCost: 'Custo da eletricidade por kWh',
+    energyDisplayMode: 'Modo de exibição de energia',
+    energyModePrintDescription: 'O painel mostra a soma da energia usada durante as impressões',
+    energyModeTotalDescription: 'O painel mostra a energia total dos plugues inteligentes',
+    fileManager: 'Gerenciador de arquivos',
+    createArchiveEntry: 'Criar entrada de arquivo ao imprimir',
+    createArchiveEntryDescription: 'Ao imprimir pelo gerenciador de arquivos, criar opcionalmente uma entrada de arquivo',
+    lowDiskSpaceWarning: 'Aviso de pouco espaço em disco',
+    lowDiskSpaceDescription: 'Mostrar aviso quando o espaço livre em disco ficar abaixo deste limite',
+    printerFirmware: 'Firmware da impressora',
+    checkFirmwareDescription: 'Verificar atualizações de firmware da Bambu Lab',
+    bambuddySoftware: 'Software Bambuddy',
+    autoCheckDescription: 'Verificar automaticamente novas versões ao iniciar',
+    checkNow: 'Verificar agora',
+    updateAvailableVersion: 'Atualização disponível: v{{version}}',
+    releaseNotes: 'Notas da versão',
+    updateViaDocker: 'Atualizar via Docker Compose:',
+    installUpdate: 'Instalar atualização',
+    latestVersionRunning: 'Você está usando a versão mais recente',
+    failedToCheckUpdates: 'Falha ao verificar atualizações: {{error}}',
+    backupRestore: 'Backup e Restauração',
+    backupRestoreDescription: 'Exportar/importar configurações e configurar backup do GitHub',
+    goToBackup: 'Ir para backup',
+    externalUrl: 'URL externa',
+    externalUrlDescription: 'A URL externa onde o Bambuddy está acessível. Usada para imagens de notificação e integrações externas.',
+    bambuddyUrl: 'URL do Bambuddy',
+    externalUrlHint: 'Inclua protocolo e porta (ex: http://192.168.1.100:8000)',
+    ftpRetry: 'Tentativa FTP',
+    ftpRetryDescription: 'Tentar novamente operações FTP quando o WiFi da impressora é instável. Aplica-se a downloads 3MF, uploads de impressão, downloads de timelapse e atualizações de firmware.',
+    autoRetryDescription: 'Tentar novamente automaticamente operações FTP que falharam',
+    retryAttempts: 'Tentativas de reenvio',
+    retryDelay: 'Atraso entre tentativas',
+    connectionTimeout: 'Tempo limite de conexão',
+    time_one: '{{count}} vez',
+    time_other: '{{count}} vezes',
+    second_one: '{{count}} segundo',
+    second_other: '{{count}} segundos',
+    nSeconds: '{{count}} segundos',
+    increaseForWeakWifi: 'Aumente para impressoras com WiFi fraco',
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: 'Conecte ao Home Assistant para controlar plugues inteligentes via API REST do HA. Suporta entidades switch, light, input_boolean e script.',
+    homeAssistantUrl: 'URL do Home Assistant',
+    longLivedAccessToken: 'Token de acesso de longa duração',
+    haTokenHint: 'Crie um token no HA: Perfil → Tokens de acesso de longa duração → Criar token',
+    connectionSuccessful: 'Conexão bem-sucedida',
+    connectionFailed: 'Falha na conexão',
+    haConnectionSuccess: 'Conectado com sucesso ao Home Assistant.',
+    haConnectionFailed: 'Falha ao conectar ao Home Assistant.',
+    mqttPublishing: 'Publicação MQTT',
+    mqttDescription: 'Publique eventos do BamBuddy para um broker MQTT externo para integração com Node-RED, Home Assistant e outros sistemas de automação.',
+    mqttEnableDescription: 'Publicar eventos no broker MQTT externo',
+    brokerHostname: 'Hostname do broker',
+    port: 'Porta',
+    usernameOptional: 'Usuário (opcional)',
+    passwordOptional: 'Senha (opcional)',
+    topicPrefix: 'Prefixo do tópico',
+    topicPrefixHint: 'Os tópicos serão: {{prefix}}/printers/<serial>/status, etc.',
+    prometheusMetrics: 'Métricas Prometheus',
+    prometheusEndpointDescription: 'Expor métricas da impressora em <code>/api/v1/metrics</code> para monitoramento Prometheus/Grafana.',
+    bearerTokenOptional: 'Token Bearer (opcional)',
+    bearerTokenHint: 'Se definido, as requisições devem incluir <code>Authorization: Bearer <token></code>',
+    metricsConnectionStatus: 'Status da conexão',
+    metricsPrinterState: 'Estado da impressora (idle/printing/etc)',
+    metricsPrintProgress: 'Progresso da impressão 0-100%',
+    metricsBedTemp: 'Temperatura da mesa',
+    metricsNozzleTemp: 'Temperatura do bico',
+    metricsPrintsTotal: 'Total de impressões por resultado',
+    metricsMore: '...e mais (camadas, ventoinhas, fila, uso de filamento)',
+    smartPlugsDescription: 'Conecte plugues inteligentes (Tasmota ou Home Assistant) para automatizar o controle de energia e rastrear o consumo energético das suas impressoras.',
+    allOn: 'Ligar todos',
+    allOff: 'Desligar todos',
+    addSmartPlug: 'Adicionar plugue inteligente',
+    energySummary: 'Resumo de energia',
+    currentPower: 'Potência atual',
+    plugsOnline: '{{reachable}}/{{total}} plugues online',
+    today: 'Hoje',
+    yesterday: 'Ontem',
+    total: 'Total',
+    enablePlugsForSummary: 'Habilite os plugues para ver o resumo de energia',
+    addNotificationProvider: 'Adicionar',
+    systemBadge: '(Sistema)',
+    creating: 'Criando...',
+    changing: 'Alterando...',
+    deleteUserAndItems: 'Excluir usuário E seus itens',
+    deleteUserKeepItems: 'Excluir usuário, manter itens (ficarão sem dono)',
+    ok: 'OK',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2872,6 +3015,143 @@ export default {
     backupFailed: 'Falha ao criar backup',
     backupFailed: 'Falha ao criar backup',
     restoreFailed: 'Falha ao restaurar backup',
     restoreFailed: 'Falha ao restaurar backup',
     restoreNote: 'A impressora virtual será parada durante a restauração',
     restoreNote: 'A impressora virtual será parada durante a restauração',
+
+    // GitHub Backup
+    githubBackup: 'Backup GitHub',
+    enabled: 'Ativado',
+    cloudLoginRequired: 'Login no Bambu Cloud necessário. Entre em Perfis → Perfis Cloud para ativar o backup GitHub.',
+    githubDescription: 'Sincronize automaticamente seus perfis com um repositório GitHub privado para backup e histórico de versões.',
+    repositoryUrl: 'URL do repositório',
+    personalAccessToken: 'Token de acesso pessoal',
+    tokenSaved: '(salvo)',
+    enterNewToken: 'Digite um novo token para atualizar',
+    tokenHint: 'Token de granularidade fina com permissão de leitura/escrita de conteúdo',
+    branch: 'Branch',
+    manualOnly: 'Apenas manual',
+    hourly: 'A cada hora',
+    daily: 'Diário',
+    weekly: 'Semanal',
+    includeInBackup: 'Incluir no backup',
+    kProfiles: 'K-Perfis',
+    kProfilesDescription: 'Calibração de avanço de pressão das impressoras conectadas',
+    noPrintersConnected: 'Nenhuma impressora conectada',
+    printersConnected: '{{connected}}/{{total}} conectadas',
+    cloudProfiles: 'Perfis Cloud',
+    cloudProfilesDescription: 'Predefinições de filamento, impressora e processo do Bambu Cloud',
+    appSettings: 'Configurações do App',
+    appSettingsDescription: 'Configuração do Bambuddy (banco de dados completo)',
+    lastBackupAt: 'Último backup:',
+    noBackupsYet: 'Nenhum backup ainda',
+    next: 'Próximo:',
+    startingBackup: 'Iniciando backup...',
+    test: 'Testar',
+    enableBackup: 'Ativar backup',
+    testConnection: 'Testar conexão',
+    enterRepoUrl: 'Digite a URL do repositório',
+    enterRepoAndToken: 'Digite a URL do repositório e o token de acesso',
+    repoRequired: 'A URL do repositório é obrigatória',
+    tokenRequired: 'O token de acesso é obrigatório',
+    githubBackupEnabled: 'Backup GitHub ativado',
+    tokenUpdated: 'Token atualizado',
+    settingsSaved: 'Configurações salvas',
+    failedToSave: 'Falha ao salvar: {{message}}',
+    backupCompleteFiles: 'Backup concluído - {{count}} arquivos atualizados',
+    backupSkippedNoChanges: 'Backup ignorado - sem alterações',
+    backupFailed2: 'Falha no backup: {{message}}',
+    clearedLogs: '{{count}} logs removidos',
+    failedToClearLogs: 'Falha ao limpar logs: {{message}}',
+
+    // History
+    history: 'Histórico',
+    clear: 'Limpar',
+    date: 'Data',
+    status: 'Status',
+    commit: 'Commit',
+
+    // Local Backup
+    localBackup: 'Backup local',
+    localBackupDescription: 'Crie um backup completo dos seus dados do Bambuddy incluindo banco de dados, arquivos, uploads e todos os ficheiros.',
+    downloadBackupLabel: 'Baixar backup',
+    completeBackupZip: 'Backup completo: banco de dados + todos os arquivos (ZIP)',
+    download: 'Baixar',
+    preparingBackup: 'Preparando backup...',
+    creatingArchive: 'Criando arquivo de backup... Isso pode demorar para backups grandes.',
+    downloadingFile: 'Baixando arquivo de backup...',
+    backupDownloaded: 'Backup baixado com sucesso',
+    failedToCreateBackup: 'Falha ao criar backup: {{message}}',
+    restore: 'Restaurar',
+    restoreReplacesAll: 'A restauração substitui todos os dados.',
+    restoreReplacesAllDetail: 'Seu banco de dados e arquivos atuais serão completamente substituídos. É necessário reiniciar após a restauração.',
+    restoreConfirmTitle: 'Restaurar backup',
+    restoreConfirmMessage: 'Tem certeza de que deseja restaurar de "{{filename}}"? Isso substituirá completamente seu banco de dados e todos os arquivos. O aplicativo precisará ser reiniciado após a restauração.',
+    restoreConfirmButton: 'Restaurar backup',
+    uploadingFile: 'Enviando arquivo de backup...',
+    backupRestoredRestart: 'Backup restaurado. Por favor, reinicie o Bambuddy.',
+    failedToRestore: 'Falha ao restaurar backup. Verifique o formato do arquivo.',
+    reloadNow: 'Recarregar agora',
+    creatingBackup: 'Criando backup',
+    restoringBackup: 'Restaurando backup',
+    preparing: 'Preparando...',
+    processing: 'Processando...',
+    doNotClosePage: 'Por favor, não feche esta página nem navegue para outro lugar. Esta operação pode levar vários minutos para backups grandes.',
+
+    // RestoreModal
+    restoring: 'Restaurando...',
+    restoreComplete: 'Restauração concluída',
+    restoreFailed2: 'Falha na restauração',
+    importSettings: 'Importar configurações de um arquivo de backup',
+    pleaseWaitRestoring: 'Aguarde enquanto seus dados estão sendo restaurados',
+    selectBackupFile: 'Clique para selecionar um arquivo de backup (.json ou .zip)',
+    duplicateHandling: 'Como funciona o tratamento de duplicatas:',
+    matchPrinters: 'Impressoras',
+    matchPrintersBy: 'correspondência por número de série',
+    matchSmartPlugs: 'Smart Plugs',
+    matchSmartPlugsBy: 'correspondência por endereço IP',
+    matchNotificationProviders: 'Provedores de notificação',
+    matchNotificationProvidersBy: 'correspondência por nome',
+    matchFilaments: 'Filamentos',
+    matchFilamentsBy: 'correspondência por nome + tipo + marca',
+    matchArchives: 'Arquivos',
+    matchArchivesBy: 'correspondência por hash de conteúdo (sempre ignorado)',
+    matchPendingUploads: 'Uploads pendentes',
+    matchPendingUploadsBy: 'correspondência por nome do arquivo',
+    matchSettingsTemplates: 'Configurações e modelos',
+    matchSettingsTemplatesBy: 'sempre sobrescritos',
+    replaceExisting: 'Substituir dados existentes',
+    keepExisting: 'Manter dados existentes',
+    overwriteDescription: 'Sobrescrever itens que já existem com dados do backup',
+    keepDescription: 'Restaurar apenas itens que ainda não existem',
+    overwriteCaution: 'Cuidado:',
+    overwriteWarning: 'A sobrescrita substituirá suas configurações atuais pelos dados do backup. Códigos de acesso das impressoras nunca são sobrescritos por segurança.',
+    cancel: 'Cancelar',
+    processingBackup: 'Processando arquivo de backup...',
+    itemsRestored: 'Itens restaurados',
+    itemsSkipped: 'Itens ignorados',
+    restored: 'Restaurados',
+    skippedAlreadyExist: 'Ignorados (já existem)',
+    filesCategory: 'Arquivos (3MF, miniaturas, etc.)',
+    andMore: '...e mais {{count}}',
+    newApiKeysGenerated: 'Novas chaves API geradas',
+    keysShownOnce: 'Estas chaves são exibidas apenas uma vez. Copie-as agora!',
+    copy: 'Copiar',
+    noDataFound: 'Nenhum dado para restaurar foi encontrado no arquivo de backup.',
+    close: 'Fechar',
+
+    // Category labels
+    categories: {
+      settings: 'Configurações',
+      notification_providers: 'Provedores de notificação',
+      notification_templates: 'Modelos de notificação',
+      smart_plugs: 'Smart Plugs',
+      printers: 'Impressoras',
+      filaments: 'Filamentos',
+      maintenance_types: 'Tipos de manutenção',
+      archives: 'Arquivos',
+      projects: 'Projetos',
+      pending_uploads: 'Uploads pendentes',
+      external_links: 'Links externos',
+      api_keys: 'Chaves API',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3250,6 +3530,319 @@ export default {
         mqttSameAsPower: 'Mesmo que o tópico de energia, ou diferente',
         mqttSameAsPower: 'Mesmo que o tópico de energia, ou diferente',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: 'Vinculado a:',
+    monitorOnly: 'Apenas monitoramento',
+    alerts: 'Alertas',
+    scheduleOn: 'Ligar {{time}}',
+    scheduleOff: 'Desligar {{time}}',
+    on: 'Ligado',
+    off: 'Desligado',
+    power: 'Potência',
+    kwhToday: 'kWh Hoje',
+    settings: 'Configurações',
+    automationSettings: 'Configurações de automação',
+    showInSwitchbar: 'Mostrar na barra de interruptores',
+    quickAccessSidebar: 'Acesso rápido pela barra lateral',
+    enabled: 'Ativado',
+    enableAutomation: 'Ativar automação para este plugue',
+    autoOn: 'Auto Ligar',
+    autoOnDescription: 'Ligar quando a impressão iniciar',
+    autoOff: 'Auto Desligar',
+    autoOffDescription: 'Desligar quando a impressão terminar (única vez)',
+    turnOffDelayMode: 'Modo de atraso para desligar',
+    time: 'Tempo',
+    temp: 'Temp',
+    delayMinutes: 'Atraso (minutos)',
+    tempThreshold: 'Limite de temperatura (°C)',
+    tempThresholdDescription: 'Desliga quando o bico esfria abaixo desta temperatura',
+    edit: 'Editar',
+    deleteConfirm: 'Tem certeza que deseja excluir "{{name}}"? Esta ação não pode ser desfeita.',
+    turnOnConfirm: 'Tem certeza que deseja ligar "{{name}}"?',
+    turnOffConfirm: 'Tem certeza que deseja desligar "{{name}}"? Isso cortará a energia do dispositivo conectado.',
+    failedToTurn: 'Falha ao {{action}} "{{name}}"',
+    unknown: 'Desconhecido',
+    // AddSmartPlugModal
+    addTitle: 'Adicionar plugue inteligente',
+    editTitle: 'Editar plugue inteligente',
+    stopScanning: 'Parar varredura',
+    discoverTasmota: 'Descobrir dispositivos Tasmota',
+    foundDevices: '{{count}} dispositivo(s) encontrado(s) - clique para selecionar:',
+    noDevicesFound: 'Nenhum dispositivo Tasmota encontrado na sua rede',
+    haNotConfigured: 'Home Assistant não está configurado. Configure em',
+    haSettingsPath: 'Configurações → Rede → Home Assistant',
+    selectEntity: 'Selecionar entidade *',
+    ipAddress: 'Endereço IP *',
+    nameLabel: 'Nome *',
+    username: 'Usuário',
+    password: 'Senha',
+    authHint: 'Deixe vazio se seu dispositivo Tasmota não requer autenticação',
+    linkToPrinter: 'Vincular à impressora',
+    noPrinter: 'Sem impressora (apenas controle manual)',
+    linkingDescription: 'A vinculação permite ligar/desligar automaticamente ao iniciar/terminar impressão',
+    powerAlerts: 'Alertas de potência',
+    alertAbove: 'Alertar se acima (W)',
+    alertBelow: 'Alertar se abaixo (W)',
+    alertDescription: 'Receba notificações quando o consumo de energia ultrapassar estes limites. Deixe vazio para desativar essa direção.',
+    dailySchedule: 'Programação diária',
+    turnOnAt: 'Ligar às',
+    turnOffAt: 'Desligar às',
+    scheduleDescription: 'Ligar/desligar automaticamente o plugue nestes horários diariamente. Deixe vazio para pular essa ação.',
+    showOnPrinterCard: 'Mostrar no cartão da impressora',
+    displayOnPrinterCard: 'Exibir botão no cartão da impressora',
+    connectedResult: 'Conectado!',
+    deviceLabel: 'Dispositivo: {{name}} - ',
+    stateLabel: 'Estado: {{state}}',
+    test: 'Testar',
+    delete: 'Excluir',
+    save: 'Salvar',
+    add: 'Adicionar',
+    cancel: 'Cancelar',
+    failedToStartScan: 'Falha ao iniciar varredura',
+    nameRequired: 'Nome é obrigatório',
+    entityRequired: 'Entidade é obrigatória para plugues Home Assistant',
+    mqttTopicRequired: 'Pelo menos um tópico MQTT deve ser configurado para potência, energia ou monitoramento de estado',
+    loadingEntities: 'Carregando entidades...',
+    loading: 'Carregando...',
+    failedToLoadEntities: 'Falha ao carregar entidades: {{error}}',
+    noEntitiesMatching: 'Nenhuma entidade encontrada correspondente a "{{search}}"',
+    noEntitiesAvailable: 'Nenhuma entidade disponível',
+    searchingEntities: 'Buscando todas as entidades ({{count}} encontradas)',
+    showingEntities: 'Mostrando switch, light, input_boolean ({{count}} disponíveis)',
+    energyMonitoringOptional: 'Monitoramento de energia (Opcional)',
+    energyMonitoringHint: 'Pesquise e selecione sensores que fornecem dados de potência/energia.',
+    powerSensorW: 'Sensor de potência (W)',
+    energyTodayKwh: 'Energia hoje (kWh)',
+    totalEnergyKwh: 'Energia total (kWh)',
+    noMatchingSensors: 'Nenhum sensor correspondente',
+    none: 'Nenhum',
+    mqttNotConfigured: 'Broker MQTT não configurado. Defina o endereço do broker em',
+    mqttSettingsPath: 'Configurações → Rede → Publicação MQTT',
+    mqttNotConfiguredSuffix: '(você não precisa ativar a publicação, apenas preencha os detalhes do broker).',
+    mqttMonitorOnlyDescription: 'Plugues MQTT recebem dados de potência/energia via assinatura MQTT. O controle liga/desliga não está disponível - use seu broker MQTT ou sistema de automação residencial.',
+    powerMonitoring: 'Monitoramento de potência',
+    energyMonitoring: 'Monitoramento de energia',
+    stateMonitoring: 'Monitoramento de estado',
+    optional: 'opcional',
+    topic: 'Tópico',
+    jsonPath: 'Caminho JSON',
+    multiplier: 'Multiplicador',
+    onValue: 'Valor ON',
+    mqttPowerHint: 'O caminho JSON extrai o valor do payload JSON (ex: "power_l1"). Deixe vazio se o tópico publica valores numéricos brutos.\nUse multiplicador 0.001 para mW→W, 1000 para kW→W.',
+    mqttEnergyHint: 'O caminho JSON extrai o valor do payload JSON. Deixe vazio para valores brutos.\nUse multiplicador 0.001 para Wh→kWh, 1000 para MWh→kWh.',
+    mqttStateHint: 'O caminho JSON extrai o valor do payload JSON. Deixe vazio para valores brutos.\nValor ON: a string exata que significa "ON". Deixe vazio para detecção automática (ON, true, 1).',
+    noSwitchesInSwitchbar: 'Nenhum interruptor na barra',
+    enableSwitchbarHint: 'Ative "Mostrar na barra de interruptores" em Configurações > Smart Plugs',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: 'E-mail',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'Notificações por e-mail SMTP',
+      telegram: 'Notificações via bot do Telegram',
+      discord: 'Enviar para canal do Discord via webhook',
+      ntfy: 'Notificações push gratuitas e auto-hospedáveis',
+      pushover: 'Notificações push simples e confiáveis',
+      callmebot: 'Notificações gratuitas via WhatsApp pelo CallMeBot',
+      webhook: 'POST HTTP genérico para qualquer URL',
+    },
+    // NotificationProviderCard
+    lastSuccess: 'Último: {{date}}',
+    error: 'Erro',
+    printer: 'Impressora:',
+    allPrinters: 'Todas as impressoras',
+    sendTestNotification: 'Enviar Notificação de Teste',
+    eventSettings: 'Configurações de Eventos',
+    enabled: 'Ativado',
+    sendFromProvider: 'Enviar notificações deste provedor',
+    // Event categories
+    printEvents: 'Eventos de Impressão',
+    printerStatus: 'Status da Impressora',
+    amsAlarms: 'Alarmes do AMS',
+    amsHtAlarms: 'Alarmes do AMS-HT',
+    printQueue: 'Fila de Impressão',
+    // Event tags (badges)
+    start: 'Início',
+    plateCheck: 'Verificação da Mesa',
+    complete: 'Concluído',
+    failed: 'Falhou',
+    stopped: 'Parado',
+    progress: 'Progresso',
+    offline: 'Offline',
+    lowFilament: 'Filamento Baixo',
+    maintenance: 'Manutenção',
+    amsHumidity: 'Umidade do AMS',
+    amsTemp: 'Temp. do AMS',
+    amsHtHumidity: 'Umidade do AMS-HT',
+    amsHtTemp: 'Temp. do AMS-HT',
+    bedCooled: 'Mesa Resfriada',
+    quiet: 'Silencioso',
+    digest: 'Resumo {{time}}',
+    // Event labels (expanded settings)
+    printStarted: 'Impressão Iniciada',
+    plateNotEmpty: 'Mesa Não Vazia',
+    plateNotEmptyDescription: 'Objetos detectados antes da impressão',
+    printCompleted: 'Impressão Concluída',
+    bedCooledLabel: 'Mesa Resfriada',
+    bedCooledDescription: 'Mesa resfriou abaixo do limite após a impressão',
+    printFailed: 'Impressão Falhou',
+    printStopped: 'Impressão Parada',
+    progressMilestones: 'Marcos de Progresso',
+    progressMilestonesDescription: 'Notificar em 25%, 50%, 75%',
+    printerOffline: 'Impressora Offline',
+    printerError: 'Erro da Impressora',
+    lowFilamentLabel: 'Filamento Baixo',
+    maintenanceDue: 'Manutenção Necessária',
+    maintenanceDueDescription: 'Notificar quando manutenção for necessária',
+    amsHumidityHigh: 'Umidade Alta do AMS',
+    amsHumidityHighDescription: 'Umidade do AMS regular excede o limite',
+    amsTemperatureHigh: 'Temperatura Alta do AMS',
+    amsTemperatureHighDescription: 'Temperatura do AMS regular excede o limite',
+    amsHtHumidityHigh: 'Umidade Alta do AMS-HT',
+    amsHtHumidityHighDescription: 'Umidade do AMS-HT excede o limite',
+    amsHtTemperatureHigh: 'Temperatura Alta do AMS-HT',
+    amsHtTemperatureHighDescription: 'Temperatura do AMS-HT excede o limite',
+    // Queue events
+    jobAdded: 'Trabalho Adicionado',
+    jobAddedDescription: 'Trabalho adicionado à fila',
+    jobAssigned: 'Trabalho Atribuído',
+    jobAssignedDescription: 'Trabalho baseado em modelo atribuído à impressora',
+    jobStarted: 'Trabalho Iniciado',
+    jobStartedDescription: 'Trabalho da fila começou a imprimir',
+    jobWaiting: 'Trabalho Aguardando',
+    jobWaitingDescription: 'Trabalho aguardando filamento',
+    jobSkipped: 'Trabalho Pulado',
+    jobSkippedDescription: 'Trabalho pulado (anterior falhou)',
+    jobFailed: 'Trabalho Falhou',
+    jobFailedDescription: 'Trabalho falhou ao iniciar',
+    queueComplete: 'Fila Concluída',
+    queueCompleteDescription: 'Todos os trabalhos da fila finalizados',
+    // Quiet hours
+    quietHours: 'Horário de Silêncio',
+    noNotificationsDuring: 'Sem notificações durante essas horas',
+    editProviderToChangeQuietHours: 'Edite o provedor para alterar o horário de silêncio',
+    // Daily digest
+    dailyDigest: 'Resumo Diário',
+    batchNotifications: 'Agrupar notificações em um único resumo diário',
+    sendAt: 'Enviar às {{time}}',
+    editProviderToChangeDigestTime: 'Edite o provedor para alterar o horário do resumo',
+    // Actions
+    edit: 'Editar',
+    deleteProvider: 'Excluir Provedor de Notificação',
+    deleteConfirm: 'Tem certeza que deseja excluir "{{name}}"? Isso não pode ser desfeito.',
+    delete: 'Excluir',
+    // AddNotificationModal
+    addTitle: 'Adicionar Provedor de Notificação',
+    editTitle: 'Editar Provedor de Notificação',
+    nameLabel: 'Nome *',
+    namePlaceholder: 'Minhas Notificações',
+    providerTypeLabel: 'Tipo de Provedor *',
+    configuration: 'Configuração',
+    testConfiguration: 'Testar Configuração',
+    printerFilter: 'Filtro de Impressora',
+    onlyFromPrinter: 'Enviar notificações apenas para eventos desta impressora',
+    quietHoursDnd: 'Horário de Silêncio (Não Perturbe)',
+    quietStart: 'Início',
+    quietEnd: 'Fim',
+    dailyDigestLabel: 'Resumo Diário',
+    sendDigestAt: 'Enviar resumo às',
+    digestCollected: 'Os eventos serão coletados e enviados como um único resumo neste horário',
+    notificationEvents: 'Eventos de Notificação',
+    progressPercent: '(25%, 50%, 75%)',
+    bedCooledAfterPrint: '(após conclusão da impressão)',
+    cancel: 'Cancelar',
+    save: 'Salvar',
+    add: 'Adicionar',
+    nameRequired: 'Nome é obrigatório',
+    fieldRequired: '{{field}} é obrigatório',
+    // Config field labels
+    phoneNumber: 'Número de Telefone',
+    apiKey: 'Chave da API',
+    serverUrl: 'URL do Servidor',
+    topic: 'Tópico',
+    authToken: 'Token de Autenticação',
+    userKey: 'Chave do Usuário',
+    appToken: 'Token do Aplicativo',
+    priority: 'Prioridade',
+    botToken: 'Token do Bot',
+    chatId: 'ID do Chat',
+    smtpServer: 'Servidor SMTP',
+    smtpPort: 'Porta SMTP',
+    security: 'Segurança',
+    authentication: 'Autenticação',
+    username: 'Usuário',
+    password: 'Senha',
+    fromEmail: 'E-mail de Origem',
+    toEmail: 'E-mail de Destino',
+    webhookUrl: 'URL do Webhook',
+    payloadFormat: 'Formato do Payload',
+    authorization: 'Autorização',
+    titleFieldName: 'Nome do Campo de Título',
+    messageFieldName: 'Nome do Campo de Mensagem',
+    // NotificationTemplateEditor
+    editTemplate: 'Editar Modelo: {{name}}',
+    titleLabel: 'Título',
+    bodyLabel: 'Corpo',
+    titlePlaceholder: 'Título da notificação...',
+    bodyPlaceholder: 'Corpo da notificação...',
+    availableVariables: 'Variáveis Disponíveis',
+    clickToInsert: 'Clique para inserir na posição do cursor no corpo',
+    livePreview: 'Pré-visualização ao Vivo',
+    hide: 'Ocultar',
+    show: 'Mostrar',
+    loadingPreview: 'Carregando pré-visualização...',
+    enterTemplateContent: 'Insira o conteúdo do modelo para ver a pré-visualização',
+    titlePreview: 'Título:',
+    bodyPreview: 'Corpo:',
+    resetToDefault: 'Restaurar Padrão',
+    titleRequired: 'Título é obrigatório',
+    bodyRequired: 'Corpo é obrigatório',
+    // NotificationLogViewer
+    notificationLog: 'Registro de Notificações',
+    showFailedOnly: 'Apenas falhas',
+    last24Hours: 'Últimas 24 horas',
+    last7Days: 'Últimos 7 dias',
+    last30Days: 'Últimos 30 dias',
+    last90Days: 'Últimos 90 dias',
+    justNow: 'Agora mesmo',
+    noFailedNotifications: 'Nenhuma notificação com falha',
+    noNotificationsLogged: 'Nenhuma notificação registrada',
+    unknownProvider: 'Provedor Desconhecido',
+    logTitle: 'Título',
+    logMessage: 'Mensagem',
+    logError: 'Erro',
+    logProvider: 'Provedor: {{type}}',
+    logTime: 'Hora: {{time}}',
+    refresh: 'Atualizar',
+    clearOld: 'Limpar Antigos',
+    statsSummary: 'Últimos {{days}} dias:',
+    statsNotifications: 'notificações',
+    statsSent: '{{count}} enviadas',
+    statsFailed: '{{count}} com falha',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: 'Impressão Iniciada',
+      print_complete: 'Impressão Concluída',
+      print_failed: 'Impressão Falhou',
+      print_stopped: 'Impressão Parada',
+      print_progress: 'Progresso',
+      printer_offline: 'Impressora Offline',
+      printer_error: 'Erro da Impressora',
+      filament_low: 'Filamento Baixo',
+      maintenance_due: 'Manutenção Necessária',
+      test: 'Teste',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

+ 593 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -1654,6 +1654,149 @@ export default {
       imported: '已导入 {{added}} 种颜色(跳过 {{skipped}} 种)',
       imported: '已导入 {{added}} 种颜色(跳过 {{skipped}} 种)',
       importFailed: '导入失败:无效的 JSON 格式',
       importFailed: '导入失败:无效的 JSON 格式',
     },
     },
+    dateFormat: '日期格式',
+    dateFormatUs: '美式 (MM/DD/YYYY)',
+    dateFormatEu: '欧式 (DD/MM/YYYY)',
+    dateFormatIso: 'ISO (YYYY-MM-DD)',
+    timeFormat: '时间格式',
+    timeFormat12: '12小时制 (3:30 PM)',
+    timeFormat24: '24小时制 (15:30)',
+    defaultPrinter: '默认打印机',
+    defaultPrinterDescription: '为上传、重印和其他操作预选此打印机。',
+    slicerBambuStudio: 'Bambu Studio',
+    slicerOrcaSlicer: 'OrcaSlicer',
+    sidebarOrderDescription: '拖拽侧边栏项目以重新排序。在此处重置为默认顺序。',
+    reset: '重置',
+    darkMode: '深色模式',
+    lightMode: '浅色模式',
+    active: '(当前)',
+    background: '背景',
+    accent: '强调色',
+    style: '样式',
+    bgNeutral: '中性',
+    bgWarm: '暖色',
+    bgCool: '冷色',
+    bgOled: 'OLED 纯黑',
+    bgSlate: '石板蓝',
+    bgForest: '森林绿',
+    accentGreen: '绿色',
+    accentTeal: '青色',
+    accentBlue: '蓝色',
+    accentOrange: '橙色',
+    accentPurple: '紫色',
+    accentRed: '红色',
+    styleClassic: '经典',
+    styleGlow: '发光',
+    styleVibrant: '鲜艳',
+    themeToggleHint: '使用侧边栏中的太阳/月亮图标在深色和浅色模式之间切换。',
+    autoArchivePrints: '自动归档打印',
+    autoArchiveDescription: '打印完成时自动保存3MF文件',
+    saveThumbnailsDescription: '从3MF文件中提取并保存预览图像',
+    captureFinishPhotoDescription: '打印完成时从打印机摄像头拍照',
+    ffmpegNotInstalled: '未安装ffmpeg',
+    ffmpegRequired: '摄像头捕获需要ffmpeg。通过 <brew>brew install ffmpeg</brew>(macOS)或 <apt>apt install ffmpeg</apt>(Linux)安装。',
+    camera: '摄像头',
+    cameraViewMode: '摄像头查看模式',
+    cameraOverlayDescription: '摄像头在主屏幕上以可调大小的覆盖层打开',
+    cameraWindowDescription: '摄像头在单独的浏览器窗口中打开',
+    externalCamerasDescription: '配置外部摄像头以替换内置打印机摄像头。支持MJPEG流、RTSP、HTTP快照和USB摄像头(V4L2)。启用后,外部摄像头将用于实时查看和完成照片。',
+    cameraPlaceholderUsb: '设备路径 (/dev/video0)',
+    cameraPlaceholderUrl: '摄像头URL (rtsp://... 或 http://...)',
+    cameraTypeMjpeg: 'MJPEG 流',
+    cameraTypeRtsp: 'RTSP 流',
+    cameraTypeSnapshot: 'HTTP 快照',
+    cameraTypeUsb: 'USB 摄像头 (V4L2)',
+    test: '测试',
+    connected: '已连接',
+    disconnected: '未连接',
+    currency: '货币',
+    defaultFilamentCost: '默认耗材成本(每公斤)',
+    electricityCost: '每千瓦时电费',
+    energyDisplayMode: '能源显示模式',
+    energyModePrintDescription: '仪表板显示打印期间使用的能源总和',
+    energyModeTotalDescription: '仪表板显示智能插座的累计能源',
+    fileManager: '文件管理器',
+    createArchiveEntry: '打印时创建归档条目',
+    createArchiveEntryDescription: '从文件管理器打印时,可选择创建归档条目',
+    lowDiskSpaceWarning: '磁盘空间不足警告',
+    lowDiskSpaceDescription: '当可用磁盘空间低于此阈值时显示警告',
+    printerFirmware: '打印机固件',
+    checkFirmwareDescription: '检查Bambu Lab的打印机固件更新',
+    bambuddySoftware: 'Bambuddy 软件',
+    autoCheckDescription: '启动时自动检查新版本',
+    checkNow: '立即检查',
+    updateAvailableVersion: '可用更新:v{{version}}',
+    releaseNotes: '发布说明',
+    updateViaDocker: '通过 Docker Compose 更新:',
+    installUpdate: '安装更新',
+    latestVersionRunning: '您正在运行最新版本',
+    failedToCheckUpdates: '检查更新失败:{{error}}',
+    backupRestore: '备份与恢复',
+    backupRestoreDescription: '导出/导入设置并配置GitHub备份',
+    goToBackup: '前往备份',
+    externalUrl: '外部URL',
+    externalUrlDescription: 'Bambuddy可访问的外部URL。用于通知图像和外部集成。',
+    bambuddyUrl: 'Bambuddy URL',
+    externalUrlHint: '包含协议和端口(例如:http://192.168.1.100:8000)',
+    ftpRetry: 'FTP重试',
+    ftpRetryDescription: '当打印机WiFi不稳定时重试FTP操作。适用于3MF下载、打印上传、延时摄影下载和固件更新。',
+    autoRetryDescription: '自动重试失败的FTP操作',
+    retryAttempts: '重试次数',
+    retryDelay: '重试延迟',
+    connectionTimeout: '连接超时',
+    time_one: '{{count}}次',
+    time_other: '{{count}}次',
+    second_one: '{{count}}秒',
+    second_other: '{{count}}秒',
+    nSeconds: '{{count}}秒',
+    increaseForWeakWifi: '对WiFi信号弱的打印机增加此值',
+    homeAssistant: 'Home Assistant',
+    homeAssistantFullDescription: '连接到Home Assistant,通过HA REST API控制智能插座。支持switch、light、input_boolean和script实体。',
+    homeAssistantUrl: 'Home Assistant URL',
+    longLivedAccessToken: '长期访问令牌',
+    haTokenHint: '在HA中创建令牌:个人资料 → 长期访问令牌 → 创建令牌',
+    connectionSuccessful: '连接成功',
+    connectionFailed: '连接失败',
+    haConnectionSuccess: '已成功连接到Home Assistant。',
+    haConnectionFailed: '连接Home Assistant失败。',
+    mqttPublishing: 'MQTT发布',
+    mqttDescription: '将BamBuddy事件发布到外部MQTT代理,用于与Node-RED、Home Assistant和其他自动化系统集成。',
+    mqttEnableDescription: '向外部MQTT代理发布事件',
+    brokerHostname: '代理主机名',
+    port: '端口',
+    usernameOptional: '用户名(可选)',
+    passwordOptional: '密码(可选)',
+    topicPrefix: '主题前缀',
+    topicPrefixHint: '主题格式:{{prefix}}/printers/<serial>/status 等',
+    prometheusMetrics: 'Prometheus 指标',
+    prometheusEndpointDescription: '在 <code>/api/v1/metrics</code> 公开打印机指标,用于Prometheus/Grafana监控。',
+    bearerTokenOptional: 'Bearer令牌(可选)',
+    bearerTokenHint: '设置后,请求必须包含 <code>Authorization: Bearer <token></code>',
+    metricsConnectionStatus: '连接状态',
+    metricsPrinterState: '打印机状态(空闲/打印中等)',
+    metricsPrintProgress: '打印进度 0-100%',
+    metricsBedTemp: '热床温度',
+    metricsNozzleTemp: '喷嘴温度',
+    metricsPrintsTotal: '按结果分类的总打印数',
+    metricsMore: '...以及更多(层数、风扇、队列、耗材用量)',
+    smartPlugsDescription: '连接智能插座(Tasmota或Home Assistant)以自动化电源控制并跟踪打印机的能源使用情况。',
+    allOn: '全部开启',
+    allOff: '全部关闭',
+    addSmartPlug: '添加智能插座',
+    energySummary: '能源概要',
+    currentPower: '当前功率',
+    plugsOnline: '{{reachable}}/{{total}} 个插座在线',
+    today: '今天',
+    yesterday: '昨天',
+    total: '总计',
+    enablePlugsForSummary: '启用插座以查看能源概要',
+    addNotificationProvider: '添加',
+    systemBadge: '(系统)',
+    creating: '创建中...',
+    changing: '修改中...',
+    deleteUserAndItems: '删除用户及其所有项目',
+    deleteUserKeepItems: '删除用户,保留项目(将变为无主项目)',
+    ok: '确定',
   },
   },
 
 
   // Notifications (for push notifications)
   // Notifications (for push notifications)
@@ -2872,6 +3015,143 @@ export default {
     backupFailed: '备份失败',
     backupFailed: '备份失败',
     restoreFailed: '恢复失败',
     restoreFailed: '恢复失败',
     restoreNote: '恢复期间虚拟打印机将停止',
     restoreNote: '恢复期间虚拟打印机将停止',
+
+    // GitHub Backup
+    githubBackup: 'GitHub 备份',
+    enabled: '已启用',
+    cloudLoginRequired: '需要登录 Bambu Cloud。请在 配置文件 → 云配置文件 中登录以启用 GitHub 备份。',
+    githubDescription: '自动将您的配置文件同步到私有 GitHub 仓库以进行备份和版本历史记录。',
+    repositoryUrl: '仓库 URL',
+    personalAccessToken: '个人访问令牌',
+    tokenSaved: '(已保存)',
+    enterNewToken: '输入新令牌以更新',
+    tokenHint: '具有内容读写权限的细粒度令牌',
+    branch: '分支',
+    manualOnly: '仅手动',
+    hourly: '每小时',
+    daily: '每天',
+    weekly: '每周',
+    includeInBackup: '包含在备份中',
+    kProfiles: 'K 配置文件',
+    kProfilesDescription: '来自已连接打印机的压力推进校准',
+    noPrintersConnected: '没有打印机连接',
+    printersConnected: '{{connected}}/{{total}} 已连接',
+    cloudProfiles: '云配置文件',
+    cloudProfilesDescription: '来自 Bambu Cloud 的耗材、打印机和工艺预设',
+    appSettings: '应用设置',
+    appSettingsDescription: 'Bambuddy 配置(完整数据库)',
+    lastBackupAt: '上次备份:',
+    noBackupsYet: '尚无备份',
+    next: '下次:',
+    startingBackup: '正在启动备份...',
+    test: '测试',
+    enableBackup: '启用备份',
+    testConnection: '测试连接',
+    enterRepoUrl: '请输入仓库 URL',
+    enterRepoAndToken: '请输入仓库 URL 和访问令牌',
+    repoRequired: '仓库 URL 为必填项',
+    tokenRequired: '访问令牌为必填项',
+    githubBackupEnabled: 'GitHub 备份已启用',
+    tokenUpdated: '令牌已更新',
+    settingsSaved: '设置已保存',
+    failedToSave: '保存失败:{{message}}',
+    backupCompleteFiles: '备份完成 - {{count}} 个文件已更新',
+    backupSkippedNoChanges: '备份已跳过 - 无更改',
+    backupFailed2: '备份失败:{{message}}',
+    clearedLogs: '已清除 {{count}} 条日志',
+    failedToClearLogs: '清除日志失败:{{message}}',
+
+    // History
+    history: '历史记录',
+    clear: '清除',
+    date: '日期',
+    status: '状态',
+    commit: '提交',
+
+    // Local Backup
+    localBackup: '本地备份',
+    localBackupDescription: '创建 Bambuddy 数据的完整备份,包括数据库、档案、上传和所有文件。',
+    downloadBackupLabel: '下载备份',
+    completeBackupZip: '完整备份:数据库 + 所有文件(ZIP)',
+    download: '下载',
+    preparingBackup: '正在准备备份...',
+    creatingArchive: '正在创建备份归档...对于大型归档可能需要一些时间。',
+    downloadingFile: '正在下载备份文件...',
+    backupDownloaded: '备份下载成功',
+    failedToCreateBackup: '创建备份失败:{{message}}',
+    restore: '恢复',
+    restoreReplacesAll: '恢复将替换所有数据。',
+    restoreReplacesAllDetail: '您当前的数据库和文件将被完全替换。恢复后需要重启。',
+    restoreConfirmTitle: '恢复备份',
+    restoreConfirmMessage: '您确定要从"{{filename}}"恢复吗?这将完全替换您当前的数据库和所有文件。恢复后需要重启应用程序。',
+    restoreConfirmButton: '恢复备份',
+    uploadingFile: '正在上传备份文件...',
+    backupRestoredRestart: '备份已恢复。请重启 Bambuddy。',
+    failedToRestore: '恢复备份失败。请检查文件格式。',
+    reloadNow: '立即重新加载',
+    creatingBackup: '正在创建备份',
+    restoringBackup: '正在恢复备份',
+    preparing: '准备中...',
+    processing: '处理中...',
+    doNotClosePage: '请不要关闭此页面或导航离开。对于大型备份,此操作可能需要几分钟。',
+
+    // RestoreModal
+    restoring: '恢复中...',
+    restoreComplete: '恢复完成',
+    restoreFailed2: '恢复失败',
+    importSettings: '从备份文件导入设置',
+    pleaseWaitRestoring: '请等待数据恢复中',
+    selectBackupFile: '点击选择备份文件(.json 或 .zip)',
+    duplicateHandling: '重复项处理方式:',
+    matchPrinters: '打印机',
+    matchPrintersBy: '按序列号匹配',
+    matchSmartPlugs: '智能插座',
+    matchSmartPlugsBy: '按 IP 地址匹配',
+    matchNotificationProviders: '通知提供者',
+    matchNotificationProvidersBy: '按名称匹配',
+    matchFilaments: '耗材',
+    matchFilamentsBy: '按名称 + 类型 + 品牌匹配',
+    matchArchives: '档案',
+    matchArchivesBy: '按内容哈希匹配(始终跳过)',
+    matchPendingUploads: '待上传',
+    matchPendingUploadsBy: '按文件名匹配',
+    matchSettingsTemplates: '设置和模板',
+    matchSettingsTemplatesBy: '始终覆盖',
+    replaceExisting: '替换现有数据',
+    keepExisting: '保留现有数据',
+    overwriteDescription: '用备份数据覆盖已存在的项目',
+    keepDescription: '仅恢复尚不存在的项目',
+    overwriteCaution: '注意:',
+    overwriteWarning: '覆盖将用备份数据替换您当前的配置。出于安全考虑,打印机访问代码永远不会被覆盖。',
+    cancel: '取消',
+    processingBackup: '正在处理备份文件...',
+    itemsRestored: '已恢复项目',
+    itemsSkipped: '已跳过项目',
+    restored: '已恢复',
+    skippedAlreadyExist: '已跳过(已存在)',
+    filesCategory: '文件(3MF、缩略图等)',
+    andMore: '...还有 {{count}} 项',
+    newApiKeysGenerated: '已生成新的 API 密钥',
+    keysShownOnce: '这些密钥仅显示一次。请立即复制!',
+    copy: '复制',
+    noDataFound: '在备份文件中未找到可恢复的数据。',
+    close: '关闭',
+
+    // Category labels
+    categories: {
+      settings: '设置',
+      notification_providers: '通知提供者',
+      notification_templates: '通知模板',
+      smart_plugs: '智能插座',
+      printers: '打印机',
+      filaments: '耗材',
+      maintenance_types: '维护类型',
+      archives: '档案',
+      projects: '项目',
+      pending_uploads: '待上传',
+      external_links: '外部链接',
+      api_keys: 'API 密钥',
+    },
   },
   },
 
 
   // Tags
   // Tags
@@ -3250,6 +3530,319 @@ export default {
         mqttSameAsPower: '与功率主题相同,或不同',
         mqttSameAsPower: '与功率主题相同,或不同',
       },
       },
     },
     },
+    // SmartPlugCard
+    linkedTo: '关联到:',
+    monitorOnly: '仅监控',
+    alerts: '警报',
+    scheduleOn: '开启 {{time}}',
+    scheduleOff: '关闭 {{time}}',
+    on: '开启',
+    off: '关闭',
+    power: '功率',
+    kwhToday: '今日kWh',
+    settings: '设置',
+    automationSettings: '自动化设置',
+    showInSwitchbar: '在开关栏显示',
+    quickAccessSidebar: '从侧边栏快速访问',
+    enabled: '已启用',
+    enableAutomation: '为此插座启用自动化',
+    autoOn: '自动开启',
+    autoOnDescription: '打印开始时开启',
+    autoOff: '自动关闭',
+    autoOffDescription: '打印完成时关闭(一次性)',
+    turnOffDelayMode: '关闭延迟模式',
+    time: '时间',
+    temp: '温度',
+    delayMinutes: '延迟(分钟)',
+    tempThreshold: '温度阈值(°C)',
+    tempThresholdDescription: '当喷嘴冷却到此温度以下时关闭',
+    edit: '编辑',
+    deleteConfirm: '确定要删除"{{name}}"吗?此操作无法撤销。',
+    turnOnConfirm: '确定要开启"{{name}}"吗?',
+    turnOffConfirm: '确定要关闭"{{name}}"吗?这将切断连接设备的电源。',
+    failedToTurn: '无法{{action}}"{{name}}"',
+    unknown: '未知',
+    // AddSmartPlugModal
+    addTitle: '添加智能插座',
+    editTitle: '编辑智能插座',
+    stopScanning: '停止扫描',
+    discoverTasmota: '发现Tasmota设备',
+    foundDevices: '找到{{count}}个设备 - 点击选择:',
+    noDevicesFound: '未在您的网络中找到Tasmota设备',
+    haNotConfigured: 'Home Assistant未配置。请在以下位置设置',
+    haSettingsPath: '设置 → 网络 → Home Assistant',
+    selectEntity: '选择实体 *',
+    ipAddress: 'IP地址 *',
+    nameLabel: '名称 *',
+    username: '用户名',
+    password: '密码',
+    authHint: '如果您的Tasmota设备不需要认证,请留空',
+    linkToPrinter: '关联打印机',
+    noPrinter: '无打印机(仅手动控制)',
+    linkingDescription: '关联后可在打印开始/完成时自动开关',
+    powerAlerts: '功率警报',
+    alertAbove: '高于时警报(W)',
+    alertBelow: '低于时警报(W)',
+    alertDescription: '当电力消耗超过这些阈值时收到通知。留空以禁用该方向。',
+    dailySchedule: '每日计划',
+    turnOnAt: '开启时间',
+    turnOffAt: '关闭时间',
+    scheduleDescription: '每天在这些时间自动开关插座。留空以跳过该操作。',
+    showOnPrinterCard: '在打印机卡片上显示',
+    displayOnPrinterCard: '在打印机卡片上显示按钮',
+    connectedResult: '已连接!',
+    deviceLabel: '设备:{{name}} - ',
+    stateLabel: '状态:{{state}}',
+    test: '测试',
+    delete: '删除',
+    save: '保存',
+    add: '添加',
+    cancel: '取消',
+    failedToStartScan: '无法开始扫描',
+    nameRequired: '名称为必填项',
+    entityRequired: 'Home Assistant插座需要实体',
+    mqttTopicRequired: '必须为功率、能源或状态监控配置至少一个MQTT主题',
+    loadingEntities: '正在加载实体...',
+    loading: '加载中...',
+    failedToLoadEntities: '加载实体失败:{{error}}',
+    noEntitiesMatching: '未找到匹配"{{search}}"的实体',
+    noEntitiesAvailable: '无可用实体',
+    searchingEntities: '搜索所有实体(找到{{count}}个)',
+    showingEntities: '显示 switch、light、input_boolean({{count}}个可用)',
+    energyMonitoringOptional: '能源监控(可选)',
+    energyMonitoringHint: '搜索并选择提供功率/能源数据的传感器。',
+    powerSensorW: '功率传感器(W)',
+    energyTodayKwh: '今日能源(kWh)',
+    totalEnergyKwh: '总能源(kWh)',
+    noMatchingSensors: '无匹配的传感器',
+    none: '无',
+    mqttNotConfigured: 'MQTT代理未配置。请在以下位置设置代理地址',
+    mqttSettingsPath: '设置 → 网络 → MQTT发布',
+    mqttNotConfiguredSuffix: '(您不需要启用发布,只需填写代理详细信息)。',
+    mqttMonitorOnlyDescription: 'MQTT插座通过MQTT订阅接收功率/能源数据。开关控制不可用 - 请使用您的MQTT代理或家庭自动化系统。',
+    powerMonitoring: '功率监控',
+    energyMonitoring: '能源监控',
+    stateMonitoring: '状态监控',
+    optional: '可选',
+    topic: '主题',
+    jsonPath: 'JSON路径',
+    multiplier: '乘数',
+    onValue: 'ON值',
+    mqttPowerHint: 'JSON路径从JSON负载中提取值(例如"power_l1")。如果主题发布原始数值,请留空。\n乘数:mW→W使用0.001,kW→W使用1000。',
+    mqttEnergyHint: 'JSON路径从JSON负载中提取值。原始值请留空。\n乘数:Wh→kWh使用0.001,MWh→kWh使用1000。',
+    mqttStateHint: 'JSON路径从JSON负载中提取值。原始值请留空。\nON值:表示"ON"的确切字符串。留空以自动检测(ON、true、1)。',
+    noSwitchesInSwitchbar: '开关栏中没有开关',
+    enableSwitchbarHint: '在设置 > 智能插座中启用"在开关栏显示"',
+  },
+
+  // Notifications
+  notifications: {
+    // Provider types
+    providerTypes: {
+      callmebot: 'CallMeBot/WhatsApp',
+      ntfy: 'ntfy',
+      pushover: 'Pushover',
+      telegram: 'Telegram',
+      email: '电子邮件',
+      discord: 'Discord',
+      webhook: 'Webhook',
+    },
+    // Provider descriptions
+    providerDescriptions: {
+      email: 'SMTP 电子邮件通知',
+      telegram: '通过 Telegram 机器人发送通知',
+      discord: '通过 Webhook 发送到 Discord 频道',
+      ntfy: '免费、可自托管的推送通知',
+      pushover: '简单、可靠的推送通知',
+      callmebot: '通过 CallMeBot 免费发送 WhatsApp 通知',
+      webhook: '通用 HTTP POST 到任意 URL',
+    },
+    // NotificationProviderCard
+    lastSuccess: '上次:{{date}}',
+    error: '错误',
+    printer: '打印机:',
+    allPrinters: '所有打印机',
+    sendTestNotification: '发送测试通知',
+    eventSettings: '事件设置',
+    enabled: '已启用',
+    sendFromProvider: '从此提供商发送通知',
+    // Event categories
+    printEvents: '打印事件',
+    printerStatus: '打印机状态',
+    amsAlarms: 'AMS 警报',
+    amsHtAlarms: 'AMS-HT 警报',
+    printQueue: '打印队列',
+    // Event tags (badges)
+    start: '开始',
+    plateCheck: '热床检测',
+    complete: '完成',
+    failed: '失败',
+    stopped: '已停止',
+    progress: '进度',
+    offline: '离线',
+    lowFilament: '耗材不足',
+    maintenance: '维护',
+    amsHumidity: 'AMS 湿度',
+    amsTemp: 'AMS 温度',
+    amsHtHumidity: 'AMS-HT 湿度',
+    amsHtTemp: 'AMS-HT 温度',
+    bedCooled: '热床已冷却',
+    quiet: '免打扰',
+    digest: '摘要 {{time}}',
+    // Event labels (expanded settings)
+    printStarted: '打印已开始',
+    plateNotEmpty: '热床非空',
+    plateNotEmptyDescription: '打印前检测到物体',
+    printCompleted: '打印已完成',
+    bedCooledLabel: '热床已冷却',
+    bedCooledDescription: '打印后热床温度降至阈值以下',
+    printFailed: '打印失败',
+    printStopped: '打印已停止',
+    progressMilestones: '进度里程碑',
+    progressMilestonesDescription: '在 25%、50%、75% 时通知',
+    printerOffline: '打印机离线',
+    printerError: '打印机错误',
+    lowFilamentLabel: '耗材不足',
+    maintenanceDue: '需要维护',
+    maintenanceDueDescription: '需要维护时通知',
+    amsHumidityHigh: 'AMS 湿度过高',
+    amsHumidityHighDescription: '普通 AMS 湿度超过阈值',
+    amsTemperatureHigh: 'AMS 温度过高',
+    amsTemperatureHighDescription: '普通 AMS 温度超过阈值',
+    amsHtHumidityHigh: 'AMS-HT 湿度过高',
+    amsHtHumidityHighDescription: 'AMS-HT 湿度超过阈值',
+    amsHtTemperatureHigh: 'AMS-HT 温度过高',
+    amsHtTemperatureHighDescription: 'AMS-HT 温度超过阈值',
+    // Queue events
+    jobAdded: '任务已添加',
+    jobAddedDescription: '任务已添加到队列',
+    jobAssigned: '任务已分配',
+    jobAssignedDescription: '基于模型的任务已分配给打印机',
+    jobStarted: '任务已开始',
+    jobStartedDescription: '队列任务已开始打印',
+    jobWaiting: '任务等待中',
+    jobWaitingDescription: '任务正在等待耗材',
+    jobSkipped: '任务已跳过',
+    jobSkippedDescription: '任务已跳过(上一个失败)',
+    jobFailed: '任务失败',
+    jobFailedDescription: '任务启动失败',
+    queueComplete: '队列已完成',
+    queueCompleteDescription: '所有队列任务已完成',
+    // Quiet hours
+    quietHours: '免打扰时段',
+    noNotificationsDuring: '在此时段内不发送通知',
+    editProviderToChangeQuietHours: '编辑提供商以更改免打扰时段',
+    // Daily digest
+    dailyDigest: '每日摘要',
+    batchNotifications: '将通知汇总为每日摘要',
+    sendAt: '发送于 {{time}}',
+    editProviderToChangeDigestTime: '编辑提供商以更改摘要时间',
+    // Actions
+    edit: '编辑',
+    deleteProvider: '删除通知提供商',
+    deleteConfirm: '确定要删除"{{name}}"吗?此操作无法撤销。',
+    delete: '删除',
+    // AddNotificationModal
+    addTitle: '添加通知提供商',
+    editTitle: '编辑通知提供商',
+    nameLabel: '名称 *',
+    namePlaceholder: '我的通知',
+    providerTypeLabel: '提供商类型 *',
+    configuration: '配置',
+    testConfiguration: '测试配置',
+    printerFilter: '打印机筛选',
+    onlyFromPrinter: '仅发送来自此打印机的事件通知',
+    quietHoursDnd: '免打扰时段',
+    quietStart: '开始',
+    quietEnd: '结束',
+    dailyDigestLabel: '每日摘要',
+    sendDigestAt: '发送摘要于',
+    digestCollected: '事件将被收集并在此时间作为单条摘要发送',
+    notificationEvents: '通知事件',
+    progressPercent: '(25%、50%、75%)',
+    bedCooledAfterPrint: '(打印完成后)',
+    cancel: '取消',
+    save: '保存',
+    add: '添加',
+    nameRequired: '名称为必填项',
+    fieldRequired: '{{field}}为必填项',
+    // Config field labels
+    phoneNumber: '电话号码',
+    apiKey: 'API 密钥',
+    serverUrl: '服务器 URL',
+    topic: '主题',
+    authToken: '认证令牌',
+    userKey: '用户密钥',
+    appToken: '应用令牌',
+    priority: '优先级',
+    botToken: '机器人令牌',
+    chatId: '聊天 ID',
+    smtpServer: 'SMTP 服务器',
+    smtpPort: 'SMTP 端口',
+    security: '安全',
+    authentication: '认证',
+    username: '用户名',
+    password: '密码',
+    fromEmail: '发件人邮箱',
+    toEmail: '收件人邮箱',
+    webhookUrl: 'Webhook URL',
+    payloadFormat: '负载格式',
+    authorization: '授权',
+    titleFieldName: '标题字段名',
+    messageFieldName: '消息字段名',
+    // NotificationTemplateEditor
+    editTemplate: '编辑模板:{{name}}',
+    titleLabel: '标题',
+    bodyLabel: '正文',
+    titlePlaceholder: '通知标题...',
+    bodyPlaceholder: '通知正文...',
+    availableVariables: '可用变量',
+    clickToInsert: '点击插入到正文光标位置',
+    livePreview: '实时预览',
+    hide: '隐藏',
+    show: '显示',
+    loadingPreview: '加载预览中...',
+    enterTemplateContent: '输入模板内容以查看预览',
+    titlePreview: '标题:',
+    bodyPreview: '正文:',
+    resetToDefault: '恢复默认',
+    titleRequired: '标题为必填项',
+    bodyRequired: '正文为必填项',
+    // NotificationLogViewer
+    notificationLog: '通知日志',
+    showFailedOnly: '仅显示失败',
+    last24Hours: '最近 24 小时',
+    last7Days: '最近 7 天',
+    last30Days: '最近 30 天',
+    last90Days: '最近 90 天',
+    justNow: '刚刚',
+    noFailedNotifications: '没有失败的通知',
+    noNotificationsLogged: '没有通知记录',
+    unknownProvider: '未知提供商',
+    logTitle: '标题',
+    logMessage: '消息',
+    logError: '错误',
+    logProvider: '提供商:{{type}}',
+    logTime: '时间:{{time}}',
+    refresh: '刷新',
+    clearOld: '清除旧记录',
+    statsSummary: '最近 {{days}} 天:',
+    statsNotifications: '条通知',
+    statsSent: '{{count}} 条已发送',
+    statsFailed: '{{count}} 条失败',
+    // Event type labels (for log viewer)
+    eventTypes: {
+      print_start: '打印已开始',
+      print_complete: '打印完成',
+      print_failed: '打印失败',
+      print_stopped: '打印已停止',
+      print_progress: '进度',
+      printer_offline: '打印机离线',
+      printer_error: '打印机错误',
+      filament_low: '耗材不足',
+      maintenance_due: '需要维护',
+      test: '测试',
+    },
   },
   },
 
 
   // Rich Text Editor
   // Rich Text Editor

Plik diff jest za duży
+ 154 - 156
frontend/src/pages/SettingsPage.tsx


Plik diff jest za duży
+ 0 - 0
static/assets/index-CskDBRt4.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CS7BiFsF.js"></script>
+    <script type="module" crossorigin src="/assets/index-CskDBRt4.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DOJtH8DG.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DOJtH8DG.css">
   </head>
   </head>
   <body>
   <body>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików