feat(i18n): update frontend components to use translations

- Add i18n support to AddSmartPlugModal.tsx
- Add i18n support to ConfigureAmsSlotModal.tsx
- Add i18n support to RichTextEditor.tsx
- Add i18n support to ExternalLinksSettings.tsx
- Replace all hardcoded English strings with t() calls

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-10 14:26:51 +00:00
co-authored by cadtoolbox
parent e57ab8bf7e
commit 903f43d71a
6 changed files with 176 additions and 168 deletions
+20 -18
View File
@@ -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 && (
@@ -957,7 +959,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="text"
value={mqttPowerTopic}
onChange={(e) => setMqttPowerTopic(e.target.value)}
placeholder="zigbee2mqtt/shelly-working-room"
placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttPowerTopic')}
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>
@@ -968,7 +970,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="text"
value={mqttPowerPath}
onChange={(e) => setMqttPowerPath(e.target.value)}
placeholder="power_l1"
placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttPowerPath')}
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>
@@ -1009,7 +1011,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="text"
value={mqttEnergyPath}
onChange={(e) => setMqttEnergyPath(e.target.value)}
placeholder="energy_l1"
placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttEnergyPath')}
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>
@@ -1050,7 +1052,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="text"
value={mqttStatePath}
onChange={(e) => setMqttStatePath(e.target.value)}
placeholder="state_l1"
placeholder={t('smartPlugs.addSmartPlug.placeholders.mqttStatePath')}
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>
@@ -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>
@@ -1087,7 +1089,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
setIpAddress(e.target.value);
setTestResult(null);
}}
placeholder="192.168.1.100"
placeholder={t('smartPlugs.addSmartPlug.placeholders.ipAddress')}
className="flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<Button
@@ -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.name')}
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>
@@ -1156,7 +1158,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="admin"
placeholder={t('smartPlugs.addSmartPlug.placeholders.username')}
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>
@@ -1166,7 +1168,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="********"
placeholder={t('smartPlugs.addSmartPlug.placeholders.password')}
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>
@@ -1225,7 +1227,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="number"
value={powerAlertHigh}
onChange={(e) => setPowerAlertHigh(e.target.value)}
placeholder="e.g. 200"
placeholder={t('smartPlugs.addSmartPlug.placeholders.powerAlertHigh')}
min="0"
max="5000"
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"
@@ -1237,7 +1239,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
type="number"
value={powerAlertLow}
onChange={(e) => setPowerAlertLow(e.target.value)}
placeholder="e.g. 10"
placeholder={t('smartPlugs.addSmartPlug.placeholders.powerAlertLow')}
min="0"
max="5000"
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"
@@ -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>
@@ -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
View File
@@ -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>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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-BA-rHKiq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BwfbnBQ9.css">
</head>
<body>