Browse Source

Merge pull request #31 from cadtoolbox/copilot/update-static-text-for-i18n

Internationalize Advanced Authentication strings
Thomas Rambach 3 months ago
parent
commit
994843e24a

+ 11 - 9
frontend/src/components/AddSmartPlugModal.tsx

@@ -1,6 +1,7 @@
 import { useState, useEffect, useRef } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Radio, Eye } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
 import { Button } from './Button';
@@ -11,6 +12,7 @@ interface AddSmartPlugModalProps {
 }
 
 export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const isEditing = !!plug;
 
@@ -469,7 +471,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               {isScanning && scanProgress.total > 0 && (
                 <div className="space-y-1">
                   <div className="flex justify-between text-xs text-bambu-gray">
-                    <span>Scanning network...</span>
+                    <span>{t('smartPlugs.addSmartPlug.scanningNetwork')}</span>
                     <span>{scanProgress.scanned} / {scanProgress.total}</span>
                   </div>
                   <div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
@@ -538,7 +540,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                       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"
                     >
-                      <option>Choose an entity...</option>
+                      <option>{t('smartPlugs.addSmartPlug.chooseEntity')}</option>
                     </select>
                   </div>
                 </div>
@@ -585,7 +587,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                               setIsEntityDropdownOpen(true);
                               setHaEntitySearch('');
                             }}
-                            placeholder="Search entities..."
+                            placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEntities')}
                             className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                           />
                           {haEntityId && !isEntityDropdownOpen && (
@@ -697,7 +699,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsPowerDropdownOpen(true);
                                   setPowerSensorSearch('');
                                 }}
-                                placeholder="Search power sensors..."
+                                placeholder={t('smartPlugs.addSmartPlug.placeholders.searchPowerSensors')}
                                 className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                               />
                               {haPowerEntity && !isPowerDropdownOpen && (
@@ -781,7 +783,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsEnergyTodayDropdownOpen(true);
                                   setEnergyTodaySearch('');
                                 }}
-                                placeholder="Search energy sensors..."
+                                placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
                                 className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                               />
                               {haEnergyTodayEntity && !isEnergyTodayDropdownOpen && (
@@ -865,7 +867,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                                   setIsEnergyTotalDropdownOpen(true);
                                   setEnergyTotalSearch('');
                                 }}
-                                placeholder="Search energy sensors..."
+                                placeholder={t('smartPlugs.addSmartPlug.placeholders.searchEnergySensors')}
                                 className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                               />
                               {haEnergyTotalEntity && !isEnergyTotalDropdownOpen && (
@@ -1060,7 +1062,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                           type="text"
                           value={mqttStateOnValue}
                           onChange={(e) => setMqttStateOnValue(e.target.value)}
-                          placeholder="ON, true, 1"
+                          placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttStateOnValue')}
                           className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
                         />
                       </div>
@@ -1128,7 +1130,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               ) : (
                 <>
                   <WifiOff className="w-5 h-5" />
-                  <span>Connection failed</span>
+                  <span>{t('smartPlugs.addSmartPlug.connectionFailed')}</span>
                 </>
               )}
             </div>
@@ -1141,7 +1143,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
               type="text"
               value={name}
               onChange={(e) => setName(e.target.value)}
-              placeholder="Living Room Plug"
+              placeholder={t('smartPlugs.addSmartPlug.placeholders.plugName')}
               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>

+ 7 - 5
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -1,6 +1,7 @@
 import { useState, useMemo, useEffect, useCallback } from 'react';
 import { useQuery, useMutation } from '@tanstack/react-query';
 import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { KProfile } from '../api/client';
 import { Button } from './Button';
@@ -216,6 +217,7 @@ export function ConfigureAmsSlotModal({
   nozzleDiameter = '0.4',
   onSuccess,
 }: ConfigureAmsSlotModalProps) {
+  const { t } = useTranslation();
   const [selectedPresetId, setSelectedPresetId] = useState<string>('');
   const [selectedKProfile, setSelectedKProfile] = useState<KProfile | null>(null);
   const [colorHex, setColorHex] = useState<string>(''); // Just the 6-char hex, no alpha
@@ -569,7 +571,7 @@ export function ConfigureAmsSlotModal({
               <div className="text-center space-y-3">
                 <CheckCircle2 className="w-16 h-16 text-bambu-green mx-auto" />
                 <p className="text-lg font-semibold text-white">Slot Configured!</p>
-                <p className="text-sm text-bambu-gray">Settings sent to printer</p>
+                <p className="text-sm text-bambu-gray">{t('configureAmsSlot.settingsSentToPrinter')}</p>
               </div>
             </div>
           )}
@@ -607,7 +609,7 @@ export function ConfigureAmsSlotModal({
                 <div className="relative">
                   <input
                     type="text"
-                    placeholder="Search presets..."
+                    placeholder={t('configureAmsSlot.searchPresets')}
                     value={searchQuery}
                     onChange={(e) => setSearchQuery(e.target.value)}
                     className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none mb-2"
@@ -634,7 +636,7 @@ export function ConfigureAmsSlotModal({
                             <span className="text-white text-sm truncate">{preset.name}</span>
                             {isUserPreset(preset.setting_id) && (
                               <span className="text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue">
-                                Custom
+                                {t('configureAmsSlot.custom')}
                               </span>
                             )}
                           </div>
@@ -750,7 +752,7 @@ export function ConfigureAmsSlotModal({
                   />
                   <input
                     type="text"
-                    placeholder="Color name or hex (e.g., brown, FF8800)"
+                    placeholder={t('configureAmsSlot.colorPlaceholder')}
                     value={colorInput}
                     onChange={(e) => {
                       const input = e.target.value;
@@ -780,7 +782,7 @@ export function ConfigureAmsSlotModal({
                         setColorInput('');
                       }}
                       className="px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded"
-                      title="Clear custom color"
+                      title={t('configureAmsSlot.clearCustomColor')}
                     >
                       Clear
                     </button>

+ 12 - 12
frontend/src/components/EmailSettings.tsx

@@ -54,7 +54,7 @@ export function EmailSettings() {
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['smtpSettings'] });
       queryClient.invalidateQueries({ queryKey: ['advancedAuthStatus'] });
-      showToast('SMTP settings saved successfully', 'success');
+      showToast(t('settings.email.success.settingsSaved'), 'success');
     },
     onError: (error: Error) => {
       showToast(error.message, 'error');
@@ -88,12 +88,12 @@ export function EmailSettings() {
   const handleSave = () => {
     // Validate required fields
     if (!smtpSettings.smtp_host || !smtpSettings.smtp_from_email) {
-      showToast('Please fill in all required fields', 'error');
+      showToast(t('settings.email.errors.requiredFields'), 'error');
       return;
     }
     // Validate auth fields when authentication is enabled
     if (smtpSettings.smtp_auth_enabled && (!smtpSettings.smtp_username)) {
-      showToast('Username is required when authentication is enabled', 'error');
+      showToast(t('settings.email.errors.usernameRequired'), 'error');
       return;
     }
     saveMutation.mutate(smtpSettings);
@@ -101,16 +101,16 @@ export function EmailSettings() {
 
   const handleTest = () => {
     if (!testEmail) {
-      showToast('Please enter a test email address', 'error');
+      showToast(t('settings.email.errors.enterTestEmail'), 'error');
       return;
     }
     if (!smtpSettings.smtp_host || !smtpSettings.smtp_from_email) {
-      showToast('Please fill in SMTP Server and From Email before testing', 'error');
+      showToast(t('settings.email.errors.smtpServerAndEmail'), 'error');
       return;
     }
     // Validate auth fields when authentication is enabled
     if (smtpSettings.smtp_auth_enabled && (!smtpSettings.smtp_username || !smtpSettings.smtp_password)) {
-      showToast('Username and Password are required when authentication is enabled', 'error');
+      showToast(t('settings.email.errors.usernamePasswordRequired'), 'error');
       return;
     }
     testMutation.mutate({
@@ -127,7 +127,7 @@ export function EmailSettings() {
 
   const handleToggleAdvancedAuth = () => {
     if (!advancedAuthStatus?.advanced_auth_enabled && !advancedAuthStatus?.smtp_configured) {
-      showToast('Please configure and test SMTP settings first', 'error');
+      showToast(t('settings.email.errors.configureSmtpFirst'), 'error');
       return;
     }
     toggleAdvancedAuthMutation.mutate(!advancedAuthStatus?.advanced_auth_enabled);
@@ -189,9 +189,9 @@ export function EmailSettings() {
                   onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_security: e.target.value as 'starttls' | 'ssl' | 'none' })}
                   className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
                 >
-                  <option value="starttls">STARTTLS (Port 587)</option>
-                  <option value="ssl">SSL/TLS (Port 465)</option>
-                  <option value="none">None (Port 25)</option>
+                  <option value="starttls">{t('settings.email.securityOptions.starttls')}</option>
+                  <option value="ssl">{t('settings.email.securityOptions.ssl')}</option>
+                  <option value="none">{t('settings.email.securityOptions.none')}</option>
                 </select>
               </div>
               <div>
@@ -203,8 +203,8 @@ export function EmailSettings() {
                   onChange={(e) => setSMTPSettings({ ...smtpSettings, smtp_auth_enabled: e.target.value === 'true' })}
                   className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
                 >
-                  <option value="true">Enabled</option>
-                  <option value="false">Disabled</option>
+                  <option value="true">{t('settings.email.authOptions.enabled')}</option>
+                  <option value="false">{t('settings.email.authOptions.disabled')}</option>
                 </select>
               </div>
             </div>

+ 5 - 3
frontend/src/components/ExternalLinksSettings.tsx

@@ -1,6 +1,7 @@
 import { useState } from 'react';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { Link2, Plus, Pencil, Trash2, GripVertical, Loader2, ExternalLink as ExternalLinkIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { ExternalLink } from '../api/client';
 import { Card, CardContent, CardHeader } from './Card';
@@ -10,6 +11,7 @@ import { ConfirmModal } from './ConfirmModal';
 import { getIconByName } from './IconPicker';
 
 export function ExternalLinksSettings() {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const [showAddModal, setShowAddModal] = useState(false);
   const [editingLink, setEditingLink] = useState<ExternalLink | null>(null);
@@ -132,7 +134,7 @@ export function ExternalLinksSettings() {
                       <button
                         onClick={() => setEditingLink(link)}
                         className="p-2 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
-                        title="Edit"
+                        title={t('common.edit')}
                       >
                         <Pencil className="w-4 h-4" />
                       </button>
@@ -140,7 +142,7 @@ export function ExternalLinksSettings() {
                         onClick={() => handleDelete(link)}
                         disabled={deleteMutation.isPending}
                         className="p-2 rounded-lg hover:bg-red-500/20 text-bambu-gray hover:text-red-400 transition-colors disabled:opacity-50"
-                        title="Delete"
+                        title={t('externalLinks.deleteLink')}
                       >
                         <Trash2 className="w-4 h-4" />
                       </button>
@@ -152,7 +154,7 @@ export function ExternalLinksSettings() {
           ) : (
             <div className="text-center py-8 text-bambu-gray">
               <Link2 className="w-8 h-8 mx-auto mb-2 opacity-50" />
-              <p>No external links configured</p>
+              <p>{t('externalLinks.noLinksConfigured')}</p>
               <p className="text-sm">Click "Add Link" to add one</p>
             </div>
           )}

+ 12 - 10
frontend/src/components/RichTextEditor.tsx

@@ -18,6 +18,7 @@ import {
   Link as LinkIcon,
   Unlink,
 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 
 interface RichTextEditorProps {
   content: string;
@@ -26,6 +27,7 @@ interface RichTextEditorProps {
 }
 
 export function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
+  const { t } = useTranslation();
   const editor = useEditor({
     extensions: [
       StarterKit.configure({
@@ -105,21 +107,21 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleBold().run()}
           isActive={editor.isActive('bold')}
-          title="Bold"
+          title={t('richTextEditor.bold')}
         >
           <Bold className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleItalic().run()}
           isActive={editor.isActive('italic')}
-          title="Italic"
+          title={t('richTextEditor.italic')}
         >
           <Italic className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleUnderline().run()}
           isActive={editor.isActive('underline')}
-          title="Underline"
+          title={t('richTextEditor.underline')}
         >
           <UnderlineIcon className="w-4 h-4" />
         </ToolbarButton>
@@ -129,14 +131,14 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleBulletList().run()}
           isActive={editor.isActive('bulletList')}
-          title="Bullet List"
+          title={t('richTextEditor.bulletList')}
         >
           <List className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().toggleOrderedList().run()}
           isActive={editor.isActive('orderedList')}
-          title="Numbered List"
+          title={t('richTextEditor.numberedList')}
         >
           <ListOrdered className="w-4 h-4" />
         </ToolbarButton>
@@ -146,21 +148,21 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={() => editor.chain().focus().setTextAlign('left').run()}
           isActive={editor.isActive({ textAlign: 'left' })}
-          title="Align Left"
+          title={t('richTextEditor.alignLeft')}
         >
           <AlignLeft className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().setTextAlign('center').run()}
           isActive={editor.isActive({ textAlign: 'center' })}
-          title="Align Center"
+          title={t('richTextEditor.alignCenter')}
         >
           <AlignCenter className="w-4 h-4" />
         </ToolbarButton>
         <ToolbarButton
           onClick={() => editor.chain().focus().setTextAlign('right').run()}
           isActive={editor.isActive({ textAlign: 'right' })}
-          title="Align Right"
+          title={t('richTextEditor.alignRight')}
         >
           <AlignRight className="w-4 h-4" />
         </ToolbarButton>
@@ -170,14 +172,14 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
         <ToolbarButton
           onClick={setLink}
           isActive={editor.isActive('link')}
-          title="Add Link"
+          title={t('richTextEditor.addLink')}
         >
           <LinkIcon className="w-4 h-4" />
         </ToolbarButton>
         {editor.isActive('link') && (
           <ToolbarButton
             onClick={() => editor.chain().focus().unsetLink().run()}
-            title="Remove Link"
+            title={t('richTextEditor.removeLink')}
           >
             <Unlink className="w-4 h-4" />
           </ToolbarButton>

+ 10 - 8
frontend/src/components/SmartPlugCard.tsx

@@ -1,6 +1,7 @@
 import { useState } from 'react';
 import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
 import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Radio, Eye } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
 import { api } from '../api/client';
 import type { SmartPlug, SmartPlugUpdate } from '../api/client';
 import { Card, CardContent } from './Card';
@@ -14,6 +15,7 @@ interface SmartPlugCardProps {
 }
 
 export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
+  const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -171,7 +173,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
               ) : (
                 <div className="flex items-center gap-1 text-sm text-status-error">
                   <WifiOff className="w-4 h-4" />
-                  <span>Offline</span>
+                  <span>{t('smartPlugs.offline')}</span>
                 </div>
               )}
               {/* Admin page link - only for Tasmota */}
@@ -181,10 +183,10 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
                   target="_blank"
                   rel="noopener noreferrer"
                   className="flex items-center gap-1 px-2 py-0.5 bg-bambu-dark hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white text-xs rounded-full transition-colors"
-                  title="Open plug admin page"
+                  title={t('smartPlugs.openPlugAdminPage')}
                 >
                   <ExternalLink className="w-3 h-3" />
-                  Admin
+                  {t('smartPlugs.admin')}
                 </a>
               )}
             </div>
@@ -449,7 +451,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Delete Confirmation */}
       {showDeleteConfirm && (
         <ConfirmModal
-          title="Delete Smart Plug"
+          title={t('smartPlugs.deleteSmartPlug')}
           message={`Are you sure you want to delete "${plug.name}"? This cannot be undone.`}
           confirmText="Delete"
           variant="danger"
@@ -464,9 +466,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Power On Confirmation */}
       {showPowerOnConfirm && (
         <ConfirmModal
-          title="Turn On Smart Plug"
+          title={t('smartPlugs.turnOnSmartPlug')}
           message={`Are you sure you want to turn on "${plug.name}"?`}
-          confirmText="Turn On"
+          confirmText={t('smartPlugs.turnOn')}
           variant="default"
           onConfirm={() => {
             controlMutation.mutate('on');
@@ -479,9 +481,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
       {/* Power Off Confirmation */}
       {showPowerOffConfirm && (
         <ConfirmModal
-          title="Turn Off Smart Plug"
+          title={t('smartPlugs.turnOffSmartPlug')}
           message={`Are you sure you want to turn off "${plug.name}"? This will cut power to the connected device.`}
-          confirmText="Turn Off"
+          confirmText={t('smartPlugs.turnOff')}
           variant="danger"
           onConfirm={() => {
             controlMutation.mutate('off');

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

@@ -1030,6 +1030,30 @@ export default {
       feature2: 'Benutzer können sich mit Benutzername oder E-Mail anmelden',
       feature3: 'Passwort vergessen Funktion ist verfügbar',
       feature4: 'Administratoren können Benutzerpasswörter per E-Mail zurücksetzen',
+      // Error messages
+      errors: {
+        requiredFields: 'Bitte füllen Sie alle Pflichtfelder aus',
+        usernameRequired: 'Benutzername ist erforderlich, wenn Authentifizierung aktiviert ist',
+        enterTestEmail: 'Bitte geben Sie eine Test-E-Mail-Adresse ein',
+        smtpServerAndEmail: 'Bitte füllen Sie SMTP-Server und Absender-E-Mail aus, bevor Sie testen',
+        usernamePasswordRequired: 'Benutzername und Passwort sind erforderlich, wenn Authentifizierung aktiviert ist',
+        configureSmtpFirst: 'Bitte konfigurieren und testen Sie zuerst die SMTP-Einstellungen',
+      },
+      // Success messages
+      success: {
+        settingsSaved: 'SMTP-Einstellungen erfolgreich gespeichert',
+      },
+      // Security options
+      securityOptions: {
+        starttls: 'STARTTLS (Port 587)',
+        ssl: 'SSL/TLS (Port 465)',
+        none: 'Keine (Port 25)',
+      },
+      // Authentication options
+      authOptions: {
+        enabled: 'Aktiviert',
+        disabled: 'Deaktiviert',
+      },
     },
     appearance: 'Erscheinungsbild',
     notifications: 'Benachrichtigungen',
@@ -2286,6 +2310,9 @@ export default {
     noPrintersAvailable: 'Keine Drucker verfügbar',
     printerBusy: 'Drucker ist beschäftigt',
     printerOffline: 'Drucker ist offline',
+    sameTypeDifferentColor: 'Gleicher Typ, andere Farbe',
+    filamentTypeNotLoaded: 'Filamenttyp nicht geladen',
+    openCalendar: 'Kalender öffnen',
   },
 
   // Backup
@@ -2640,4 +2667,348 @@ export default {
     replaceCarbonFilter: 'Aktivkohlefilter ersetzen',
     lubricateLeftNozzleRail: 'Linke Düsenschiene schmieren (H2-Serie)',
   },
+
+  // Smart Plugs
+  smartPlugs: {
+    offline: 'Offline',
+    admin: 'Admin',
+    openPlugAdminPage: 'Plug-Admin-Seite öffnen',
+    deleteSmartPlug: 'Smart Plug löschen',
+    turnOnSmartPlug: 'Smart Plug einschalten',
+    turnOffSmartPlug: 'Smart Plug ausschalten',
+    turnOn: 'Einschalten',
+    turnOff: 'Ausschalten',
+    addSmartPlug: {
+      scanningNetwork: 'Netzwerk wird durchsucht...',
+      chooseEntity: 'Entität auswählen...',
+      connectionFailed: 'Verbindung fehlgeschlagen',
+      searchEntities: 'Entitäten suchen...',
+      searchPowerSensors: 'Leistungssensoren suchen...',
+      searchEnergySensors: 'Energiesensoren suchen...',
+      placeholders: {
+        plugName: 'Wohnzimmer Steckdose',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: 'Gleich wie Leistungs-Topic oder anders',
+      },
+    },
+  },
+
+  // Rich Text Editor
+  richTextEditor: {
+    bold: 'Fett',
+    italic: 'Kursiv',
+    underline: 'Unterstrichen',
+    bulletList: 'Aufzählungsliste',
+    numberedList: 'Nummerierte Liste',
+    alignLeft: 'Linksbündig',
+    alignCenter: 'Zentriert',
+    alignRight: 'Rechtsbündig',
+    addLink: 'Link hinzufügen',
+    removeLink: 'Link entfernen',
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: 'Keine externen Links konfiguriert',
+    deleteLink: 'Link löschen',
+    removeCustomIcon: 'Benutzerdefiniertes Symbol entfernen',
+    placeholders: {
+      linkName: 'Mein Link',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'Tastaturkürzel',
+    navigation: 'Navigation',
+    archivesSection: 'Archive',
+    kProfilesSection: 'K-Profile',
+    generalSection: 'Allgemein',
+    shortcuts: {
+      goToPrinters: 'Zu Drucker gehen',
+      goToArchives: 'Zu Archiv gehen',
+      goToQueue: 'Zur Warteschlange gehen',
+      goToStats: 'Zu Statistiken gehen',
+      goToProfiles: 'Zu Cloud-Profilen gehen',
+      goToSettings: 'Zu Einstellungen gehen',
+      focusSearch: 'Suche fokussieren',
+      openUploadModal: 'Upload-Modal öffnen',
+      clearSelection: 'Auswahl löschen / Eingabe aufheben',
+      contextMenu: 'Kontextmenü auf Karten',
+      refreshProfiles: 'Profile aktualisieren',
+      newProfile: 'Neues Profil',
+      exitSelectionMode: 'Auswahlmodus beenden',
+      showHelp: 'Diese Hilfe anzeigen',
+    },
+    footer: 'Drücken Sie Esc oder klicken Sie außerhalb, um zu schließen',
+  },
+
+  // Notification Log
+  notificationLog: {
+    title: 'Benachrichtigungsprotokoll',
+    events: {
+      printStarted: 'Druck gestartet',
+      printComplete: 'Druck abgeschlossen',
+      printFailed: 'Druck fehlgeschlagen',
+      printStopped: 'Druck gestoppt',
+      progress: 'Fortschritt',
+      printerOffline: 'Drucker offline',
+      printerError: 'Druckerfehler',
+      lowFilament: 'Wenig Filament',
+      maintenanceDue: 'Wartung fällig',
+      test: 'Test',
+    },
+    timeAgo: {
+      justNow: 'Gerade eben',
+      minutesAgo: 'vor {{minutes}}m',
+      hoursAgo: 'vor {{hours}}h',
+    },
+  },
+
+  // Restore/Backup Modal
+  restoreBackup: {
+    title: 'Backup wiederherstellen',
+    restoring: 'Wird wiederhergestellt...',
+    restoreComplete: 'Wiederherstellung abgeschlossen',
+    restoreFailed: 'Wiederherstellung fehlgeschlagen',
+    importSettings: 'Einstellungen aus Backup-Datei importieren',
+    pleaseWait: 'Bitte warten Sie, während Ihre Daten wiederhergestellt werden',
+    clickToSelect: 'Klicken Sie, um Backup-Datei auszuwählen (.json oder .zip)',
+    howDuplicateHandling: 'So funktioniert die Duplikatbehandlung:',
+    categories: {
+      printers: 'Drucker',
+      smartPlugs: 'Smart Plugs',
+      notificationProviders: 'Benachrichtigungsanbieter',
+      filaments: 'Filamente',
+      archives: 'Archive',
+      pendingUploads: 'Ausstehende Uploads',
+      settingsTemplates: 'Einstellungen & Vorlagen',
+    },
+    matchingInfo: {
+      printers: 'abgeglichen nach Seriennummer',
+      smartPlugs: 'abgeglichen nach IP-Adresse',
+      notificationProviders: 'abgeglichen nach Name',
+      filaments: 'abgeglichen nach Name + Typ + Marke',
+      archives: 'abgeglichen nach Inhalts-Hash',
+      pendingUploads: 'abgeglichen nach Dateiname',
+      settingsTemplates: 'immer überschrieben',
+    },
+    replaceExisting: 'Vorhandene Daten ersetzen',
+    keepExisting: 'Vorhandene Daten behalten',
+    replaceDescription: 'Bereits vorhandene Elemente mit Backup-Daten überschreiben',
+    keepDescription: 'Nur Elemente wiederherstellen, die noch nicht existieren',
+    caution: 'Vorsicht:',
+    cautionText: 'Das Überschreiben ersetzt Ihre aktuellen Konfigurationen durch Backup-Daten. Drucker-Zugangscodes werden aus Sicherheitsgründen niemals überschrieben.',
+    itemsRestored: 'Wiederhergestellte Elemente',
+    itemsSkipped: 'Übersprungene Elemente',
+    restored: 'Wiederhergestellt',
+    skipped: 'Übersprungen (existieren bereits)',
+    filesLabel: 'Dateien (3MF, Thumbnails, etc.)',
+    newApiKeysGenerated: 'Neue API-Schlüssel generiert',
+    newApiKeysWarning: 'Diese Schlüssel werden nur einmal angezeigt. Kopieren Sie sie jetzt!',
+    processingBackup: 'Backup-Datei wird verarbeitet...',
+    noDataFound: 'In der Backup-Datei wurden keine wiederherzustellenden Daten gefunden.',
+    failedToRestore: 'Backup konnte nicht wiederhergestellt werden. Bitte überprüfen Sie das Dateiformat.',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'Backup exportieren',
+    selectData: 'Zu exportierende Daten auswählen',
+    selectAll: 'Alle auswählen',
+    selectNone: 'Keine auswählen',
+    categoryDescriptions: {
+      settings: 'Sprache, Theme, Update-Einstellungen',
+      notifications: 'ntfy, Pushover, Discord, usw.',
+      templates: 'Benutzerdefinierte Nachrichtenvorlagen',
+      smartPlugs: 'Tasmota-Plug-Konfigurationen',
+      externalLinks: 'Seitenleiste Links zu externen Diensten',
+      printers: 'Druckerinformationen (Zugangscodes ausgeschlossen)',
+      plateDetection: 'Leere Platten-Referenzbilder',
+      filaments: 'Filamenttypen und -kosten',
+      maintenance: 'Benutzerdefinierte Wartungspläne',
+      archives: 'Alle Druckdaten + Dateien (3MF, Thumbnails, Fotos)',
+      projects: 'Projekte, BOM-Elemente und Anhänge',
+      pendingUploads: 'Virtueller Drucker-Uploads zur Überprüfung',
+      apiKeys: 'Webhook-API-Schlüssel (neue Schlüssel bei Import generiert)',
+    },
+    requiresPrinters: 'Drucker müssen ausgewählt sein',
+    zipFileWarning: 'ZIP-Datei wird erstellt.',
+    zipFileDescription: 'Enthält alle 3MF-Dateien, Thumbnails, Zeitraffer und Fotos. Dies kann eine Weile dauern und zu einer großen Datei führen.',
+    includeAccessCodes: 'Zugangscodes einschließen',
+    includeAccessCodesDescription: 'Für die Übertragung auf eine andere Maschine',
+    includeAccessCodesWarning: 'Zugangscodes werden im Klartext eingeschlossen. Bewahren Sie diese Backup-Datei sicher auf!',
+    categoriesSelected: '{{selectedCount}} Kategorien ausgewählt',
+  },
+
+  // Pending Uploads Panel
+  pendingUploads: {
+    placeholders: {
+      notes: 'Notizen zu diesem Druck hinzufügen...',
+    },
+    discardUpload: 'Upload verwerfen',
+    archiveAllUploads: 'Alle Uploads archivieren',
+    discardAllUploads: 'Alle Uploads verwerfen',
+    archive: 'Archivieren',
+    timeAgo: {
+      justNow: 'Gerade eben',
+      minutesAgo: 'vor {{minutes}}m',
+      hoursAgo: 'vor {{hours}}h',
+      daysAgo: 'vor {{days}}d',
+    },
+  },
+
+  // API Browser
+  apiBrowser: {
+    placeholders: {
+      requestBody: 'JSON-Anforderungstext...',
+      searchEndpoints: 'Endpunkte suchen...',
+    },
+  },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    searchPresets: 'Voreinstellungen suchen...',
+    colorPlaceholder: 'Farbname oder Hex (z.B. braun, FF8800)',
+    clearCustomColor: 'Benutzerdefinierte Farbe löschen',
+    noCloudPresets: 'Keine Cloud-Voreinstellungen. Melden Sie sich bei Bambu Cloud an, um zu synchronisieren.',
+    noMatchingPresets: 'Keine passenden Voreinstellungen gefunden.',
+    custom: 'Benutzerdefiniert',
+    settingsSentToPrinter: 'Einstellungen an Drucker gesendet',
+    filamentProfile: 'Filamentprofil',
+  },
+
+  // GitHub Backup Settings
+  githubBackup: {
+    title: 'GitHub-Backup',
+    history: 'Verlauf',
+    downloadBackup: 'Backup herunterladen',
+    restoreBackup: 'Backup wiederherstellen',
+    noBackupsYet: 'Noch keine Backups',
+  },
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'Tags suchen...',
+    renameTag: 'Tag umbenennen',
+    deleteTag: 'Tag löschen',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: 'Benachrichtigungstitel...',
+      body: 'Benachrichtigungstext...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: 'Neuen Tag eingeben...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: 'Foto löschen',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'Spulen-UUID kopieren',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'Hat Notiz',
+    copyProfile: 'Profil kopieren',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'Menü öffnen',
+    noPermissionSystemInfo: 'Sie haben keine Berechtigung zum Anzeigen von Systeminformationen',
+  },
+
+  // Dashboard
+  dashboard: {
+    dragToReorder: 'Ziehen zum Neuordnen',
+    hideWidget: 'Widget ausblenden',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: 'Benachrichtigungsanbieter löschen',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'Dateimanager schließen',
+    sortFiles: 'Dateien sortieren',
+    goToParentFolder: 'Zum übergeordneten Ordner gehen',
+    threeView: '3D-Ansicht',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'Stream aktualisieren',
+    close: 'Schließen',
+    zoomOut: 'Verkleinern',
+    resetZoom: 'Zoom zurücksetzen',
+    zoomIn: 'Vergrößern',
+    dragToResize: 'Ziehen zum Größe ändern',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: '5s zurückspringen',
+    skipForward5s: '5s vorspringen',
+  },
+
+  // Notification Providers
+  notificationProviders: {
+    descriptions: {
+      email: 'SMTP-E-Mail-Benachrichtigungen',
+      telegram: 'Benachrichtigungen über Telegram-Bot',
+      discord: 'An Discord-Kanal über Webhook senden',
+      ntfy: 'Kostenlose, selbst hostbare Push-Benachrichtigungen',
+      pushover: 'Einfache, zuverlässige Push-Benachrichtigungen',
+      callmebot: 'Kostenlose WhatsApp-Benachrichtigungen über CallMeBot',
+      webhook: 'Generischer HTTP POST zu beliebiger URL',
+    },
+  },
+
+  // Log Viewer
+  logViewer: {
+    searchPlaceholder: 'Nachricht oder Logger-Name suchen...',
+    noLogEntries: 'Keine Logeinträge gefunden',
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'Keine Schalter in Schalterleiste',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'Titel',
+      designer: 'Designer',
+      license: 'Lizenz',
+      description: 'Beschreibung eingeben...',
+      profileTitle: 'Profil-Titel',
+      profileDescription: 'Profilbeschreibung...',
+    },
+  },
+
+  // Spoolman Settings
+  spoolmanSettings: {},
 };

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

@@ -1030,6 +1030,30 @@ export default {
       feature2: 'Users can login with username or email',
       feature3: 'Forgot password feature is available',
       feature4: 'Admins can reset user passwords via email',
+      // Error messages
+      errors: {
+        requiredFields: 'Please fill in all required fields',
+        usernameRequired: 'Username is required when authentication is enabled',
+        enterTestEmail: 'Please enter a test email address',
+        smtpServerAndEmail: 'Please fill in SMTP Server and From Email before testing',
+        usernamePasswordRequired: 'Username and Password are required when authentication is enabled',
+        configureSmtpFirst: 'Please configure and test SMTP settings first',
+      },
+      // Success messages
+      success: {
+        settingsSaved: 'SMTP settings saved successfully',
+      },
+      // Security options
+      securityOptions: {
+        starttls: 'STARTTLS (Port 587)',
+        ssl: 'SSL/TLS (Port 465)',
+        none: 'None (Port 25)',
+      },
+      // Authentication options
+      authOptions: {
+        enabled: 'Enabled',
+        disabled: 'Disabled',
+      },
     },
     appearance: 'Appearance',
     notifications: 'Notifications',
@@ -2286,6 +2310,9 @@ export default {
     noPrintersAvailable: 'No printers available',
     printerBusy: 'Printer is busy',
     printerOffline: 'Printer is offline',
+    sameTypeDifferentColor: 'Same type, different color',
+    filamentTypeNotLoaded: 'Filament type not loaded',
+    openCalendar: 'Open calendar',
   },
 
   // Backup
@@ -2338,6 +2365,7 @@ export default {
   },
 
   // Edit archive modal
+  // Edit Archive Modal
   editArchive: {
     title: 'Edit Archive',
     name: 'Name',
@@ -2640,4 +2668,348 @@ export default {
     replaceCarbonFilter: 'Replace activated carbon filter',
     lubricateLeftNozzleRail: 'Lubricate left nozzle rail (H2 series)',
   },
+
+  // Smart Plugs
+  smartPlugs: {
+    offline: 'Offline',
+    admin: 'Admin',
+    openPlugAdminPage: 'Open plug admin page',
+    deleteSmartPlug: 'Delete Smart Plug',
+    turnOnSmartPlug: 'Turn On Smart Plug',
+    turnOffSmartPlug: 'Turn Off Smart Plug',
+    turnOn: 'Turn On',
+    turnOff: 'Turn Off',
+    addSmartPlug: {
+      scanningNetwork: 'Scanning network...',
+      chooseEntity: 'Choose an entity...',
+      connectionFailed: 'Connection failed',
+      searchEntities: 'Search entities...',
+      searchPowerSensors: 'Search power sensors...',
+      searchEnergySensors: 'Search energy sensors...',
+      placeholders: {
+        plugName: 'Living Room Plug',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: 'Same as power topic, or different',
+      },
+    },
+  },
+
+  // Rich Text Editor
+  richTextEditor: {
+    bold: 'Bold',
+    italic: 'Italic',
+    underline: 'Underline',
+    bulletList: 'Bullet List',
+    numberedList: 'Numbered List',
+    alignLeft: 'Align Left',
+    alignCenter: 'Align Center',
+    alignRight: 'Align Right',
+    addLink: 'Add Link',
+    removeLink: 'Remove Link',
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: 'No external links configured',
+    deleteLink: 'Delete Link',
+    removeCustomIcon: 'Remove custom icon',
+    placeholders: {
+      linkName: 'My Link',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'Keyboard Shortcuts',
+    navigation: 'Navigation',
+    archivesSection: 'Archives',
+    kProfilesSection: 'K-Profiles',
+    generalSection: 'General',
+    shortcuts: {
+      goToPrinters: 'Go to Printers',
+      goToArchives: 'Go to Archives',
+      goToQueue: 'Go to Queue',
+      goToStats: 'Go to Statistics',
+      goToProfiles: 'Go to Cloud Profiles',
+      goToSettings: 'Go to Settings',
+      focusSearch: 'Focus search',
+      openUploadModal: 'Open upload modal',
+      clearSelection: 'Clear selection / blur input',
+      contextMenu: 'Context menu on cards',
+      refreshProfiles: 'Refresh profiles',
+      newProfile: 'New profile',
+      exitSelectionMode: 'Exit selection mode',
+      showHelp: 'Show this help',
+    },
+    footer: 'Press Esc or click outside to close',
+  },
+
+  // Notification Log
+  notificationLog: {
+    title: 'Notification Log',
+    events: {
+      printStarted: 'Print Started',
+      printComplete: 'Print Complete',
+      printFailed: 'Print Failed',
+      printStopped: 'Print Stopped',
+      progress: 'Progress',
+      printerOffline: 'Printer Offline',
+      printerError: 'Printer Error',
+      lowFilament: 'Low Filament',
+      maintenanceDue: 'Maintenance Due',
+      test: 'Test',
+    },
+    timeAgo: {
+      justNow: 'Just now',
+      minutesAgo: '{{minutes}}m ago',
+      hoursAgo: '{{hours}}h ago',
+    },
+  },
+
+  // Restore/Backup Modal
+  restoreBackup: {
+    title: 'Restore Backup',
+    restoring: 'Restoring...',
+    restoreComplete: 'Restore Complete',
+    restoreFailed: 'Restore Failed',
+    importSettings: 'Import settings from a backup file',
+    pleaseWait: 'Please wait while your data is being restored',
+    clickToSelect: 'Click to select backup file (.json or .zip)',
+    howDuplicateHandling: 'How duplicate handling works:',
+    categories: {
+      printers: 'Printers',
+      smartPlugs: 'Smart Plugs',
+      notificationProviders: 'Notification Providers',
+      filaments: 'Filaments',
+      archives: 'Archives',
+      pendingUploads: 'Pending Uploads',
+      settingsTemplates: 'Settings & Templates',
+    },
+    matchingInfo: {
+      printers: 'matched by serial number',
+      smartPlugs: 'matched by IP address',
+      notificationProviders: 'matched by name',
+      filaments: 'matched by name + type + brand',
+      archives: 'matched by content hash',
+      pendingUploads: 'matched by filename',
+      settingsTemplates: 'always overwritten',
+    },
+    replaceExisting: 'Replace existing data',
+    keepExisting: 'Keep existing data',
+    replaceDescription: 'Overwrite items that already exist with backup data',
+    keepDescription: 'Only restore items that don\'t already exist',
+    caution: 'Caution:',
+    cautionText: 'Overwriting will replace your current configurations with backup data. Printer access codes are never overwritten for security.',
+    itemsRestored: 'Items Restored',
+    itemsSkipped: 'Items Skipped',
+    restored: 'Restored',
+    skipped: 'Skipped (already exist)',
+    filesLabel: 'Files (3MF, thumbnails, etc.)',
+    newApiKeysGenerated: 'New API Keys Generated',
+    newApiKeysWarning: 'These keys are only shown once. Copy them now!',
+    processingBackup: 'Processing backup file...',
+    noDataFound: 'No data was found to restore in the backup file.',
+    failedToRestore: 'Failed to restore backup. Please check the file format.',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'Export Backup',
+    selectData: 'Select data to include',
+    selectAll: 'Select All',
+    selectNone: 'Select None',
+    categoryDescriptions: {
+      settings: 'Language, theme, update preferences',
+      notifications: 'ntfy, Pushover, Discord, etc.',
+      templates: 'Custom message templates',
+      smartPlugs: 'Tasmota plug configurations',
+      externalLinks: 'Sidebar links to external services',
+      printers: 'Printer info (access codes excluded)',
+      plateDetection: 'Empty plate reference images',
+      filaments: 'Filament types and costs',
+      maintenance: 'Custom maintenance schedules',
+      archives: 'All print data + files (3MF, thumbnails, photos)',
+      projects: 'Projects, BOM items, and attachments',
+      pendingUploads: 'Virtual printer uploads awaiting review',
+      apiKeys: 'Webhook API keys (new keys generated on import)',
+    },
+    requiresPrinters: 'Requires Printers to be selected',
+    zipFileWarning: 'ZIP file will be created.',
+    zipFileDescription: 'Includes all 3MF files, thumbnails, timelapses, and photos. This may take a while and result in a large file.',
+    includeAccessCodes: 'Include Access Codes',
+    includeAccessCodesDescription: 'For transferring to another machine',
+    includeAccessCodesWarning: 'Access codes will be included in plain text. Keep this backup file secure!',
+    categoriesSelected: '{{selectedCount}} categories selected',
+  },
+
+  // Pending Uploads Panel
+  pendingUploads: {
+    placeholders: {
+      notes: 'Add notes about this print...',
+    },
+    discardUpload: 'Discard Upload',
+    archiveAllUploads: 'Archive All Uploads',
+    discardAllUploads: 'Discard All Uploads',
+    archive: 'Archive',
+    timeAgo: {
+      justNow: 'Just now',
+      minutesAgo: '{{minutes}}m ago',
+      hoursAgo: '{{hours}}h ago',
+      daysAgo: '{{days}}d ago',
+    },
+  },
+
+  // API Browser
+  apiBrowser: {
+    placeholders: {
+      requestBody: 'JSON request body...',
+      searchEndpoints: 'Search endpoints...',
+    },
+  },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    searchPresets: 'Search presets...',
+    colorPlaceholder: 'Color name or hex (e.g., brown, FF8800)',
+    clearCustomColor: 'Clear custom color',
+    noCloudPresets: 'No cloud presets. Login to Bambu Cloud to sync.',
+    noMatchingPresets: 'No matching presets found.',
+    custom: 'Custom',
+    settingsSentToPrinter: 'Settings sent to printer',
+    filamentProfile: 'Filament Profile',
+  },
+
+  // GitHub Backup Settings
+  githubBackup: {
+    title: 'GitHub Backup',
+    history: 'History',
+    downloadBackup: 'Download Backup',
+    restoreBackup: 'Restore Backup',
+    noBackupsYet: 'No backups yet',
+  },
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'Search tags...',
+    renameTag: 'Rename tag',
+    deleteTag: 'Delete tag',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: 'Notification title...',
+      body: 'Notification body...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: 'Enter new tag...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: 'Delete Photo',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'Copy spool UUID',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'Has note',
+    copyProfile: 'Copy profile',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'Open menu',
+    noPermissionSystemInfo: 'You do not have permission to view system information',
+  },
+
+  // Dashboard
+  dashboard: {
+    dragToReorder: 'Drag to reorder',
+    hideWidget: 'Hide widget',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: 'Delete Notification Provider',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'Close file manager',
+    sortFiles: 'Sort files',
+    goToParentFolder: 'Go to parent folder',
+    threeView: '3D View',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'Refresh stream',
+    close: 'Close',
+    zoomOut: 'Zoom out',
+    resetZoom: 'Reset zoom',
+    zoomIn: 'Zoom in',
+    dragToResize: 'Drag to resize',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: 'Skip back 5s',
+    skipForward5s: 'Skip forward 5s',
+  },
+
+  // Notification Providers
+  notificationProviders: {
+    descriptions: {
+      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',
+    },
+  },
+
+  // Log Viewer
+  logViewer: {
+    searchPlaceholder: 'Search message or logger name...',
+    noLogEntries: 'No log entries found',
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'No switches in switchbar',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'Title',
+      designer: 'Designer',
+      license: 'License',
+      description: 'Enter description...',
+      profileTitle: 'Profile Title',
+      profileDescription: 'Profile description...',
+    },
+  },
+
+  // Spoolman Settings
+  spoolmanSettings: {},
 };

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

@@ -1182,6 +1182,30 @@ export default {
       feature2: 'ユーザーはユーザー名またはメールでログインできます',
       feature3: 'パスワード忘れ機能が利用可能です',
       feature4: '管理者はメールでユーザーパスワードをリセットできます',
+      // Error messages
+      errors: {
+        requiredFields: 'すべての必須フィールドに入力してください',
+        usernameRequired: '認証が有効な場合、ユーザー名は必須です',
+        enterTestEmail: 'テストメールアドレスを入力してください',
+        smtpServerAndEmail: 'テストする前にSMTPサーバーと送信元メールを入力してください',
+        usernamePasswordRequired: '認証が有効な場合、ユーザー名とパスワードは必須です',
+        configureSmtpFirst: '最初にSMTP設定を構成してテストしてください',
+      },
+      // Success messages
+      success: {
+        settingsSaved: 'SMTP設定を保存しました',
+      },
+      // Security options
+      securityOptions: {
+        starttls: 'STARTTLS (ポート 587)',
+        ssl: 'SSL/TLS (ポート 465)',
+        none: 'なし (ポート 25)',
+      },
+      // Authentication options
+      authOptions: {
+        enabled: '有効',
+        disabled: '無効',
+      },
     },
 
     // General - Date/Time
@@ -1801,6 +1825,20 @@ export default {
       ams_humidity_high: 'AMS湿度高',
       ams_temperature_high: 'AMS温度高',
       test: 'テスト',
+      printStarted: '印刷開始',
+      printComplete: '印刷完了',
+      printFailed: '印刷失敗',
+      printStopped: '印刷停止',
+      progress: '進捗',
+      printerOffline: 'プリンターオフライン',
+      printerError: 'プリンターエラー',
+      lowFilament: 'フィラメント残量少',
+      maintenanceDue: 'メンテナンス期限',
+    },
+    timeAgo: {
+      justNow: 'たった今',
+      minutesAgo: '{{minutes}}分前',
+      hoursAgo: '{{hours}}時間前',
     },
   },
 
@@ -2078,6 +2116,7 @@ export default {
     selectAModel: 'モデルを選択...',
     locationFilterLabel: 'ロケーションフィルター(任意)',
     anyLocation: 'すべてのロケーション',
+    openCalendar: 'カレンダーを開く',
   },
 
   // ログインページ
@@ -2514,6 +2553,19 @@ export default {
     discardedCount: '{{count}}ファイルを破棄しました',
     archiveAllFailed: 'すべてのアーカイブに失敗しました',
     discardAllFailed: 'すべての破棄に失敗しました',
+    placeholders: {
+      tags: '例: 機能的、プロトタイプ、ギフト',
+      notes: 'この印刷についてのメモを追加...',
+    },
+    discardUpload: 'アップロードを破棄',
+    archiveAllUploads: 'すべてのアップロードをアーカイブ',
+    discardAllUploads: 'すべてのアップロードを破棄',
+    timeAgo: {
+      justNow: 'たった今',
+      minutesAgo: '{{minutes}}分前',
+      hoursAgo: '{{hours}}時間前',
+      daysAgo: '{{days}}日前',
+    },
   },
 
   // キーボードショートカット
@@ -2558,6 +2610,7 @@ export default {
     emptyLog: 'ログファイルが空またはクリアされています',
     autoRefreshing: '2秒ごとに自動更新中',
     clickStart: '開始をクリックしてライブログストリーミングを有効にする',
+    noLogEntries: 'ログエントリが見つかりません',
   },
 
   // アーカイブ編集モーダル
@@ -3097,6 +3150,10 @@ export default {
     collapseAll: 'すべて折りたたむ',
     swaggerUI: 'Swagger UI',
     endpointCount: '{{categories}}カテゴリー内{{count}}エンドポイント',
+    placeholders: {
+      requestBody: 'JSONリクエストボディ...',
+      searchEndpoints: 'エンドポイントを検索...',
+    },
   },
   githubBackup: {
     title: 'GitHubバックアップ',
@@ -3190,4 +3247,285 @@ export default {
       unsupportedFormat: 'サポートされていないファイル形式です',
     },
   },
+
+  // Maintenance type descriptions (built-in)
+  maintenanceDescriptions: {
+    lubricateRails: 'リニアレールに潤滑剤を塗布してスムーズな動きを実現',
+    cleanNozzle: 'ホットエンドとノズルを清掃して詰まりを防止',
+    checkBelts: 'ベルトの張力を確認して正確な印刷を実現',
+    cleanBuildPlate: 'ビルドプレートを清掃して接着力を向上',
+    checkExtruder: 'エクストルーダーギアの摩耗を検査',
+    checkCooling: '冷却ファンが正常に動作していることを確認',
+    generalInspection: 'プリンターの一般的な点検',
+    cleanCarbonRods: 'カーボンロッドを清掃して摩擦を軽減',
+    checkPtfeTube: 'PTFEチューブの摩耗や損傷を検査',
+    replaceHepaFilter: 'HEPAフィルターを交換して空気品質を向上',
+    replaceCarbonFilter: '活性炭フィルターを交換',
+    lubricateLeftNozzleRail: '左ノズルレールに潤滑剤を塗布(H2シリーズ)',
+  },
+
+  // Smart Plugs
+  smartPlugs: {
+    offline: 'オフライン',
+    admin: '管理',
+    openPlugAdminPage: 'プラグ管理ページを開く',
+    deleteSmartPlug: 'スマートプラグを削除',
+    turnOnSmartPlug: 'スマートプラグをオンにする',
+    turnOffSmartPlug: 'スマートプラグをオフにする',
+    turnOn: 'オンにする',
+    turnOff: 'オフにする',
+    addSmartPlug: {
+      scanningNetwork: 'ネットワークをスキャン中...',
+      chooseEntity: 'エンティティを選択...',
+      connectionFailed: '接続失敗',
+      searchEntities: 'エンティティを検索...',
+      searchPowerSensors: '電力センサーを検索...',
+      searchEnergySensors: 'エネルギーセンサーを検索...',
+      placeholders: {
+        plugName: 'リビングルームプラグ',
+        mqttStateOnValue: 'ON, true, 1',
+        mqttSameAsPower: '電力トピックと同じ、または異なる',
+      },
+    },
+  },
+
+  // External Links
+  externalLinks: {
+    noLinksConfigured: '外部リンクが設定されていません',
+    deleteLink: 'リンクを削除',
+    removeCustomIcon: 'カスタムアイコンを削除',
+    placeholders: {
+      linkName: 'マイリンク',
+    },
+  },
+
+  // Keyboard Shortcuts Modal
+  keyboardShortcuts: {
+    title: 'キーボードショートカット',
+    navigation: 'ナビゲーション',
+    archivesSection: 'アーカイブ',
+    kProfilesSection: 'Kプロファイル',
+    generalSection: '全般',
+    shortcuts: {
+      goToPrinters: 'プリンターへ移動',
+      goToArchives: 'アーカイブへ移動',
+      goToQueue: 'キューへ移動',
+      goToStats: '統計へ移動',
+      goToProfiles: 'クラウドプロファイルへ移動',
+      goToSettings: '設定へ移動',
+      focusSearch: '検索にフォーカス',
+      openUploadModal: 'アップロードモーダルを開く',
+      clearSelection: '選択をクリア / 入力をぼかす',
+      contextMenu: 'カードのコンテキストメニュー',
+      refreshProfiles: 'プロファイルを更新',
+      newProfile: '新しいプロファイル',
+      exitSelectionMode: '選択モードを終了',
+      showHelp: 'このヘルプを表示',
+    },
+    footer: 'Escキーを押すか外側をクリックして閉じます',
+  },
+
+  // Keyboard Shortcuts Modal
+  restoreBackup: {
+    title: 'バックアップを復元',
+    restoring: '復元中...',
+    restoreComplete: '復元完了',
+    restoreFailed: '復元失敗',
+    importSettings: 'バックアップファイルから設定をインポート',
+    pleaseWait: 'データの復元中です。しばらくお待ちください',
+    clickToSelect: 'クリックしてバックアップファイルを選択(.jsonまたは.zip)',
+    howDuplicateHandling: '重複の処理方法:',
+    categories: {
+      printers: 'プリンター',
+      smartPlugs: 'スマートプラグ',
+      notificationProviders: '通知プロバイダー',
+      filaments: 'フィラメント',
+      archives: 'アーカイブ',
+      pendingUploads: '保留中のアップロード',
+      settingsTemplates: '設定とテンプレート',
+    },
+    matchingInfo: {
+      printers: 'シリアル番号で照合',
+      smartPlugs: 'IPアドレスで照合',
+      notificationProviders: '名前で照合',
+      filaments: '名前+タイプ+ブランドで照合',
+      archives: 'コンテンツハッシュで照合',
+      pendingUploads: 'ファイル名で照合',
+      settingsTemplates: '常に上書き',
+    },
+    replaceExisting: '既存データを置き換え',
+    keepExisting: '既存データを保持',
+    replaceDescription: '既に存在するアイテムをバックアップデータで上書き',
+    keepDescription: 'まだ存在しないアイテムのみを復元',
+    caution: '注意:',
+    cautionText: '上書きすると現在の構成がバックアップデータに置き換えられます。セキュリティ上の理由から、プリンターのアクセスコードは上書きされません。',
+    itemsRestored: '復元されたアイテム',
+    itemsSkipped: 'スキップされたアイテム',
+    restored: '復元済み',
+    skipped: 'スキップ(既に存在)',
+    filesLabel: 'ファイル(3MF、サムネイルなど)',
+    newApiKeysGenerated: '新しいAPIキーが生成されました',
+    newApiKeysWarning: 'これらのキーは一度だけ表示されます。今すぐコピーしてください!',
+    processingBackup: 'バックアップファイルを処理中...',
+    noDataFound: 'バックアップファイルに復元するデータが見つかりませんでした。',
+    failedToRestore: 'バックアップの復元に失敗しました。ファイル形式を確認してください。',
+  },
+
+  // Backup Export Modal
+  backupExport: {
+    title: 'バックアップをエクスポート',
+    selectData: '含めるデータを選択',
+    selectAll: 'すべて選択',
+    selectNone: 'なし',
+    categoryDescriptions: {
+      settings: '言語、テーマ、更新設定',
+      notifications: 'ntfy、Pushover、Discordなど',
+      templates: 'カスタムメッセージテンプレート',
+      smartPlugs: 'Tasmotaプラグ設定',
+      externalLinks: 'サイドバーの外部サービスへのリンク',
+      printers: 'プリンター情報(アクセスコード除外)',
+      plateDetection: '空プレート参照画像',
+      filaments: 'フィラメントの種類とコスト',
+      maintenance: 'カスタムメンテナンススケジュール',
+      archives: 'すべての印刷データ+ファイル(3MF、サムネイル、写真)',
+      projects: 'プロジェクト、BOMアイテム、添付ファイル',
+      pendingUploads: '仮想プリンターアップロード待機中',
+      apiKeys: 'Webhook APIキー(インポート時に新しいキーが生成されます)',
+    },
+    requiresPrinters: 'プリンターを選択する必要があります',
+    zipFileWarning: 'ZIPファイルが作成されます。',
+    zipFileDescription: 'すべての3MFファイル、サムネイル、タイムラプス、写真が含まれます。これには時間がかかり、大きなファイルになる可能性があります。',
+    includeAccessCodes: 'アクセスコードを含める',
+    includeAccessCodesDescription: '別のマシンへの転送用',
+    includeAccessCodesWarning: 'アクセスコードはプレーンテキストで含まれます。このバックアップファイルを安全に保管してください!',
+    categoriesSelected: '{{selectedCount}}カテゴリー選択済み',
+  },
+
+  // Configure AMS Slot Modal
+  configureAmsSlot: {
+    searchPresets: 'プリセットを検索...',
+    colorPlaceholder: '色名またはHex(例: 茶色、FF8800)',
+    clearCustomColor: 'カスタム色をクリア',
+    noCloudPresets: 'クラウドプリセットがありません。Bambu Cloudにログインして同期してください。',
+    noMatchingPresets: '一致するプリセットが見つかりません。',
+    custom: 'カスタム',
+    settingsSentToPrinter: '設定をプリンターに送信しました',
+    filamentProfile: 'フィラメントプロファイル',
+  },
+
+  // GitHub Backup Settings (additional keys)
+  githubBackupSettings: {},
+
+  // Email Settings
+  emailSettings: {
+    placeholders: {
+      fromName: 'BamBuddy',
+    },
+  },
+
+  // Tag Management Modal
+  tagManagement: {
+    searchTags: 'タグを検索...',
+    renameTag: 'タグ名を変更',
+    deleteTag: 'タグを削除',
+  },
+
+  // Notification Template Editor
+  notificationTemplates: {
+    placeholders: {
+      title: '通知タイトル...',
+      body: '通知本文...',
+    },
+  },
+
+  // Batch Tag Modal
+  batchTag: {
+    placeholders: {
+      newTag: '新しいタグを入力...',
+    },
+  },
+
+  // Photo Gallery Modal
+  photoGallery: {
+    deletePhoto: '写真を削除',
+  },
+
+  // Filament Hover Card
+  filamentHoverCard: {
+    copySpoolUuid: 'スプールUUIDをコピー',
+  },
+
+  // K Profiles View
+  kProfilesView: {
+    hasNote: 'メモあり',
+    copyProfile: 'プロファイルをコピー',
+  },
+
+  // Layout/Navigation
+  layout: {
+    openMenu: 'メニューを開く',
+    noPermissionSystemInfo: 'システム情報を表示する権限がありません',
+  },
+
+  // Notification Provider Card
+  notificationProviderCard: {
+    deleteNotificationProvider: '通知プロバイダーを削除',
+  },
+
+  // File Manager Modal
+  fileManagerModal: {
+    closeFileManager: 'ファイルマネージャーを閉じる',
+    sortFiles: 'ファイルを並べ替え',
+    goToParentFolder: '親フォルダーへ移動',
+    threeView: '3Dビュー',
+  },
+
+  // Embedded Camera Viewer
+  embeddedCameraViewer: {
+    refreshStream: 'ストリームを更新',
+    close: '閉じる',
+    zoomOut: 'ズームアウト',
+    resetZoom: 'ズームをリセット',
+    zoomIn: 'ズームイン',
+    dragToResize: 'ドラッグしてサイズ変更',
+  },
+
+  // Timelapse Viewer
+  timelapseViewer: {
+    skipBack5s: '5秒戻る',
+    skipForward5s: '5秒進む',
+  },
+
+  // Notification Providers
+  notificationProviders: {
+    descriptions: {
+      email: 'SMTP電子メール通知',
+      telegram: 'Telegramボット経由の通知',
+      discord: 'Webhookを介してDiscordチャンネルに送信',
+      ntfy: '無料でセルフホスト可能なプッシュ通知',
+      pushover: 'シンプルで信頼性の高いプッシュ通知',
+      callmebot: 'CallMeBot経由の無料WhatsApp通知',
+      webhook: '任意のURLへのジェネリックHTTP POST',
+    },
+  },
+
+  // Switchbar Popover
+  switchbarPopover: {
+    noSwitchesInSwitchbar: 'スイッチバーにスイッチがありません',
+  },
+
+  // Project Page Modal
+  projectPageModal: {
+    placeholders: {
+      title: 'タイトル',
+      designer: 'デザイナー',
+      license: 'ライセンス',
+      description: '説明を入力...',
+      profileTitle: 'プロファイルタイトル',
+      profileDescription: 'プロファイルの説明...',
+    },
+  },
+
+  // Spoolman Settings
+  spoolmanSettings: {},
 };

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-QQNcmTSY.js


+ 1 - 1
static/index.html

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

Some files were not shown because too many files changed in this diff