diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd1445d8..f8f5fcec3 100644 --- a/CHANGELOG.md +++ b/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. ### 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. - **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. diff --git a/frontend/src/components/AddNotificationModal.tsx b/frontend/src/components/AddNotificationModal.tsx index 8c5c3ad2b..b48c8e567 100644 --- a/frontend/src/components/AddNotificationModal.tsx +++ b/frontend/src/components/AddNotificationModal.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { X, Save, Loader2, Send, CheckCircle, XCircle } from 'lucide-react'; import { api } from '../api/client'; import type { NotificationProvider, NotificationProviderCreate, NotificationProviderUpdate, ProviderType } from '../api/client'; @@ -11,17 +12,10 @@ interface AddNotificationModalProps { 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) { + const { t } = useTranslation(); const queryClient = useQueryClient(); const isEditing = !!provider; @@ -112,7 +106,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal setError(null); if (!name.trim()) { - setError('Name is required'); + setError(t('notifications.nameRequired')); return; } @@ -120,7 +114,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal const requiredFields = getRequiredFields(providerType); for (const field of requiredFields) { if (!config[field.key]?.trim()) { - setError(`${field.label} is required`); + setError(t('notifications.fieldRequired', { field: field.label })); return; } } @@ -239,7 +233,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal {/* Header */}

- {isEditing ? 'Edit Notification Provider' : 'Add Notification Provider'} + {isEditing ? t('notifications.editTitle') : t('notifications.addTitle')}

{/* Provider Type */}
- +

- {PROVIDER_OPTIONS.find(o => o.value === providerType)?.description} + {t(`notifications.providerDescriptions.${providerType}`, '')}

{/* Provider-specific configuration */}
-

Configuration

+

{t('notifications.configuration')}

{configFields .filter((field) => !('showIf' in field) || (field as { showIf?: (cfg: Record) => boolean }).showIf?.(config) !== false) .map((field) => ( @@ -351,7 +345,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal ) : ( )} - Test Configuration + {t('notifications.testConfiguration')}
@@ -378,13 +372,13 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal {/* Link to Printer */}
- +

- Only send notifications for events from this printer + {t('notifications.onlyFromPrinter')}

{/* Quiet Hours */}
- +
- +
- +
- -

Batch notifications into a single daily summary

+ +

{t('notifications.batchNotifications')}

{dailyDigestEnabled && (
- +

- Events will be collected and sent as a single summary at this time + {t('notifications.digestCollected')}

)} @@ -459,39 +453,39 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal {/* Event Toggles */}
-

Notification Events

+

{t('notifications.notificationEvents')}

{/* Print Events */}
-

Print Events

+

{t('notifications.printEvents')}

- Start + {t('notifications.start')}
- Complete + {t('notifications.complete')}
- Failed + {t('notifications.failed')}
- Stopped + {t('notifications.stopped')}
- Progress - (25%, 50%, 75%) + {t('notifications.progress')} + {t('notifications.progressPercent')}
- Bed Cooled - (after print completes) + {t('notifications.bedCooled')} + {t('notifications.bedCooledAfterPrint')}
@@ -500,22 +494,22 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal {/* Printer Status Events */}
-

Printer Status

+

{t('notifications.printerStatus')}

- Offline + {t('notifications.offline')}
- Error + {t('notifications.error')}
- Low Filament + {t('notifications.lowFilament')}
- Maintenance + {t('notifications.maintenance')}
@@ -530,7 +524,7 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal onClick={onClose} className="flex-1" > - Cancel + {t('notifications.cancel')}
diff --git a/frontend/src/components/AddSmartPlugModal.tsx b/frontend/src/components/AddSmartPlugModal.tsx index 257cdeed9..e6fdddeae 100644 --- a/frontend/src/components/AddSmartPlugModal.tsx +++ b/frontend/src/components/AddSmartPlugModal.tsx @@ -207,7 +207,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { } catch (err) { setIsScanning(false); 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); if (!name.trim()) { - setError('Name is required'); + setError(t('smartPlugs.nameRequired')); return; } @@ -305,7 +305,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { } if (plugType === 'homeassistant' && !haEntityId) { - setError('Entity is required for Home Assistant plugs'); + setError(t('smartPlugs.entityRequired')); return; } @@ -316,7 +316,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { const hasState = mqttStateTopic.trim(); if (!hasPower && !hasEnergy && !hasState) { - setError('At least one MQTT topic must be configured for power, energy, or state monitoring'); + setError(t('smartPlugs.mqttTopicRequired')); return; } } @@ -379,7 +379,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {/* Header */}

- {isEditing ? 'Edit Smart Plug' : 'Add Smart Plug'} + {isEditing ? t('smartPlugs.editTitle') : t('smartPlugs.addTitle')}

) : ( )} @@ -486,7 +486,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {/* Discovered devices */} {discoveredDevices.length > 0 && (
-

Found {discoveredDevices.length} device(s) - click to select:

+

{t('smartPlugs.foundDevices', { count: discoveredDevices.length })}

{discoveredDevices.map((device) => (
@@ -531,11 +531,11 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {!haConfigured && (
- Home Assistant is not configured. Set it up in{' '} - Settings → Network → Home Assistant + {t('smartPlugs.haNotConfigured')}{' '} + {t('smartPlugs.haSettingsPath')}
- + - Loading... + {t('smartPlugs.loading')}
)} {!haEntitiesLoading && availableEntities.length === 0 && (
{debouncedSearch - ? `No entities found matching "${debouncedSearch}"` - : 'No entities available'} + ? t('smartPlugs.noEntitiesMatching', { search: debouncedSearch }) + : t('smartPlugs.noEntitiesAvailable')}
)} {!haEntitiesLoading && availableEntities.map((entity) => ( @@ -652,8 +652,8 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {

{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 })}

); @@ -664,9 +664,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
-

Energy Monitoring (Optional)

+

{t('smartPlugs.energyMonitoringOptional')}

- Search and select sensors that provide power/energy data. + {t('smartPlugs.energyMonitoringHint')}

@@ -685,7 +685,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { return (
- +
- None + {t('smartPlugs.none')} {filteredPowerSensors.map((sensor) => (
)} @@ -769,7 +769,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { return (
- +
- None + {t('smartPlugs.none')} {filteredEnergySensors.map((sensor) => (
)} @@ -853,7 +853,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { return (
- +
- None + {t('smartPlugs.none')} {filteredEnergySensors.map((sensor) => (
)} @@ -934,9 +934,9 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {/* MQTT broker not configured */} {!settings?.mqtt_broker && (
- MQTT broker not configured. Set broker address in{' '} - Settings → Network → MQTT Publishing - {' '}(you don't need to enable publishing, just fill in the broker details). + {t('smartPlugs.mqttNotConfigured')}{' '} + {t('smartPlugs.mqttSettingsPath')} + {' '}{t('smartPlugs.mqttNotConfiguredSuffix')}
)} @@ -944,17 +944,17 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {settings?.mqtt_broker && ( <>
-

Monitor Only

+

{t('smartPlugs.monitorOnly')}

- 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')}

{/* Power Section */}
-

Power Monitoring

+

{t('smartPlugs.powerMonitoring')}

- +
- +
- +
-

- JSON path extracts value from JSON payload (e.g., "power_l1"). Leave empty if topic publishes raw numeric values.
- Use multiplier 0.001 for mW→W, 1000 for kW→W. +

+ {t('smartPlugs.mqttPowerHint')}

{/* Energy Section */}
-

Energy Monitoring (optional)

+

{t('smartPlugs.energyMonitoring')} ({t('smartPlugs.optional')})

- +
- +
- +
-

- JSON path extracts value from JSON payload. Leave empty for raw values.
- Use multiplier 0.001 for Wh→kWh, 1000 for MWh→kWh. +

+ {t('smartPlugs.mqttEnergyHint')}

{/* State Section */}
-

State Monitoring (optional)

+

{t('smartPlugs.stateMonitoring')} ({t('smartPlugs.optional')})

- +
- +
- +
-

- JSON path extracts value from JSON payload. Leave empty for raw values.
- ON value: the exact string that means "ON". Leave empty for auto-detect (ON, true, 1). +

+ {t('smartPlugs.mqttStateHint')}

@@ -1080,7 +1077,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {/* IP Address - only show for Tasmota */} {plugType === 'tasmota' && (
- +
)} - Test + {t('smartPlugs.test')}
@@ -1120,10 +1117,10 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { <>
-

Connected!

+

{t('smartPlugs.connectedResult')}

- {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 })}

@@ -1138,7 +1135,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {/* Name */}
- +
- +
- +

- Leave empty if your Tasmota device doesn't require authentication + {t('smartPlugs.authHint')}

)} @@ -1182,13 +1179,13 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) { {/* Link to Printer - not shown for MQTT plugs (monitor-only) */} {plugType !== 'mqtt' && (
- +

- Linking enables automatic on/off when prints start/complete + {t('smartPlugs.linkingDescription')}

)} @@ -1206,7 +1203,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
- Power Alerts + {t('smartPlugs.powerAlerts')}