From 032145b51746b242fb7d47e03ea649415aff204a Mon Sep 17 00:00:00 2001 From: skyboooox Date: Wed, 17 Jun 2026 16:59:52 +0800 Subject: [PATCH] Add Simplified Chinese localization --- frontend/src/app/audit/page.tsx | 158 +- .../dashboard/components/ActivityHeatmap.tsx | 31 +- .../components/CompactQuickAccess.tsx | 16 +- .../components/EnhancedQuickStats.tsx | 72 +- .../app/dashboard/components/FeatureGrid.tsx | 20 +- .../components/RobotFleetSection.tsx | 46 +- .../components/RobotUsageAnalytics.tsx | 32 +- .../components/UserActivitySection.tsx | 67 +- .../dashboard/components/WelcomeSection.tsx | 55 +- frontend/src/app/health/page.tsx | 78 +- frontend/src/app/labs/page.tsx | 6 +- frontend/src/app/log/page.tsx | 87 +- frontend/src/app/maps/page.tsx | 100 +- frontend/src/app/missions/page.tsx | 155 +- frontend/src/app/profile/page.tsx | 100 +- frontend/src/app/settings/page.tsx | 12 +- frontend/src/components/avatar-upload.tsx | 26 +- .../src/components/cockpit/missions-panel.tsx | 23 +- .../components/dashboard/AddWidgetModal.tsx | 82 +- frontend/src/components/extras-bar.tsx | 6 +- .../components/health/WifiControlPanel.tsx | 132 +- frontend/src/components/login.tsx | 24 +- .../src/components/map-editor/MapEditor.tsx | 22 +- frontend/src/components/map-view-native.tsx | 24 +- frontend/src/components/map-view-nav2.tsx | 74 +- frontend/src/components/map-view.tsx | 24 +- frontend/src/components/mission-map-view.tsx | 86 +- .../components/mission-map-warning-banner.tsx | 22 +- .../src/components/moondream-detections.tsx | 138 +- .../src/components/robot-connection-popup.tsx | 12 +- frontend/src/components/sidebar.tsx | 8 +- frontend/src/components/simple-map-view.tsx | 32 +- .../src/components/soundboard-sound-clips.tsx | 70 +- .../components/weather/WeatherDashboard.tsx | 21 +- .../widgets/MapsManagementWidget.tsx | 42 +- .../src/components/widgets/MissionsWidget.tsx | 42 +- .../widgets/RecentDetectionsWidget.tsx | 18 +- .../src/components/widgets/RecorderWidget.tsx | 58 +- .../components/widgets/SoundClipsWidget.tsx | 60 +- frontend/src/components/yolo-filter-bar.tsx | 70 +- .../src/components/yolo-settings-dialog.tsx | 48 +- frontend/src/contexts/LanguageContext.tsx | 36 +- frontend/src/utils/translations/en.ts | 789 ++++++++++ frontend/src/utils/translations/index.ts | 5 +- frontend/src/utils/translations/pt.ts | 789 ++++++++++ frontend/src/utils/translations/zh-CN.ts | 1292 +++++++++++++++++ 46 files changed, 4105 insertions(+), 1005 deletions(-) create mode 100644 frontend/src/utils/translations/zh-CN.ts diff --git a/frontend/src/app/audit/page.tsx b/frontend/src/app/audit/page.tsx index da24ad2..d8fbaca 100644 --- a/frontend/src/app/audit/page.tsx +++ b/frontend/src/app/audit/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { useSupabase } from '@/contexts/SupabaseProvider'; +import { useLanguage } from '@/contexts/LanguageContext'; import { format, subDays, startOfDay, endOfDay, parseISO } from 'date-fns'; import { Search, @@ -98,6 +99,7 @@ const criticalActions = [ export default function AuditPage() { const { user, supabase } = useSupabase(); + const { t } = useLanguage(); const [logs, setLogs] = useState([]); const [filteredLogs, setFilteredLogs] = useState([]); const [loading, setLoading] = useState(true); @@ -120,8 +122,38 @@ export default function AuditPage() { const [viewMode, setViewMode] = useState<'timeline' | 'analytics'>('analytics'); useEffect(() => { - document.title = 'Advanced Audit - BotBot'; - }, []); + document.title = `${t('auditLog', 'advancedPageTitle')} - BotBot`; + }, [t]); + + const getDateRangeSummary = () => { + if (selectedDateRange === 'all') { + return t('auditLog', 'allTime'); + } + + return t('auditLog', 'lastRange').replace( + '{range}', + t('auditLog', 'daysRange').replace('{count}', selectedDateRange) + ); + }; + + const getEventTypeLabel = (type: string) => { + const labels: Record = { + all: t('auditLog', 'allEvents'), + auth: t('auditLog', 'authentication'), + robot: t('auditLog', 'robot'), + command: t('auditLog', 'commands'), + system: t('auditLog', 'system'), + data: t('auditLog', 'data'), + mission: t('auditLog', 'mission'), + navigation: t('auditLog', 'navigation'), + audio: t('auditLog', 'audio'), + camera: t('auditLog', 'camera'), + safety: t('auditLog', 'safety'), + export: t('auditLog', 'export'), + }; + + return labels[type] || type; + }; // Fetch audit logs with date filtering const fetchLogs = async () => { @@ -240,7 +272,17 @@ export default function AuditPage() { // Export logs with enhanced format const exportLogs = () => { const csv = [ - ['Date', 'Time', 'Event Type', 'Action', 'Robot', 'Robot ID', 'IP Address', 'User Agent', 'Details'], + [ + t('auditLog', 'dateCsv'), + t('auditLog', 'timeCsv'), + t('auditLog', 'eventTypeCsv'), + t('auditLog', 'actionCsv'), + t('auditLog', 'robotCsv'), + t('auditLog', 'robotIdCsv'), + t('auditLog', 'ipAddressCsv'), + t('auditLog', 'userAgentCsv'), + t('auditLog', 'detailsCsv') + ], ...filteredLogs.map(log => [ format(new Date(log.created_at), 'yyyy-MM-dd'), format(new Date(log.created_at), 'HH:mm:ss'), @@ -311,7 +353,7 @@ export default function AuditPage() {
-

Loading audit data...

+

{t('auditLog', 'loadingData')}

); @@ -324,10 +366,10 @@ export default function AuditPage() { {/* Header */}

- Advanced Audit System + {t('auditLog', 'advancedPageTitle')}

- Comprehensive tracking and analytics for all operator activities + {t('auditLog', 'advancedPageDescription')}

@@ -336,12 +378,12 @@ export default function AuditPage() {
-

Total Events

+

{t('auditLog', 'totalEvents')}

{stats.totalEvents.toLocaleString()}

- Last {selectedDateRange === 'all' ? 'all time' : `${selectedDateRange} days`} + {getDateRangeSummary()}

@@ -351,12 +393,12 @@ export default function AuditPage() {
-

Events Today

+

{t('auditLog', 'eventsToday')}

{stats.eventsToday.toLocaleString()}

- Since midnight + {t('auditLog', 'sinceMidnight')}

@@ -366,12 +408,14 @@ export default function AuditPage() {
-

Active Robots

+

{t('auditLog', 'activeRobots')}

{stats.uniqueRobots}

- {stats.mostActiveRobot ? `Most active: ${stats.mostActiveRobot}` : 'No robots active'} + {stats.mostActiveRobot + ? t('auditLog', 'mostActiveRobot').replace('{robotName}', stats.mostActiveRobot) + : t('auditLog', 'noRobotsActive')}

@@ -381,12 +425,12 @@ export default function AuditPage() {
-

Critical Events

+

{t('auditLog', 'criticalEvents')}

{stats.criticalEvents}

- Safety & failures + {t('auditLog', 'safetyAndFailures')}

@@ -408,7 +452,7 @@ export default function AuditPage() { }`} > - Analytics + {t('auditLog', 'analytics')}
@@ -429,7 +473,7 @@ export default function AuditPage() { setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-gray-50 dark:bg-botbot-dark border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" @@ -445,11 +489,11 @@ export default function AuditPage() { onChange={(e) => setSelectedDateRange(e.target.value)} className="px-4 py-2 bg-gray-50 dark:bg-botbot-dark border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" > - - - - - + + + + + {/* Event Type Filter */} @@ -458,18 +502,18 @@ export default function AuditPage() { onChange={(e) => setSelectedEventType(e.target.value)} className="px-4 py-2 bg-gray-50 dark:bg-botbot-dark border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" > - - - - - - - - - - - - + + + + + + + + + + + +
@@ -483,14 +527,14 @@ export default function AuditPage() { disabled={isRefreshing} > - Refresh + {t('auditLog', 'refresh')}
@@ -516,7 +560,7 @@ export default function AuditPage() { {/* Event Types */}

- Event Distribution by Type + {t('auditLog', 'eventDistributionByType')}

{Object.entries(stats.eventsByType).map(([type, count]) => { @@ -530,7 +574,7 @@ export default function AuditPage() {
- {type} + {getEventTypeLabel(type)} {count} ({percentage}%) @@ -552,7 +596,7 @@ export default function AuditPage() { {/* Hourly Activity */}

- 24-Hour Activity Pattern + {t('auditLog', 'activityPattern24h')}

{stats.hourlyDistribution.map((count, hour) => { @@ -563,7 +607,9 @@ export default function AuditPage() { key={hour} className="flex-1 bg-purple-600 dark:bg-purple-500 rounded-t hover:bg-purple-700 dark:hover:bg-purple-400 transition-colors relative group" style={{ height: `${height}%` }} - title={`${hour}:00 - ${count} events`} + title={t('auditLog', 'hourlyActivityTitle') + .replace('{hour}', String(hour)) + .replace('{count}', String(count))} > {count} @@ -585,7 +631,7 @@ export default function AuditPage() { {/* Top Actions */}

- Most Frequent Actions + {t('auditLog', 'mostFrequentActions')}

{Object.entries(stats.eventsByAction) @@ -623,19 +669,19 @@ export default function AuditPage() { - Time + {t('auditLog', 'time')} - Type + {t('auditLog', 'type')} - Action + {t('auditLog', 'action')} - Robot + {t('auditLog', 'robot')} - Details + {t('auditLog', 'details')} @@ -646,7 +692,7 @@ export default function AuditPage() {
-

No audit logs found

+

{t('auditLog', 'noAuditLogsFound')}

@@ -668,7 +714,7 @@ export default function AuditPage() {
- {format(new Date(log.created_at), 'MMM dd, yyyy')} + {format(new Date(log.created_at), 'yyyy-MM-dd')}
{format(new Date(log.created_at), 'HH:mm:ss')} @@ -696,7 +742,7 @@ export default function AuditPage() { {log.event_details && Object.keys(log.event_details).length > 0 ? (
- View details + {t('auditLog', 'viewDetails')}
                                       {JSON.stringify(log.event_details, null, 2)}
@@ -730,12 +776,12 @@ export default function AuditPage() {
             

- Clear All Audit Logs + {t('auditLog', 'clearAllTitle')}

- Are you sure you want to clear all {logs.length} audit logs? This action cannot be undone. + {t('auditLog', 'clearAllConfirm').replace('{count}', String(logs.length))}

@@ -743,7 +789,7 @@ export default function AuditPage() { onClick={() => setShowDeleteModal(false)} className="px-4 py-2 bg-gray-200 dark:bg-botbot-dark text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-botbot-dark/80 transition-colors" > - Cancel + {t('common', 'cancel')} @@ -770,4 +816,4 @@ export default function AuditPage() { )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/ActivityHeatmap.tsx b/frontend/src/app/dashboard/components/ActivityHeatmap.tsx index 90d8712..3b212bf 100644 --- a/frontend/src/app/dashboard/components/ActivityHeatmap.tsx +++ b/frontend/src/app/dashboard/components/ActivityHeatmap.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { useSupabase } from '@/contexts/SupabaseProvider'; +import { useLanguage } from '@/contexts/LanguageContext'; import { Activity, TrendingUp } from 'lucide-react'; interface HourlyActivity { @@ -12,13 +13,22 @@ interface HourlyActivity { export default function ActivityHeatmap() { const { supabase, user } = useSupabase(); + const { t } = useLanguage(); const [hourlyData, setHourlyData] = useState([]); const [loading, setLoading] = useState(true); const [maxActivity, setMaxActivity] = useState(0); const [totalActions, setTotalActions] = useState(0); const [weeklyActions, setWeeklyActions] = useState(0); - const days = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const days = [ + t('dashboard', 'sundayShort'), + t('dashboard', 'mondayShort'), + t('dashboard', 'tuesdayShort'), + t('dashboard', 'wednesdayShort'), + t('dashboard', 'thursdayShort'), + t('dashboard', 'fridayShort'), + t('dashboard', 'saturdayShort'), + ]; const hours = Array.from({ length: 24 }, (_, i) => i); useEffect(() => { @@ -121,10 +131,10 @@ export default function ActivityHeatmap() {

- Activity Pattern + {t('dashboard', 'activityPattern')}

- Last 30 days + {t('dashboard', 'last30Days')}

@@ -134,11 +144,11 @@ export default function ActivityHeatmap() { {totalActions.toLocaleString()} - total + {t('dashboard', 'total')}
- {avgDailyActions}/day avg + {t('dashboard', 'dayAverage').replace('{count}', String(avgDailyActions))}
@@ -184,7 +194,10 @@ export default function ActivityHeatmap() { hover:scale-125 hover:z-10 hover:shadow-lg cursor-pointer ${getHeatmapColor(count)} `} - title={`${days[dayIndex]} ${hour}:00 - ${count} actions`} + title={t('dashboard', 'activityCellTitle') + .replace('{day}', days[dayIndex]) + .replace('{hour}', String(hour)) + .replace('{count}', String(count))} /> ); })} @@ -196,7 +209,7 @@ export default function ActivityHeatmap() { {/* Compact Legend */}
- Less + {t('dashboard', 'less')}
{[0, 20, 40, 60, 80, 100].map((_, i) => (
))}
- More + {t('dashboard', 'more')}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/CompactQuickAccess.tsx b/frontend/src/app/dashboard/components/CompactQuickAccess.tsx index c6c70a2..7df66b2 100644 --- a/frontend/src/app/dashboard/components/CompactQuickAccess.tsx +++ b/frontend/src/app/dashboard/components/CompactQuickAccess.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'next/navigation'; import { Network, Home, Layout, ArrowRight, Sparkles } from 'lucide-react'; +import { useLanguage } from '@/contexts/LanguageContext'; interface QuickAccessItem { icon: React.ReactNode; @@ -14,29 +15,30 @@ interface QuickAccessItem { export default function CompactQuickAccess() { const router = useRouter(); + const { t } = useLanguage(); const items: QuickAccessItem[] = [ { icon: , - title: "Cockpit", + title: t('dashboard', 'cockpit'), href: "/cockpit", gradient: "from-violet-500 to-purple-600", - description: "Control & monitor your robot", + description: t('dashboard', 'cockpitDescription'), highlight: true }, { icon: , - title: "Fleet Manager", + title: t('dashboard', 'fleetManager'), href: "/fleet", gradient: "from-blue-500 to-cyan-600", - description: "Manage all your robots" + description: t('dashboard', 'fleetManagerDescription') }, { icon: , - title: "My UI", + title: t('dashboard', 'myUI'), href: "/my-ui", gradient: "from-green-500 to-emerald-600", - description: "Customize your workspace" + description: t('dashboard', 'myUIDescription') } ]; @@ -113,4 +115,4 @@ export default function CompactQuickAccess() { ))}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/EnhancedQuickStats.tsx b/frontend/src/app/dashboard/components/EnhancedQuickStats.tsx index cb3d730..ab221fb 100644 --- a/frontend/src/app/dashboard/components/EnhancedQuickStats.tsx +++ b/frontend/src/app/dashboard/components/EnhancedQuickStats.tsx @@ -8,6 +8,7 @@ import { } from 'lucide-react'; import { useRobotConnection } from '@/contexts/RobotConnectionContext'; import { useSupabase } from '@/contexts/SupabaseProvider'; +import { useLanguage } from '@/contexts/LanguageContext'; import type { Database } from '@/types/database.types'; type Robot = Database['public']['Tables']['robots']['Row']; @@ -112,6 +113,7 @@ function StatCard({ export default function EnhancedQuickStats() { const { connection } = useRobotConnection(); const { supabase, user } = useSupabase(); + const { t } = useLanguage(); const [loading, setLoading] = useState(true); // Basic stats @@ -123,9 +125,9 @@ export default function EnhancedQuickStats() { const [monthlyActions, setMonthlyActions] = useState(0); // Enhanced stats - const [memberDuration, setMemberDuration] = useState('New'); + const [memberDuration, setMemberDuration] = useState(''); const [peakHour, setPeakHour] = useState('--'); - const [mostUsedRobot, setMostUsedRobot] = useState('None'); + const [mostUsedRobot, setMostUsedRobot] = useState(''); const [activeDays, setActiveDays] = useState(0); const [missionsTotal, setMissionsTotal] = useState(0); const [detectionsCount, setDetectionsCount] = useState(0); @@ -159,13 +161,13 @@ export default function EnhancedQuickStats() { const diffYears = Math.floor(diffDays / 365); if (diffYears > 0) { - setMemberDuration(`${diffYears}+ year${diffYears > 1 ? 's' : ''}`); + setMemberDuration(`${diffYears}+ ${t('dashboard', diffYears > 1 ? 'years' : 'year')}`); } else if (diffMonths > 0) { - setMemberDuration(`${diffMonths} month${diffMonths > 1 ? 's' : ''}`); + setMemberDuration(`${diffMonths} ${t('dashboard', diffMonths > 1 ? 'months' : 'month')}`); } else if (diffDays > 0) { - setMemberDuration(`${diffDays} day${diffDays > 1 ? 's' : ''}`); + setMemberDuration(`${diffDays} ${t('dashboard', diffDays > 1 ? 'days' : 'day')}`); } else { - setMemberDuration('New member'); + setMemberDuration(t('dashboard', 'newMember')); } } @@ -325,7 +327,7 @@ export default function EnhancedQuickStats() { }; fetchAllStats(); - }, [user, supabase]); + }, [user, supabase, t]); const onlineRobots = connection.online ? 1 : 0; @@ -335,11 +337,11 @@ export default function EnhancedQuickStats() {
} - label="Fleet Size" + label={t('dashboard', 'fleetSize')} value={robots.length} - subValue={`${onlineRobots} online now`} + subValue={`${onlineRobots} ${t('dashboard', 'onlineNow')}`} trend={robots.length > 0 ? 'up' : 'neutral'} - trendValue={onlineRobots > 0 ? 'Active' : 'Offline'} + trendValue={onlineRobots > 0 ? t('dashboard', 'active') : t('dashboard', 'offline')} gradient="from-violet-500 to-purple-600" variant="gradient" loading={loading} @@ -347,11 +349,11 @@ export default function EnhancedQuickStats() { } - label="Total Actions" + label={t('dashboard', 'totalActions')} value={totalActions > 9999 ? `${(totalActions / 1000).toFixed(1)}k` : totalActions} - subValue={`${activeDays} active days`} + subValue={`${activeDays} ${t('dashboard', 'activeDays')}`} trend={monthlyActions > 500 ? 'up' : 'down'} - trendValue={`${monthlyActions} this month`} + trendValue={`${monthlyActions} ${t('dashboard', 'thisMonth')}`} gradient="from-blue-500 to-cyan-600" variant="glass" loading={loading} @@ -359,8 +361,8 @@ export default function EnhancedQuickStats() { } - label="User Since" - value={memberDuration} + label={t('dashboard', 'userSince')} + value={memberDuration || t('dashboard', 'new')} subValue={userProfile ? new Date(userProfile.created_at).toLocaleDateString() : '--'} gradient="from-emerald-500 to-teal-600" variant="default" @@ -369,9 +371,9 @@ export default function EnhancedQuickStats() { } - label="Favorite Robot" - value={mostUsedRobot} - subValue={robots.filter(r => r.is_favorite).length > 0 ? 'Most used' : 'No favorite set'} + label={t('dashboard', 'favoriteRobot')} + value={mostUsedRobot || t('dashboard', 'noFavoriteSet')} + subValue={robots.filter(r => r.is_favorite).length > 0 ? t('dashboard', 'mostUsed') : t('dashboard', 'noFavoriteSet')} gradient="from-yellow-500 to-orange-600" variant="neon" loading={loading} @@ -382,11 +384,11 @@ export default function EnhancedQuickStats() {
} - label="Today" + label={t('dashboard', 'today')} value={todayActions} - subValue="actions" + subValue={t('dashboard', 'actions')} trend={todayActions > 50 ? 'up' : todayActions > 10 ? 'neutral' : 'down'} - trendValue={todayActions > 50 ? 'Very active' : todayActions > 10 ? 'Active' : 'Quiet'} + trendValue={todayActions > 50 ? t('dashboard', 'veryActive') : todayActions > 10 ? t('dashboard', 'active') : t('dashboard', 'quiet')} gradient="from-indigo-500 to-blue-600" variant="glass" size="small" @@ -395,11 +397,11 @@ export default function EnhancedQuickStats() { } - label="This Week" + label={t('dashboard', 'thisWeek')} value={weeklyActions} - subValue="actions" + subValue={t('dashboard', 'actions')} trend={weeklyActions > 200 ? 'up' : 'neutral'} - trendValue={`Avg ${Math.round(weeklyActions / 7)}/day`} + trendValue={t('dashboard', 'avgPerDay').replace('{count}', String(Math.round(weeklyActions / 7)))} gradient="from-pink-500 to-rose-600" variant="glass" size="small" @@ -408,9 +410,9 @@ export default function EnhancedQuickStats() { } - label="Peak Hour" + label={t('dashboard', 'peakHour')} value={peakHour} - subValue="most active" + subValue={t('dashboard', 'mostActive')} gradient="from-purple-500 to-violet-600" variant="glass" size="small" @@ -422,9 +424,9 @@ export default function EnhancedQuickStats() {
} - label="Missions" + label={t('dashboard', 'missions')} value={missionsTotal} - subValue="created" + subValue={t('dashboard', 'created')} gradient="from-red-500 to-pink-600" variant="default" size="small" @@ -433,9 +435,9 @@ export default function EnhancedQuickStats() { } - label="AI Detections" + label={t('dashboard', 'aiDetections')} value={detectionsCount} - subValue={avgConfidence > 0 ? `${(avgConfidence * 100).toFixed(0)}% conf` : 'YOLO active'} + subValue={avgConfidence > 0 ? `${(avgConfidence * 100).toFixed(0)}% ${t('dashboard', 'confidenceAbbrev')}` : t('dashboard', 'yoloActive')} gradient="from-green-500 to-emerald-600" variant="default" size="small" @@ -444,9 +446,9 @@ export default function EnhancedQuickStats() { } - label="Sound Library" + label={t('dashboard', 'soundLibrary')} value={soundsCount} - subValue="audio clips" + subValue={t('dashboard', 'audioClips')} gradient="from-orange-500 to-red-600" variant="default" size="small" @@ -455,9 +457,9 @@ export default function EnhancedQuickStats() { } - label="Storage" + label={t('dashboard', 'storage')} value={storageUsed} - subValue="used" + subValue={t('dashboard', 'used')} gradient="from-gray-600 to-gray-800" variant="default" size="small" @@ -466,4 +468,4 @@ export default function EnhancedQuickStats() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/FeatureGrid.tsx b/frontend/src/app/dashboard/components/FeatureGrid.tsx index c002870..98b2ab9 100644 --- a/frontend/src/app/dashboard/components/FeatureGrid.tsx +++ b/frontend/src/app/dashboard/components/FeatureGrid.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'next/navigation'; import { Network, Home, Layout, ArrowUpRight } from 'lucide-react'; +import { useLanguage } from '@/contexts/LanguageContext'; interface FeatureCardProps { icon: React.ReactNode; @@ -121,27 +122,28 @@ function FeatureCard({ } export default function FeatureGrid() { + const { t } = useLanguage(); const features = [ { icon: , - title: "Robot Fleet", - description: "Manage and connect to your entire robot fleet with real-time status monitoring", + title: t('dashboard', 'robotFleet'), + description: t('dashboard', 'robotFleetDescription'), href: "/fleet", gradient: "from-violet-500 to-purple-600", variant: 'gradient' as const, }, { icon: , - title: "Cockpit", - description: "Control center for robot operations, telemetry, and real-time diagnostics", + title: t('dashboard', 'cockpit'), + description: t('dashboard', 'cockpitDescription'), href: "/cockpit", gradient: "from-blue-500 to-cyan-600", variant: 'glass' as const, }, { icon: , - title: "My UI", - description: "Customize your dashboard with widgets and personalized layouts", + title: t('dashboard', 'myUI'), + description: t('dashboard', 'myUIDescription'), href: "/my-ui", gradient: "from-green-500 to-emerald-600", variant: 'glass' as const, @@ -152,8 +154,8 @@ export default function FeatureGrid() {
-

Quick Access

-

Your most used features and tools

+

{t('dashboard', 'quickAccess')}

+

{t('dashboard', 'quickAccessDescription')}

@@ -168,4 +170,4 @@ export default function FeatureGrid() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/RobotFleetSection.tsx b/frontend/src/app/dashboard/components/RobotFleetSection.tsx index 3c82fff..ab3e886 100644 --- a/frontend/src/app/dashboard/components/RobotFleetSection.tsx +++ b/frontend/src/app/dashboard/components/RobotFleetSection.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; import { Bot, Wifi, WifiOff, Star, Plus, ArrowRight, Loader2 } from 'lucide-react'; import { useRobotConnection } from '@/contexts/RobotConnectionContext'; import { useSupabase } from '@/contexts/SupabaseProvider'; +import { useLanguage } from '@/contexts/LanguageContext'; import { Database } from '@/types/database.types'; import Button from '@/components/ui/button'; @@ -15,9 +16,10 @@ interface RobotCardProps { isConnected: boolean; isConnecting: boolean; onConnect: () => void; + t: ReturnType['t']; } -function RobotCard({ robot, isConnected, isConnecting, onConnect }: RobotCardProps) { +function RobotCard({ robot, isConnected, isConnecting, onConnect, t }: RobotCardProps) { const statusGradient = isConnected ? 'from-green-400 to-emerald-500' : 'from-gray-400 to-gray-500'; @@ -70,7 +72,7 @@ function RobotCard({ robot, isConnected, isConnecting, onConnect }: RobotCardPro {robot.name}

- {robot.type || 'Robot'} • ID: {robot.id.slice(0, 8)} + {robot.type || t('dashboard', 'robot')} • ID: {robot.id.slice(0, 8)}

@@ -88,20 +90,20 @@ function RobotCard({ robot, isConnected, isConnecting, onConnect }: RobotCardPro
- Live + {t('dashboard', 'live')} ) : ( <>
- Offline + {t('dashboard', 'offline')} )}
{isConnected && ( -
-
- Active +
+
+ {t('dashboard', 'active')}
)}
@@ -117,19 +119,19 @@ function RobotCard({ robot, isConnected, isConnecting, onConnect }: RobotCardPro {isConnecting ? ( <> - Connecting... + {t('dashboard', 'connecting')} ) : ( <> - Connect + {t('dashboard', 'connect')} )} ) : (
- Connected + {t('dashboard', 'connected')}
)}
@@ -142,6 +144,7 @@ export default function RobotFleetSection() { const router = useRouter(); const { connection, connectionStatus, connectToRobotWithInfo } = useRobotConnection(); const { supabase, user } = useSupabase(); + const { t } = useLanguage(); const [robots, setRobots] = useState([]); const [loading, setLoading] = useState(true); const [connectingRobotId, setConnectingRobotId] = useState(null); @@ -188,7 +191,7 @@ export default function RobotFleetSection() { return (
-

Your Robots

+

{t('dashboard', 'yourRobots')}

{[1, 2, 3, 4, 5, 6].map((i) => ( @@ -214,7 +217,7 @@ export default function RobotFleetSection() { return (
-

Your Robots

+

{t('dashboard', 'yourRobots')}

@@ -222,13 +225,13 @@ export default function RobotFleetSection() {

- No Robots Yet + {t('dashboard', 'noRobotsYet')}

- Get started by adding your first robot + {t('dashboard', 'getStartedAddFirstRobot')}

@@ -263,6 +266,7 @@ export default function RobotFleetSection() { isConnected={connection.connectedRobot?.id === robot.id && connection.online} isConnecting={connectingRobotId === robot.id && connectionStatus === 'connecting'} onConnect={() => handleConnect(robot)} + t={t} /> ))} @@ -282,14 +286,14 @@ export default function RobotFleetSection() {

- Add Robot + {t('dashboard', 'addRobot')}

- Connect new device + {t('dashboard', 'connectNewDevice')}

); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/RobotUsageAnalytics.tsx b/frontend/src/app/dashboard/components/RobotUsageAnalytics.tsx index 7593302..2935220 100644 --- a/frontend/src/app/dashboard/components/RobotUsageAnalytics.tsx +++ b/frontend/src/app/dashboard/components/RobotUsageAnalytics.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { useSupabase } from '@/contexts/SupabaseProvider'; +import { useLanguage } from '@/contexts/LanguageContext'; import { Bot, TrendingUp, Star, Clock, Zap, ChevronDown, ChevronUp } from 'lucide-react'; import { Database } from '@/types/database.types'; @@ -17,6 +18,7 @@ interface RobotUsage { export default function RobotUsageAnalytics() { const { supabase, user } = useSupabase(); + const { t } = useLanguage(); const [robotUsage, setRobotUsage] = useState([]); const [loading, setLoading] = useState(true); const [totalUsage, setTotalUsage] = useState(0); @@ -144,7 +146,7 @@ export default function RobotUsageAnalytics() { }, [user, supabase]); const formatLastUsed = (timestamp: string | null) => { - if (!timestamp) return 'Never'; + if (!timestamp) return t('dashboard', 'never'); const date = new Date(timestamp); const now = new Date(); @@ -153,10 +155,10 @@ export default function RobotUsageAnalytics() { const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); - if (diffMins < 1) return 'Just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; + if (diffMins < 1) return t('dashboard', 'justNow'); + if (diffMins < 60) return t('dashboard', 'minutesAgo').replace('{count}', String(diffMins)); + if (diffHours < 24) return t('dashboard', 'hoursAgo').replace('{count}', String(diffHours)); + if (diffDays < 7) return t('dashboard', 'daysAgo').replace('{count}', String(diffDays)); return date.toLocaleDateString(); }; @@ -188,7 +190,7 @@ export default function RobotUsageAnalytics() {
-

No robots configured yet

+

{t('dashboard', 'noRobotsConfiguredYet')}

); @@ -206,10 +208,10 @@ export default function RobotUsageAnalytics() {

- Robot Fleet Usage + {t('dashboard', 'robotFleetUsage')}

- {robotUsage.length} robot{robotUsage.length !== 1 ? 's' : ''} + {t('dashboard', robotUsage.length === 1 ? 'robotCountSingular' : 'robotCount').replace('{count}', String(robotUsage.length))}

@@ -218,12 +220,12 @@ export default function RobotUsageAnalytics() { {totalUsage.toLocaleString()} - actions + {t('dashboard', 'actions')}
{mostActiveTime && (
- Peak: {mostActiveTime} + {t('dashboard', 'peak')}: {mostActiveTime}
)}
@@ -260,7 +262,7 @@ export default function RobotUsageAnalytics() {

- {usage.usageCount.toLocaleString()} actions + {usage.usageCount.toLocaleString()} {t('dashboard', 'actions')}

@@ -271,7 +273,7 @@ export default function RobotUsageAnalytics() {

- Active + {t('dashboard', 'active')}
)} @@ -313,16 +315,16 @@ export default function RobotUsageAnalytics() { {isExpanded ? ( <> - Show less + {t('dashboard', 'showLess')} ) : ( <> - Show {robotUsage.length - 3} more + {t('dashboard', 'showMore').replace('{count}', String(robotUsage.length - 3))} )} )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/UserActivitySection.tsx b/frontend/src/app/dashboard/components/UserActivitySection.tsx index 18a669a..fecf9c9 100644 --- a/frontend/src/app/dashboard/components/UserActivitySection.tsx +++ b/frontend/src/app/dashboard/components/UserActivitySection.tsx @@ -17,6 +17,8 @@ import { } from 'lucide-react'; import { useSupabase } from '@/contexts/SupabaseProvider'; import { useRouter } from 'next/navigation'; +import { useLanguage } from '@/contexts/LanguageContext'; +import type { NestedTranslationKey } from '@/utils/translations'; interface ActivityItem { id: string; @@ -28,8 +30,8 @@ interface ActivityItem { interface AchievementBadge { id: string; - title: string; - description: string; + titleKey: NestedTranslationKey<'dashboard'>; + descriptionKey: NestedTranslationKey<'dashboard'>; icon: React.ReactNode; unlocked: boolean; progress: number; @@ -39,6 +41,7 @@ interface AchievementBadge { export default function UserActivitySection() { const router = useRouter(); const { user, supabase } = useSupabase(); + const { t } = useLanguage(); const [recentActivity, setRecentActivity] = useState([]); const [achievements, setAchievements] = useState([]); const [weeklyProgress, setWeeklyProgress] = useState([0, 0, 0, 0, 0, 0, 0]); @@ -60,7 +63,7 @@ export default function UserActivitySection() { if (logs) { const activities = logs.map((log) => ({ id: log.id, - action: log.action || 'Action performed', + action: log.action || '', timestamp: new Date(log.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', @@ -79,8 +82,8 @@ export default function UserActivitySection() { setAchievements([ { id: '1', - title: 'First Connection', - description: 'Connect your first robot', + titleKey: 'achievementFirstConnection', + descriptionKey: 'achievementFirstConnectionDescription', icon: , unlocked: true, progress: 1, @@ -88,8 +91,8 @@ export default function UserActivitySection() { }, { id: '2', - title: 'Fleet Commander', - description: 'Manage 5+ robots', + titleKey: 'achievementFleetCommander', + descriptionKey: 'achievementFleetCommanderDescription', icon: , unlocked: false, progress: 3, @@ -97,8 +100,8 @@ export default function UserActivitySection() { }, { id: '3', - title: 'Power User', - description: '1000+ actions performed', + titleKey: 'achievementPowerUser', + descriptionKey: 'achievementPowerUserDescription', icon: , unlocked: false, progress: 750, @@ -106,8 +109,8 @@ export default function UserActivitySection() { }, { id: '4', - title: 'Global Operator', - description: 'Control robots remotely', + titleKey: 'achievementGlobalOperator', + descriptionKey: 'achievementGlobalOperatorDescription', icon: , unlocked: true, progress: 1, @@ -140,7 +143,15 @@ export default function UserActivitySection() { return 'from-gray-400 to-gray-500'; }; - const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const days = [ + t('dashboard', 'mondayAbbrev'), + t('dashboard', 'tuesdayAbbrev'), + t('dashboard', 'wednesdayAbbrev'), + t('dashboard', 'thursdayAbbrev'), + t('dashboard', 'fridayAbbrev'), + t('dashboard', 'saturdayAbbrev'), + t('dashboard', 'sundayAbbrev'), + ]; const maxProgress = Math.max(...weeklyProgress); return ( @@ -149,14 +160,14 @@ export default function UserActivitySection() {
-

Recent Activity

-

Your latest actions

+

{t('dashboard', 'recentActivity')}

+

{t('dashboard', 'yourLatestActions')}

@@ -188,7 +199,7 @@ export default function UserActivitySection() {

- {activity.action} + {activity.action || t('dashboard', 'actionPerformed')}

@@ -202,7 +213,7 @@ export default function UserActivitySection() { ) : (

-

No recent activity

+

{t('dashboard', 'noRecentActivity')}

)}
@@ -211,8 +222,8 @@ export default function UserActivitySection() {
-

Weekly Progress

-

Activity over the week

+

{t('dashboard', 'weeklyProgress')}

+

{t('dashboard', 'activityOverTheWeek')}

@@ -243,10 +254,10 @@ export default function UserActivitySection() {
- Actions performed + {t('dashboard', 'actionsPerformed')}
- {weeklyProgress.reduce((a, b) => a + b, 0)} total + {t('dashboard', 'totalCount').replace('{count}', String(weeklyProgress.reduce((a, b) => a + b, 0)))}
@@ -255,8 +266,8 @@ export default function UserActivitySection() {
-

Achievements

-

Your milestones

+

{t('dashboard', 'achievements')}

+

{t('dashboard', 'yourMilestones')}

@@ -289,15 +300,15 @@ export default function UserActivitySection() { : 'text-gray-500 dark:text-gray-400' }`} > - {achievement.title} + {t('dashboard', achievement.titleKey)} {achievement.unlocked && ( - UNLOCKED + {t('dashboard', 'unlocked')} )}

- {achievement.description} + {t('dashboard', achievement.descriptionKey)}

{/* Progress bar */} @@ -305,7 +316,7 @@ export default function UserActivitySection() {
- Progress + {t('dashboard', 'progress')} {achievement.progress}/{achievement.total} @@ -329,4 +340,4 @@ export default function UserActivitySection() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/dashboard/components/WelcomeSection.tsx b/frontend/src/app/dashboard/components/WelcomeSection.tsx index 72aa938..06b26a3 100644 --- a/frontend/src/app/dashboard/components/WelcomeSection.tsx +++ b/frontend/src/app/dashboard/components/WelcomeSection.tsx @@ -3,12 +3,14 @@ import { useEffect, useState } from 'react'; import { User, Calendar, Clock, Activity, TrendingUp, Shield, Sparkles } from 'lucide-react'; import { useSupabase } from '@/contexts/SupabaseProvider'; +import { useLanguage } from '@/contexts/LanguageContext'; import { Database } from '@/types/database.types'; type UserProfile = Database['public']['Tables']['user_profiles']['Row']; export default function WelcomeSection() { const { user, supabase } = useSupabase(); + const { language, t } = useLanguage(); const [userProfile, setUserProfile] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [lastSeen, setLastSeen] = useState(''); @@ -45,10 +47,10 @@ export default function WelcomeSection() { const diffMs = now.getTime() - lastActivity.getTime(); const diffMins = Math.floor(diffMs / 60000); - if (diffMins < 1) setLastSeen('Just now'); - else if (diffMins < 60) setLastSeen(`${diffMins}m ago`); - else if (diffMins < 1440) setLastSeen(`${Math.floor(diffMins / 60)}h ago`); - else setLastSeen(`${Math.floor(diffMins / 1440)}d ago`); + if (diffMins < 1) setLastSeen(t('dashboard', 'justNow')); + else if (diffMins < 60) setLastSeen(t('dashboard', 'minutesAgo').replace('{count}', String(diffMins))); + else if (diffMins < 1440) setLastSeen(t('dashboard', 'hoursAgo').replace('{count}', String(Math.floor(diffMins / 60)))); + else setLastSeen(t('dashboard', 'daysAgo').replace('{count}', String(Math.floor(diffMins / 1440)))); } // Fetch weekly activity count @@ -129,16 +131,16 @@ export default function WelcomeSection() { const diffYears = Math.floor(diffDays / 365); if (diffYears > 0) { - setMemberSince(`${diffYears} ${diffYears === 1 ? 'year' : 'years'}`); + setMemberSince(`${diffYears} ${t('dashboard', diffYears === 1 ? 'year' : 'years')}`); } else if (diffMonths > 0) { - setMemberSince(`${diffMonths} ${diffMonths === 1 ? 'month' : 'months'}`); + setMemberSince(`${diffMonths} ${t('dashboard', diffMonths === 1 ? 'month' : 'months')}`); } else if (diffDays > 7) { const weeks = Math.floor(diffDays / 7); - setMemberSince(`${weeks} ${weeks === 1 ? 'week' : 'weeks'}`); + setMemberSince(`${weeks} ${t('dashboard', weeks === 1 ? 'week' : 'weeks')}`); } else if (diffDays > 0) { - setMemberSince(`${diffDays} ${diffDays === 1 ? 'day' : 'days'}`); + setMemberSince(`${diffDays} ${t('dashboard', diffDays === 1 ? 'day' : 'days')}`); } else { - setMemberSince('Today'); + setMemberSince(t('dashboard', 'today')); } } } catch (error) { @@ -147,7 +149,7 @@ export default function WelcomeSection() { }; fetchUserProfile(); - }, [user, supabase]); + }, [user, supabase, t]); useEffect(() => { const timer = setInterval(() => { @@ -159,13 +161,14 @@ export default function WelcomeSection() { const getGreeting = () => { const hour = currentTime.getHours(); - if (hour < 12) return 'Good Morning'; - if (hour < 17) return 'Good Afternoon'; - return 'Good Evening'; + if (hour < 12) return t('dashboard', 'goodMorning'); + if (hour < 17) return t('dashboard', 'goodAfternoon'); + return t('dashboard', 'goodEvening'); }; const formatDate = () => { - return currentTime.toLocaleDateString('en-US', { + const locale = language === 'zh-CN' ? 'zh-CN' : language === 'pt' ? 'pt-BR' : 'en-US'; + return currentTime.toLocaleDateString(locale, { weekday: 'short', month: 'short', day: 'numeric', @@ -202,7 +205,7 @@ export default function WelcomeSection() { {userProfile?.avatar_url ? ( User Avatar ) : ( @@ -220,29 +223,33 @@ export default function WelcomeSection() {

- {getGreeting()}, {userProfile?.name || user?.email?.split('@')[0] || 'User'}! + {getGreeting()}, {userProfile?.name || user?.email?.split('@')[0] || t('dashboard', 'user')}!

-

Welcome back to BotBrain

+

{t('dashboard', 'welcomeBackToBotBrain')}

{/* User stats in a row */}
- Active {lastSeen || 'Now'} + {t('dashboard', 'active')} {lastSeen || t('dashboard', 'now')}
- {weeklyActivity} actions this week + {t('dashboard', 'actionsThisWeek').replace('{count}', String(weeklyActivity))}
{streak > 0 && (
- {streak} day{streak !== 1 ? 's' : ''} streak + + {t('dashboard', streak === 1 ? 'dayStreak' : 'daysStreak').replace('{count}', String(streak))} +
)}
- User for {memberSince || '0 days'} + + {t('dashboard', 'userFor').replace('{duration}', memberSince || `0 ${t('dashboard', 'days')}`)} +
@@ -255,7 +262,7 @@ export default function WelcomeSection() {
-

Current Time

+

{t('dashboard', 'currentTime')}

{formatTime()}

@@ -265,7 +272,7 @@ export default function WelcomeSection() {
-

Today's Date

+

{t('dashboard', 'todaysDate')}

{formatDate()}

@@ -275,4 +282,4 @@ export default function WelcomeSection() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/health/page.tsx b/frontend/src/app/health/page.tsx index 63bb373..fea7cd3 100644 --- a/frontend/src/app/health/page.tsx +++ b/frontend/src/app/health/page.tsx @@ -264,7 +264,7 @@ function DiagnosticItem({ diagnostic, t }: { diagnostic: DiagnosticStatus; t: an ))} {diagnostic.values.length > 3 && (

- +{diagnostic.values.length - 3} more values + {t('health', 'moreValues').replace('{count}', String(diagnostic.values.length - 3))}

)} @@ -305,7 +305,7 @@ function StateMachineRow({ @@ -318,19 +318,19 @@ function StateMachineRow({ onClick={() => onStart(node.name)} className="px-3 py-1 text-xs font-medium text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/20 rounded hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors" > - Start + {t('health', 'start')} @@ -353,7 +353,7 @@ function StateMachineRow({

{node.displayName}

-

Module Details

+

{t('health', 'moduleDetails')}

@@ -364,7 +364,7 @@ function StateMachineRow({ onClick={() => setShowIdDialog(false)} className="w-full px-4 py-2 text-sm font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors" > - Close + {t('health', 'close')}
@@ -589,7 +589,7 @@ export default function HealthPage() {

{t('health', 'systemInformation')}

{!hasJetsonData && connection.online && ( - Waiting for data... + {t('health', 'waitingForData')} )} @@ -600,7 +600,7 @@ export default function HealthPage() { className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/20 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > - {isRebooting ? 'Rebooting...' : 'Reboot System'} + {isRebooting ? t('health', 'rebooting') : t('health', 'rebootSystem')} )} @@ -660,7 +660,7 @@ export default function HealthPage() { <>
-

WiFi

+

{t('health', 'wifi')}

{networkModeStatus.ssid && (

{networkModeStatus.ssid}

)} @@ -671,7 +671,7 @@ export default function HealthPage() { <>
-

4G/Cellular

+

{t('health', 'cellular')}

{networkModeStatus.interface && (

{networkModeStatus.interface}

)} @@ -682,7 +682,7 @@ export default function HealthPage() { <>
-

Hotspot

+

{t('health', 'hotspot')}

{networkModeStatus.ssid && (

{networkModeStatus.ssid}

)} @@ -693,8 +693,8 @@ export default function HealthPage() { <>
-

Offline

-

No network

+

{t('health', 'offline')}

+

{t('health', 'noNetwork')}

)} @@ -716,9 +716,9 @@ export default function HealthPage() { {formatLatency(networkMetrics.latency)}

- {networkMetrics.latency < 0 ? 'Measuring...' : - networkMetrics.latency < 50 ? 'Excellent' : - networkMetrics.latency < 150 ? 'Good' : 'Poor'} + {networkMetrics.latency < 0 ? t('health', 'measuring') : + networkMetrics.latency < 50 ? t('health', 'excellent') : + networkMetrics.latency < 150 ? t('health', 'good') : t('health', 'poor')}

@@ -733,7 +733,7 @@ export default function HealthPage() {

{formatDataRate(networkMetrics.dataIn)}

-

Incoming data

+

{t('health', 'incomingData')}

@@ -747,7 +747,7 @@ export default function HealthPage() {

{formatDataRate(networkMetrics.dataOut)}

-

Outgoing data

+

{t('health', 'outgoingData')}

@@ -758,10 +758,10 @@ export default function HealthPage() {
{t('health', 'connectionQuality')} - {networkMetrics.latency < 0 ? 'Measuring...' : - networkMetrics.latency < 50 && networkMetrics.dataIn > 0 ? 'Excellent' : - networkMetrics.latency < 150 && networkMetrics.dataIn > 0 ? 'Good' : - networkMetrics.dataIn > 0 ? 'Fair' : 'Poor'} + {networkMetrics.latency < 0 ? t('health', 'measuring') : + networkMetrics.latency < 50 && networkMetrics.dataIn > 0 ? t('health', 'excellent') : + networkMetrics.latency < 150 && networkMetrics.dataIn > 0 ? t('health', 'good') : + networkMetrics.dataIn > 0 ? t('health', 'fair') : t('health', 'poor')}
@@ -811,7 +811,7 @@ export default function HealthPage() { /> {/* Individual CPU cores */}
-

Individual Cores

+

{t('health', 'individualCores')}

{data.cpus.map((cpu) => ( @@ -822,7 +822,7 @@ export default function HealthPage() { ) : (
{!connection.online ? t('health', 'notConnectedDescription') : - !hasJetsonData ? 'Waiting for CPU data...' : + !hasJetsonData ? t('health', 'waitingForCpuData') : t('health', 'noCpuData')}
)} @@ -933,7 +933,7 @@ export default function HealthPage() { {/* Power rails details */} {data.power.rails.length > 0 && (
-

Power Rails

+

{t('health', 'powerRails')}

{data.power.rails.map((rail, index) => ( @@ -1051,19 +1051,19 @@ export default function HealthPage() { {errors.length > 0 && ( - {errors.length} {errors.length === 1 ? 'Error' : 'Errors'} + {errors.length} {errors.length === 1 ? t('health', 'error') : t('health', 'errors')} )} {warnings.length > 0 && ( - {warnings.length} {warnings.length === 1 ? 'Warning' : 'Warnings'} + {warnings.length} {warnings.length === 1 ? t('health', 'warning') : t('health', 'warnings')} )} {issues.length === 0 && diagnostics.length > 0 && ( - All Systems OK + {t('health', 'allSystemsOk')} )}
@@ -1073,7 +1073,7 @@ export default function HealthPage() { {issues.length > 0 ? (
-

Active Issues

+

{t('health', 'activeIssues')}

{issues.map((diagnostic, idx) => ( @@ -1087,7 +1087,7 @@ export default function HealthPage() { - {diagnostics.filter(d => d.level === DiagnosticLevel.OK).length} systems operating normally + {t('health', 'systemsOperatingNormally').replace('{count}', String(diagnostics.filter(d => d.level === DiagnosticLevel.OK).length))}
@@ -1109,8 +1109,8 @@ export default function HealthPage() { /* No diagnostics data */
{connection.online ? - 'Waiting for diagnostics data...' : - 'Connect to robot to view diagnostics' + t('health', 'loadingDescription') : + t('health', 'connectToRobotToViewDiagnostics') }
)} @@ -1130,10 +1130,10 @@ export default function HealthPage() {

- Confirm System Reboot + {t('health', 'confirmSystemReboot')}

- Are you sure you want to reboot the system? This will temporarily disconnect all services and the robot will be unavailable for a few minutes. + {t('health', 'confirmSystemRebootDescription')}

@@ -1168,4 +1168,4 @@ export default function HealthPage() { )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/labs/page.tsx b/frontend/src/app/labs/page.tsx index 98b15d3..dd5f05d 100644 --- a/frontend/src/app/labs/page.tsx +++ b/frontend/src/app/labs/page.tsx @@ -72,9 +72,9 @@ export default function LabsPage() { badge: 'alpha' as const, }, { - name: 'Weather', + name: t('labs', 'weather'), icon: Cloud, - description: 'Simple weather viewer for current conditions', + description: t('labs', 'weatherDescription'), action: () => router.push('/weather'), badge: 'alpha' as const, }, @@ -187,4 +187,4 @@ export default function LabsPage() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/log/page.tsx b/frontend/src/app/log/page.tsx index 0ebe545..53d2f13 100644 --- a/frontend/src/app/log/page.tsx +++ b/frontend/src/app/log/page.tsx @@ -6,6 +6,7 @@ import { format } from 'date-fns'; import { Search, Filter, Download, Activity, Bot, Command, Settings, Database as DatabaseIcon, RefreshCw, TestTube, Trash2, AlertTriangle, Navigation, Volume2, Camera, Shield, FileDown, Map } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { auditLogger } from '@/utils/audit-logger'; +import { useLanguage } from '@/contexts/LanguageContext'; // Force dynamic rendering to prevent static generation issues export const dynamic = 'force-dynamic'; @@ -53,6 +54,7 @@ const eventTypeIcons: Record> = { export default function LogPage() { const { user, supabase } = useSupabase(); + const { t } = useLanguage(); const [logs, setLogs] = useState([]); const [filteredLogs, setFilteredLogs] = useState([]); const [loading, setLoading] = useState(true); @@ -63,8 +65,8 @@ export default function LogPage() { useEffect(() => { // Set page title - document.title = 'Audit - BotBot'; - }, []); + document.title = `${t('auditLog', 'pageTitle')} - BotBot`; + }, [t]); const [isDeleting, setIsDeleting] = useState(false); // Fetch audit logs @@ -120,7 +122,14 @@ export default function LogPage() { // Export logs as CSV const exportLogs = () => { const csv = [ - ['Date', 'Time', 'Event Type', 'Action', 'Robot', 'Details'], + [ + t('auditLog', 'dateCsv'), + t('auditLog', 'timeCsv'), + t('auditLog', 'eventTypeCsv'), + t('auditLog', 'actionCsv'), + t('auditLog', 'robotCsv'), + t('auditLog', 'detailsCsv'), + ], ...filteredLogs.map(log => [ format(new Date(log.created_at), 'yyyy-MM-dd'), format(new Date(log.created_at), 'HH:mm:ss'), @@ -154,7 +163,7 @@ export default function LogPage() { event_details: { test: true, timestamp: new Date().toISOString(), - message: 'Test audit log entry' + message: t('auditLog', 'testAuditMessage') } }); console.log('[Test] Test audit log created, refreshing...'); @@ -206,7 +215,7 @@ export default function LogPage() {
-

Loading audit logs...

+

{t('auditLog', 'loading')}

); @@ -219,10 +228,10 @@ export default function LogPage() { {/* Header */}

- Audit Log + {t('auditLog', 'pageTitle')}

- Track all activities and events in your application + {t('auditLog', 'pageDescription')}

@@ -235,7 +244,7 @@ export default function LogPage() { setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-gray-50 dark:bg-botbot-dark border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400" @@ -251,18 +260,18 @@ export default function LogPage() { onChange={(e) => setSelectedEventType(e.target.value)} className="px-4 py-2 bg-gray-50 dark:bg-botbot-dark border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400" > - - - - - - - - - - - - + + + + + + + + + + + +
@@ -276,14 +285,14 @@ export default function LogPage() { disabled={isRefreshing} > - Refresh + {t('auditLog', 'refresh')} {process.env.NODE_ENV === 'development' && ( )}
@@ -318,19 +327,19 @@ export default function LogPage() { - Date & Time + {t('auditLog', 'dateAndTime')} - Type + {t('auditLog', 'type')} - Action + {t('auditLog', 'action')} - Robot + {t('auditLog', 'robot')} - Details + {t('auditLog', 'details')} @@ -341,7 +350,7 @@ export default function LogPage() {
-

No audit logs found

+

{t('auditLog', 'noAuditLogsFound')}

@@ -384,7 +393,7 @@ export default function LogPage() { {log.event_details && Object.keys(log.event_details).length > 0 ? (
- View details + {t('auditLog', 'viewDetails')}
                                     {JSON.stringify(log.event_details, null, 2)}
@@ -422,7 +431,7 @@ export default function LogPage() {
                       

- {type} Events + {t('auditLog', 'eventsLabel').replace('{type}', type)}

{count} @@ -448,12 +457,12 @@ export default function LogPage() {

- Delete All Audit Logs + {t('auditLog', 'deleteAllTitle')}

- Are you sure you want to delete all {filteredLogs.length} audit logs? This action cannot be undone. + {t('auditLog', 'deleteAllConfirm').replace('{count}', String(filteredLogs.length))}

@@ -461,7 +470,7 @@ export default function LogPage() { onClick={() => setShowDeleteModal(false)} className="px-4 py-2 bg-gray-200 dark:bg-botbot-dark text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-botbot-dark/80 transition-colors" > - Cancel + {t('common', 'cancel')} @@ -488,4 +497,4 @@ export default function LogPage() { )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/maps/page.tsx b/frontend/src/app/maps/page.tsx index d014261..4d8050d 100644 --- a/frontend/src/app/maps/page.tsx +++ b/frontend/src/app/maps/page.tsx @@ -10,6 +10,7 @@ import { JoysticksWrapper } from '@/components/ui/JoysticksWrapper'; import useRobotActions from '@/hooks/ros/useRobotActions'; import { useHeader } from '@/contexts/HeaderContext'; import { useWakeLock } from '@/hooks/useWakeLock'; +import { useLanguage } from '@/contexts/LanguageContext'; interface MapLocation { id: string; @@ -37,6 +38,7 @@ export default function MapsPage() { const { dispatch } = useNotifications(); const { joystickEnabled, setJoystickEnabled } = useHeader(); + const { t } = useLanguage(); const robotActions = useRobotActions(); const { @@ -106,8 +108,8 @@ export default function MapsPage() { id: index.toString(), name: map.name, path: map.path, - lastUpdated: map.lastModified || 'Unknown', - size: map.size || 'Unknown', + lastUpdated: map.lastModified || t('maps', 'unknown'), + size: map.size || t('maps', 'unknown'), })); setLocations(mapsWithIds); @@ -174,8 +176,8 @@ export default function MapsPage() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Map Loaded', - message: `Successfully loaded and activated map: ${mapName}`, + title: t('maps', 'mapLoadedTitle'), + message: t('maps', 'mapLoadedMessage').replace('{mapName}', mapName), }, }); } catch (error) { @@ -184,8 +186,8 @@ export default function MapsPage() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to Load Map', - message: `Could not load map: ${mapName}`, + title: t('maps', 'failedToLoadMapTitle'), + message: t('maps', 'failedToLoadMapMessage').replace('{mapName}', mapName), }, }); } finally { @@ -213,8 +215,8 @@ export default function MapsPage() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to Delete Map', - message: `Could not delete map: ${mapName}`, + title: t('maps', 'failedToDeleteMapTitle'), + message: t('maps', 'failedToDeleteMapMessage').replace('{mapName}', mapName), }, }); } finally { @@ -228,10 +230,10 @@ export default function MapsPage() { {/* Header */}

- Maps Management + {t('maps', 'pageTitle')}

- View and manage robot navigation maps + {t('maps', 'pageDescription')}

@@ -252,7 +254,7 @@ export default function MapsPage() { @@ -261,7 +263,7 @@ export default function MapsPage() {
{/* Camera Feed */}
-

Live Camera

+

{t('maps', 'liveCamera')}

- {showJoysticks ? 'Hide' : 'Show'} Joysticks + {showJoysticks ? t('maps', 'hideJoysticks') : t('maps', 'showJoysticks')}
{/* Robot Control Buttons */}
-

Robot Controls

+

{t('maps', 'robotControls')}

{/* Stand Up/Down */}
@@ -317,18 +319,18 @@ export default function MapsPage() {
@@ -343,7 +345,7 @@ export default function MapsPage() { @@ -351,12 +353,12 @@ export default function MapsPage() {

- Map Viewer + {t('maps', 'mapViewer')}

{selectedLocation ? locations.find(l => l.id === selectedLocation)?.name - : 'Current Map'} + : t('maps', 'currentMap')}
@@ -374,8 +376,8 @@ export default function MapsPage() { type: 'ADD_NOTIFICATION', payload: { type: 'info', - title: 'Set Home Position', - message: 'Click on the map to set home position, then drag to set orientation', + title: t('maps', 'setHomePositionTitle'), + message: t('maps', 'setHomePositionMessage'), }, }); } else { @@ -396,7 +398,7 @@ export default function MapsPage() { {/* Mapping Control */}

- Mapping Control + {t('maps', 'mappingControl')}

{/* Map Name Input - Shows when Start Mapping is clicked */} @@ -411,7 +413,7 @@ export default function MapsPage() { handleStartMapping(); } }} - placeholder="Enter map name..." + placeholder={t('maps', 'mapNamePlaceholder')} className="w-full px-3 py-2 text-sm bg-white dark:bg-botbot-darker border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" autoFocus /> @@ -425,7 +427,7 @@ export default function MapsPage() {
- Mapping: {mapName} + {t('maps', 'mappingLabel').replace('{mapName}', mapName)}
@@ -449,17 +451,17 @@ export default function MapsPage() { {isMapping ? ( <> - Stop Mapping + {t('maps', 'stopMapping')} ) : showMapNameInput && mapName.trim() ? ( <> - Start Mapping + {t('maps', 'startMapping')} ) : ( <> - Start New Map + {t('maps', 'startNewMap')} )} @@ -473,7 +475,7 @@ export default function MapsPage() { }} className="w-full px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors duration-200" > - Cancel + {t('maps', 'cancel')} )}
@@ -483,13 +485,13 @@ export default function MapsPage() {

- Available Maps + {t('maps', 'availableMaps')}

@@ -498,7 +500,7 @@ export default function MapsPage() { {!isConnected ? (

- Connect to robot to view maps + {t('maps', 'connectToRobotToViewMaps')}

) : isLoading && locations.length === 0 ? ( @@ -508,7 +510,7 @@ export default function MapsPage() { ) : locations.length === 0 ? (

- No maps available + {t('maps', 'noMapsAvailable')}

) : ( @@ -543,21 +545,21 @@ export default function MapsPage() {
- Deleting... + {t('maps', 'deleting')}
) : loadingMapId === location.id ? (
- Loading... + {t('maps', 'loading')}
) : selectedLocation === location.id ? ( <>
- Active + {t('maps', 'active')}
@@ -579,7 +581,7 @@ export default function MapsPage() { handleLoadMap(location.id, location.path, location.name); }} > - Load Map + {t('maps', 'loadMap')} @@ -603,10 +605,10 @@ export default function MapsPage() {

- Delete "{location.name}"? + {t('maps', 'deleteMapTitle').replace('{mapName}', location.name)}

- This action cannot be undone. + {t('maps', 'deleteMapWarning')}

@@ -618,7 +620,7 @@ export default function MapsPage() { }} className="flex-1 px-3 py-1.5 text-xs font-medium bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors duration-200" > - Delete + {t('maps', 'delete')}
@@ -650,4 +652,4 @@ export default function MapsPage() { )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/missions/page.tsx b/frontend/src/app/missions/page.tsx index 06d2a58..13a6c12 100644 --- a/frontend/src/app/missions/page.tsx +++ b/frontend/src/app/missions/page.tsx @@ -16,6 +16,7 @@ import { useNotifications } from '@/contexts/NotificationsContext'; import { useRosMappingServices } from '@/hooks/ros/useRosMappingServices'; import { useMapMissionCompatibility } from '@/hooks/useMapMissionCompatibility'; import { MissionMapWarningBanner } from '@/components/mission-map-warning-banner'; +import { useLanguage } from '@/contexts/LanguageContext'; // Force dynamic rendering to prevent static generation issues export const dynamic = 'force-dynamic'; @@ -46,11 +47,12 @@ export default function Missions() { const [showNavPlan, setShowNavPlan] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const { t } = useLanguage(); useEffect(() => { // Set page title - document.title = 'Missions - BotBot'; - }, []); + document.title = `${t('missions', 'title')} - BotBot`; + }, [t]); const { light, statusBeforeEmergency, antiCollision } = useRobotCustomModeContext(); const { robotStatus } = useRobotStatus(); const robotActions = useRobotActionsTransitions(robotStatus); @@ -81,8 +83,11 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Map Switched', - message: `Loaded map: ${selectedMission?.map_name?.replace(/\.db$/i, '')}` + title: t('missions', 'mapSwitchedTitle'), + message: t('missions', 'mapSwitchedMessage').replace( + '{mapName}', + selectedMission?.map_name?.replace(/\.db$/i, '') || t('missions', 'unknown') + ) } }); } @@ -91,8 +96,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to switch map', - message: 'Could not load the mission map' + title: t('missions', 'failedToSwitchMapTitle'), + message: t('missions', 'failedToSwitchMapMessage') } }); } finally { @@ -154,11 +159,11 @@ export default function Missions() { notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - type: 'error', - title: 'Failed to load missions', - message: 'Please try refreshing the page' - } - }); + type: 'error', + title: t('missions', 'failedToLoadMissionsTitle'), + message: t('missions', 'refreshPageMessage') + } + }); } finally { setIsLoading(false); } @@ -212,8 +217,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Mission created', - message: `Mission "${newMission.name}" has been created` + title: t('missions', 'missionCreatedTitle'), + message: t('missions', 'missionCreatedMessage').replace('{missionName}', newMission.name) } }); } catch (error) { @@ -222,8 +227,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to create mission', - message: 'Please try again' + title: t('missions', 'failedToCreateMissionTitle'), + message: t('missions', 'tryAgainMessage') } }); } finally { @@ -257,8 +262,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Mission updated', - message: `Mission has been renamed to "${updated.name}"` + title: t('missions', 'missionUpdatedTitle'), + message: t('missions', 'missionUpdatedMessage').replace('{missionName}', updated.name) } }); } catch (error) { @@ -267,8 +272,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to update mission', - message: 'Please try again' + title: t('missions', 'failedToUpdateMissionTitle'), + message: t('missions', 'tryAgainMessage') } }); } finally { @@ -296,8 +301,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Mission deleted', - message: 'Mission has been successfully deleted' + title: t('missions', 'missionDeletedTitle'), + message: t('missions', 'missionDeletedMessage') } }); } catch (error) { @@ -306,8 +311,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to delete mission', - message: 'Please try again' + title: t('missions', 'failedToDeleteMissionTitle'), + message: t('missions', 'tryAgainMessage') } }); } finally { @@ -327,8 +332,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'info', - title: 'Saving waypoints', - message: 'Please wait while waypoints are saved...' + title: t('missions', 'savingWaypointsTitle'), + message: t('missions', 'savingWaypointsMessage') } }); @@ -347,8 +352,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'No waypoints found', - message: 'Please add waypoints and wait for them to save' + title: t('missions', 'noWaypointsFoundTitle'), + message: t('missions', 'noWaypointsFoundMessage') } }); return; @@ -368,8 +373,10 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Mission started', - message: `Mission "${freshMission.name}" is now active with ${freshMission.waypoints.length} waypoints` + title: t('missions', 'missionStartedTitle'), + message: t('missions', 'missionStartedMessage') + .replace('{missionName}', freshMission.name) + .replace('{count}', String(freshMission.waypoints.length)) } }); } catch (error) { @@ -378,8 +385,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to start mission', - message: 'Please try again' + title: t('missions', 'failedToStartMissionTitle'), + message: t('missions', 'tryAgainMessage') } }); } finally { @@ -401,8 +408,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Mission stopped', - message: 'Mission has been stopped' + title: t('missions', 'missionStoppedTitle'), + message: t('missions', 'missionStoppedMessage') } }); } catch (error) { @@ -413,7 +420,7 @@ export default function Missions() { console.error('Error stringified:', JSON.stringify(error, null, 2)); // Try different ways to extract error message - let errorMessage = 'Unknown error occurred'; + let errorMessage = t('missions', 'unknownError'); if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'string') { @@ -426,7 +433,7 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to stop mission', + title: t('missions', 'failedToStopMissionTitle'), message: errorMessage } }); @@ -513,8 +520,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to save waypoints', - message: error instanceof Error ? error.message : 'Your changes may not have been saved' + title: t('missions', 'failedToSaveWaypointsTitle'), + message: error instanceof Error ? error.message : t('missions', 'changesMayNotHaveBeenSaved') } }); } finally { @@ -558,8 +565,8 @@ export default function Missions() { type: 'ADD_NOTIFICATION', payload: { type: 'info', - title: 'Navigation Complete', - message: 'Mission has finished' + title: t('missions', 'navigationCompleteTitle'), + message: t('missions', 'navigationCompleteMessage') } }); }; @@ -617,12 +624,12 @@ export default function Missions() {

- Missions + {t('missions', 'title')}

@@ -644,7 +651,7 @@ export default function Missions() { type="text" value={newMissionName} onChange={(e) => setNewMissionName(e.target.value)} - placeholder="Mission name..." + placeholder={t('missions', 'missionNamePlaceholder')} className="w-full px-3 py-2 bg-gray-50 dark:bg-botbot-darker text-gray-800 dark:text-white rounded-md border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-botbot-purple mb-2" autoFocus onKeyPress={(e) => e.key === 'Enter' && createMission()} @@ -660,7 +667,7 @@ export default function Missions() { ) : ( )} - Create + {t('missions', 'create')}
@@ -682,14 +689,14 @@ export default function Missions() {

- No missions yet + {t('missions', 'noMissionsYet')}

) : ( @@ -755,21 +762,21 @@ export default function Missions() { {activeMissionId === mission.id ? ( - Active + {t('missions', 'active')} ) : mission.is_active ? ( - Paused + {t('missions', 'paused')} ) : null}

- Created {mission.created_at ? new Date(mission.created_at).toLocaleDateString() : 'Unknown'} + {t('missions', 'created')} {mission.created_at ? new Date(mission.created_at).toLocaleDateString() : t('missions', 'unknown')}

- {mission.waypoints?.length || 0} waypoints + {mission.waypoints?.length || 0} {(mission.waypoints?.length || 0) === 1 ? t('missions', 'waypoint') : t('missions', 'waypoints')}

{mission.map_name && (

@@ -788,19 +795,19 @@ export default function Missions() { className="flex-1 px-3 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded-md transition-colors text-sm font-medium flex items-center justify-center gap-1" > - Edit + {t('missions', 'edit')}

@@ -822,18 +829,18 @@ export default function Missions() {

- {selectedMission ? `Mission: ${selectedMission.name}` : 'Mission Editor'} + {selectedMission ? t('missions', 'missionTitle').replace('{missionName}', selectedMission.name) : t('missions', 'missionEditor')}

{activeMissionId && (
- Mission Running + {t('missions', 'missionRunning')}
)} {!activeMissionId && selectedMission?.is_active && (
- Mission Paused + {t('missions', 'missionPaused')}
)}
@@ -846,17 +853,17 @@ export default function Missions() {
- Waypoints: {currentWaypointCount} + {t('missions', 'waypoints')}: {currentWaypointCount}
- Pending + {t('missions', 'pending')}
- Active + {t('missions', 'active')}
- Reached + {t('missions', 'reached')}
@@ -868,15 +875,15 @@ export default function Missions() { ? 'bg-green-500 hover:bg-green-600 text-white' : 'bg-gray-500 hover:bg-gray-600 text-white' }`} - title="Toggle navigation plan overlay" + title={t('missions', 'toggleNavPlanOverlay')} > - Nav Plan + {t('missions', 'navPlan')} {isSaving && ( - Saving waypoints... + {t('missions', 'savingWaypoints')} )} {activeMissionId === selectedMission.id ? ( @@ -886,7 +893,7 @@ export default function Missions() { className="px-3 py-1.5 bg-red-500 hover:bg-red-600 disabled:bg-gray-400 text-white rounded-md transition-colors text-sm font-medium flex items-center gap-1" > - Stop + {t('missions', 'stop')} ) : ( )}
@@ -955,16 +962,16 @@ export default function Missions() {

- No Mission Selected + {t('missions', 'noMissionSelected')}

- Select a mission from the left panel or create a new one to start planning waypoints + {t('missions', 'noMissionSelectedDescription')}

@@ -992,7 +999,7 @@ export default function Missions() { {/* Camera Feed at Top */}
-

Camera View

+

{t('missions', 'cameraView')}

@@ -1000,7 +1007,7 @@ export default function Missions() { {/* Robot Controls Below Camera */}
- +
{/* Emergency button first */}
@@ -1054,4 +1061,4 @@ export default function Missions() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index b890293..f82978d 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -12,7 +12,7 @@ import dynamic from 'next/dynamic'; import ChangePasswordPopup from '@/components/change-password-popup'; import ColorPicker from '@/components/color-picker'; import { auditLogger } from '@/utils/audit-logger'; -import { SpeedMode, SPEED_MODE_MULTIPLIERS, SPEED_MODE_LABELS, SPEED_MODE_DESCRIPTIONS, DEFAULT_SPEED_MODE } from '@/types/speed-mode'; +import { SpeedMode, SPEED_MODE_MULTIPLIERS, DEFAULT_SPEED_MODE } from '@/types/speed-mode'; // Dynamically import AvatarUpload with SSR disabled to avoid window.confirm issues const AvatarUpload = dynamic( @@ -43,8 +43,20 @@ export default function ProfilePage() { useEffect(() => { // Set page title - document.title = 'Profile - BotBot'; - }, []); + document.title = `${t('profile', 'title')} - BotBot`; + }, [t]); + + const speedModeLabels: Record = { + beginner: t('profile', 'speedModeBeginner'), + normal: t('profile', 'speedModeNormal'), + insane: t('profile', 'speedModeInsane'), + }; + + const speedModeDescriptions: Record = { + beginner: t('profile', 'speedModeBeginnerDescription'), + normal: t('profile', 'speedModeNormalDescription'), + insane: t('profile', 'speedModeInsaneDescription'), + }; // Track original values to detect changes const [originalName, setOriginalName] = useState(''); @@ -310,10 +322,10 @@ export default function ProfilePage() {

- {t('profile', 'title') || 'Profile Settings'} + {t('profile', 'title')}

- Manage your account settings and preferences + {t('profile', 'pageDescription')}

@@ -331,17 +343,17 @@ export default function ProfilePage() { {saveStatus === 'saving' ? ( <>
- {t('common', 'saving') || 'Saving...'} + {t('common', 'saving')} ) : saveStatus === 'saved' ? ( <> - {t('common', 'saved') || 'Saved'} + {t('common', 'saved')} ) : ( <> - {t('common', 'save') || 'Save Changes'} + {t('profile', 'saveChanges')} )} @@ -355,7 +367,7 @@ export default function ProfilePage() { {/* Avatar and Name Card */}

- Personal Information + {t('profile', 'personalInformation')}

{/* Avatar Upload Section */} @@ -372,7 +384,7 @@ export default function ProfilePage() { {/* Name Field */}

- {t('profile', 'nameDescription') || 'Your display name in the application'} + {t('profile', 'nameDescription')}

@@ -394,14 +406,14 @@ export default function ProfilePage() { {/* Account Information Card */}

- Account Information + {t('profile', 'accountInformation')}

{/* Email Field */}

- {t('profile', 'emailDescription') || 'Your email address cannot be changed'} + {t('profile', 'emailDescription')}

{/* User ID Field */}

- {t('profile', 'userIdDescription') || 'Your unique user identifier'} + {t('profile', 'userIdDescription')}

@@ -441,7 +453,7 @@ export default function ProfilePage() { {/* Security Actions Card */}

- Security + {t('profile', 'security')}

@@ -454,7 +466,7 @@ export default function ProfilePage() { transition-all duration-200 border border-gray-200 dark:border-botbot-darker" > - {t('profile', 'changePassword') || 'Change Password'} + {t('profile', 'changePassword')} {/* Sign Out Button */} @@ -467,7 +479,7 @@ export default function ProfilePage() { transition-all duration-200" > - {t('userProfile', 'logout') || 'Sign Out'} + {t('userProfile', 'logout')}
@@ -478,7 +490,7 @@ export default function ProfilePage() { {/* Appearance Settings Card */}

- Appearance + {t('profile', 'appearance')}

@@ -493,15 +505,15 @@ export default function ProfilePage() { {/* Hide Branding Toggle */}

- Hide BotBot Logo + {t('profile', 'hideBotBotLogo')}

- Remove the BotBot logo from the navigation bar + {t('profile', 'hideBotBotLogoDescription')}

@@ -602,12 +614,12 @@ export default function ProfilePage() { {/* Speed & Control Card */}

- Speed & Control + {t('profile', 'speedAndControl')}

{/* Segmented Control */} @@ -623,20 +635,20 @@ export default function ProfilePage() { : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200' }`} > - {SPEED_MODE_LABELS[mode]} + {speedModeLabels[mode]} ))}
{/* Description for selected mode */}

- {SPEED_MODE_DESCRIPTIONS[speedMode]} + {speedModeDescriptions[speedMode]}

{/* Visual indicator */}
- Speed Level + {t('profile', 'speedLevel')} {Math.round(SPEED_MODE_MULTIPLIERS[speedMode] * 100)}% @@ -694,15 +706,17 @@ export default function ProfilePage() { {/* Title */}

- Enable Insane Mode? + {t('profile', 'enableInsaneModeTitle')}

{/* Message */}

- This will set robot speed to 100% of maximum velocity. + {t('profile', 'enableInsaneModeDescriptionPrefix')}{' '} + 100%{' '} + {t('profile', 'enableInsaneModeDescriptionSuffix')}

- Only recommended for experienced operators in controlled environments. + {t('profile', 'insaneModeRecommendation')}

{/* Buttons */} @@ -714,7 +728,7 @@ export default function ProfilePage() { hover:bg-gray-100 dark:hover:bg-botbot-darker transition-colors focus:outline-none focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-600" > - Cancel + {t('common', 'cancel')}
@@ -732,4 +746,4 @@ export default function ProfilePage() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 845c9b9..29f3e43 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -203,17 +203,17 @@ export default function SettingsPage() { {saveStatus === 'saving' ? ( <>
- Saving... + {t('common', 'saving')} ) : saveStatus === 'saved' ? ( <> - Saved + {t('common', 'saved')} ) : ( <> - Save + {t('common', 'save')} )} @@ -317,9 +317,9 @@ export default function SettingsPage() { {/* Joystick Visualization Only Mode */}
-

Joystick Visualization Only

+

{t('settings', 'joystickVisualizationOnly')}

- Display joystick input in visualizer without sending commands to robot + {t('settings', 'joystickVisualizationOnlyDescription')}

); -} \ No newline at end of file +} diff --git a/frontend/src/components/avatar-upload.tsx b/frontend/src/components/avatar-upload.tsx index 008b57c..f0cabed 100644 --- a/frontend/src/components/avatar-upload.tsx +++ b/frontend/src/components/avatar-upload.tsx @@ -29,27 +29,27 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat if (!file) return; if (!supabase) { - alert('Database connection not available'); + alert(t('profile', 'databaseUnavailable')); return; } // Check authentication const { data: { session }, error: sessionError } = await supabase.auth.getSession(); if (sessionError || !session) { - alert('You must be logged in to upload an avatar'); + alert(t('profile', 'avatarLoginRequired')); return; } // Validate file type const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']; if (!validTypes.includes(file.type)) { - alert('Please select a valid image file (JPEG, PNG, WebP, or GIF)'); + alert(t('profile', 'invalidAvatarFile')); return; } // Validate file size (5MB) if (file.size > 5 * 1024 * 1024) { - alert('File size must be less than 5MB'); + alert(t('profile', 'avatarFileSizeLimit')); return; } @@ -110,7 +110,7 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat }); } catch (error: any) { console.error('Error uploading avatar:', error); - const errorMessage = error?.message || error?.error_description || 'Error uploading avatar. Please try again.'; + const errorMessage = error?.message || error?.error_description || t('profile', 'avatarUploadError'); alert(errorMessage); } finally { setUploading(false); @@ -120,7 +120,7 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat const handleDelete = async () => { if (!avatarUrl || !supabase) return; - const confirmDelete = window.confirm('Are you sure you want to delete your avatar?'); + const confirmDelete = window.confirm(t('profile', 'deleteAvatarConfirm')); if (!confirmDelete) return; setUploading(true); @@ -151,7 +151,7 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat }); } catch (error) { console.error('Error deleting avatar:', error); - alert('Error deleting avatar. Please try again.'); + alert(t('profile', 'deleteAvatarError')); } finally { setUploading(false); } @@ -165,7 +165,7 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat {previewUrl ? ( Avatar ) : ( @@ -192,7 +192,7 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat @@ -218,16 +218,16 @@ export default function AvatarUpload({ avatarUrl, userId, onAvatarUpdate }: Avat {uploading ? ( <> - Uploading... + {t('profile', 'uploadingAvatar')} ) : ( - previewUrl ? 'Change Avatar' : 'Upload Avatar' + previewUrl ? t('profile', 'changeAvatar') : t('profile', 'uploadAvatar') )}

- Click on the avatar or button to upload. Max 5MB. JPEG, PNG, WebP, or GIF. + {t('profile', 'avatarUploadHelp')}

); -} \ No newline at end of file +} diff --git a/frontend/src/components/cockpit/missions-panel.tsx b/frontend/src/components/cockpit/missions-panel.tsx index 76ef3e7..45ade9f 100644 --- a/frontend/src/components/cockpit/missions-panel.tsx +++ b/frontend/src/components/cockpit/missions-panel.tsx @@ -9,6 +9,7 @@ import useFollowWaypoints from '@/hooks/ros/useFollowWaypoints'; import { missionsService, MissionWithWaypoints } from '@/services/missions'; import { cn } from '@/utils/cn'; import { useActiveMission } from '@/contexts/ActiveMissionContext'; +import { useLanguage } from '@/contexts/LanguageContext'; // Helper to normalize map names for comparison function normalizeMapName(name: string): string { @@ -34,6 +35,7 @@ interface MissionItemProps { function MissionItem({ mission, isSelected, isRunning, progress, onSelect, disabled }: MissionItemProps) { const waypointCount = mission.waypoints?.length || 0; + const { t } = useLanguage(); return (
- {waypointCount} {waypointCount === 1 ? 'waypoint' : 'waypoints'} + {waypointCount} {waypointCount === 1 ? t('missions', 'waypoint') : t('missions', 'waypoints')}
@@ -86,6 +88,7 @@ function MissionItem({ mission, isSelected, isRunning, progress, onSelect, disab } export default function MissionsPanel() { + const { t } = useLanguage(); const { connectionStatus } = useRobotConnection(); const { getCurrentDatabase, isConnected: rosConnected } = useRosMappingServices(); const { @@ -250,7 +253,7 @@ export default function MissionsPanel() { title={
- Missions + {t('missions', 'title')}
} className="h-full !flex !flex-col" @@ -264,7 +267,7 @@ export default function MissionsPanel() { "hover:bg-gray-100 dark:hover:bg-botbot-darker", isRefreshing && "animate-spin" )} - title="Refresh missions" + title={t('missions', 'refreshMissions')} > @@ -274,22 +277,22 @@ export default function MissionsPanel() {
{isLoading ? (
- Loading... + {t('missions', 'loading')}
) : !isRobotConnected ? (
- Connect to robot to view missions + {t('missions', 'connectToRobotToViewMissions')}
) : !currentMapName ? (
- Load a map to see missions + {t('missions', 'loadMapToSeeMissions')}
) : filteredMissions.length === 0 ? (
- No missions for current map + {t('missions', 'noMissionsForCurrentMap')}
) : ( filteredMissions.map(mission => ( @@ -328,12 +331,12 @@ export default function MissionsPanel() { {canStop ? ( <> - Stop Mission + {t('missions', 'stopMission')} ) : ( <> - Start Mission + {t('missions', 'startMission')} )} diff --git a/frontend/src/components/dashboard/AddWidgetModal.tsx b/frontend/src/components/dashboard/AddWidgetModal.tsx index 28567d4..8d20734 100644 --- a/frontend/src/components/dashboard/AddWidgetModal.tsx +++ b/frontend/src/components/dashboard/AddWidgetModal.tsx @@ -87,21 +87,21 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP { type: 'camera' as WidgetType, label: t('myUI', 'widgetCamera'), - description: 'Live video feed from robot cameras', + description: t('myUI', 'widgetCameraDescription'), icon: , category: 'visualization', }, { type: 'visualization3d' as WidgetType, label: t('myUI', 'widget3DVisualization'), - description: '3D robot model and environment', + description: t('myUI', 'widget3DVisualizationDescription'), icon: , category: 'visualization', }, { type: 'map' as WidgetType, label: t('myUI', 'widgetMapView'), - description: 'Live map and navigation view', + description: t('myUI', 'widgetMapViewDescription'), icon: , category: 'visualization', }, @@ -109,35 +109,35 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP { type: 'joystick' as WidgetType, label: t('myUI', 'widgetJoystick'), - description: 'Virtual joystick for robot control', + description: t('myUI', 'widgetJoystickDescription'), icon: , category: 'control', }, { type: 'button' as WidgetType, label: t('myUI', 'widgetButton'), - description: 'Custom action button', + description: t('myUI', 'widgetButtonDescription'), icon: , category: 'control', }, { type: 'buttonGroup' as WidgetType, label: t('myUI', 'widgetButtonGroup'), - description: 'Multiple action buttons', + description: t('myUI', 'widgetButtonGroupDescription'), icon: , category: 'control', }, { type: 'delivery' as WidgetType, - label: 'Delivery', - description: 'Delivery management controls', + label: t('myUI', 'widgetDelivery'), + description: t('myUI', 'widgetDeliveryDescription'), icon: , category: 'control', }, { type: 'missions' as WidgetType, - label: 'Missions', - description: 'Manage and execute robot missions', + label: t('myUI', 'widgetMissions'), + description: t('myUI', 'widgetMissionsDescription'), icon: , category: 'control', }, @@ -145,35 +145,35 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP { type: 'audio' as WidgetType, label: t('myUI', 'widgetAudioStream'), - description: 'Audio streaming controls', + description: t('myUI', 'widgetAudioStreamDescription'), icon: , category: 'media', }, { type: 'microphone' as WidgetType, - label: 'Microphone', - description: 'Voice input and recording', + label: t('myUI', 'widgetMicrophone'), + description: t('myUI', 'widgetMicrophoneDescription'), icon: , category: 'media', }, { type: 'ttsPresets' as WidgetType, - label: 'TTS Presets', - description: 'Text-to-speech presets', + label: t('myUI', 'widgetTTSPresets'), + description: t('myUI', 'widgetTTSPresetsDescription'), icon: , category: 'media', }, { type: 'soundClips' as WidgetType, - label: 'Sound Clips', - description: 'Play pre-recorded sounds', + label: t('myUI', 'widgetSoundClips'), + description: t('myUI', 'widgetSoundClipsDescription'), icon: , category: 'media', }, { type: 'recorder' as WidgetType, - label: 'Recorder', - description: 'Audio/video recording', + label: t('myUI', 'widgetRecorder'), + description: t('myUI', 'widgetRecorderDescription'), icon: , category: 'media', }, @@ -181,28 +181,28 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP { type: 'gauge' as WidgetType, label: t('myUI', 'widgetGauge'), - description: 'Display numeric values', + description: t('myUI', 'widgetGaugeDescription'), icon: , category: 'information', }, { type: 'sidewaysgauge' as WidgetType, label: t('myUI', 'widgetSidewaysGauge'), - description: 'Horizontal gauge display', + description: t('myUI', 'widgetSidewaysGaugeDescription'), icon: , category: 'information', }, { type: 'info' as WidgetType, label: t('myUI', 'widgetInformation'), - description: 'Display robot information', + description: t('myUI', 'widgetInformationDescription'), icon: , category: 'information', }, { type: 'mapsManagement' as WidgetType, - label: 'Maps Management', - description: 'Manage saved maps', + label: t('myUI', 'widgetMapsManagement'), + description: t('myUI', 'widgetMapsManagementDescription'), icon: , category: 'information', }, @@ -210,21 +210,21 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP { type: 'chat' as WidgetType, label: t('myUI', 'widgetChat'), - description: 'Chat interface', + description: t('myUI', 'widgetChatDescription'), icon: , category: 'ai', }, { type: 'aiStream' as WidgetType, - label: 'AI Stream', - description: 'AI processing stream', + label: t('myUI', 'widgetAIStream'), + description: t('myUI', 'widgetAIStreamDescription'), icon: , category: 'ai', }, { type: 'recentDetections' as WidgetType, - label: 'Recent Detections', - description: 'Object detection history', + label: t('myUI', 'widgetRecentDetections'), + description: t('myUI', 'widgetRecentDetectionsDescription'), icon: , category: 'ai', }, @@ -236,12 +236,12 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP : allWidgetOptions.filter(widget => !proOnlyWidgetTypes.includes(widget.type)); const categories = [ - { id: 'all', label: 'All Widgets', icon: '🎯' }, - { id: 'visualization', label: 'Visualization', icon: '👁️' }, - { id: 'control', label: 'Control', icon: '🎮' }, - { id: 'media', label: 'Media', icon: '🎵' }, - { id: 'information', label: 'Information', icon: '📊' }, - { id: 'ai', label: 'AI & Smart', icon: '🤖' }, + { id: 'all', label: t('myUI', 'categoryAllWidgets'), icon: '🎯' }, + { id: 'visualization', label: t('myUI', 'categoryVisualization'), icon: '👁️' }, + { id: 'control', label: t('myUI', 'categoryControl'), icon: '🎮' }, + { id: 'media', label: t('myUI', 'categoryMedia'), icon: '🎵' }, + { id: 'information', label: t('myUI', 'categoryInformation'), icon: '📊' }, + { id: 'ai', label: t('myUI', 'categoryAiSmart'), icon: '🤖' }, ]; const filteredWidgets = widgetOptions.filter(widget => { @@ -284,7 +284,7 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP

- Add Widget to Dashboard + {t('myUI', 'addWidgetToDashboard')}

@@ -414,4 +414,4 @@ export function AddWidgetModal({ isOpen, onClose, onAddWidget }: AddWidgetModalP )} ); -} \ No newline at end of file +} diff --git a/frontend/src/components/extras-bar.tsx b/frontend/src/components/extras-bar.tsx index 9f7438f..eafee88 100644 --- a/frontend/src/components/extras-bar.tsx +++ b/frontend/src/components/extras-bar.tsx @@ -280,8 +280,8 @@ export function ExtrasBar() { setShowLanguageSelector(false); }} > - {code === 'en' ? '🇬🇧' : '🇧🇷'} - {code === 'en' ? 'English' : 'Português'} + {languageNames[code].split(' ')[0]} + {languageNames[code].replace(/^[^ ]+ /, '')} ))}
@@ -293,4 +293,4 @@ export function ExtrasBar() { ); -} \ No newline at end of file +} diff --git a/frontend/src/components/health/WifiControlPanel.tsx b/frontend/src/components/health/WifiControlPanel.tsx index 8a8039e..cbb8f5c 100644 --- a/frontend/src/components/health/WifiControlPanel.tsx +++ b/frontend/src/components/health/WifiControlPanel.tsx @@ -27,6 +27,7 @@ import useNetworkModeStatus from '@/hooks/ros/useNetworkModeStatus'; import { useNotifications } from '@/contexts/NotificationsContext'; import { useRobotConnection } from '@/contexts/RobotConnectionContext'; import { WifiNetwork, WifiAuthType, signalToBars } from '@/types/WifiControl'; +import { useLanguage } from '@/contexts/LanguageContext'; interface ConnectionDialogProps { network: WifiNetwork; @@ -36,6 +37,7 @@ interface ConnectionDialogProps { } function ConnectionDialog({ network, onConnect, onCancel, isConnecting }: ConnectionDialogProps) { + const { t } = useLanguage(); const [psk, setPsk] = useState(''); const [identity, setIdentity] = useState(''); const [password, setPassword] = useState(''); @@ -56,13 +58,13 @@ function ConnectionDialog({ network, onConnect, onCancel, isConnecting }: Connec

- Connect to {network.ssid} + {t('health', 'connectToSsid').replace('{ssid}', network.ssid)}

{network.security === WifiAuthType.PSK && (
@@ -81,7 +83,7 @@ function ConnectionDialog({ network, onConnect, onCancel, isConnecting }: Connec <>
@@ -121,7 +123,7 @@ function ConnectionDialog({ network, onConnect, onCancel, isConnecting }: Connec focus:ring-violet-500" /> - Save network for automatic connection + {t('health', 'saveNetworkForAutomaticConnection')}
@@ -133,7 +135,7 @@ function ConnectionDialog({ network, onConnect, onCancel, isConnecting }: Connec className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white transition-colors disabled:opacity-50" > - Cancel + {t('common', 'cancel')} @@ -181,6 +183,7 @@ function SignalBars({ bars }: { bars: number }) { export function WifiControlPanel() { const { dispatch } = useNotifications(); + const { t } = useLanguage(); const networkStatus = useNetworkModeStatus(); const { connection } = useRobotConnection(); const { networks, isScanning, error: scanError, scanNetworks } = useWifiNetworks(); @@ -308,9 +311,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Radio', + title: t('health', 'wifiRadioTitle'), type: 'success', - message: `WiFi ${newState ? 'enabled' : 'disabled'} successfully` + message: newState ? t('health', 'wifiEnabledSuccess') : t('health', 'wifiDisabledSuccess') } }); @@ -330,9 +333,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Radio', + title: t('health', 'wifiRadioTitle'), type: 'error', - message: 'WiFi toggle failed - check robot WiFi hardware' + message: t('health', 'wifiToggleFailedHardware') } }); } @@ -341,9 +344,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Radio', + title: t('health', 'wifiRadioTitle'), type: 'error', - message: `Failed to ${newState ? 'enable' : 'disable'} WiFi` + message: newState ? t('health', 'failedToEnableWifi') : t('health', 'failedToDisableWifi') } }); } @@ -359,18 +362,19 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Network Scan', + title: t('health', 'networkScanTitle'), type: 'success', - message: `Found ${foundNetworks.length} network${foundNetworks.length > 1 ? 's' : ''}` + message: (foundNetworks.length === 1 ? t('health', 'foundNetwork') : t('health', 'foundNetworks')) + .replace('{count}', String(foundNetworks.length)) } }); } else { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Network Scan', + title: t('health', 'networkScanTitle'), type: 'info', - message: 'No networks found. Make sure WiFi is enabled on the robot.' + message: t('health', 'noNetworksFoundScanMessage') } }); } @@ -379,9 +383,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Network Scan', + title: t('health', 'networkScanTitle'), type: 'error', - message: scanError || 'Failed to scan networks. Check robot connection.' + message: scanError || t('health', 'failedToScanNetworks') } }); } @@ -398,9 +402,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Connection', + title: t('health', 'wifiConnectionTitle'), type: 'success', - message: `Connected to ${network.ssid}` + message: t('health', 'connectedToNetwork').replace('{networkName}', network.ssid) } }); @@ -415,9 +419,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Connection', + title: t('health', 'wifiConnectionTitle'), type: 'error', - message: connectionError || `Failed to connect to ${network.ssid}` + message: connectionError || t('health', 'failedToConnectNetwork').replace('{networkName}', network.ssid) } }); } @@ -431,7 +435,7 @@ export function WifiControlPanel() { // This matches the ROS2 service call: ros2 service call /connect_wifi bot_jetson_stats_interfaces/srv/ConnectWifi "{ssid: 'networkName'}" if (!connection.ros || !connection.online) { - throw new Error('Not connected to robot'); + throw new Error(t('health', 'notConnectedToRobot')); } const service = new ROSLIB.Service({ @@ -445,7 +449,7 @@ export function WifiControlPanel() { await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { - reject(new Error('Connection timeout')); + reject(new Error(t('health', 'connectionTimeout'))); }, 30000); service.callService( @@ -455,7 +459,7 @@ export function WifiControlPanel() { if (response && (response.success || response === true)) { resolve(response); } else { - reject(new Error(response?.message || 'Failed to connect to saved network')); + reject(new Error(response?.message || t('health', 'failedToConnectSavedNetwork'))); } }, (error: string) => { @@ -468,9 +472,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Connection', + title: t('health', 'wifiConnectionTitle'), type: 'success', - message: `Connected to ${networkName}` + message: t('health', 'connectedToNetwork').replace('{networkName}', networkName) } }); @@ -484,9 +488,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'WiFi Connection', + title: t('health', 'wifiConnectionTitle'), type: 'error', - message: `Failed to connect to ${networkName}. The saved profile may be outdated.` + message: t('health', 'failedToConnectSavedNetworkMessage').replace('{networkName}', networkName) } }); setConnectingSavedNetwork(null); @@ -495,7 +499,7 @@ export function WifiControlPanel() { const handleForgetNetwork = async (networkName: string) => { // Confirm before forgetting - if (!confirm(`Are you sure you want to forget the network "${networkName}"? You'll need to re-enter the password to connect again.`)) { + if (!confirm(t('health', 'forgetNetworkConfirm').replace('{networkName}', networkName))) { return; } @@ -503,7 +507,7 @@ export function WifiControlPanel() { try { // Call the forget network service if (!connection.ros || !connection.online) { - throw new Error('Not connected to robot'); + throw new Error(t('health', 'notConnectedToRobot')); } const service = new ROSLIB.Service({ @@ -521,7 +525,7 @@ export function WifiControlPanel() { if (response && (response.success || response === true)) { resolve(response); } else { - reject(new Error(response?.message || 'Failed to forget network')); + reject(new Error(response?.message || t('health', 'failedToForgetNetwork'))); } }, (error: string) => { @@ -533,9 +537,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Network Forgotten', + title: t('health', 'networkForgottenTitle'), type: 'success', - message: `Successfully removed ${networkName} from saved networks` + message: t('health', 'networkForgottenMessage').replace('{networkName}', networkName) } }); @@ -545,9 +549,9 @@ export function WifiControlPanel() { dispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Forget Network', + title: t('health', 'forgetNetworkTitle'), type: 'error', - message: `Failed to forget ${networkName}` + message: t('health', 'failedToForgetNetworkMessage').replace('{networkName}', networkName) } }); } finally { @@ -605,7 +609,7 @@ export function WifiControlPanel() {
-

WiFi Settings

+

{t('health', 'wifiSettings')}

@@ -631,11 +635,11 @@ export function WifiControlPanel() { )}

- {isConnected ? currentNetwork : 'Not Connected'} + {isConnected ? currentNetwork : t('health', 'notConnectedTitle')}

{networkStatus?.mode && (

- Mode: {networkStatus.mode} + {t('health', 'modeLabel').replace('{mode}', networkStatus.mode)}

)}
@@ -646,7 +650,7 @@ export function WifiControlPanel() {
- IP: {networkInfo.wifiIp} + {t('health', 'ipAddress').replace('{ip}', networkInfo.wifiIp)}
)} @@ -655,13 +659,13 @@ export function WifiControlPanel() {
- 4G: {networkInfo.fourGIp || 'Active'} + 4G: {networkInfo.fourGIp || t('health', 'activeStatus')}
)}
- WiFi {wifiEnabled ? 'Enabled' : 'Disabled'} + {t('health', 'wifiStateLabel').replace('{state}', wifiEnabled ? t('health', 'enabled') : t('health', 'disabled'))}
@@ -703,12 +707,12 @@ export function WifiControlPanel() {

- Available Networks + {t('health', 'availableNetworks')}

{scanError && ( - Scan error + {t('health', 'scanError')} )}
@@ -749,14 +753,14 @@ export function WifiControlPanel() { dark:hover:bg-violet-900/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > - Connect + {t('health', 'connect')} )}
)) ) : (

- No networks found + {t('health', 'noNetworksFound')}

)}
@@ -780,7 +784,7 @@ export function WifiControlPanel() { }} className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300" > - Saved Networks ({savedNetworks.length}) + {t('health', 'savedNetworks').replace('{count}', String(savedNetworks.length))} {showSavedNetworks ? : } {showSavedNetworks && ( @@ -795,7 +799,7 @@ export function WifiControlPanel() { }} className="p-1 text-gray-600 dark:text-gray-400 hover:text-violet-600 dark:hover:text-violet-400 transition-colors" - title="Refresh saved networks" + title={t('health', 'refreshSavedNetworks')} > @@ -812,7 +816,7 @@ export function WifiControlPanel() { {network} {networkStatus?.ssid === network && ( - (Connected) + {t('health', 'connectedTag')} )}
@@ -823,17 +827,17 @@ export function WifiControlPanel() { className="px-3 py-1 text-xs bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1" - title={`Connect to ${network}`} + title={t('health', 'connectToNetwork').replace('{networkName}', network)} > {connectingSavedNetwork === network ? ( <> - Connecting... + {t('health', 'connecting')} ) : ( <> - Connect + {t('health', 'connect')} )} @@ -844,7 +848,7 @@ export function WifiControlPanel() { className="p-1 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - title={`Forget ${network}`} + title={t('health', 'forgetNetwork').replace('{networkName}', network)} > {forgettingNetwork === network ? ( @@ -858,7 +862,7 @@ export function WifiControlPanel() {
) : (

- No saved networks found + {t('health', 'noSavedNetworksFound')}

)}
@@ -894,4 +898,4 @@ export function WifiControlPanel() { })()}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/login.tsx b/frontend/src/components/login.tsx index 28ac560..38ad2a5 100644 --- a/frontend/src/components/login.tsx +++ b/frontend/src/components/login.tsx @@ -139,13 +139,13 @@ export default function Login() { if (!response.ok) { // More user-friendly error messages if (data.error?.includes('Invalid login credentials')) { - setError('The email or password you entered is incorrect. Please try again.'); + setError(t('login', 'invalidCredentials')); } else if (data.error?.includes('Email not confirmed')) { - setError('Please check your email to confirm your account before logging in.'); + setError(t('login', 'emailNotConfirmed')); } else if (data.error?.includes('Too many requests')) { - setError('Too many login attempts. Please wait a few minutes and try again.'); + setError(t('login', 'tooManyLoginAttempts')); } else { - setError(data.error || 'Unable to log in. Please check your credentials and try again.'); + setError(data.error || t('login', 'loginError')); } setLoading(false); } else if (data.success) { @@ -241,9 +241,9 @@ export default function Login() {
)} @@ -391,7 +391,7 @@ export default function Login() { {mode === 'signup' ? (t('login', 'creatingAccount') || 'Creating account...') - : 'Verifying your credentials...'} + : t('login', 'verifyingCredentials')}
@@ -414,7 +414,7 @@ export default function Login() { label={t('login', 'email') || 'Email'} name="email" value={formData.email} - placeholder="Enter your email" + placeholder={t('login', 'emailPlaceholder')} type="email" onChange={handleChange} autoComplete="username" @@ -424,7 +424,7 @@ export default function Login() { label={t('login', 'password') || 'Password'} name="password" value={formData.password} - placeholder="Enter your password" + placeholder={t('login', 'passwordPlaceholder')} type="password" onChange={handleChange} autoComplete={mode === 'signup' ? 'new-password' : 'current-password'} diff --git a/frontend/src/components/map-editor/MapEditor.tsx b/frontend/src/components/map-editor/MapEditor.tsx index c9ea3d8..4886d58 100644 --- a/frontend/src/components/map-editor/MapEditor.tsx +++ b/frontend/src/components/map-editor/MapEditor.tsx @@ -24,6 +24,7 @@ import { import { parsePGM, parseYAML, canvasToPGM, generateYAML, quantizeColor, createMapZip, MapMetadata } from '@/utils/ros/mapFileUtils'; import { setupWebGL } from '@/utils/webgl/mapShaders'; import { cn } from '@/utils/cn'; +import { useLanguage } from '@/contexts/LanguageContext'; export type DrawingTool = 'brush' | 'eraser' | 'line' | 'rectangle' | 'circle' | 'polygon'; export type MapRegion = 'occupied' | 'free' | 'unknown'; @@ -33,6 +34,7 @@ interface MapEditorProps { } export function MapEditor({ className }: MapEditorProps) { + const { t } = useLanguage(); const canvasRef = useRef(null); const previewCanvasRef = useRef(null); const toolPreviewCanvasRef = useRef(null); @@ -640,7 +642,7 @@ export function MapEditor({ className }: MapEditorProps) { setHistoryIndex(0); } catch (error) { console.error('Error parsing PGM file:', error); - alert('Failed to parse PGM file. Please ensure it\'s a valid P5 format.'); + alert(t('maps', 'failedParsePgm')); } } else if (file.name.endsWith('.png')) { // Handle PNG files @@ -702,15 +704,15 @@ export function MapEditor({ className }: MapEditorProps) { img.onerror = () => { console.error('Error loading PNG file'); - alert('Failed to load PNG file. Please ensure it\'s a valid PNG image.'); + alert(t('maps', 'failedLoadPng')); URL.revokeObjectURL(url); }; img.src = url; } else { - alert('Please select a PGM or PNG file.'); + alert(t('maps', 'selectPgmOrPng')); } - }, []); + }, [t]); const handleYamlUpload = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -722,9 +724,9 @@ export function MapEditor({ className }: MapEditorProps) { setMapMetadata(metadata); } catch (error) { console.error('Error parsing YAML file:', error); - alert('Failed to parse YAML file.'); + alert(t('maps', 'failedParseYaml')); } - }, []); + }, [t]); // Drawing functions const getMousePos = useCallback((e: React.MouseEvent) => { @@ -978,14 +980,14 @@ export function MapEditor({ className }: MapEditorProps) { className="flex items-center gap-2 px-3 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 transition-colors" > - Upload Map (PGM/PNG) + {t('maps', 'uploadMapPgmPng')}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/map-view-native.tsx b/frontend/src/components/map-view-native.tsx index 9b52684..4e66da7 100644 --- a/frontend/src/components/map-view-native.tsx +++ b/frontend/src/components/map-view-native.tsx @@ -618,7 +618,7 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) { @@ -630,7 +630,7 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) { ? 'bg-emerald-500 dark:bg-emerald-600 text-white border-emerald-600 dark:border-emerald-700' : 'border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={isGoalMode ? "Cancel goal setting" : "Set navigation goal"} + title={isGoalMode ? t('maps', 'cancelGoalSetting') : t('maps', 'setNavigationGoal')} disabled={!nav2Connected || isNavigating} > @@ -639,21 +639,21 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) { @@ -664,7 +664,7 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) { ? 'bg-green-500 dark:bg-green-600 text-white border-green-600 dark:border-green-700 hover:bg-green-600 dark:hover:bg-green-700' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-white border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={showNavPlan ? "Hide navigation plan" : "Show navigation plan"} + title={showNavPlan ? t('maps', 'hideNavigationPlan') : t('maps', 'showNavigationPlan')} > @@ -699,7 +699,7 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) {
-

Loading map...

+

{t('maps', 'loadingMap')}

)} @@ -719,17 +719,17 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) {
- Robot Position + {t('maps', 'robotPosition')}
- Walls + {t('maps', 'walls')}
{goalPosition && (
- {isNavigating ? 'Navigating...' : 'Nav Goal'} + {isNavigating ? t('maps', 'navigating') : t('maps', 'navGoal')}
)} @@ -741,10 +741,10 @@ export default function MapViewNative({ className = '' }: MapViewNativeProps) {
- Click on map to set navigation goal + {t('maps', 'clickOnMapToSetNavigationGoal')}
)}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/map-view-nav2.tsx b/frontend/src/components/map-view-nav2.tsx index 2331fe7..9413077 100644 --- a/frontend/src/components/map-view-nav2.tsx +++ b/frontend/src/components/map-view-nav2.tsx @@ -809,8 +809,8 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to Set Home', - message: 'Could not set home position. Please check ROS connection.' + title: t('maps', 'failedToSetHomeTitle'), + message: t('maps', 'failedToSetHomeMessage') } }); } @@ -955,8 +955,8 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { type: 'ADD_NOTIFICATION', payload: { type: 'info', - title: 'Navigation Stopped', - message: 'Robot navigation has been cancelled' + title: t('maps', 'navigationStoppedTitle'), + message: t('maps', 'navigationStoppedMessage') } }); } else { @@ -965,8 +965,8 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Stop Failed', - message: 'Failed to stop navigation. Please try again.' + title: t('maps', 'stopFailedTitle'), + message: t('maps', 'stopFailedMessage') } }); } @@ -976,8 +976,8 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Stop Failed', - message: 'An error occurred while stopping navigation.' + title: t('maps', 'stopFailedTitle'), + message: t('maps', 'stopErrorMessage') } }); } @@ -1002,8 +1002,8 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Navigation Started', - message: 'Navigating to destination' + title: t('maps', 'navigationStartedTitle'), + message: t('maps', 'navigationStartedMessage') } }); } else { @@ -1012,8 +1012,8 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Navigation Failed', - message: navigationError || 'Failed to start navigation. Please check Nav2 is running.' + title: t('maps', 'navigationFailedTitle'), + message: navigationError || t('maps', 'navigationFailedMessage') } }); } @@ -1050,7 +1050,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { ? 'bg-orange-500 dark:bg-orange-600 border-orange-600 dark:border-orange-700' : 'bg-red-500 dark:bg-red-600 border-red-600 dark:border-red-700 hover:bg-red-600 dark:hover:bg-red-700 active:bg-red-700 dark:active:bg-red-800 animate-pulse' } text-white w-12 h-12 rounded-lg border-2 shadow-lg transition-all transform hover:scale-110 active:scale-95 flex items-center justify-center relative z-10`} - title={isStopping ? "Stopping navigation..." : "Stop navigation - Click to stop the robot"} + title={isStopping ? t('maps', 'stoppingNavigation') : t('maps', 'stopNavigationClick')} > {isStopping ? (
@@ -1062,7 +1062,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { {!isStopping && (
- Stop Navigation + {t('maps', 'stopNavigation')}
@@ -1080,7 +1080,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { ? 'bg-green-500 dark:bg-green-600 text-white border-green-600 dark:border-green-700' : 'border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={isSetHomeMode ? "Cancel set home" : "Set home position"} + title={isSetHomeMode ? t('maps', 'cancelSetHome') : t('maps', 'setHomePositionTooltip')} disabled={!nav2Connected} > @@ -1095,7 +1095,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { ? 'bg-blue-500 dark:bg-blue-600 text-white border-blue-600 dark:border-blue-700' : 'border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={isWaypointMode ? "Cancel navigation mode" : "Navigate to location"} + title={isWaypointMode ? t('maps', 'cancelNavigationMode') : t('maps', 'navigateToLocation')} disabled={!nav2Connected || isNavigating} > {isWaypointMode ? : } @@ -1105,7 +1105,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { @@ -1125,21 +1125,21 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { @@ -1150,7 +1150,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { ? 'bg-green-500 dark:bg-green-600 text-white border-green-600 dark:border-green-700 hover:bg-green-600 dark:hover:bg-green-700' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-white border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={showNavPlan ? "Hide navigation plan" : "Show navigation plan"} + title={showNavPlan ? t('maps', 'hideNavigationPlan') : t('maps', 'showNavigationPlan')} > @@ -1161,7 +1161,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { ? 'bg-orange-500 dark:bg-orange-600 text-white border-orange-600 dark:border-orange-700 hover:bg-orange-600 dark:hover:bg-orange-700' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-white border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={showLocalCostmap ? "Hide local costmap" : "Show local costmap"} + title={showLocalCostmap ? t('maps', 'hideLocalCostmap') : t('maps', 'showLocalCostmap')} > @@ -1171,7 +1171,9 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { {isMissionActive && missionWaypoints.length > 0 && (
- Mission: {missionWaypointIndex + 1}/{missionWaypoints.length} + {t('maps', 'missionProgress') + .replace('{current}', String(missionWaypointIndex + 1)) + .replace('{total}', String(missionWaypoints.length))}
)} @@ -1179,7 +1181,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { {/* Navigation goal indicator - bottom left */} {waypoint && !isNavigating && !isStopping && (
- Goal set + {t('maps', 'goalSet')}
)} @@ -1190,11 +1192,11 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { {isStopping ? (
- Stopping navigation... + {t('maps', 'stoppingNavigation')}
) : ( <> -
Navigating to destination...
+
{t('maps', 'navigatingToDestination')}
{navigationError && (
{navigationError}
)} @@ -1233,7 +1235,7 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) {
-

Loading map...

+

{t('maps', 'loadingMap')}

)} @@ -1253,30 +1255,30 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) {
- Robot + {t('maps', 'robot')}
{(isWaypointMode || waypoint) && (
- Goal + {t('maps', 'goal')}
)} {isMissionActive && missionWaypoints.length > 0 && (
- Mission + {t('maps', 'mission')}
)} {showNavPlan && (
- Plan + {t('maps', 'plan')}
)} {showLocalCostmap && (
- Costmap + {t('maps', 'costmap')}
)}
@@ -1286,17 +1288,17 @@ export default function MapViewNav2({ className = '' }: MapViewNav2Props) { {isWaypointMode && (

- Click to set navigation goal • Drag to set orientation • ESC to cancel + {t('maps', 'setNavigationGoalInstructions')}

)} {isSetHomeMode && (

- Click to set home position • Drag to set orientation • ESC to cancel + {t('maps', 'setHomePositionInstructions')}

)}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/map-view.tsx b/frontend/src/components/map-view.tsx index da03e81..477a7d9 100644 --- a/frontend/src/components/map-view.tsx +++ b/frontend/src/components/map-view.tsx @@ -68,7 +68,7 @@ export default function MapView({ className = '' }: MapViewProps) { const availableMapTopics = commonMapTopics.filter(topic => result.topics.includes(topic)); if (availableMapTopics.length === 0) { console.warn('No standard map topics found. You may need to run a map server or navigation stack.'); - setMapError('No map topics available. Please ensure map_server or navigation is running.'); + setMapError(t('maps', 'noMapTopicsAvailable')); } else { console.log('Found map topics:', availableMapTopics); } @@ -143,7 +143,7 @@ export default function MapView({ className = '' }: MapViewProps) { initializeMap(); } catch (error) { console.error('Error loading map scripts:', error); - setMapError('Failed to load map libraries'); + setMapError(t('maps', 'failedToLoadMapLibraries')); } }; @@ -216,7 +216,7 @@ export default function MapView({ className = '' }: MapViewProps) { console.log('OccupancyGridClient created successfully for topic:', mapTopic); } catch (error) { console.error('Error creating OccupancyGridClient:', error); - setMapError('Failed to create map client'); + setMapError(t('maps', 'failedToCreateMapClient')); return; } @@ -469,7 +469,7 @@ export default function MapView({ className = '' }: MapViewProps) { }; } catch (error) { console.error('Error initializing map:', error); - setMapError(error instanceof Error ? error.message : 'Failed to initialize map viewer'); + setMapError(error instanceof Error ? error.message : t('maps', 'failedToInitializeMapViewer')); // Try a fallback approach with manual canvas rendering if (tryFallbackMapRenderingRef.current) { @@ -691,7 +691,7 @@ export default function MapView({ className = '' }: MapViewProps) { resetBtn.style.color = 'white'; resetBtn.style.cursor = 'pointer'; resetBtn.style.borderRadius = '4px'; - resetBtn.title = 'Reset view'; + resetBtn.title = t('maps', 'resetView'); zoomInBtn.onclick = (e) => { e.preventDefault(); @@ -898,7 +898,7 @@ export default function MapView({ className = '' }: MapViewProps) { setIsMapLoaded(false); setMapError(null); }; - }, [connection.ros, connection.online, showCostmap]); + }, [connection.ros, connection.online, showCostmap, t]); if (!connection.online) { return ( @@ -918,9 +918,9 @@ export default function MapView({ className = '' }: MapViewProps) {
@@ -936,7 +936,7 @@ export default function MapView({ className = '' }: MapViewProps) {
-

Loading map...

+

{t('maps', 'loadingMap')}

@@ -967,12 +967,12 @@ export default function MapView({ className = '' }: MapViewProps) {
- Robot Position + {t('maps', 'robotPosition')}
- {showCostmap ? 'Obstacles' : 'Walls'} + {showCostmap ? t('maps', 'obstacles') : t('maps', 'walls')}
diff --git a/frontend/src/components/mission-map-view.tsx b/frontend/src/components/mission-map-view.tsx index aeb5642..f968738 100644 --- a/frontend/src/components/mission-map-view.tsx +++ b/frontend/src/components/mission-map-view.tsx @@ -137,8 +137,8 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Error', - message: 'No waypoints defined. Please add waypoints first.', + title: t('missions', 'navigationErrorTitle'), + message: t('missions', 'noWaypointsDefinedMessage'), type: 'error' } }); @@ -149,8 +149,8 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Error', - message: 'Nav2 FollowWaypoints action server not available. Ensure Nav2 is running on the robot.', + title: t('missions', 'navigationErrorTitle'), + message: t('missions', 'nav2ActionUnavailableMessage'), type: 'error' } }); @@ -179,8 +179,8 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Failed', - message: navigationError?.message || 'Failed to start navigation. Check robot connection and Nav2 status.', + title: t('missions', 'navigationFailedTitle'), + message: navigationError?.message || t('maps', 'navigationFailedMessage'), type: 'error' } }); @@ -188,13 +188,13 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Started', - message: `Navigating through ${waypoints.length} waypoints`, + title: t('missions', 'navigationStartedTitle'), + message: t('missions', 'navigatingThroughWaypoints').replace('{count}', String(waypoints.length)), type: 'success' } }); } - }, [waypoints, isActionServerAvailable, startFollowWaypoints, navigationError, notificationDispatch]); + }, [waypoints, isActionServerAvailable, startFollowWaypoints, navigationError, notificationDispatch, t]); const stopNavigation = useCallback(() => { try { @@ -204,8 +204,8 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Cancelled', - message: 'Mission navigation has been stopped', + title: t('missions', 'navigationCancelledTitle'), + message: t('missions', 'missionNavigationStopped'), type: 'info' } }); @@ -214,8 +214,8 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Cancel Failed', - message: 'Failed to cancel navigation. Please check robot connection.', + title: t('missions', 'cancelFailedTitle'), + message: t('missions', 'cancelFailedMessage'), type: 'error' } }); @@ -225,13 +225,13 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Cancel Error', - message: 'An error occurred while cancelling navigation', + title: t('missions', 'cancelErrorTitle'), + message: t('missions', 'cancelErrorMessage'), type: 'error' } }); } - }, [cancelFollowWaypoints, notificationDispatch]); + }, [cancelFollowWaypoints, notificationDispatch, t]); // Notify parent of waypoint changes when they are modified by user actions // Use a ref to store previous waypoints to detect real changes @@ -346,8 +346,8 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Complete', - message: 'Successfully reached all waypoints!', + title: t('missions', 'navigationCompleteTitle'), + message: t('missions', 'successfullyReachedAllWaypoints'), type: 'success' } }); @@ -363,7 +363,7 @@ export default function MissionMapView({ notificationDispatch({ type: 'ADD_NOTIFICATION', payload: { - title: 'Navigation Failed', + title: t('missions', 'navigationFailedTitle'), message: navigationError.message, type: 'error' } @@ -374,7 +374,7 @@ export default function MissionMapView({ hasStartedNavigationRef.current = false; prevWaypointIndexRef.current = 0; } - }, [navigationStatus, navigationError, waypoints, navigationProgress.missedWaypoints, onNavigationComplete, notificationDispatch]); + }, [navigationStatus, navigationError, waypoints, navigationProgress.missedWaypoints, onNavigationComplete, notificationDispatch, t]); const renderMap = useCallback(() => { if (!canvasRef.current || !occupancyGrid || !containerRef.current) return; @@ -1084,17 +1084,17 @@ export default function MissionMapView({ ? 'bg-blue-500 hover:bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600' }`} - title="Toggle edit mode (E)" + title={t('missions', 'toggleEditMode')} > {isEditMode ? ( <> - Exit Edit + {t('missions', 'exitEdit')} ) : ( <> - Edit + {t('missions', 'edit')} )} @@ -1103,9 +1103,9 @@ export default function MissionMapView({ )}
@@ -1116,21 +1116,21 @@ export default function MissionMapView({ @@ -1138,7 +1138,7 @@ export default function MissionMapView({ @@ -1150,7 +1150,7 @@ export default function MissionMapView({ ? 'bg-orange-500 dark:bg-orange-600 text-white border-orange-600 dark:border-orange-700 hover:bg-orange-600 dark:hover:bg-orange-700' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-white border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700' }`} - title={showLocalCostmap ? "Hide local costmap" : "Show local costmap"} + title={showLocalCostmap ? t('maps', 'hideLocalCostmap') : t('maps', 'showLocalCostmap')} > @@ -1182,12 +1182,12 @@ export default function MissionMapView({
-

Loading map...

+

{t('maps', 'loadingMap')}

- Waiting for map data from {mapTopic} + {t('missions', 'waitingForMapDataFrom').replace('{topic}', mapTopic)}

- This may take up to 15 seconds + {t('missions', 'mayTake15Seconds')}

@@ -1204,10 +1204,10 @@ export default function MissionMapView({ onClick={retry} className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors text-sm font-medium" > - Retry Loading Map + {t('missions', 'retryLoadingMap')}

- Make sure the map server is running on the robot + {t('missions', 'mapServerRunningHelp')}

@@ -1219,42 +1219,44 @@ export default function MissionMapView({
- Robot Position + {t('missions', 'robotPosition')}
{waypoints.length > 0 && (
- Waypoints + {t('missions', 'waypoints')}
)} {isNav2Navigating && (
- Navigating ({nav2CurrentIndex + 1}/{waypoints.length}) + {t('missions', 'navigatingProgress') + .replace('{current}', String(nav2CurrentIndex + 1)) + .replace('{total}', String(waypoints.length))}
)} {!nav2Connected && (
- Nav2 disconnected + {t('missions', 'nav2Disconnected')}
)} {showNavPlan && navPlan && navPlan.poses.length > 0 && (
- Navigation Plan + {t('missions', 'navigationPlan')}
)} {showLocalCostmap && (
- Costmap + {t('missions', 'costmap')}
)}
)}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/mission-map-warning-banner.tsx b/frontend/src/components/mission-map-warning-banner.tsx index c47ab31..e6d3bb7 100644 --- a/frontend/src/components/mission-map-warning-banner.tsx +++ b/frontend/src/components/mission-map-warning-banner.tsx @@ -1,6 +1,7 @@ 'use client'; import { AlertTriangle, Map, Loader2 } from 'lucide-react'; +import { useLanguage } from '@/contexts/LanguageContext'; interface MissionMapWarningBannerProps { missionMapName: string; @@ -24,20 +25,25 @@ export function MissionMapWarningBanner({ isLoading = false, className = '', }: MissionMapWarningBannerProps) { + const { t } = useLanguage(); + const formattedMissionMapName = formatMapName(missionMapName); + const formattedCurrentMapName = currentMapName ? formatMapName(currentMapName) : null; + const mismatchMessage = formattedCurrentMapName + ? t('missions', 'mapMismatchCurrent') + .replace('{missionMapName}', formattedMissionMapName) + .replace('{currentMapName}', formattedCurrentMapName) + : t('missions', 'mapMismatchNoCurrentMap') + .replace('{missionMapName}', formattedMissionMapName); + return (

- Map Mismatch + {t('missions', 'mapMismatchTitle')}

- Mission created on {formatMapName(missionMapName)} - {currentMapName ? ( - <>, currently {formatMapName(currentMapName)} is loaded - ) : ( - <>, but no map is currently loaded - )} + {mismatchMessage}

); diff --git a/frontend/src/components/moondream-detections.tsx b/frontend/src/components/moondream-detections.tsx index ec1ee41..689a8b2 100644 --- a/frontend/src/components/moondream-detections.tsx +++ b/frontend/src/components/moondream-detections.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { Send, Eye, HelpCircle, Brain, Search, MapPin, Loader2, X } from 'lucide-react'; import useMoondreamServices, { MoondreamBoundingBox, MoondreamPoint } from '@/hooks/ros/useMoondreamServices'; +import { useLanguage } from '@/contexts/LanguageContext'; interface MoondreamDetectionsProps { expandedView?: boolean; @@ -13,16 +14,12 @@ interface MoondreamDetectionsProps { type MoonDreamMode = 'caption' | 'query-no-reasoning' | 'query-with-reasoning' | 'detect' | 'point'; interface ModeConfigBase { - title: string; - description: string; icon: typeof Eye; color: string; } interface ModeConfigWithInput extends ModeConfigBase { hasInput: true; - inputPlaceholder: string; - inputLabel: string; } interface ModeConfigWithoutInput extends ModeConfigBase { @@ -33,47 +30,29 @@ type ModeConfigType = ModeConfigWithInput | ModeConfigWithoutInput; const modeConfig: Record = { 'caption': { - title: 'Caption', - description: 'Generate a description of the image', icon: Eye, color: 'from-blue-500 to-cyan-500', hasInput: false, }, 'query-no-reasoning': { - title: 'Query - No Reasoning', - description: 'Ask a question about the image', icon: HelpCircle, color: 'from-purple-500 to-pink-500', hasInput: true, - inputPlaceholder: 'Ask a question about the image...', - inputLabel: 'Question', }, 'query-with-reasoning': { - title: 'Query - With Reasoning', - description: 'Ask a question with detailed reasoning', icon: Brain, color: 'from-green-500 to-emerald-500', hasInput: true, - inputPlaceholder: 'Ask a question for detailed analysis...', - inputLabel: 'Question', }, 'detect': { - title: 'Detect', - description: 'Detect specific objects in the image', icon: Search, color: 'from-orange-500 to-red-500', hasInput: true, - inputPlaceholder: 'Describe what to detect...', - inputLabel: 'Description', }, 'point': { - title: 'Point', - description: 'Point to specific locations in the image', icon: MapPin, color: 'from-indigo-500 to-purple-500', hasInput: true, - inputPlaceholder: 'Describe what to point to...', - inputLabel: 'Description', }, }; @@ -82,9 +61,10 @@ export default function MoondreamDetections({ onBoundingBoxesChange, onPointsChange }: MoondreamDetectionsProps) { + const { t } = useLanguage(); const [selectedMode, setSelectedMode] = useState('caption'); const [inputValue, setInputValue] = useState(''); - const [response, setResponse] = useState('Ready to interact with MoonDream. Select a mode and submit to begin.'); + const [response, setResponse] = useState(t('aiDetections', 'moonDreamReady')); const [error, setError] = useState(null); const { @@ -101,6 +81,44 @@ export default function MoondreamDetections({ const currentMode = modeConfig[selectedMode]; const ModeIcon = currentMode.icon; + const getModeTitle = (mode: MoonDreamMode) => { + const titles: Record = { + caption: t('aiDetections', 'moonDreamCaption'), + 'query-no-reasoning': t('aiDetections', 'moonDreamQueryNoReasoning'), + 'query-with-reasoning': t('aiDetections', 'moonDreamQueryWithReasoning'), + detect: t('aiDetections', 'moonDreamDetect'), + point: t('aiDetections', 'moonDreamPoint'), + }; + return titles[mode]; + }; + + const getModeDescription = (mode: MoonDreamMode) => { + const descriptions: Record = { + caption: t('aiDetections', 'moonDreamCaptionDescription'), + 'query-no-reasoning': t('aiDetections', 'moonDreamQueryNoReasoningDescription'), + 'query-with-reasoning': t('aiDetections', 'moonDreamQueryWithReasoningDescription'), + detect: t('aiDetections', 'moonDreamDetectDescription'), + point: t('aiDetections', 'moonDreamPointDescription'), + }; + return descriptions[mode]; + }; + + const getInputLabel = (mode: MoonDreamMode) => ( + mode === 'query-no-reasoning' || mode === 'query-with-reasoning' + ? t('aiDetections', 'moonDreamQuestionLabel') + : t('aiDetections', 'moonDreamDescriptionLabel') + ); + + const getInputPlaceholder = (mode: MoonDreamMode) => { + const placeholders: Partial> = { + 'query-no-reasoning': t('aiDetections', 'moonDreamQuestionPlaceholder'), + 'query-with-reasoning': t('aiDetections', 'moonDreamReasoningPlaceholder'), + detect: t('aiDetections', 'moonDreamDetectPlaceholder'), + point: t('aiDetections', 'moonDreamPointPlaceholder'), + }; + return placeholders[mode] || ''; + }; + // Update parent component when bounding boxes or points change useEffect(() => { if (onBoundingBoxesChange) { @@ -127,70 +145,78 @@ export default function MoondreamDetections({ if (result.success && result.caption) { setResponse(result.caption); } else { - setError(result.error || 'Failed to generate caption'); + setError(result.error || t('aiDetections', 'moonDreamFailedCaption')); } break; case 'query-no-reasoning': if (!inputValue.trim()) { - setError('Please enter a question'); + setError(t('aiDetections', 'moonDreamEnterQuestion')); return; } result = await callQuery(inputValue, false); if (result.success && result.answer) { setResponse(result.answer); } else { - setError(result.error || 'Failed to answer question'); + setError(result.error || t('aiDetections', 'moonDreamFailedQuestion')); } break; case 'query-with-reasoning': if (!inputValue.trim()) { - setError('Please enter a question'); + setError(t('aiDetections', 'moonDreamEnterQuestion')); return; } result = await callQuery(inputValue, true); if (result.success) { if (result.reasoning) { - setResponse(`${result.reasoning.text}\n\nAnswer: ${result.answer}`); + setResponse(`${result.reasoning.text}\n\n${t('aiDetections', 'moonDreamAnswerLabel')}: ${result.answer}`); } else if (result.answer) { setResponse(result.answer); } } else { - setError(result.error || 'Failed to answer question with reasoning'); + setError(result.error || t('aiDetections', 'moonDreamFailedReasoning')); } break; case 'detect': if (!inputValue.trim()) { - setError('Please enter a description'); + setError(t('aiDetections', 'moonDreamEnterDescription')); return; } result = await callDetect(inputValue); if (result.success && result.objects) { const count = result.objects.length; - setResponse(`Found ${count} object${count !== 1 ? 's' : ''} matching "${inputValue}". ${count > 0 ? 'Purple bounding boxes shown on the live stream.' : ''}`); + setResponse(t('aiDetections', 'moonDreamDetectResult') + .replace('{count}', String(count)) + .replace('{objectLabel}', count === 1 ? t('aiDetections', 'moonDreamObjectSingular') : t('aiDetections', 'moonDreamObjectPlural')) + .replace('{input}', inputValue) + .replace('{overlayMessage}', count > 0 ? t('aiDetections', 'moonDreamBoundingBoxesShown') : '')); } else { - setError(result.error || 'Failed to detect objects'); + setError(result.error || t('aiDetections', 'moonDreamFailedDetect')); } break; case 'point': if (!inputValue.trim()) { - setError('Please enter a description'); + setError(t('aiDetections', 'moonDreamEnterDescription')); return; } result = await callPoint(inputValue); if (result.success && result.points) { const count = result.points.length; - setResponse(`Identified ${count} point${count !== 1 ? 's' : ''} for "${inputValue}". ${count > 0 ? 'Red markers shown on the live stream.' : ''}`); + setResponse(t('aiDetections', 'moonDreamPointResult') + .replace('{count}', String(count)) + .replace('{pointLabel}', count === 1 ? t('aiDetections', 'moonDreamPointSingular') : t('aiDetections', 'moonDreamPointPlural')) + .replace('{input}', inputValue) + .replace('{overlayMessage}', count > 0 ? t('aiDetections', 'moonDreamRedMarkersShown') : '')); } else { - setError(result.error || 'Failed to identify points'); + setError(result.error || t('aiDetections', 'moonDreamFailedPoint')); } break; } } catch (err) { - setError('An unexpected error occurred. Please try again.'); + setError(t('aiDetections', 'moonDreamUnexpectedError')); console.error('MoonDream error:', err); } }; @@ -211,7 +237,7 @@ export default function MoondreamDetections({ onClick={() => { setSelectedMode(mode); setInputValue(''); // Clear input when switching modes - setResponse('Ready to interact with MoonDream. Select a mode and submit to begin.'); + setResponse(t('aiDetections', 'moonDreamReady')); setError(null); clearOverlays(); // Clear any existing overlays }} @@ -246,7 +272,7 @@ export default function MoondreamDetections({ : 'text-gray-700 dark:text-gray-300' } `}> - {config.title} + {getModeTitle(mode)}
@@ -275,10 +301,10 @@ export default function MoondreamDetections({

- {currentMode.title} + {getModeTitle(selectedMode)}

- {currentMode.description} + {getModeDescription(selectedMode)}

@@ -289,7 +315,7 @@ export default function MoondreamDetections({ {currentMode.hasInput && (
setInputValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && inputValue.trim() && handleSubmit()} - placeholder={(currentMode as ModeConfigWithInput).inputPlaceholder} + placeholder={getInputPlaceholder(selectedMode)} className={` flex-1 px-5 py-3 rounded-lg text-base border border-gray-300 dark:border-gray-600 @@ -327,7 +353,7 @@ export default function MoondreamDetections({ ) : ( )} - {loading ? 'Processing...' : 'Send'} + {loading ? t('aiDetections', 'moonDreamProcessing') : t('aiDetections', 'moonDreamSend')} {/* Clear button for Detect and Point modes */} @@ -335,7 +361,7 @@ export default function MoondreamDetections({ )}
@@ -376,7 +402,7 @@ export default function MoondreamDetections({ ) : ( )} - {loading ? 'Generating...' : 'Generate Caption'} + {loading ? t('aiDetections', 'moonDreamGenerating') : t('aiDetections', 'moonDreamGenerateCaption')}
)} @@ -389,7 +415,7 @@ export default function MoondreamDetections({
- MoonDream Response + {t('aiDetections', 'moonDreamResponse')}
@@ -397,19 +423,19 @@ export default function MoondreamDetections({ {(boundingBoxes.length > 0 || points.length > 0) && (
- {boundingBoxes.length > 0 && `${boundingBoxes.length} box${boundingBoxes.length !== 1 ? 'es' : ''}`} + {boundingBoxes.length > 0 && `${boundingBoxes.length} ${boundingBoxes.length === 1 ? t('aiDetections', 'moonDreamBoxSingular') : t('aiDetections', 'moonDreamBoxPlural')}`} {boundingBoxes.length > 0 && points.length > 0 && ', '} - {points.length > 0 && `${points.length} point${points.length !== 1 ? 's' : ''}`} + {points.length > 0 && `${points.length} ${points.length === 1 ? t('aiDetections', 'moonDreamPointSingular') : t('aiDetections', 'moonDreamPointPlural')}`}
)} @@ -455,11 +481,11 @@ export default function MoondreamDetections({ {/* Response metadata */}
- Mode: {currentMode.title} + {t('aiDetections', 'moonDreamMode')}: {getModeTitle(selectedMode)} {inputValue && currentMode.hasInput && ( - Input: "{inputValue}" + {t('aiDetections', 'moonDreamInput')}: "{inputValue}" )}
@@ -467,4 +493,4 @@ export default function MoondreamDetections({
); -} \ No newline at end of file +} diff --git a/frontend/src/components/robot-connection-popup.tsx b/frontend/src/components/robot-connection-popup.tsx index 241b1bf..12b79ed 100644 --- a/frontend/src/components/robot-connection-popup.tsx +++ b/frontend/src/components/robot-connection-popup.tsx @@ -49,13 +49,13 @@ export default function RobotConnectionPopup({

{connection.online && connection.connectedRobot - ? `Connected to ${connection.connectedRobot.name}` - : 'Not Connected'} + ? `${t('connectionPopup', 'connectedTo')} ${connection.connectedRobot.name}` + : t('connectionPopup', 'notConnected')}

{connection.online && connection.connectedRobot ? connection.connectedRobot.address - : 'No robot connected'} + : t('connectionPopup', 'noRobotConnected')}

@@ -70,10 +70,10 @@ export default function RobotConnectionPopup({

- Manage Robot Fleet + {t('connectionPopup', 'manageRobotFleet')}

- Add, edit, or connect to your robots + {t('connectionPopup', 'manageRobotFleetDescription')}

@@ -83,7 +83,7 @@ export default function RobotConnectionPopup({ {/* Info message */}

- Click above to manage your robot connections + {t('connectionPopup', 'manageRobotConnections')}

diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 6a6ef98..07a495f 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -84,7 +84,7 @@ export function Sidebar() { // Get language emoji based on current language const getLanguageEmoji = () => { - return language === 'en' ? '🇬🇧' : '🇧🇷'; + return languageNames[language].split(' ')[0]; }; // Handle escape key to close panels @@ -303,8 +303,8 @@ export function Sidebar() { setShowLanguageSelector(false); }} > - {code === 'en' ? '🇬🇧' : '🇧🇷'} - {code === 'en' ? 'English' : 'Português'} + {languageNames[code].split(' ')[0]} + {languageNames[code].replace(/^[^ ]+ /, '')} ))} @@ -316,4 +316,4 @@ export function Sidebar() { ); -} \ No newline at end of file +} diff --git a/frontend/src/components/simple-map-view.tsx b/frontend/src/components/simple-map-view.tsx index 735dcd7..0a83088 100644 --- a/frontend/src/components/simple-map-view.tsx +++ b/frontend/src/components/simple-map-view.tsx @@ -7,6 +7,7 @@ import useOccupancyGrid from '@/hooks/ros/useOccupancyGrid'; import useMapPose from '@/hooks/ros/useMapPose'; import useInitialPose from '@/hooks/ros/useInitialPose'; import { useNotifications } from '@/contexts/NotificationsContext'; +import { useLanguage } from '@/contexts/LanguageContext'; interface SimpleMapViewProps { className?: string; @@ -27,6 +28,7 @@ interface ViewState { export default function SimpleMapView({ className = '', isMapping = false, isSettingHome = false, onHomeSet }: SimpleMapViewProps) { const { connection } = useRobotConnection(); const { dispatch: notificationDispatch } = useNotifications(); + const { t } = useLanguage(); const { publishInitialPose } = useInitialPose(); const canvasRef = useRef(null); const containerRef = useRef(null); @@ -565,8 +567,8 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to Set Home', - message: 'Could not set home position. Please check ROS connection.' + title: t('maps', 'failedToSetHomeTitle'), + message: t('maps', 'failedToSetHomeMessage') } }); } @@ -704,7 +706,7 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet {/* Loading overlay */} {isLoading && (
-
Loading map...
+
{t('maps', 'loadingMap')}
)} @@ -712,7 +714,7 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet {error && (
-

Failed to load map

+

{t('maps', 'failedToLoadMapGeneric')}

{error}

@@ -721,8 +723,8 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet {/* Instructions for Set Home mode */} {isSettingHome && (
-

Setting Home Position

-

Click on map to place • Drag to set orientation • ESC to cancel

+

{t('maps', 'settingHomePosition')}

+

{t('maps', 'setHomePositionInstructions')}

)} @@ -730,7 +732,7 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet {robotPose && (

- Position: ({robotPose.x.toFixed(2)}, {robotPose.y.toFixed(2)}) + {t('maps', 'position')}: ({robotPose.x.toFixed(2)}, {robotPose.y.toFixed(2)})

)} @@ -748,7 +750,7 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet ? 'bg-green-500/80 hover:bg-green-600/80 text-white' : 'bg-white/10 hover:bg-white/20 text-white' }`} - title={isSettingHome ? "Cancel set home" : "Set home position"} + title={isSettingHome ? t('maps', 'cancelSetHome') : t('maps', 'setHomePositionTooltip')} > @@ -756,28 +758,28 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet @@ -788,14 +790,14 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet {/* Connection status */} {connection && (
- Map Connected + {t('maps', 'mapConnected')}
)} {/* Instructions when setting home */} {isSettingHome && (

- Click to set home position • Drag to set orientation • ESC to cancel + {t('maps', 'setHomePositionInstructions')}

)} @@ -803,4 +805,4 @@ export default function SimpleMapView({ className = '', isMapping = false, isSet ); -} \ No newline at end of file +} diff --git a/frontend/src/components/soundboard-sound-clips.tsx b/frontend/src/components/soundboard-sound-clips.tsx index 93c3d7f..822a251 100644 --- a/frontend/src/components/soundboard-sound-clips.tsx +++ b/frontend/src/components/soundboard-sound-clips.tsx @@ -6,6 +6,7 @@ import { createClient } from '@/utils/supabase/client'; import { useSupabase } from '@/contexts/SupabaseProvider'; import { useRosPlayAudio } from '@/hooks/ros/useRosPlayAudio'; import { useRobotConnection } from '@/contexts/RobotConnectionContext'; +import { useLanguage } from '@/contexts/LanguageContext'; interface SoundClip { id: string; @@ -45,6 +46,7 @@ export function SoundboardSoundClips() { const supabase = createClient(); const { playAudioOnRobot } = useRosPlayAudio(); const { connectionStatus } = useRobotConnection(); + const { t } = useLanguage(); const [clips, setClips] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); @@ -173,7 +175,7 @@ export function SoundboardSoundClips() { // Generate default name with timestamp const now = new Date(); - const defaultName = `Recording ${now.toLocaleTimeString()}`; + const defaultName = `${t('soundClips', 'recordingDefaultName')} ${now.toLocaleTimeString()}`; setRecordingName(defaultName); // Start timer @@ -183,7 +185,7 @@ export function SoundboardSoundClips() { } catch (error) { console.error('Error starting recording:', error); - alert('Failed to access microphone. Please check your permissions.'); + alert(t('soundClips', 'failedMicrophoneAccess')); } }; @@ -298,7 +300,7 @@ export function SoundboardSoundClips() { } catch (error) { console.error('Error uploading recording:', error); - alert('Failed to upload recording'); + alert(t('soundClips', 'failedUploadRecording')); } finally { setUploading(false); } @@ -327,13 +329,13 @@ export function SoundboardSoundClips() { // Check file size (1GB limit) if (file.size > 1024 * 1024 * 1024) { - alert('File size must be less than 1GB'); + alert(t('soundClips', 'fileSizeLimit')); return; } // Check if it's an audio file if (!file.type.startsWith('audio/')) { - alert('Please upload an audio file'); + alert(t('soundClips', 'audioFileRequired')); return; } @@ -410,7 +412,7 @@ export function SoundboardSoundClips() { } } catch (error) { console.error('Error uploading file:', error); - alert('Failed to upload sound clip'); + alert(t('soundClips', 'failedUploadSoundClip')); } finally { setUploading(false); } @@ -427,7 +429,7 @@ export function SoundboardSoundClips() { }; const handleDelete = async (clip: SoundClip) => { - if (!confirm(`Delete "${clip.name}"?`)) return; + if (!confirm(t('soundClips', 'deleteConfirm').replace('{clipName}', clip.name))) return; try { // Delete from storage @@ -449,7 +451,7 @@ export function SoundboardSoundClips() { setClips(clips.filter(c => c.id !== clip.id)); } catch (error) { console.error('Error deleting sound clip:', error); - alert('Failed to delete sound clip'); + alert(t('soundClips', 'failedDeleteSoundClip')); } }; @@ -484,7 +486,7 @@ export function SoundboardSoundClips() { setEditingClip(null); } catch (error) { console.error('Error updating sound clip:', error); - alert('Failed to update sound clip'); + alert(t('soundClips', 'failedUpdateSoundClip')); } }; @@ -524,7 +526,7 @@ export function SoundboardSoundClips() { } } catch (error) { console.error('Error playing sound clip:', error); - alert('Failed to play sound clip'); + alert(t('soundClips', 'failedToPlay')); } } }; @@ -560,7 +562,7 @@ export function SoundboardSoundClips() { if (!user) { return (
-

Please log in to manage sound clips

+

{t('soundClips', 'loginRequired')}

); } @@ -571,7 +573,7 @@ export function SoundboardSoundClips() {

- Sound Clips + {t('soundClips', 'title')}

{!showUploadForm && !showRecordingForm && (
@@ -580,14 +582,14 @@ export function SoundboardSoundClips() { className="flex items-center px-3 py-1 bg-red-500 text-white text-xs rounded-full hover:bg-red-600 transition-all hover:scale-105" > - Record + {t('soundClips', 'record')}
)} @@ -607,14 +609,14 @@ export function SoundboardSoundClips() {
- Recording... {formatRecordingTime(recordingTime)} + {t('soundClips', 'recording')} {formatRecordingTime(recordingTime)} ) : ( <> - Recording ready ({formatRecordingTime(recordingTime)}) + {t('soundClips', 'recordingReady')} ({formatRecordingTime(recordingTime)}) )} @@ -682,7 +684,7 @@ export function SoundboardSoundClips() { value={recordingName} onChange={(e) => setRecordingName(e.target.value)} className="flex-1 bg-white dark:bg-botbot-darker text-gray-800 dark:text-white px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 focus:border-botbot-accent focus:outline-none text-sm" - placeholder="Recording name..." + placeholder={t('soundClips', 'recordingNamePlaceholder')} autoFocus /> @@ -696,12 +698,12 @@ export function SoundboardSoundClips() { {uploading ? ( <> - Saving... + {t('soundClips', 'saving')} ) : ( <> - Save + {t('soundClips', 'save')} )} @@ -709,7 +711,7 @@ export function SoundboardSoundClips() { onClick={cancelRecording} className="px-3 py-1.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors text-sm" > - Cancel + {t('soundClips', 'cancel')} @@ -765,7 +767,7 @@ export function SoundboardSoundClips() { value={uploadName} onChange={(e) => setUploadName(e.target.value)} className="flex-1 bg-white dark:bg-botbot-darker text-gray-800 dark:text-white px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 focus:border-botbot-accent focus:outline-none text-sm" - placeholder="Sound clip name..." + placeholder={t('soundClips', 'soundClipNamePlaceholder')} autoFocus /> @@ -779,12 +781,12 @@ export function SoundboardSoundClips() { {uploading ? ( <> - Uploading... + {t('soundClips', 'uploading')} ) : ( <> - Upload + {t('soundClips', 'upload')} )} @@ -792,7 +794,7 @@ export function SoundboardSoundClips() { onClick={cancelUpload} className="px-3 py-1.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors text-sm" > - Cancel + {t('soundClips', 'cancel')} @@ -809,8 +811,8 @@ export function SoundboardSoundClips() { ) : clips.length === 0 ? (
-

No sound clips yet

-

Click "Record" or "Add Clip" to get started

+

{t('soundClips', 'emptyTitle')}

+

{t('soundClips', 'emptyDescription')}

) : (
@@ -832,7 +834,7 @@ export function SoundboardSoundClips() { value={editName} onChange={(e) => setEditName(e.target.value)} className="flex-1 px-3 py-1.5 bg-white dark:bg-botbot-dark rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400" - placeholder="Clip name" + placeholder={t('soundClips', 'clipNamePlaceholder')} autoFocus />
@@ -866,13 +868,13 @@ export function SoundboardSoundClips() { onClick={handleSaveEdit} className="flex-1 py-1.5 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white rounded-xl text-sm font-medium transition-all duration-200 shadow-sm hover:shadow-md" > - Save + {t('soundClips', 'save')} @@ -889,7 +891,9 @@ export function SoundboardSoundClips() { className={`relative w-full p-3 bg-gradient-to-br from-purple-50 via-pink-50 to-purple-50 dark:from-botbot-darker/90 dark:via-botbot-darker/70 dark:to-botbot-darker/90 hover:from-purple-100 hover:via-pink-100 hover:to-purple-100 dark:hover:from-botbot-dark/90 dark:hover:via-botbot-dark/70 dark:hover:to-botbot-dark/90 rounded-2xl transition-all duration-200 group border border-purple-100 dark:border-botbot-dark/50 hover:border-purple-200 dark:hover:border-botbot-accent/30 shadow-sm hover:shadow-lg hover:scale-[1.02] active:scale-[0.98] focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2 ${ connectionStatus !== 'connected' ? 'opacity-60 cursor-not-allowed' : '' }`} - title={connectionStatus !== 'connected' ? 'Connect to robot to play on robot' : `Play "${clip.name}" on robot`} + title={connectionStatus !== 'connected' + ? t('soundClips', 'connectToRobotToPlay') + : t('soundClips', 'playOnRobot').replace('{clipName}', clip.name)} >
@@ -914,7 +918,7 @@ export function SoundboardSoundClips() { handlePlayInBrowser(clip); }} className="p-1.5 bg-white/80 dark:bg-botbot-dark/80 rounded-lg hover:bg-white dark:hover:bg-botbot-dark transition-all focus:outline-none" - title="Play in browser" + title={t('soundClips', 'playInBrowser')} > {playingClip === clip.id ? ( @@ -929,7 +933,7 @@ export function SoundboardSoundClips() { {connectionStatus === 'connected' && (
- Click to play on robot + {t('soundClips', 'clickToPlayOnRobot')}
)} @@ -984,4 +988,4 @@ export function SoundboardSoundClips() { />
); -} \ No newline at end of file +} diff --git a/frontend/src/components/weather/WeatherDashboard.tsx b/frontend/src/components/weather/WeatherDashboard.tsx index f7fb2f6..78da1f3 100644 --- a/frontend/src/components/weather/WeatherDashboard.tsx +++ b/frontend/src/components/weather/WeatherDashboard.tsx @@ -36,6 +36,7 @@ import { ArrowDown } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import { useLanguage } from '@/contexts/LanguageContext'; interface WeatherData { properties: { @@ -212,6 +213,7 @@ const UVIndexIndicator: React.FC<{ index: number }> = ({ index }) => { }; const WeatherDashboard: React.FC = () => { + const { t } = useLanguage(); const [weatherData, setWeatherData] = useState(null); const [location, setLocation] = useState(null); const [loading, setLoading] = useState(true); @@ -302,7 +304,7 @@ const WeatherDashboard: React.FC = () => { } ); const data = await response.json(); - loc.placeName = data.address?.city || data.address?.town || data.address?.village || 'Unknown Location'; + loc.placeName = data.address?.city || data.address?.town || data.address?.village || t('weather', 'unknownLocation'); } catch (err) { loc.placeName = `${loc.latitude.toFixed(4)}, ${loc.longitude.toFixed(4)}`; } @@ -310,15 +312,15 @@ const WeatherDashboard: React.FC = () => { setLocation(loc); }, (err) => { - setError('Unable to get location. Please enable location services.'); + setError(t('weather', 'locationUnavailable')); setLoading(false); } ); } else { - setError('Geolocation is not supported by your browser.'); + setError(t('weather', 'geolocationUnsupported')); setLoading(false); } - }, []); + }, [t]); // Fetch weather data const fetchWeatherData = useCallback(async () => { @@ -351,13 +353,13 @@ const WeatherDashboard: React.FC = () => { setError(null); } catch (err) { - setError('Failed to fetch weather data. Please try again.'); + setError(t('weather', 'failedToFetch')); console.error('Weather fetch error:', err); } finally { setLoading(false); setIsRefreshing(false); } - }, [location]); + }, [location, t]); useEffect(() => { if (location) { @@ -1006,14 +1008,11 @@ const WeatherDashboard: React.FC = () => { >
- - Weather data provided by MET Norway API. Location determined using browser's Geolocation API. - Only your coordinates are shared with weather services to fetch local conditions. Data updates every 5 minutes. - + {t('weather', 'attribution')}
); }; -export default WeatherDashboard; \ No newline at end of file +export default WeatherDashboard; diff --git a/frontend/src/components/widgets/MapsManagementWidget.tsx b/frontend/src/components/widgets/MapsManagementWidget.tsx index 43d9661..8264eba 100644 --- a/frontend/src/components/widgets/MapsManagementWidget.tsx +++ b/frontend/src/components/widgets/MapsManagementWidget.tsx @@ -16,6 +16,7 @@ import { } from 'lucide-react'; import { useRosMappingServices } from '@/hooks/ros/useRosMappingServices'; import { useNotifications } from '@/contexts/NotificationsContext'; +import { useLanguage } from '@/contexts/LanguageContext'; interface MapLocation { id: string; @@ -57,6 +58,7 @@ export function MapsManagementWidget({ const intervalRef = useRef(null); const { dispatch } = useNotifications(); + const { t } = useLanguage(); const { isConnected, @@ -110,8 +112,8 @@ export function MapsManagementWidget({ id: index.toString(), name: map.name, path: map.path, - lastUpdated: map.lastModified || 'Unknown', - size: map.size || 'Unknown', + lastUpdated: map.lastModified || t('maps', 'unknown'), + size: map.size || t('maps', 'unknown'), })); setLocations(mapsWithIds); } catch (error) { @@ -163,8 +165,8 @@ export function MapsManagementWidget({ type: 'ADD_NOTIFICATION', payload: { type: 'success', - title: 'Map Loaded', - message: `Successfully loaded: ${mapName}`, + title: t('maps', 'mapLoadedTitle'), + message: t('maps', 'mapLoadedShortMessage').replace('{mapName}', mapName), }, }); } catch (error) { @@ -173,8 +175,8 @@ export function MapsManagementWidget({ type: 'ADD_NOTIFICATION', payload: { type: 'error', - title: 'Failed to Load Map', - message: `Could not load: ${mapName}`, + title: t('maps', 'failedToLoadMapTitle'), + message: t('maps', 'failedToLoadMapShortMessage').replace('{mapName}', mapName), }, }); } finally { @@ -188,7 +190,7 @@ export function MapsManagementWidget({ title={
- Maps Management + {t('maps', 'pageTitle')}
} initialPosition={initialPosition} @@ -209,7 +211,7 @@ export function MapsManagementWidget({ className="flex items-center justify-between w-full text-left" >

- Map View {selectedLocation && + {t('maps', 'mapView')} {selectedLocation && ({locations.find(l => l.id === selectedLocation)?.name}) @@ -227,7 +229,7 @@ export function MapsManagementWidget({ {/* Mapping Control Section */}

- Mapping Control + {t('maps', 'mappingControl')}

@@ -243,7 +245,7 @@ export function MapsManagementWidget({ handleStartMapping(); } }} - placeholder="Enter map name..." + placeholder={t('maps', 'mapNamePlaceholder')} className="w-full px-3 py-1.5 text-sm bg-white dark:bg-botbot-darker border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" autoFocus /> @@ -281,17 +283,17 @@ export function MapsManagementWidget({ {isMapping ? ( <> - Stop Mapping + {t('maps', 'stopMapping')} ) : showMapNameInput && mapName.trim() ? ( <> - Start Mapping + {t('maps', 'startMapping')} ) : ( <> - Start New Map + {t('maps', 'startNewMap')} )} @@ -305,7 +307,7 @@ export function MapsManagementWidget({ }} className="w-full px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors duration-200" > - Cancel + {t('maps', 'cancel')} )}
@@ -318,7 +320,7 @@ export function MapsManagementWidget({ onClick={() => setShowMapsList(!showMapsList)} className="flex items-center gap-1 text-sm font-medium text-gray-700 dark:text-gray-300" > - Available Maps + {t('maps', 'availableMaps')} {showMapsList ? ( ) : ( @@ -329,7 +331,7 @@ export function MapsManagementWidget({ onClick={fetchMaps} disabled={!isConnected || isLoading} className="p-1 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-botbot-darkest/80 rounded transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" - title="Refresh maps list" + title={t('maps', 'refreshMaps')} > @@ -340,7 +342,7 @@ export function MapsManagementWidget({ {!isConnected ? (

- Connect to robot to view maps + {t('maps', 'connectToRobotToViewMaps')}

) : isLoading && locations.length === 0 ? ( @@ -350,7 +352,7 @@ export function MapsManagementWidget({ ) : locations.length === 0 ? (

- No maps available + {t('maps', 'noMapsAvailable')}

) : ( @@ -393,7 +395,7 @@ export function MapsManagementWidget({ handleLoadMap(location.id, location.path, location.name); }} > - Load + {t('maps', 'load')} )}
@@ -409,4 +411,4 @@ export function MapsManagementWidget({ ); -} \ No newline at end of file +} diff --git a/frontend/src/components/widgets/MissionsWidget.tsx b/frontend/src/components/widgets/MissionsWidget.tsx index a913cc2..8849cce 100644 --- a/frontend/src/components/widgets/MissionsWidget.tsx +++ b/frontend/src/components/widgets/MissionsWidget.tsx @@ -9,6 +9,7 @@ import useFollowWaypoints from '@/hooks/ros/useFollowWaypoints'; import { missionsService, MissionWithWaypoints } from '@/services/missions'; import { useDashboard } from '@/contexts/DashboardContext'; import { cn } from '@/utils/cn'; +import { useLanguage } from '@/contexts/LanguageContext'; // Helper to normalize map names for comparison function normalizeMapName(name: string): string { @@ -34,6 +35,7 @@ interface MissionItemProps { function MissionItem({ mission, isSelected, isRunning, progress, onSelect, disabled }: MissionItemProps) { const waypointCount = mission.waypoints?.length || 0; + const { t } = useLanguage(); return ( @@ -315,22 +319,22 @@ export function MissionsWidget({
{isLoading ? (
- Loading... + {t('missions', 'loading')}
) : !isRobotConnected ? (
- Connect to robot to view missions + {t('missions', 'connectToRobotToViewMissions')}
) : !currentMapName ? (
- Load a map to see missions + {t('missions', 'loadMapToSeeMissions')}
) : filteredMissions.length === 0 ? (
- No missions for current map + {t('missions', 'noMissionsForCurrentMap')}
) : ( filteredMissions.map(mission => ( @@ -367,15 +371,15 @@ export function MissionsWidget({ )} > {canStop ? ( - <> - - Stop Mission - - ) : ( - <> - - Start Mission - + <> + + {t('missions', 'stopMission')} + + ) : ( + <> + + {t('missions', 'startMission')} + )}
diff --git a/frontend/src/components/widgets/RecentDetectionsWidget.tsx b/frontend/src/components/widgets/RecentDetectionsWidget.tsx index 7f742c0..ca52864 100644 --- a/frontend/src/components/widgets/RecentDetectionsWidget.tsx +++ b/frontend/src/components/widgets/RecentDetectionsWidget.tsx @@ -7,6 +7,7 @@ import { useSupabase } from '@/contexts/SupabaseProvider'; import { formatDistanceToNow } from 'date-fns'; import { useRouter } from 'next/navigation'; import { getSignedImageUrl } from '@/utils/supabase-storage'; +import { useLanguage } from '@/contexts/LanguageContext'; interface Detection { id: number; @@ -41,6 +42,7 @@ export function RecentDetectionsWidget({ initialSize = { width: 400, height: 500 }, }: RecentDetectionsWidgetProps) { const { supabase, user } = useSupabase(); + const { t } = useLanguage(); const router = useRouter(); const [detections, setDetections] = useState([]); const [loading, setLoading] = useState(true); @@ -84,7 +86,7 @@ export function RecentDetectionsWidget({ } catch (err) { console.error('Error fetching detections:', err); if (isMountedRef.current) { - setError('Failed to load detections'); + setError(t('aiDetections', 'failedToLoad')); } } finally { if (isMountedRef.current) { @@ -204,7 +206,7 @@ export function RecentDetectionsWidget({ return ( - Showing {Math.min(detections.length, maxItems)} recent + {t('aiDetections', 'showingRecent').replace('{count}', String(Math.min(detections.length, maxItems)))} {loading ? ( @@ -248,7 +250,7 @@ export function RecentDetectionsWidget({
-

No detections yet

+

{t('aiDetections', 'noDetectionsYet')}

) : ( @@ -316,7 +318,7 @@ export function RecentDetectionsWidget({ text-botbot-primary dark:text-botbot-primary rounded-lg transition-colors text-sm font-medium flex items-center justify-center gap-2 flex-shrink-0" > - View All Detections + {t('aiDetections', 'viewAllDetections')} @@ -324,4 +326,4 @@ export function RecentDetectionsWidget({
); -} \ No newline at end of file +} diff --git a/frontend/src/components/widgets/RecorderWidget.tsx b/frontend/src/components/widgets/RecorderWidget.tsx index 766a8f8..08050ba 100644 --- a/frontend/src/components/widgets/RecorderWidget.tsx +++ b/frontend/src/components/widgets/RecorderWidget.tsx @@ -170,10 +170,32 @@ export function RecorderWidget({ return selectedStreams.filter(streamId => isRecording(streamId)).length; }, [selectedStreams, isRecording]); + const getStreamName = (streamId: string) => { + const streamNames: Record = { + 'main-camera': t('myUI', 'mainCamera'), + 'thermal-camera': t('myUI', 'thermalCamera'), + 'rgb-camera': t('myUI', 'rgbCamera'), + }; + + return streamNames[streamId] || streamId; + }; + + const formatActiveStreams = (count: number) => ( + count === 1 + ? t('myUI', 'streamActive').replace('{count}', String(count)) + : t('myUI', 'streamsActive').replace('{count}', String(count)) + ); + + const formatSelectedStreams = (count: number) => ( + count === 1 + ? t('myUI', 'streamSelected').replace('{count}', String(count)) + : t('myUI', 'streamsSelected').replace('{count}', String(count)) + ); + return (

- Select Streams to Record + {t('myUI', 'selectStreamsToRecord')}

{AVAILABLE_STREAMS.map(stream => ( @@ -206,7 +228,7 @@ export function RecorderWidget({ focus:ring-2 focus:ring-primary dark:focus:ring-botbot-accent" /> - {stream.name} + {getStreamName(stream.id)} {isRecording(stream.id) && ( @@ -218,14 +240,14 @@ export function RecorderWidget({

- File Format + {t('myUI', 'fileFormat')}

- Format: {videoFormat.toUpperCase()} + {t('myUI', 'format')}: {videoFormat.toUpperCase()}

- Change in Settings to update + {t('myUI', 'changeInSettingsToUpdate')}

@@ -238,7 +260,7 @@ export function RecorderWidget({ {!connection.online ? (
-

Robot Offline

+

{t('myUI', 'robotOffline')}

) : isAnyRecording ? (
@@ -250,17 +272,17 @@ export function RecorderWidget({
-

Recording

+

{t('myUI', 'recording')}

- {activeRecordingCount} stream{activeRecordingCount !== 1 ? 's' : ''} active + {formatActiveStreams(activeRecordingCount)}

) : (
-

Ready to Record

+

{t('myUI', 'readyToRecord')}

- {selectedStreams.length} stream{selectedStreams.length !== 1 ? 's' : ''} selected + {formatSelectedStreams(selectedStreams.length)}

)} @@ -275,12 +297,12 @@ export function RecorderWidget({ {/* Stream List */}

- Selected Streams: + {t('myUI', 'selectedStreams')}

{selectedStreams.length === 0 ? (

- No streams selected + {t('myUI', 'noStreamsSelected')}

) : ( selectedStreams.map(streamId => { @@ -292,7 +314,7 @@ export function RecorderWidget({ bg-gray-50 dark:bg-botbot-darker px-2 py-1 rounded" > - {stream.name} + {getStreamName(stream.id)} {isRecording(streamId) && ( @@ -325,17 +347,17 @@ export function RecorderWidget({ {isProcessing ? ( <>
- Processing... + {t('myUI', 'processing')} ) : isAnyRecording ? ( <> - Stop & Save + {t('myUI', 'stopAndSave')} ) : ( <> - Start Recording + {t('myUI', 'startRecording')} )} @@ -345,4 +367,4 @@ export function RecorderWidget({
); -} \ No newline at end of file +} diff --git a/frontend/src/components/widgets/SoundClipsWidget.tsx b/frontend/src/components/widgets/SoundClipsWidget.tsx index db8a024..bce8638 100644 --- a/frontend/src/components/widgets/SoundClipsWidget.tsx +++ b/frontend/src/components/widgets/SoundClipsWidget.tsx @@ -6,6 +6,7 @@ import { createClient } from '@/utils/supabase/client'; import { useSupabase } from '@/contexts/SupabaseProvider'; import { useRosPlayAudio } from '@/hooks/ros/useRosPlayAudio'; import { useRobotConnection } from '@/contexts/RobotConnectionContext'; +import { useLanguage } from '@/contexts/LanguageContext'; import { Music, Plus, @@ -74,12 +75,13 @@ export function SoundClipsWidget({ onEndDrag, initialPosition, initialSize = { width: 400, height: 450 }, - title = 'Sound Clips', + title, }: SoundClipsWidgetProps) { const { user } = useSupabase(); const supabase = createClient(); const { playAudioOnRobot } = useRosPlayAudio(); const { connectionStatus } = useRobotConnection(); + const { t } = useLanguage(); const [clips, setClips] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); @@ -180,7 +182,7 @@ export function SoundClipsWidget({ // Generate default name with timestamp const now = new Date(); - const defaultName = `Recording ${now.toLocaleTimeString()}`; + const defaultName = `${t('soundClips', 'recordingDefaultName')} ${now.toLocaleTimeString()}`; setRecordingName(defaultName); // Start timer @@ -190,7 +192,7 @@ export function SoundClipsWidget({ } catch (error) { console.error('Error starting recording:', error); - alert('Failed to access microphone. Please check your permissions.'); + alert(t('soundClips', 'failedMicrophoneAccess')); } }; @@ -274,7 +276,7 @@ export function SoundClipsWidget({ } catch (error) { console.error('Error uploading recording:', error); - alert('Failed to upload recording'); + alert(t('soundClips', 'failedUploadRecording')); } finally { setUploading(false); } @@ -303,13 +305,13 @@ export function SoundClipsWidget({ // Check file size (1GB limit) if (file.size > 1024 * 1024 * 1024) { - alert('File size must be less than 1GB'); + alert(t('soundClips', 'fileSizeLimit')); return; } // Check if it's an audio file if (!file.type.startsWith('audio/')) { - alert('Please upload an audio file'); + alert(t('soundClips', 'audioFileRequired')); return; } @@ -386,7 +388,7 @@ export function SoundClipsWidget({ } } catch (error) { console.error('Error uploading file:', error); - alert('Failed to upload sound clip'); + alert(t('soundClips', 'failedUploadSoundClip')); } finally { setUploading(false); } @@ -403,7 +405,7 @@ export function SoundClipsWidget({ }; const handleDelete = async (clip: SoundClip) => { - if (!confirm(`Delete "${clip.name}"?`)) return; + if (!confirm(t('soundClips', 'deleteConfirm').replace('{clipName}', clip.name))) return; try { // Delete from storage @@ -425,7 +427,7 @@ export function SoundClipsWidget({ setClips(clips.filter(c => c.id !== clip.id)); } catch (error) { console.error('Error deleting sound clip:', error); - alert('Failed to delete sound clip'); + alert(t('soundClips', 'failedDeleteSoundClip')); } }; @@ -460,7 +462,7 @@ export function SoundClipsWidget({ setEditingClip(null); } catch (error) { console.error('Error updating sound clip:', error); - alert('Failed to update sound clip'); + alert(t('soundClips', 'failedUpdateSoundClip')); } }; @@ -500,7 +502,7 @@ export function SoundClipsWidget({ } } catch (error) { console.error('Error playing sound clip:', error); - alert('Failed to play sound clip'); + alert(t('soundClips', 'failedToPlay')); } } }; @@ -539,7 +541,7 @@ export function SoundClipsWidget({ title={
- {title} + {title || t('soundClips', 'title')}
} onRemove={onRemove} @@ -554,7 +556,7 @@ export function SoundClipsWidget({ {/* Header */}
- {clips.length} clips + {t('soundClips', 'clipsCount').replace('{count}', String(clips.length))}
{!showUploadForm && !showRecordingForm && (
@@ -563,14 +565,14 @@ export function SoundClipsWidget({ className="flex items-center px-2 py-1 bg-red-500 text-white text-xs rounded-full hover:bg-red-600 transition-all hover:scale-105" > - Record + {t('soundClips', 'record')}
)} @@ -587,14 +589,14 @@ export function SoundClipsWidget({ <> - Recording... {formatRecordingTime(recordingTime)} + {t('soundClips', 'recording')} {formatRecordingTime(recordingTime)} ) : ( <> - Ready ({formatRecordingTime(recordingTime)}) + {t('soundClips', 'ready')} ({formatRecordingTime(recordingTime)}) )} @@ -646,7 +648,7 @@ export function SoundClipsWidget({ value={recordingName} onChange={(e) => setRecordingName(e.target.value)} className="flex-1 bg-white dark:bg-botbot-darker text-gray-800 dark:text-white px-2 py-1 rounded-lg border border-gray-200 dark:border-gray-700 focus:border-botbot-accent focus:outline-none text-xs" - placeholder="Recording name..." + placeholder={t('soundClips', 'recordingNamePlaceholder')} autoFocus /> @@ -723,7 +725,7 @@ export function SoundClipsWidget({ value={uploadName} onChange={(e) => setUploadName(e.target.value)} className="flex-1 bg-white dark:bg-botbot-darker text-gray-800 dark:text-white px-2 py-1 rounded-lg border border-gray-200 dark:border-gray-700 focus:border-botbot-accent focus:outline-none text-xs" - placeholder="Sound clip name..." + placeholder={t('soundClips', 'soundClipNamePlaceholder')} autoFocus /> @@ -756,7 +758,7 @@ export function SoundClipsWidget({
{!user ? (
-

Please log in to manage sound clips

+

{t('soundClips', 'loginRequired')}

) : loading ? (
@@ -765,8 +767,8 @@ export function SoundClipsWidget({ ) : clips.length === 0 ? (
-

No sound clips yet

-

Click "Record" or "Add" to get started

+

{t('soundClips', 'emptyTitle')}

+

{t('soundClips', 'emptyDescriptionShort')}

) : (
@@ -788,7 +790,7 @@ export function SoundClipsWidget({ value={editName} onChange={(e) => setEditName(e.target.value)} className="flex-1 px-2 py-1 bg-white dark:bg-botbot-dark rounded-lg text-xs font-medium outline-none focus:ring-1 focus:ring-purple-500 dark:focus:ring-purple-400" - placeholder="Clip name" + placeholder={t('soundClips', 'clipNamePlaceholder')} autoFocus />
@@ -822,13 +824,13 @@ export function SoundClipsWidget({ onClick={handleSaveEdit} className="flex-1 py-1 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white rounded-lg text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md" > - Save + {t('soundClips', 'save')}
@@ -845,7 +847,9 @@ export function SoundClipsWidget({ className={`relative w-full p-2 bg-gradient-to-br from-purple-50 via-pink-50 to-purple-50 dark:from-botbot-darker/90 dark:via-botbot-darker/70 dark:to-botbot-darker/90 hover:from-purple-100 hover:via-pink-100 hover:to-purple-100 dark:hover:from-botbot-dark/90 dark:hover:via-botbot-dark/70 dark:hover:to-botbot-dark/90 rounded-xl transition-all duration-200 group border border-purple-100 dark:border-botbot-dark/50 hover:border-purple-200 dark:hover:border-botbot-accent/30 shadow-sm hover:shadow-md hover:scale-[1.02] active:scale-[0.98] focus:outline-none focus-visible:ring-1 focus-visible:ring-purple-500 focus-visible:ring-offset-1 ${ connectionStatus !== 'connected' ? 'opacity-60 cursor-not-allowed' : '' }`} - title={connectionStatus !== 'connected' ? 'Connect to robot to play on robot' : `Play "${clip.name}" on robot`} + title={connectionStatus !== 'connected' + ? t('soundClips', 'connectToRobotToPlay') + : t('soundClips', 'playOnRobot').replace('{clipName}', clip.name)} >
@@ -870,7 +874,7 @@ export function SoundClipsWidget({ handlePlayInBrowser(clip); }} className="p-1 bg-white/80 dark:bg-botbot-dark/80 rounded-lg hover:bg-white dark:hover:bg-botbot-dark transition-all focus:outline-none" - title="Play in browser" + title={t('soundClips', 'playInBrowser')} > {playingClip === clip.id ? ( @@ -932,4 +936,4 @@ export function SoundClipsWidget({
); -} \ No newline at end of file +} diff --git a/frontend/src/components/yolo-filter-bar.tsx b/frontend/src/components/yolo-filter-bar.tsx index 8e7bff3..3f72c8f 100644 --- a/frontend/src/components/yolo-filter-bar.tsx +++ b/frontend/src/components/yolo-filter-bar.tsx @@ -4,6 +4,7 @@ import { Clock, Image, Sparkles, ChevronDown, X, Trash2, RefreshCw, Settings } f import { useState, useEffect, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { cn } from '@/utils/cn'; +import { useLanguage } from '@/contexts/LanguageContext'; export type TimeFilter = 'all' | '1h' | '6h' | '24h' | '7d'; export type ImageFilter = 'all' | 'with-images' | 'without-images'; @@ -25,27 +26,6 @@ interface YoloFilterBarProps { onOpenSettings?: () => void; } -const timeOptions: { value: TimeFilter; label: string }[] = [ - { value: 'all', label: 'All time' }, - { value: '1h', label: 'Last hour' }, - { value: '6h', label: 'Last 6 hours' }, - { value: '24h', label: 'Last 24 hours' }, - { value: '7d', label: 'Last 7 days' }, -]; - -const imageOptions: { value: ImageFilter; label: string }[] = [ - { value: 'all', label: 'All' }, - { value: 'with-images', label: 'With images' }, - { value: 'without-images', label: 'Without images' }, -]; - -const confidenceOptions: { value: ConfidenceFilter; label: string; description: string }[] = [ - { value: 'all', label: 'All', description: 'Any confidence' }, - { value: 'high', label: 'High', description: '≥ 90%' }, - { value: 'medium', label: 'Medium', description: '70-89%' }, - { value: 'low', label: 'Low', description: '< 70%' }, -]; - interface DropdownButtonProps { icon: React.ReactNode; label: string; @@ -210,14 +190,34 @@ export default function YoloFilterBar({ onRealtimeToggle, onOpenSettings, }: YoloFilterBarProps) { + const { t } = useLanguage(); const hasActiveFilters = timeFilter !== 'all' || imageFilter !== 'all' || confidenceFilter !== 'all'; + const timeOptions: { value: TimeFilter; label: string }[] = [ + { value: 'all', label: t('aiDetections', 'allTime') }, + { value: '1h', label: t('aiDetections', 'lastHour') }, + { value: '6h', label: t('aiDetections', 'last6Hours') }, + { value: '24h', label: t('aiDetections', 'last24Hours') }, + { value: '7d', label: t('aiDetections', 'last7Days') }, + ]; + const imageOptions: { value: ImageFilter; label: string }[] = [ + { value: 'all', label: t('aiDetections', 'all') }, + { value: 'with-images', label: t('aiDetections', 'withImages') }, + { value: 'without-images', label: t('aiDetections', 'withoutImages') }, + ]; + const confidenceOptions: { value: ConfidenceFilter; label: string; description: string }[] = [ + { value: 'all', label: t('aiDetections', 'all'), description: t('aiDetections', 'anyConfidence') }, + { value: 'high', label: t('aiDetections', 'high'), description: '≥ 90%' }, + { value: 'medium', label: t('aiDetections', 'medium'), description: '70-89%' }, + { value: 'low', label: t('aiDetections', 'low'), description: '< 70%' }, + ]; + const detectionLabel = filteredCount === 1 ? t('aiDetections', 'detection') : t('aiDetections', 'detections'); return (
} - label="Time" + label={t('aiDetections', 'time')} value={timeFilter} options={timeOptions} onChange={setTimeFilter} @@ -226,7 +226,7 @@ export default function YoloFilterBar({ } - label="Images" + label={t('aiDetections', 'images')} value={imageFilter} options={imageOptions} onChange={setImageFilter} @@ -235,7 +235,7 @@ export default function YoloFilterBar({ } - label="Confidence" + label={t('aiDetections', 'confidence')} value={confidenceFilter} options={confidenceOptions} onChange={setConfidenceFilter} @@ -248,7 +248,7 @@ export default function YoloFilterBar({ className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors" > - Clear filters + {t('aiDetections', 'clearFilters')} )} @@ -263,13 +263,13 @@ export default function YoloFilterBar({ ? "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-300 dark:border-green-700" : "bg-white dark:bg-botbot-darker hover:bg-gray-50 dark:hover:bg-botbot-dark text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700" )} - title={realtimeEnabled ? "Disable auto-refresh" : "Enable auto-refresh every 2 seconds"} + title={realtimeEnabled ? t('aiDetections', 'disableAutoRefresh') : t('aiDetections', 'enableAutoRefresh')} > - Realtime + {t('aiDetections', 'realtime')} {realtimeEnabled && 2s}
@@ -284,10 +284,10 @@ export default function YoloFilterBar({ "border border-gray-200 dark:border-gray-700", "bg-white dark:bg-botbot-darker hover:bg-gray-50 dark:hover:bg-botbot-dark text-gray-700 dark:text-gray-300" )} - title="Upload settings" + title={t('aiDetections', 'uploadSettings')} > - Settings + {t('aiDetections', 'settings')}
)} @@ -300,24 +300,26 @@ export default function YoloFilterBar({ {filteredCount} - of + {t('aiDetections', 'of')} )} {totalCount} - detection{totalCount !== 1 ? 's' : ''} + {totalCount === 1 ? t('aiDetections', 'detection') : t('aiDetections', 'detections')}
{onDeleteAll && filteredCount > 0 && ( )}
); -} \ No newline at end of file +} diff --git a/frontend/src/components/yolo-settings-dialog.tsx b/frontend/src/components/yolo-settings-dialog.tsx index 4bf821d..8b3902d 100644 --- a/frontend/src/components/yolo-settings-dialog.tsx +++ b/frontend/src/components/yolo-settings-dialog.tsx @@ -6,6 +6,7 @@ import { X, Search, Loader2, Check, AlertCircle, RefreshCw } from 'lucide-react' import { cn } from '@/utils/cn'; import { useYoloUploaderConfig } from '@/hooks/ros/useYoloUploaderConfig'; import { useRobotConnection } from '@/contexts/RobotConnectionContext'; +import { useLanguage } from '@/contexts/LanguageContext'; interface YoloSettingsDialogProps { isOpen: boolean; @@ -125,6 +126,7 @@ const ALL_CLASSES = Object.values(YOLO_CLASSES).flat(); export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDialogProps) { const { connection } = useRobotConnection(); + const { t } = useLanguage(); const { getParameters, applySettings, isLoading, isFetching, lastError, clearError } = useYoloUploaderConfig(); const [minConfidence, setMinConfidence] = useState(0.5); @@ -176,7 +178,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial } setHasFetched(true); } catch (error) { - setFetchError(error instanceof Error ? error.message : 'Failed to fetch settings'); + setFetchError(error instanceof Error ? error.message : t('aiDetections', 'failedToFetchSettings')); } }, [connection.online, getParameters]); @@ -286,10 +288,10 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial

- Detection Upload Settings + {t('aiDetections', 'settingsTitle')}

- Configure which detections are saved to the database + {t('aiDetections', 'settingsDescription')}

)} @@ -318,7 +320,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial

- Could not load current settings: {fetchError} + {t('aiDetections', 'couldNotLoadSettings').replace('{error}', fetchError)}

)} @@ -336,7 +338,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial

- Robot is not connected. Connect to load and apply settings. + {t('aiDetections', 'robotNotConnectedSettings')}

)} @@ -348,7 +350,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial

- Only detections with confidence above this threshold will be saved + {t('aiDetections', 'minimumConfidenceDescription')}

@@ -382,23 +384,25 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial
- {selectedObjects.size === 0 ? 'All objects' : `${selectedObjects.size} selected`} + {selectedObjects.size === 0 + ? t('aiDetections', 'allObjects') + : t('aiDetections', 'selectedCount').replace('{count}', String(selectedObjects.size))}
@@ -408,7 +412,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-botbot-dark focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400" @@ -434,7 +438,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial ))} {selectedObjects.size > 10 && ( - +{selectedObjects.size - 10} more + {t('aiDetections', 'moreObjects').replace('{count}', String(selectedObjects.size - 10))} )}
@@ -467,15 +471,15 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial ))} {Object.keys(filteredCategories).length === 0 && (
- No objects match your search + {t('aiDetections', 'noObjectsMatch')}
)}

{selectedObjects.size === 0 - ? 'All detected objects will be saved. Select specific objects to filter.' - : 'Only selected objects will be saved to the database'} + ? t('aiDetections', 'allObjectsWillBeSaved') + : t('aiDetections', 'onlySelectedObjectsSaved')}

@@ -496,7 +500,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial {success && (
-

Settings applied successfully!

+

{t('aiDetections', 'settingsApplied')}

)} @@ -506,7 +510,7 @@ export default function YoloSettingsDialog({ isOpen, onClose }: YoloSettingsDial disabled={isLoading} className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-botbot-dark rounded-lg transition-colors disabled:opacity-50" > - Cancel + {t('aiDetections', 'cancel')} diff --git a/frontend/src/contexts/LanguageContext.tsx b/frontend/src/contexts/LanguageContext.tsx index 820ac9b..4a9074c 100644 --- a/frontend/src/contexts/LanguageContext.tsx +++ b/frontend/src/contexts/LanguageContext.tsx @@ -10,27 +10,43 @@ type LanguageContextType = { }; const LanguageContext = createContext(undefined); +const supportedLanguages = ['en', 'pt', 'zh-CN'] as const; type LanguageProviderProps = { children: ReactNode; }; +function isSupportedLanguage(language: string): language is LanguageCode { + return supportedLanguages.includes(language as LanguageCode); +} + +function detectBrowserLanguage(): LanguageCode { + const browserLanguages = [ + ...(navigator.languages || []), + navigator.language, + ].filter(Boolean).map((lang) => lang.toLowerCase()); + + if (browserLanguages.some((lang) => lang === 'zh' || lang.startsWith('zh-'))) { + return 'zh-CN'; + } + + if (browserLanguages.some((lang) => lang.startsWith('pt'))) { + return 'pt'; + } + + return 'en'; +} + export function LanguageProvider({ children }: LanguageProviderProps) { const [language, setLanguageState] = useState('pt'); // Default to Portuguese useEffect(() => { // Load saved language preference from localStorage if available - const savedLanguage = localStorage.getItem('language') as LanguageCode | null; - if (savedLanguage && ['en', 'pt'].includes(savedLanguage)) { + const savedLanguage = localStorage.getItem('language'); + if (savedLanguage && isSupportedLanguage(savedLanguage)) { setLanguageState(savedLanguage); } else { - // Auto-detect from browser language - const browserLang = navigator.language?.toLowerCase() || ''; - if (browserLang.startsWith('pt')) { - setLanguageState('pt'); - } else { - setLanguageState('en'); - } + setLanguageState(detectBrowserLanguage()); } }, []); @@ -59,4 +75,4 @@ export function useLanguage() { throw new Error('useLanguage must be used within a LanguageProvider'); } return context; -} \ No newline at end of file +} diff --git a/frontend/src/utils/translations/en.ts b/frontend/src/utils/translations/en.ts index c25aa36..95a5c86 100644 --- a/frontend/src/utils/translations/en.ts +++ b/frontend/src/utils/translations/en.ts @@ -68,12 +68,15 @@ export const enTranslations = { // Form fields name: 'Name', namePlaceholder: 'Enter your name (optional)', + emailPlaceholder: 'Enter your email', + passwordPlaceholder: 'Enter your password', confirmPassword: 'Confirm Password', confirmPasswordPlaceholder: 'Re-enter your password', // Buttons createAccountButton: 'Create Account', creatingAccount: 'Creating account...', loggingIn: 'Logging in...', + verifyingCredentials: 'Verifying your credentials...', // Password strength passwordStrength: 'Password Strength', passwordVeryWeak: 'Very Weak', @@ -92,6 +95,12 @@ export const enTranslations = { rateLimitExceeded: 'Too many attempts. Please wait a few minutes and try again.', signupError: 'Unable to create account. Please try again.', networkError: 'Connection error. Please check your internet and try again.', + invalidCredentials: 'The email or password you entered is incorrect. Please try again.', + emailNotConfirmed: 'Please check your email to confirm your account before logging in.', + tooManyLoginAttempts: 'Too many login attempts. Please wait a few minutes and try again.', + loginError: 'Unable to log in. Please check your credentials and try again.', + loginSuccessRedirect: 'Success! Redirecting to your dashboard...', + selectLanguage: 'Select language', // Legal privacyPolicy: 'Privacy Policy', termsOfService: 'Terms of Service', @@ -157,6 +166,12 @@ export const enTranslations = { buttonReconnect: 'Reconnect', buttonConnecting: 'Connecting...', buttonConnect: 'Connect', + connectedTo: 'Connected to', + notConnected: 'Not Connected', + noRobotConnected: 'No robot connected', + manageRobotFleet: 'Manage Robot Fleet', + manageRobotFleetDescription: 'Add, edit, or connect to your robots', + manageRobotConnections: 'Click above to manage your robot connections', }, robotHeader: { connected: 'Connected', @@ -261,6 +276,26 @@ export const enTranslations = { deleteLayoutTitle: 'Delete Layout', delete: 'Delete', updateLayoutTitle: 'Update Layout', + recorder: 'Recorder', + selectStreamsToRecord: 'Select Streams to Record', + fileFormat: 'File Format', + format: 'Format', + changeInSettingsToUpdate: 'Change in Settings to update', + robotOffline: 'Robot Offline', + recording: 'Recording', + streamActive: '{count} stream active', + streamsActive: '{count} streams active', + readyToRecord: 'Ready to Record', + streamSelected: '{count} stream selected', + streamsSelected: '{count} streams selected', + selectedStreams: 'Selected Streams:', + noStreamsSelected: 'No streams selected', + processing: 'Processing...', + stopAndSave: 'Stop & Save', + startRecording: 'Start Recording', + mainCamera: 'Main Camera', + thermalCamera: 'Thermal Camera', + rgbCamera: 'RGB Camera', // Widget labels widgetCamera: 'Camera', widgetGauge: 'Gauge', @@ -273,6 +308,46 @@ export const enTranslations = { widgetJoystick: 'Joystick', widgetAudioStream: 'Audio Stream', widgetMapView: 'Map View', + addWidgetToDashboard: 'Add Widget to Dashboard', + searchWidgets: 'Search widgets...', + noWidgetsFound: 'No widgets found matching your search.', + widgetAvailable: '{count} widget available', + widgetsAvailable: '{count} widgets available', + categoryAllWidgets: 'All Widgets', + categoryVisualization: 'Visualization', + categoryControl: 'Control', + categoryMedia: 'Media', + categoryInformation: 'Information', + categoryAiSmart: 'AI & Smart', + widgetCameraDescription: 'Live video feed from robot cameras', + widget3DVisualizationDescription: '3D robot model and environment', + widgetMapViewDescription: 'Live map and navigation view', + widgetJoystickDescription: 'Virtual joystick for robot control', + widgetButtonDescription: 'Custom action button', + widgetButtonGroupDescription: 'Multiple action buttons', + widgetDelivery: 'Delivery', + widgetDeliveryDescription: 'Delivery management controls', + widgetMissions: 'Missions', + widgetMissionsDescription: 'Manage and execute robot missions', + widgetAudioStreamDescription: 'Audio streaming controls', + widgetMicrophone: 'Microphone', + widgetMicrophoneDescription: 'Voice input and recording', + widgetTTSPresets: 'TTS Presets', + widgetTTSPresetsDescription: 'Text-to-speech presets', + widgetSoundClips: 'Sound Clips', + widgetSoundClipsDescription: 'Play pre-recorded sounds', + widgetRecorder: 'Recorder', + widgetRecorderDescription: 'Audio/video recording', + widgetGaugeDescription: 'Display numeric values', + widgetSidewaysGaugeDescription: 'Horizontal gauge display', + widgetInformationDescription: 'Display robot information', + widgetMapsManagement: 'Maps Management', + widgetMapsManagementDescription: 'Manage saved maps', + widgetChatDescription: 'Chat interface', + widgetAIStream: 'AI Stream', + widgetAIStreamDescription: 'AI processing stream', + widgetRecentDetections: 'Recent Detections', + widgetRecentDetectionsDescription: 'Object detection history', // Layout names layoutDefault: 'Default', layoutCommandInput: 'Command Input', @@ -313,9 +388,55 @@ export const enTranslations = { notMapped: 'Not Mapped', resetToDefault: 'Reset to Default', clearAll: 'Clear All', + joystickVisualizationOnly: 'Joystick Visualization Only', + joystickVisualizationOnlyDescription: 'Display joystick input in visualizer without sending commands to robot', }, profile: { title: 'Profile', + pageDescription: 'Manage your account settings and preferences', + saveChanges: 'Save Changes', + personalInformation: 'Personal Information', + accountInformation: 'Account Information', + security: 'Security', + appearance: 'Appearance', + brandingOptions: 'Branding Options', + hideBotBotLogo: 'Hide BotBot Logo', + hideBotBotLogoDescription: 'Remove the BotBot logo from the navigation bar', + systemSettings: 'System Settings', + privacyAndLogging: 'Privacy & Logging', + auditLogging: 'Audit Logging', + auditLoggingDescription: 'Log all user actions for security and analytics purposes', + robotConnection: 'Robot Connection', + connectionTimeout: 'Connection Timeout', + seconds: 'seconds', + connectionTimeoutDescription: 'Time to wait for robot connection before timing out (1-120 seconds, default: 20)', + speedAndControl: 'Speed & Control', + speedMode: 'Speed Mode', + speedLevel: 'Speed Level', + speedModeBeginner: 'Beginner', + speedModeNormal: 'Normal', + speedModeInsane: 'Insane', + speedModeBeginnerDescription: '20% speed - Safe for learning', + speedModeNormalDescription: '80% speed - Balanced performance', + speedModeInsaneDescription: '100% speed - Maximum velocity', + enableInsaneModeTitle: 'Enable Insane Mode?', + enableInsaneModeDescriptionPrefix: 'This will set robot speed to', + enableInsaneModeDescriptionSuffix: 'of maximum velocity.', + insaneModeRecommendation: 'Only recommended for experienced operators in controlled environments.', + enableInsane: 'Enable Insane', + avatarAlt: 'Avatar', + deleteAvatar: 'Delete', + changeAvatar: 'Change Avatar', + uploadAvatar: 'Upload Avatar', + uploadingAvatar: 'Uploading...', + avatarUploadHelp: 'Click on the avatar or button to upload. Max 5MB. JPEG, PNG, WebP, or GIF.', + databaseUnavailable: 'Database connection not available', + avatarLoginRequired: 'You must be logged in to upload an avatar', + invalidAvatarFile: 'Please select a valid image file (JPEG, PNG, WebP, or GIF)', + avatarFileSizeLimit: 'File size must be less than 5MB', + avatarUploadError: 'Error uploading avatar. Please try again.', + deleteAvatarConfirm: 'Are you sure you want to delete your avatar?', + deleteAvatarError: 'Error deleting avatar. Please try again.', name: 'Name', namePlaceholder: 'Enter your name', nameDescription: 'Your name or Company Name', @@ -348,6 +469,540 @@ export const enTranslations = { saved: 'Saved', cancel: 'Cancel', }, + dashboard: { + cockpit: 'Cockpit', + cockpitDescription: 'Control and monitor your robot', + fleetManager: 'Fleet Manager', + fleetManagerDescription: 'Manage all your robots', + myUI: 'My UI', + myUIDescription: 'Customize your workspace', + goodMorning: 'Good Morning', + goodAfternoon: 'Good Afternoon', + goodEvening: 'Good Evening', + welcomeBackToBotBrain: 'Welcome back to BotBrain', + user: 'User', + userAvatar: 'User Avatar', + active: 'Active', + now: 'Now', + justNow: 'Just now', + minutesAgo: '{count}m ago', + hoursAgo: '{count}h ago', + daysAgo: '{count}d ago', + actionsThisWeek: '{count} actions this week', + dayStreak: '{count} day streak', + daysStreak: '{count} days streak', + userFor: 'User for {duration}', + currentTime: 'Current Time', + todaysDate: "Today's Date", + today: 'Today', + year: 'year', + years: 'years', + month: 'month', + months: 'months', + week: 'week', + weeks: 'weeks', + day: 'day', + days: 'days', + fleetSize: 'Fleet Size', + onlineNow: 'online now', + totalActions: 'Total Actions', + activeDays: 'active days', + thisMonth: 'this month', + userSince: 'User Since', + new: 'New', + newMember: 'New member', + favoriteRobot: 'Favorite Robot', + mostUsed: 'Most used', + noFavoriteSet: 'No favorite set', + offline: 'Offline', + actions: 'actions', + veryActive: 'Very active', + quiet: 'Quiet', + thisWeek: 'This Week', + avgPerDay: 'Avg {count}/day', + peakHour: 'Peak Hour', + mostActive: 'most active', + missions: 'Missions', + created: 'created', + aiDetections: 'AI Detections', + confidenceAbbrev: 'conf', + yoloActive: 'YOLO active', + soundLibrary: 'Sound Library', + audioClips: 'audio clips', + storage: 'Storage', + used: 'used', + activityPattern: 'Activity Pattern', + last30Days: 'Last 30 days', + total: 'total', + dayAverage: '{count}/day avg', + less: 'Less', + more: 'More', + activityCellTitle: '{day} {hour}:00 - {count} actions', + sundayShort: 'S', + mondayShort: 'M', + tuesdayShort: 'T', + wednesdayShort: 'W', + thursdayShort: 'T', + fridayShort: 'F', + saturdayShort: 'S', + noRobotsConfiguredYet: 'No robots configured yet', + robotFleetUsage: 'Robot Fleet Usage', + robotCount: '{count} robots', + robotCountSingular: '{count} robot', + peak: 'Peak', + showLess: 'Show less', + showMore: 'Show {count} more', + never: 'Never', + yourRobots: 'Your Robots', + robot: 'Robot', + live: 'Live', + connected: 'Connected', + connecting: 'Connecting...', + connect: 'Connect', + noRobotsYet: 'No Robots Yet', + getStartedAddFirstRobot: 'Get started by adding your first robot', + addYourFirstRobot: 'Add Your First Robot', + yourRobotFleet: 'Your Robot Fleet', + yourRobotFleetDescription: 'Manage and monitor your connected robots', + manageFleet: 'Manage Fleet', + addRobot: 'Add Robot', + connectNewDevice: 'Connect new device', + robotFleet: 'Robot Fleet', + robotFleetDescription: 'Manage and connect to your entire robot fleet with real-time status monitoring', + quickAccess: 'Quick Access', + quickAccessDescription: 'Your most used features and tools', + recentActivity: 'Recent Activity', + yourLatestActions: 'Your latest actions', + viewAll: 'View all', + noRecentActivity: 'No recent activity', + weeklyProgress: 'Weekly Progress', + activityOverTheWeek: 'Activity over the week', + actionsPerformed: 'Actions performed', + actionPerformed: 'Action performed', + totalCount: '{count} total', + achievements: 'Achievements', + yourMilestones: 'Your milestones', + unlocked: 'Unlocked', + progress: 'Progress', + mondayAbbrev: 'Mon', + tuesdayAbbrev: 'Tue', + wednesdayAbbrev: 'Wed', + thursdayAbbrev: 'Thu', + fridayAbbrev: 'Fri', + saturdayAbbrev: 'Sat', + sundayAbbrev: 'Sun', + achievementFirstConnection: 'First Connection', + achievementFirstConnectionDescription: 'Connect your first robot', + achievementFleetCommander: 'Fleet Commander', + achievementFleetCommanderDescription: 'Manage 5+ robots', + achievementPowerUser: 'Power User', + achievementPowerUserDescription: '1000+ actions performed', + achievementGlobalOperator: 'Global Operator', + achievementGlobalOperatorDescription: 'Control robots remotely', + }, + maps: { + pageTitle: 'Maps Management', + pageDescription: 'View and manage robot navigation maps', + liveCamera: 'Live Camera', + robotControls: 'Robot Controls', + showJoysticks: 'Show Joysticks', + hideJoysticks: 'Hide Joysticks', + hidePanel: 'Hide panel', + showControlPanel: 'Show control panel', + mapViewer: 'Map Viewer', + currentMap: 'Current Map', + mapView: 'Map View', + mappingControl: 'Mapping Control', + mapNamePlaceholder: 'Enter map name...', + mappingLabel: 'Mapping: {mapName}', + stopMapping: 'Stop Mapping', + startMapping: 'Start Mapping', + startNewMap: 'Start New Map', + cancel: 'Cancel', + availableMaps: 'Available Maps', + refreshMaps: 'Refresh maps list', + connectToRobotToViewMaps: 'Connect to robot to view maps', + noMapsAvailable: 'No maps available', + deleting: 'Deleting...', + loading: 'Loading...', + active: 'Active', + loadMap: 'Load Map', + load: 'Load', + delete: 'Delete', + deleteMap: 'Delete map', + deleteMapTitle: 'Delete "{mapName}"?', + deleteMapWarning: 'This action cannot be undone.', + mapLoadedTitle: 'Map Loaded', + mapLoadedMessage: 'Successfully loaded and activated map: {mapName}', + mapLoadedShortMessage: 'Successfully loaded: {mapName}', + failedToLoadMapTitle: 'Failed to Load Map', + failedToLoadMapMessage: 'Could not load map: {mapName}', + failedToLoadMapShortMessage: 'Could not load: {mapName}', + failedToDeleteMapTitle: 'Failed to Delete Map', + failedToDeleteMapMessage: 'Could not delete map: {mapName}', + setHomePositionTitle: 'Set Home Position', + setHomePositionMessage: 'Click on the map to set home position, then drag to set orientation', + cancelSetHome: 'Cancel set home', + setHomePositionTooltip: 'Set home position', + stopNavigation: 'Stop Navigation', + cancelNavigationMode: 'Cancel navigation mode', + navigateToLocation: 'Navigate to location', + clearNavigationGoal: 'Clear navigation goal', + zoomIn: 'Zoom In', + zoomOut: 'Zoom Out', + resetView: 'Reset View', + centerOnRobot: 'Center on Robot', + stoppingNavigation: 'Stopping navigation...', + stopNavigationClick: 'Stop navigation - Click to stop the robot', + cancelGoalSetting: 'Cancel goal setting', + setNavigationGoal: 'Set navigation goal', + hideNavigationPlan: 'Hide navigation plan', + showNavigationPlan: 'Show navigation plan', + refreshMapData: 'Refresh map data', + hideLocalCostmap: 'Hide local costmap', + showLocalCostmap: 'Show local costmap', + failedToSetHomeTitle: 'Failed to Set Home', + failedToSetHomeMessage: 'Could not set home position. Please check ROS connection.', + loadingMap: 'Loading map...', + failedToLoadMapGeneric: 'Failed to load map', + useFallbackRenderer: 'Use fallback renderer', + noMapTopicsAvailable: 'No map topics available. Please ensure map_server or navigation is running.', + failedToLoadMapLibraries: 'Failed to load map libraries', + failedToCreateMapClient: 'Failed to create map client', + failedToInitializeMapViewer: 'Failed to initialize map viewer', + navigationStoppedTitle: 'Navigation Stopped', + navigationStoppedMessage: 'Robot navigation has been cancelled', + stopFailedTitle: 'Stop Failed', + stopFailedMessage: 'Failed to stop navigation. Please try again.', + stopErrorMessage: 'An error occurred while stopping navigation.', + navigationStartedTitle: 'Navigation Started', + navigationStartedMessage: 'Navigating to destination', + navigationFailedTitle: 'Navigation Failed', + navigationFailedMessage: 'Failed to start navigation. Please check Nav2 is running.', + failedParsePgm: "Failed to parse PGM file. Please ensure it's a valid P5 format.", + failedLoadPng: "Failed to load PNG file. Please ensure it's a valid PNG image.", + selectPgmOrPng: 'Please select a PGM or PNG file.', + failedParseYaml: 'Failed to parse YAML file.', + uploadMapPgmPng: 'Upload Map (PGM/PNG)', + uploadYaml: 'Upload YAML', + saveMap: 'Save Map', + unknown: 'Unknown', + showBaseMap: 'Show base map', + showCostmap: 'Show costmap', + costmap: 'Costmap', + baseMap: 'Base Map', + robotPosition: 'Robot Position', + walls: 'Walls', + obstacles: 'Obstacles', + navigating: 'Navigating...', + navGoal: 'Nav Goal', + clickOnMapToSetNavigationGoal: 'Click on map to set navigation goal', + missionProgress: 'Mission: {current}/{total}', + goalSet: 'Goal set', + navigatingToDestination: 'Navigating to destination...', + robot: 'Robot', + goal: 'Goal', + mission: 'Mission', + plan: 'Plan', + setNavigationGoalInstructions: 'Click to set navigation goal - Drag to set orientation - ESC to cancel', + setHomePositionInstructions: 'Click to set home position - Drag to set orientation - ESC to cancel', + settingHomePosition: 'Setting Home Position', + position: 'Position', + mapConnected: 'Map Connected', + }, + missions: { + title: 'Missions', + createNewMission: 'Create new mission', + missionNamePlaceholder: 'Mission name...', + create: 'Create', + cancel: 'Cancel', + noMissionsYet: 'No missions yet', + createFirstMission: 'Create First Mission', + active: 'Active', + paused: 'Paused', + running: 'Running', + created: 'Created', + unknown: 'Unknown', + waypoint: 'waypoint', + waypoints: 'waypoints', + waypointShort: 'wp', + waypointsShort: 'wps', + edit: 'Edit', + delete: 'Delete', + deleteMissionConfirm: 'Delete mission "{missionName}"?', + missionEditor: 'Mission Editor', + missionTitle: 'Mission: {missionName}', + missionRunning: 'Mission Running', + missionPaused: 'Mission Paused', + pending: 'Pending', + reached: 'Reached', + navPlan: 'Nav Plan', + toggleNavPlanOverlay: 'Toggle navigation plan overlay', + savingWaypoints: 'Saving waypoints...', + stop: 'Stop', + start: 'Start', + resume: 'Resume', + switchToCorrectMapFirst: 'Switch to the correct map first', + resumeMission: 'Resume mission', + startMissionTooltip: 'Start mission', + addWaypointsFirst: 'Add waypoints first', + noMissionSelected: 'No Mission Selected', + noMissionSelectedDescription: 'Select a mission from the left panel or create a new one to start planning waypoints', + openMissionList: 'Open Mission List', + cameraView: 'Camera View', + robotControls: 'Robot Controls', + mapSwitchedTitle: 'Map Switched', + mapSwitchedMessage: 'Loaded map: {mapName}', + failedToSwitchMapTitle: 'Failed to switch map', + failedToSwitchMapMessage: 'Could not load the mission map', + failedToLoadMissionsTitle: 'Failed to load missions', + refreshPageMessage: 'Please try refreshing the page', + missionCreatedTitle: 'Mission created', + missionCreatedMessage: 'Mission "{missionName}" has been created', + failedToCreateMissionTitle: 'Failed to create mission', + tryAgainMessage: 'Please try again', + missionUpdatedTitle: 'Mission updated', + missionUpdatedMessage: 'Mission has been renamed to "{missionName}"', + failedToUpdateMissionTitle: 'Failed to update mission', + missionDeletedTitle: 'Mission deleted', + missionDeletedMessage: 'Mission has been successfully deleted', + failedToDeleteMissionTitle: 'Failed to delete mission', + savingWaypointsTitle: 'Saving waypoints', + savingWaypointsMessage: 'Please wait while waypoints are saved...', + noWaypointsFoundTitle: 'No waypoints found', + noWaypointsFoundMessage: 'Please add waypoints and wait for them to save', + missionStartedTitle: 'Mission started', + missionStartedMessage: 'Mission "{missionName}" is now active with {count} waypoints', + failedToStartMissionTitle: 'Failed to start mission', + missionStoppedTitle: 'Mission stopped', + missionStoppedMessage: 'Mission has been stopped', + failedToStopMissionTitle: 'Failed to stop mission', + unknownError: 'Unknown error occurred', + failedToSaveWaypointsTitle: 'Failed to save waypoints', + changesMayNotHaveBeenSaved: 'Your changes may not have been saved', + navigationCompleteTitle: 'Navigation Complete', + navigationCompleteMessage: 'Mission has finished', + navigationStartedTitle: 'Navigation Started', + navigationFailedTitle: 'Navigation Failed', + navigationErrorTitle: 'Navigation Error', + noWaypointsDefinedMessage: 'No waypoints defined. Please add waypoints first.', + nav2ActionUnavailableMessage: 'Nav2 FollowWaypoints action server not available. Ensure Nav2 is running on the robot.', + navigatingThroughWaypoints: 'Navigating through {count} waypoints', + navigationCancelledTitle: 'Navigation Cancelled', + missionNavigationStopped: 'Mission navigation has been stopped', + cancelFailedTitle: 'Cancel Failed', + cancelFailedMessage: 'Failed to cancel navigation. Please check robot connection.', + cancelErrorTitle: 'Cancel Error', + cancelErrorMessage: 'An error occurred while cancelling navigation', + successfullyReachedAllWaypoints: 'Successfully reached all waypoints!', + toggleEditMode: 'Toggle edit mode (E)', + exitEdit: 'Exit Edit', + clearAllWaypoints: 'Clear all waypoints', + clear: 'Clear', + waitingForMapDataFrom: 'Waiting for map data from {topic}', + mayTake15Seconds: 'This may take up to 15 seconds', + widgetName: 'Widget Name', + widgetNamePlaceholder: 'Enter widget name', + refreshMissions: 'Refresh missions', + loading: 'Loading...', + connectToRobotToViewMissions: 'Connect to robot to view missions', + loadMapToSeeMissions: 'Load a map to see missions', + noMissionsForCurrentMap: 'No missions for current map', + stopMission: 'Stop Mission', + startMission: 'Start Mission', + mapMismatchTitle: 'Map Mismatch', + mapMismatchCurrent: 'Mission created on {missionMapName}, currently {currentMapName} is loaded', + mapMismatchNoCurrentMap: 'Mission created on {missionMapName}, but no map is currently loaded', + switchMap: 'Switch Map', + retryLoadingMap: 'Retry Loading Map', + mapServerRunningHelp: 'Make sure the map server is running on the robot', + robotPosition: 'Robot Position', + navigatingProgress: 'Navigating ({current}/{total})', + nav2Disconnected: 'Nav2 disconnected', + navigationPlan: 'Navigation Plan', + costmap: 'Costmap', + }, + auditLog: { + pageTitle: 'Audit Log', + pageDescription: 'Track all activities and events in your application', + advancedPageTitle: 'Advanced Audit System', + advancedPageDescription: 'Comprehensive tracking and analytics for all operator activities', + loading: 'Loading audit logs...', + loadingData: 'Loading audit data...', + searchPlaceholder: 'Search logs...', + totalEvents: 'Total Events', + eventsToday: 'Events Today', + activeRobots: 'Active Robots', + criticalEvents: 'Critical Events', + lastRange: 'Last {range}', + allTime: 'all time', + daysRange: '{count} days', + sinceMidnight: 'Since midnight', + mostActiveRobot: 'Most active: {robotName}', + noRobotsActive: 'No robots active', + safetyAndFailures: 'Safety & failures', + analytics: 'Analytics', + timeline: 'Timeline', + last24Hours: 'Last 24 hours', + last7Days: 'Last 7 days', + last30Days: 'Last 30 days', + last90Days: 'Last 90 days', + clear: 'Clear', + eventDistributionByType: 'Event Distribution by Type', + activityPattern24h: '24-Hour Activity Pattern', + hourlyActivityTitle: '{hour}:00 - {count} events', + mostFrequentActions: 'Most Frequent Actions', + time: 'Time', + allEvents: 'All Events', + authentication: 'Authentication', + robot: 'Robot', + commands: 'Commands', + system: 'System', + data: 'Data', + mission: 'Mission', + navigation: 'Navigation', + audio: 'Audio', + camera: 'Camera', + safety: 'Safety', + export: 'Export', + refresh: 'Refresh', + exportButton: 'Export', + deleteAll: 'Delete All', + clearAll: 'Clear All', + testLog: 'Test Log', + createTestLogTitle: 'Create a test audit log entry', + dateAndTime: 'Date & Time', + type: 'Type', + action: 'Action', + details: 'Details', + noAuditLogsFound: 'No audit logs found', + viewDetails: 'View details', + eventsLabel: '{type} Events', + deleteAllTitle: 'Delete All Audit Logs', + deleteAllConfirm: 'Are you sure you want to delete all {count} audit logs? This action cannot be undone.', + clearAllTitle: 'Clear All Audit Logs', + clearAllConfirm: 'Are you sure you want to clear all {count} audit logs? This action cannot be undone.', + deleting: 'Deleting...', + dateCsv: 'Date', + timeCsv: 'Time', + eventTypeCsv: 'Event Type', + actionCsv: 'Action', + robotCsv: 'Robot', + robotIdCsv: 'Robot ID', + ipAddressCsv: 'IP Address', + userAgentCsv: 'User Agent', + detailsCsv: 'Details', + testAuditMessage: 'Test audit log entry', + }, + aiDetections: { + recentTitle: 'Recent AI Detections', + failedToLoad: 'Failed to load detections', + showingRecent: 'Showing {count} recent', + autoRefreshEnabled: 'Auto-refresh enabled', + autoRefreshDisabled: 'Auto-refresh disabled', + live: 'Live', + paused: 'Paused', + noDetectionsYet: 'No detections yet', + viewAllDetections: 'View All Detections', + settingsTitle: 'Detection Upload Settings', + settingsDescription: 'Configure which detections are saved to the database', + loadingCurrentSettings: 'Loading current settings...', + couldNotLoadSettings: 'Could not load current settings: {error}', + retry: 'Retry', + robotNotConnectedSettings: 'Robot is not connected. Connect to load and apply settings.', + minimumConfidence: 'Minimum Confidence', + minimumConfidenceDescription: 'Only detections with confidence above this threshold will be saved', + objectFilter: 'Object Filter', + allObjects: 'All objects', + selectedCount: '{count} selected', + selectAll: 'Select All', + clear: 'Clear', + searchObjects: 'Search objects...', + moreObjects: '+{count} more', + noObjectsMatch: 'No objects match your search', + allObjectsWillBeSaved: 'All detected objects will be saved. Select specific objects to filter.', + onlySelectedObjectsSaved: 'Only selected objects will be saved to the database', + settingsApplied: 'Settings applied successfully!', + cancel: 'Cancel', + applying: 'Applying...', + applySettings: 'Apply Settings', + failedToFetchSettings: 'Failed to fetch settings', + uploadSettings: 'Upload settings', + settings: 'Settings', + time: 'Time', + images: 'Images', + confidence: 'Confidence', + allTime: 'All time', + lastHour: 'Last hour', + last6Hours: 'Last 6 hours', + last24Hours: 'Last 24 hours', + last7Days: 'Last 7 days', + all: 'All', + withImages: 'With images', + withoutImages: 'Without images', + anyConfidence: 'Any confidence', + high: 'High', + medium: 'Medium', + low: 'Low', + clearFilters: 'Clear filters', + disableAutoRefresh: 'Disable auto-refresh', + enableAutoRefresh: 'Enable auto-refresh every 2 seconds', + realtime: 'Realtime', + of: 'of', + detection: 'detection', + detections: 'detections', + deleteDetectionsTitle: 'Delete {count} {detectionLabel}', + delete: 'Delete', + moonDreamReady: 'Ready to interact with MoonDream. Select a mode and submit to begin.', + moonDreamCaption: 'Caption', + moonDreamCaptionDescription: 'Generate a description of the image', + moonDreamQueryNoReasoning: 'Query - No Reasoning', + moonDreamQueryNoReasoningDescription: 'Ask a question about the image', + moonDreamQueryWithReasoning: 'Query - With Reasoning', + moonDreamQueryWithReasoningDescription: 'Ask a question with detailed reasoning', + moonDreamDetect: 'Detect', + moonDreamDetectDescription: 'Detect specific objects in the image', + moonDreamPoint: 'Point', + moonDreamPointDescription: 'Point to specific locations in the image', + moonDreamQuestionPlaceholder: 'Ask a question about the image...', + moonDreamReasoningPlaceholder: 'Ask a question for detailed analysis...', + moonDreamDetectPlaceholder: 'Describe what to detect...', + moonDreamPointPlaceholder: 'Describe what to point to...', + moonDreamQuestionLabel: 'Question', + moonDreamDescriptionLabel: 'Description', + moonDreamAnswerLabel: 'Answer', + moonDreamFailedCaption: 'Failed to generate caption', + moonDreamEnterQuestion: 'Please enter a question', + moonDreamFailedQuestion: 'Failed to answer question', + moonDreamFailedReasoning: 'Failed to answer question with reasoning', + moonDreamEnterDescription: 'Please enter a description', + moonDreamDetectResult: 'Found {count} {objectLabel} matching "{input}". {overlayMessage}', + moonDreamObjectSingular: 'object', + moonDreamObjectPlural: 'objects', + moonDreamBoundingBoxesShown: 'Purple bounding boxes shown on the live stream.', + moonDreamFailedDetect: 'Failed to detect objects', + moonDreamPointResult: 'Identified {count} {pointLabel} for "{input}". {overlayMessage}', + moonDreamPointSingular: 'point', + moonDreamPointPlural: 'points', + moonDreamRedMarkersShown: 'Red markers shown on the live stream.', + moonDreamFailedPoint: 'Failed to identify points', + moonDreamUnexpectedError: 'An unexpected error occurred. Please try again.', + moonDreamOverlaysCleared: 'Overlays cleared from the live stream.', + moonDreamClearOverlaysTitle: 'Clear all bounding boxes and points from the live stream', + moonDreamClear: 'Clear', + moonDreamAllOverlaysCleared: 'All overlays have been cleared from the live stream.', + moonDreamClearAllOverlaysTitle: 'Clear all overlays', + moonDreamClearAll: 'Clear All', + moonDreamResponse: 'MoonDream Response', + moonDreamProcessing: 'Processing...', + moonDreamSend: 'Send', + moonDreamGenerating: 'Generating...', + moonDreamGenerateCaption: 'Generate Caption', + moonDreamBoxSingular: 'box', + moonDreamBoxPlural: 'boxes', + moonDreamMode: 'Mode', + moonDreamInput: 'Input', + }, help: { pageTitle: 'Help Center', pageSubtitle: 'Get support, access documentation, and learn safety guidelines', @@ -374,6 +1029,7 @@ export const enTranslations = { pageTitle: 'Health Monitor', connected: 'Connected', connecting: 'Connecting...', + connect: 'Connect', disconnected: 'Disconnected', loadingTitle: 'Loading Health Data', loadingDescription: 'Waiting for diagnostics data...', @@ -443,8 +1099,96 @@ export const enTranslations = { node: 'Node', state: 'State', actions: 'Actions', + start: 'Start', + stop: 'Stop', + reset: 'Reset', + close: 'Close', + viewModuleId: 'View module ID', + moduleDetails: 'Module Details', + moreValues: '+{count} more values', + powerRails: 'Power Rails', // Diagnostics robotDiagnostics: 'Diagnostics', + waitingForData: 'Waiting for data...', + rebooting: 'Rebooting...', + rebootSystem: 'Reboot System', + wifi: 'WiFi', + cellular: '4G/Cellular', + hotspot: 'Hotspot', + offline: 'Offline', + noNetwork: 'No network', + measuring: 'Measuring...', + excellent: 'Excellent', + good: 'Good', + fair: 'Fair', + poor: 'Poor', + incomingData: 'Incoming data', + outgoingData: 'Outgoing data', + individualCores: 'Individual Cores', + waitingForCpuData: 'Waiting for CPU data...', + error: 'Error', + errors: 'Errors', + warning: 'Warning', + warnings: 'Warnings', + allSystemsOk: 'All Systems OK', + activeIssues: 'Active Issues', + systemsOperatingNormally: '{count} systems operating normally', + connectToRobotToViewDiagnostics: 'Connect to robot to view diagnostics', + confirmSystemReboot: 'Confirm System Reboot', + confirmSystemRebootDescription: 'Are you sure you want to reboot the system? This will temporarily disconnect all services and the robot will be unavailable for a few minutes.', + rebootNow: 'Reboot Now', + wifiSettings: 'WiFi Settings', + scanForNetworks: 'Scan for networks', + modeLabel: 'Mode: {mode}', + ipAddress: 'IP: {ip}', + activeStatus: 'Active', + wifiStateLabel: 'WiFi {state}', + enabled: 'Enabled', + disabled: 'Disabled', + connectToRobotToToggleWifi: 'Connect to robot to toggle WiFi', + clickToDisableWifi: 'Click to disable WiFi', + clickToEnableWifi: 'Click to enable WiFi', + wifiDisabledHelp: 'WiFi is disabled. Enable WiFi to scan for networks and manage connections.', + availableNetworks: 'Available Networks', + scanError: 'Scan error', + noNetworksFound: 'No networks found', + savedNetworks: 'Saved Networks ({count})', + refreshSavedNetworks: 'Refresh saved networks', + connectedTag: '(Connected)', + connectToNetwork: 'Connect to {networkName}', + forgetNetwork: 'Forget {networkName}', + noSavedNetworksFound: 'No saved networks found', + connectToSsid: 'Connect to {ssid}', + password: 'Password', + username: 'Username', + wifiPasswordPlaceholder: 'Enter WiFi password', + usernamePlaceholder: 'Enter username', + passwordPlaceholder: 'Enter password', + saveNetworkForAutomaticConnection: 'Save network for automatic connection', + wifiRadioTitle: 'WiFi Radio', + wifiEnabledSuccess: 'WiFi enabled successfully', + wifiDisabledSuccess: 'WiFi disabled successfully', + wifiToggleFailedHardware: 'WiFi toggle failed - check robot WiFi hardware', + failedToEnableWifi: 'Failed to enable WiFi', + failedToDisableWifi: 'Failed to disable WiFi', + networkScanTitle: 'Network Scan', + foundNetwork: 'Found {count} network', + foundNetworks: 'Found {count} networks', + noNetworksFoundScanMessage: 'No networks found. Make sure WiFi is enabled on the robot.', + failedToScanNetworks: 'Failed to scan networks. Check robot connection.', + wifiConnectionTitle: 'WiFi Connection', + connectedToNetwork: 'Connected to {networkName}', + failedToConnectNetwork: 'Failed to connect to {networkName}', + failedToConnectSavedNetwork: 'Failed to connect to saved network', + failedToConnectSavedNetworkMessage: 'Failed to connect to {networkName}. The saved profile may be outdated.', + notConnectedToRobot: 'Not connected to robot', + connectionTimeout: 'Connection timeout', + forgetNetworkConfirm: 'Are you sure you want to forget the network "{networkName}"? You will need to re-enter the password to connect again.', + failedToForgetNetwork: 'Failed to forget network', + networkForgottenTitle: 'Network Forgotten', + networkForgottenMessage: 'Successfully removed {networkName} from saved networks', + forgetNetworkTitle: 'Forget Network', + failedToForgetNetworkMessage: 'Failed to forget {networkName}', }, labs: { pageTitle: 'Labs', @@ -459,6 +1203,7 @@ export const enTranslations = { missions: 'Missions', ai: 'AI', soundboard: 'Soundboard', + weather: 'Weather', // Feature descriptions chatDescription: 'AI-powered chat interface for natural robot interactions.', mapEditDescription: 'Advanced map editing tools for customizing robot navigation paths.', @@ -469,6 +1214,7 @@ export const enTranslations = { missionsDescription: 'Autonomous navigation mission planning and execution.', aiDescription: 'Advanced AI capabilities and model management for robotics.', soundboardDescription: 'Interactive audio control with TTS presets and sound clips management for robot audio playback.', + weatherDescription: 'Simple weather viewer for current conditions', // Badges alpha: 'Alpha', beta: 'Beta', @@ -484,6 +1230,49 @@ export const enTranslations = { confirm: 'Confirm', cancel: 'Cancel', }, + soundClips: { + title: 'Sound Clips', + record: 'Record', + add: 'Add', + addClip: 'Add Clip', + recording: 'Recording...', + recordingDefaultName: 'Recording', + recordingReady: 'Recording ready', + ready: 'Ready', + recordingNamePlaceholder: 'Recording name...', + soundClipNamePlaceholder: 'Sound clip name...', + clipNamePlaceholder: 'Clip name', + saving: 'Saving...', + save: 'Save', + cancel: 'Cancel', + uploading: 'Uploading...', + upload: 'Upload', + clipsCount: '{count} clips', + loginRequired: 'Please log in to manage sound clips', + emptyTitle: 'No sound clips yet', + emptyDescription: 'Click "Record" or "Add Clip" to get started', + emptyDescriptionShort: 'Click "Record" or "Add" to get started', + failedMicrophoneAccess: 'Failed to access microphone. Please check your permissions.', + fileSizeLimit: 'File size must be less than 1GB', + audioFileRequired: 'Please upload an audio file', + failedUploadRecording: 'Failed to upload recording', + failedUploadSoundClip: 'Failed to upload sound clip', + failedDeleteSoundClip: 'Failed to delete sound clip', + failedUpdateSoundClip: 'Failed to update sound clip', + failedToPlay: 'Failed to play sound clip', + deleteConfirm: 'Delete "{clipName}"?', + playInBrowser: 'Play in browser', + connectToRobotToPlay: 'Connect to robot to play on robot', + playOnRobot: 'Play "{clipName}" on robot', + clickToPlayOnRobot: 'Click to play on robot', + }, + weather: { + unknownLocation: 'Unknown Location', + locationUnavailable: 'Unable to get location. Please enable location services.', + geolocationUnsupported: 'Geolocation is not supported by your browser.', + failedToFetch: 'Failed to fetch weather data. Please try again.', + attribution: "Weather data provided by MET Norway API. Location determined using browser's Geolocation API. Only your coordinates are shared with weather services to fetch local conditions. Data updates every 5 minutes.", + }, fleet: { pageTitle: 'Fleet Management', addRobot: 'Add Robot', diff --git a/frontend/src/utils/translations/index.ts b/frontend/src/utils/translations/index.ts index ec069fa..ab98dd3 100644 --- a/frontend/src/utils/translations/index.ts +++ b/frontend/src/utils/translations/index.ts @@ -1,18 +1,21 @@ import { enTranslations } from './en'; import { ptTranslations } from './pt'; +import { zhCNTranslations } from './zh-CN'; export type TranslationKey = keyof typeof enTranslations; export type NestedTranslationKey = keyof (typeof enTranslations)[T]; -export type LanguageCode = 'en' | 'pt'; +export type LanguageCode = 'en' | 'pt' | 'zh-CN'; export const translations = { en: enTranslations, pt: ptTranslations, + 'zh-CN': zhCNTranslations, }; export const languageNames = { en: '🇬🇧 English', pt: '🇧🇷 Português', + 'zh-CN': '🇨🇳 简体中文', }; diff --git a/frontend/src/utils/translations/pt.ts b/frontend/src/utils/translations/pt.ts index aac7c82..abf8d4f 100644 --- a/frontend/src/utils/translations/pt.ts +++ b/frontend/src/utils/translations/pt.ts @@ -68,12 +68,15 @@ export const ptTranslations = { // Form fields name: 'Nome', namePlaceholder: 'Digite seu nome (opcional)', + emailPlaceholder: 'Digite seu e-mail', + passwordPlaceholder: 'Digite sua senha', confirmPassword: 'Confirmar Senha', confirmPasswordPlaceholder: 'Digite a senha novamente', // Buttons createAccountButton: 'Criar Conta', creatingAccount: 'Criando conta...', loggingIn: 'Entrando...', + verifyingCredentials: 'Verificando suas credenciais...', // Password strength passwordStrength: 'Força da Senha', passwordVeryWeak: 'Muito Fraca', @@ -92,6 +95,12 @@ export const ptTranslations = { rateLimitExceeded: 'Muitas tentativas. Por favor, aguarde alguns minutos e tente novamente.', signupError: 'Não foi possível criar a conta. Por favor, tente novamente.', networkError: 'Erro de conexão. Por favor, verifique sua internet e tente novamente.', + invalidCredentials: 'O e-mail ou a senha informados estão incorretos. Tente novamente.', + emailNotConfirmed: 'Verifique seu e-mail para confirmar sua conta antes de entrar.', + tooManyLoginAttempts: 'Muitas tentativas de login. Aguarde alguns minutos e tente novamente.', + loginError: 'Não foi possível entrar. Verifique suas credenciais e tente novamente.', + loginSuccessRedirect: 'Sucesso! Redirecionando para seu dashboard...', + selectLanguage: 'Selecionar idioma', // Legal privacyPolicy: 'Política de Privacidade', termsOfService: 'Termos de Serviço', @@ -158,6 +167,12 @@ export const ptTranslations = { buttonReconnect: 'Reconectar', buttonConnecting: 'Conectando...', buttonConnect: 'Conectar', + connectedTo: 'Conectado a', + notConnected: 'Não Conectado', + noRobotConnected: 'Nenhum robô conectado', + manageRobotFleet: 'Gerenciar Frota de Robôs', + manageRobotFleetDescription: 'Adicione, edite ou conecte seus robôs', + manageRobotConnections: 'Clique acima para gerenciar suas conexões de robô', }, robotHeader: { connected: 'Conectado', @@ -263,6 +278,26 @@ export const ptTranslations = { deleteLayoutTitle: 'Excluir Layout', delete: 'Excluir', updateLayoutTitle: 'Atualizar Layout', + recorder: 'Gravador', + selectStreamsToRecord: 'Selecionar Streams para Gravar', + fileFormat: 'Formato do Arquivo', + format: 'Formato', + changeInSettingsToUpdate: 'Altere nas Configurações para atualizar', + robotOffline: 'Robô Offline', + recording: 'Gravando', + streamActive: '{count} stream ativo', + streamsActive: '{count} streams ativos', + readyToRecord: 'Pronto para Gravar', + streamSelected: '{count} stream selecionado', + streamsSelected: '{count} streams selecionados', + selectedStreams: 'Streams Selecionados:', + noStreamsSelected: 'Nenhum stream selecionado', + processing: 'Processando...', + stopAndSave: 'Parar e Salvar', + startRecording: 'Iniciar Gravação', + mainCamera: 'Câmera Principal', + thermalCamera: 'Câmera Térmica', + rgbCamera: 'Câmera RGB', // Widget labels widgetCamera: 'Câmera', widgetGauge: 'Medidor', @@ -275,6 +310,46 @@ export const ptTranslations = { widgetJoystick: 'Joystick', widgetAudioStream: 'Transmissão de Áudio', widgetMapView: 'Visualização do Mapa', + addWidgetToDashboard: 'Adicionar Widget ao Dashboard', + searchWidgets: 'Buscar widgets...', + noWidgetsFound: 'Nenhum widget encontrado para sua busca.', + widgetAvailable: '{count} widget disponível', + widgetsAvailable: '{count} widgets disponíveis', + categoryAllWidgets: 'Todos os Widgets', + categoryVisualization: 'Visualização', + categoryControl: 'Controle', + categoryMedia: 'Mídia', + categoryInformation: 'Informações', + categoryAiSmart: 'IA e Inteligente', + widgetCameraDescription: 'Feed de vídeo ao vivo das câmeras do robô', + widget3DVisualizationDescription: 'Modelo 3D do robô e ambiente', + widgetMapViewDescription: 'Mapa ao vivo e visão de navegação', + widgetJoystickDescription: 'Joystick virtual para controle do robô', + widgetButtonDescription: 'Botão de ação personalizado', + widgetButtonGroupDescription: 'Vários botões de ação', + widgetDelivery: 'Entrega', + widgetDeliveryDescription: 'Controles de gerenciamento de entrega', + widgetMissions: 'Missões', + widgetMissionsDescription: 'Gerencie e execute missões do robô', + widgetAudioStreamDescription: 'Controles de streaming de áudio', + widgetMicrophone: 'Microfone', + widgetMicrophoneDescription: 'Entrada de voz e gravação', + widgetTTSPresets: 'Presets TTS', + widgetTTSPresetsDescription: 'Presets de texto para fala', + widgetSoundClips: 'Clipes de Som', + widgetSoundClipsDescription: 'Reproduza sons pré-gravados', + widgetRecorder: 'Gravador', + widgetRecorderDescription: 'Gravação de áudio/vídeo', + widgetGaugeDescription: 'Exibe valores numéricos', + widgetSidewaysGaugeDescription: 'Exibição de medidor horizontal', + widgetInformationDescription: 'Exibe informações do robô', + widgetMapsManagement: 'Gerenciamento de Mapas', + widgetMapsManagementDescription: 'Gerencie mapas salvos', + widgetChatDescription: 'Interface de chat', + widgetAIStream: 'Stream de IA', + widgetAIStreamDescription: 'Stream de processamento de IA', + widgetRecentDetections: 'Detecções Recentes', + widgetRecentDetectionsDescription: 'Histórico de detecção de objetos', // Layout names layoutDefault: 'Padrão', layoutCommandInput: 'Entrada de Comando', @@ -316,9 +391,55 @@ export const ptTranslations = { notMapped: 'Não Mapeado', resetToDefault: 'Restaurar Padrão', clearAll: 'Limpar Tudo', + joystickVisualizationOnly: 'Somente Visualização do Joystick', + joystickVisualizationOnlyDescription: 'Exibir entrada do joystick no visualizador sem enviar comandos ao robô', }, profile: { title: 'Perfil', + pageDescription: 'Gerencie as configurações e preferências da sua conta', + saveChanges: 'Salvar Alterações', + personalInformation: 'Informações Pessoais', + accountInformation: 'Informações da Conta', + security: 'Segurança', + appearance: 'Aparência', + brandingOptions: 'Opções de Marca', + hideBotBotLogo: 'Ocultar Logo BotBot', + hideBotBotLogoDescription: 'Remover o logo BotBot da barra de navegação', + systemSettings: 'Configurações do Sistema', + privacyAndLogging: 'Privacidade e Registros', + auditLogging: 'Registro de Auditoria', + auditLoggingDescription: 'Registrar todas as ações do usuário para segurança e análise', + robotConnection: 'Conexão do Robô', + connectionTimeout: 'Tempo Limite da Conexão', + seconds: 'segundos', + connectionTimeoutDescription: 'Tempo de espera pela conexão do robô antes de expirar (1-120 segundos, padrão: 20)', + speedAndControl: 'Velocidade e Controle', + speedMode: 'Modo de Velocidade', + speedLevel: 'Nível de Velocidade', + speedModeBeginner: 'Iniciante', + speedModeNormal: 'Normal', + speedModeInsane: 'Insano', + speedModeBeginnerDescription: '20% de velocidade - seguro para aprendizado', + speedModeNormalDescription: '80% de velocidade - desempenho equilibrado', + speedModeInsaneDescription: '100% de velocidade - velocidade máxima', + enableInsaneModeTitle: 'Ativar Modo Insano?', + enableInsaneModeDescriptionPrefix: 'Isso definirá a velocidade do robô para', + enableInsaneModeDescriptionSuffix: 'da velocidade máxima.', + insaneModeRecommendation: 'Recomendado apenas para operadores experientes em ambientes controlados.', + enableInsane: 'Ativar Insano', + avatarAlt: 'Avatar', + deleteAvatar: 'Excluir', + changeAvatar: 'Alterar Avatar', + uploadAvatar: 'Enviar Avatar', + uploadingAvatar: 'Enviando...', + avatarUploadHelp: 'Clique no avatar ou no botão para enviar. Máx. 5MB. JPEG, PNG, WebP ou GIF.', + databaseUnavailable: 'Conexão com o banco de dados indisponível', + avatarLoginRequired: 'Você precisa estar logado para enviar um avatar', + invalidAvatarFile: 'Selecione um arquivo de imagem válido (JPEG, PNG, WebP ou GIF)', + avatarFileSizeLimit: 'O arquivo deve ter menos de 5MB', + avatarUploadError: 'Erro ao enviar avatar. Tente novamente.', + deleteAvatarConfirm: 'Tem certeza que deseja excluir seu avatar?', + deleteAvatarError: 'Erro ao excluir avatar. Tente novamente.', name: 'Nome', namePlaceholder: 'Digite seu nome', nameDescription: 'Seu nome ou Nome da Empresa', @@ -351,6 +472,540 @@ export const ptTranslations = { saved: 'Salvo', cancel: 'Cancelar', }, + dashboard: { + cockpit: 'Cockpit', + cockpitDescription: 'Controle e monitore seu robô', + fleetManager: 'Gerenciador de Frota', + fleetManagerDescription: 'Gerencie todos os seus robôs', + myUI: 'Minha UI', + myUIDescription: 'Personalize seu workspace', + goodMorning: 'Bom Dia', + goodAfternoon: 'Boa Tarde', + goodEvening: 'Boa Noite', + welcomeBackToBotBrain: 'Bem-vindo de volta ao BotBrain', + user: 'Usuário', + userAvatar: 'Avatar do usuário', + active: 'Ativo', + now: 'Agora', + justNow: 'Agora mesmo', + minutesAgo: 'há {count} min', + hoursAgo: 'há {count} h', + daysAgo: 'há {count} d', + actionsThisWeek: '{count} ações esta semana', + dayStreak: '{count} dia de sequência', + daysStreak: '{count} dias de sequência', + userFor: 'Usuário há {duration}', + currentTime: 'Hora Atual', + todaysDate: 'Data de Hoje', + today: 'Hoje', + year: 'ano', + years: 'anos', + month: 'mês', + months: 'meses', + week: 'semana', + weeks: 'semanas', + day: 'dia', + days: 'dias', + fleetSize: 'Tamanho da Frota', + onlineNow: 'online agora', + totalActions: 'Ações Totais', + activeDays: 'dias ativos', + thisMonth: 'este mês', + userSince: 'Usuário Desde', + new: 'Novo', + newMember: 'Novo membro', + favoriteRobot: 'Robô Favorito', + mostUsed: 'Mais usado', + noFavoriteSet: 'Nenhum favorito definido', + offline: 'Offline', + actions: 'ações', + veryActive: 'Muito ativo', + quiet: 'Calmo', + thisWeek: 'Esta Semana', + avgPerDay: 'Média {count}/dia', + peakHour: 'Horário de Pico', + mostActive: 'mais ativo', + missions: 'Missões', + created: 'criadas', + aiDetections: 'Detecções de IA', + confidenceAbbrev: 'conf', + yoloActive: 'YOLO ativo', + soundLibrary: 'Biblioteca de Sons', + audioClips: 'clipes de áudio', + storage: 'Armazenamento', + used: 'usado', + activityPattern: 'Padrão de Atividade', + last30Days: 'Últimos 30 dias', + total: 'total', + dayAverage: 'média {count}/dia', + less: 'Menos', + more: 'Mais', + activityCellTitle: '{day} {hour}:00 - {count} ações', + sundayShort: 'D', + mondayShort: 'S', + tuesdayShort: 'T', + wednesdayShort: 'Q', + thursdayShort: 'Q', + fridayShort: 'S', + saturdayShort: 'S', + noRobotsConfiguredYet: 'Nenhum robô configurado ainda', + robotFleetUsage: 'Uso da Frota de Robôs', + robotCount: '{count} robôs', + robotCountSingular: '{count} robô', + peak: 'Pico', + showLess: 'Mostrar menos', + showMore: 'Mostrar mais {count}', + never: 'Nunca', + yourRobots: 'Seus Robôs', + robot: 'Robô', + live: 'Ao Vivo', + connected: 'Conectado', + connecting: 'Conectando...', + connect: 'Conectar', + noRobotsYet: 'Nenhum Robô Ainda', + getStartedAddFirstRobot: 'Comece adicionando seu primeiro robô', + addYourFirstRobot: 'Adicionar Primeiro Robô', + yourRobotFleet: 'Sua Frota de Robôs', + yourRobotFleetDescription: 'Gerencie e monitore seus robôs conectados', + manageFleet: 'Gerenciar Frota', + addRobot: 'Adicionar Robô', + connectNewDevice: 'Conectar novo dispositivo', + robotFleet: 'Frota de Robôs', + robotFleetDescription: 'Gerencie e conecte toda a sua frota de robôs com monitoramento de status em tempo real', + quickAccess: 'Acesso Rápido', + quickAccessDescription: 'Seus recursos e ferramentas mais usados', + recentActivity: 'Atividade Recente', + yourLatestActions: 'Suas ações mais recentes', + viewAll: 'Ver tudo', + noRecentActivity: 'Nenhuma atividade recente', + weeklyProgress: 'Progresso Semanal', + activityOverTheWeek: 'Atividade ao longo da semana', + actionsPerformed: 'Ações realizadas', + actionPerformed: 'Ação realizada', + totalCount: '{count} no total', + achievements: 'Conquistas', + yourMilestones: 'Seus marcos', + unlocked: 'Desbloqueado', + progress: 'Progresso', + mondayAbbrev: 'Seg', + tuesdayAbbrev: 'Ter', + wednesdayAbbrev: 'Qua', + thursdayAbbrev: 'Qui', + fridayAbbrev: 'Sex', + saturdayAbbrev: 'Sáb', + sundayAbbrev: 'Dom', + achievementFirstConnection: 'Primeira Conexão', + achievementFirstConnectionDescription: 'Conecte seu primeiro robô', + achievementFleetCommander: 'Comandante da Frota', + achievementFleetCommanderDescription: 'Gerencie 5+ robôs', + achievementPowerUser: 'Usuário Avançado', + achievementPowerUserDescription: '1000+ ações realizadas', + achievementGlobalOperator: 'Operador Global', + achievementGlobalOperatorDescription: 'Controle robôs remotamente', + }, + maps: { + pageTitle: 'Gerenciamento de Mapas', + pageDescription: 'Visualize e gerencie mapas de navegação do robô', + liveCamera: 'Câmera ao Vivo', + robotControls: 'Controles do Robô', + showJoysticks: 'Mostrar Joysticks', + hideJoysticks: 'Ocultar Joysticks', + hidePanel: 'Ocultar painel', + showControlPanel: 'Mostrar painel de controle', + mapViewer: 'Visualizador de Mapa', + currentMap: 'Mapa Atual', + mapView: 'Visualização do Mapa', + mappingControl: 'Controle de Mapeamento', + mapNamePlaceholder: 'Digite o nome do mapa...', + mappingLabel: 'Mapeando: {mapName}', + stopMapping: 'Parar Mapeamento', + startMapping: 'Iniciar Mapeamento', + startNewMap: 'Iniciar Novo Mapa', + cancel: 'Cancelar', + availableMaps: 'Mapas Disponíveis', + refreshMaps: 'Atualizar lista de mapas', + connectToRobotToViewMaps: 'Conecte ao robô para ver mapas', + noMapsAvailable: 'Nenhum mapa disponível', + deleting: 'Excluindo...', + loading: 'Carregando...', + active: 'Ativo', + loadMap: 'Carregar Mapa', + load: 'Carregar', + delete: 'Excluir', + deleteMap: 'Excluir mapa', + deleteMapTitle: 'Excluir "{mapName}"?', + deleteMapWarning: 'Esta ação não pode ser desfeita.', + mapLoadedTitle: 'Mapa Carregado', + mapLoadedMessage: 'Mapa carregado e ativado com sucesso: {mapName}', + mapLoadedShortMessage: 'Carregado com sucesso: {mapName}', + failedToLoadMapTitle: 'Falha ao Carregar Mapa', + failedToLoadMapMessage: 'Não foi possível carregar o mapa: {mapName}', + failedToLoadMapShortMessage: 'Não foi possível carregar: {mapName}', + failedToDeleteMapTitle: 'Falha ao Excluir Mapa', + failedToDeleteMapMessage: 'Não foi possível excluir o mapa: {mapName}', + setHomePositionTitle: 'Definir Posição Inicial', + setHomePositionMessage: 'Clique no mapa para definir a posição inicial e arraste para definir a orientação', + cancelSetHome: 'Cancelar definição de home', + setHomePositionTooltip: 'Definir posição inicial', + stopNavigation: 'Parar Navegação', + cancelNavigationMode: 'Cancelar modo de navegação', + navigateToLocation: 'Navegar para local', + clearNavigationGoal: 'Limpar objetivo de navegação', + zoomIn: 'Aproximar', + zoomOut: 'Afastar', + resetView: 'Redefinir Visualização', + centerOnRobot: 'Centralizar no Robô', + stoppingNavigation: 'Parando navegação...', + stopNavigationClick: 'Parar navegação - clique para parar o robô', + cancelGoalSetting: 'Cancelar definição de objetivo', + setNavigationGoal: 'Definir objetivo de navegação', + hideNavigationPlan: 'Ocultar plano de navegação', + showNavigationPlan: 'Mostrar plano de navegação', + refreshMapData: 'Atualizar dados do mapa', + hideLocalCostmap: 'Ocultar costmap local', + showLocalCostmap: 'Mostrar costmap local', + failedToSetHomeTitle: 'Falha ao Definir Home', + failedToSetHomeMessage: 'Não foi possível definir a posição home. Verifique a conexão ROS.', + loadingMap: 'Carregando mapa...', + failedToLoadMapGeneric: 'Falha ao carregar mapa', + useFallbackRenderer: 'Usar renderizador alternativo', + noMapTopicsAvailable: 'Nenhum tópico de mapa disponível. Verifique se map_server ou navigation está em execução.', + failedToLoadMapLibraries: 'Falha ao carregar bibliotecas de mapa', + failedToCreateMapClient: 'Falha ao criar cliente de mapa', + failedToInitializeMapViewer: 'Falha ao inicializar visualizador de mapa', + navigationStoppedTitle: 'Navegação Parada', + navigationStoppedMessage: 'A navegação do robô foi cancelada', + stopFailedTitle: 'Falha ao Parar', + stopFailedMessage: 'Falha ao parar a navegação. Tente novamente.', + stopErrorMessage: 'Ocorreu um erro ao parar a navegação.', + navigationStartedTitle: 'Navegação Iniciada', + navigationStartedMessage: 'Navegando até o destino', + navigationFailedTitle: 'Falha na Navegação', + navigationFailedMessage: 'Falha ao iniciar navegação. Verifique se o Nav2 está em execução.', + failedParsePgm: 'Falha ao analisar arquivo PGM. Verifique se é um formato P5 válido.', + failedLoadPng: 'Falha ao carregar arquivo PNG. Verifique se é uma imagem PNG válida.', + selectPgmOrPng: 'Selecione um arquivo PGM ou PNG.', + failedParseYaml: 'Falha ao analisar arquivo YAML.', + uploadMapPgmPng: 'Enviar Mapa (PGM/PNG)', + uploadYaml: 'Enviar YAML', + saveMap: 'Salvar Mapa', + unknown: 'Desconhecido', + showBaseMap: 'Mostrar mapa base', + showCostmap: 'Mostrar costmap', + costmap: 'Costmap', + baseMap: 'Mapa Base', + robotPosition: 'Posição do Robô', + walls: 'Paredes', + obstacles: 'Obstáculos', + navigating: 'Navegando...', + navGoal: 'Objetivo Nav', + clickOnMapToSetNavigationGoal: 'Clique no mapa para definir o objetivo de navegação', + missionProgress: 'Missão: {current}/{total}', + goalSet: 'Objetivo definido', + navigatingToDestination: 'Navegando até o destino...', + robot: 'Robô', + goal: 'Objetivo', + mission: 'Missão', + plan: 'Plano', + setNavigationGoalInstructions: 'Clique para definir objetivo de navegação - arraste para definir orientação - ESC para cancelar', + setHomePositionInstructions: 'Clique para definir posição home - arraste para definir orientação - ESC para cancelar', + settingHomePosition: 'Definindo Posição Home', + position: 'Posição', + mapConnected: 'Mapa Conectado', + }, + missions: { + title: 'Missões', + createNewMission: 'Criar nova missão', + missionNamePlaceholder: 'Nome da missão...', + create: 'Criar', + cancel: 'Cancelar', + noMissionsYet: 'Nenhuma missão ainda', + createFirstMission: 'Criar Primeira Missão', + active: 'Ativa', + paused: 'Pausada', + running: 'Em execução', + created: 'Criada', + unknown: 'Desconhecido', + waypoint: 'waypoint', + waypoints: 'waypoints', + waypointShort: 'wp', + waypointsShort: 'wps', + edit: 'Editar', + delete: 'Excluir', + deleteMissionConfirm: 'Excluir missão "{missionName}"?', + missionEditor: 'Editor de Missão', + missionTitle: 'Missão: {missionName}', + missionRunning: 'Missão em Execução', + missionPaused: 'Missão Pausada', + pending: 'Pendente', + reached: 'Alcançado', + navPlan: 'Plano Nav', + toggleNavPlanOverlay: 'Alternar sobreposição do plano de navegação', + savingWaypoints: 'Salvando waypoints...', + stop: 'Parar', + start: 'Iniciar', + resume: 'Retomar', + switchToCorrectMapFirst: 'Mude primeiro para o mapa correto', + resumeMission: 'Retomar missão', + startMissionTooltip: 'Iniciar missão', + addWaypointsFirst: 'Adicione waypoints primeiro', + noMissionSelected: 'Nenhuma Missão Selecionada', + noMissionSelectedDescription: 'Selecione uma missão no painel esquerdo ou crie uma nova para começar a planejar waypoints', + openMissionList: 'Abrir Lista de Missões', + cameraView: 'Visão da Câmera', + robotControls: 'Controles do Robô', + mapSwitchedTitle: 'Mapa Alterado', + mapSwitchedMessage: 'Mapa carregado: {mapName}', + failedToSwitchMapTitle: 'Falha ao alterar mapa', + failedToSwitchMapMessage: 'Não foi possível carregar o mapa da missão', + failedToLoadMissionsTitle: 'Falha ao carregar missões', + refreshPageMessage: 'Tente atualizar a página', + missionCreatedTitle: 'Missão criada', + missionCreatedMessage: 'A missão "{missionName}" foi criada', + failedToCreateMissionTitle: 'Falha ao criar missão', + tryAgainMessage: 'Tente novamente', + missionUpdatedTitle: 'Missão atualizada', + missionUpdatedMessage: 'A missão foi renomeada para "{missionName}"', + failedToUpdateMissionTitle: 'Falha ao atualizar missão', + missionDeletedTitle: 'Missão excluída', + missionDeletedMessage: 'A missão foi excluída com sucesso', + failedToDeleteMissionTitle: 'Falha ao excluir missão', + savingWaypointsTitle: 'Salvando waypoints', + savingWaypointsMessage: 'Aguarde enquanto os waypoints são salvos...', + noWaypointsFoundTitle: 'Nenhum waypoint encontrado', + noWaypointsFoundMessage: 'Adicione waypoints e aguarde até que sejam salvos', + missionStartedTitle: 'Missão iniciada', + missionStartedMessage: 'A missão "{missionName}" agora está ativa com {count} waypoints', + failedToStartMissionTitle: 'Falha ao iniciar missão', + missionStoppedTitle: 'Missão parada', + missionStoppedMessage: 'A missão foi parada', + failedToStopMissionTitle: 'Falha ao parar missão', + unknownError: 'Ocorreu um erro desconhecido', + failedToSaveWaypointsTitle: 'Falha ao salvar waypoints', + changesMayNotHaveBeenSaved: 'Suas alterações podem não ter sido salvas', + navigationCompleteTitle: 'Navegação Concluída', + navigationCompleteMessage: 'A missão foi finalizada', + navigationStartedTitle: 'Navegação Iniciada', + navigationFailedTitle: 'Falha na Navegação', + navigationErrorTitle: 'Erro de Navegação', + noWaypointsDefinedMessage: 'Nenhum waypoint definido. Adicione waypoints primeiro.', + nav2ActionUnavailableMessage: 'Servidor da ação Nav2 FollowWaypoints indisponível. Verifique se o Nav2 está em execução no robô.', + navigatingThroughWaypoints: 'Navegando por {count} waypoints', + navigationCancelledTitle: 'Navegação Cancelada', + missionNavigationStopped: 'A navegação da missão foi parada', + cancelFailedTitle: 'Falha ao Cancelar', + cancelFailedMessage: 'Falha ao cancelar navegação. Verifique a conexão do robô.', + cancelErrorTitle: 'Erro ao Cancelar', + cancelErrorMessage: 'Ocorreu um erro ao cancelar a navegação', + successfullyReachedAllWaypoints: 'Todos os waypoints foram alcançados com sucesso!', + toggleEditMode: 'Alternar modo de edição (E)', + exitEdit: 'Sair da Edição', + clearAllWaypoints: 'Limpar todos os waypoints', + clear: 'Limpar', + waitingForMapDataFrom: 'Aguardando dados do mapa de {topic}', + mayTake15Seconds: 'Isso pode levar até 15 segundos', + widgetName: 'Nome do Widget', + widgetNamePlaceholder: 'Digite o nome do widget', + refreshMissions: 'Atualizar missões', + loading: 'Carregando...', + connectToRobotToViewMissions: 'Conecte ao robô para ver missões', + loadMapToSeeMissions: 'Carregue um mapa para ver missões', + noMissionsForCurrentMap: 'Nenhuma missão para o mapa atual', + stopMission: 'Parar Missão', + startMission: 'Iniciar Missão', + mapMismatchTitle: 'Mapa Incompatível', + mapMismatchCurrent: 'Missão criada em {missionMapName}, atualmente {currentMapName} está carregado', + mapMismatchNoCurrentMap: 'Missão criada em {missionMapName}, mas nenhum mapa está carregado', + switchMap: 'Trocar Mapa', + retryLoadingMap: 'Tentar Carregar Mapa Novamente', + mapServerRunningHelp: 'Verifique se o map server está em execução no robô', + robotPosition: 'Posição do Robô', + navigatingProgress: 'Navegando ({current}/{total})', + nav2Disconnected: 'Nav2 desconectado', + navigationPlan: 'Plano de Navegação', + costmap: 'Costmap', + }, + auditLog: { + pageTitle: 'Registro de Auditoria', + pageDescription: 'Acompanhe todas as atividades e eventos da sua aplicação', + advancedPageTitle: 'Sistema Avançado de Auditoria', + advancedPageDescription: 'Rastreamento e análise completos de todas as atividades dos operadores', + loading: 'Carregando registros de auditoria...', + loadingData: 'Carregando dados de auditoria...', + searchPlaceholder: 'Buscar registros...', + totalEvents: 'Total de Eventos', + eventsToday: 'Eventos Hoje', + activeRobots: 'Robôs Ativos', + criticalEvents: 'Eventos Críticos', + lastRange: 'Últimos {range}', + allTime: 'todo o período', + daysRange: '{count} dias', + sinceMidnight: 'Desde meia-noite', + mostActiveRobot: 'Mais ativo: {robotName}', + noRobotsActive: 'Nenhum robô ativo', + safetyAndFailures: 'Segurança e falhas', + analytics: 'Análise', + timeline: 'Linha do Tempo', + last24Hours: 'Últimas 24 horas', + last7Days: 'Últimos 7 dias', + last30Days: 'Últimos 30 dias', + last90Days: 'Últimos 90 dias', + clear: 'Limpar', + eventDistributionByType: 'Distribuição de Eventos por Tipo', + activityPattern24h: 'Padrão de Atividade em 24 Horas', + hourlyActivityTitle: '{hour}:00 - {count} eventos', + mostFrequentActions: 'Ações Mais Frequentes', + time: 'Hora', + allEvents: 'Todos os Eventos', + authentication: 'Autenticação', + robot: 'Robô', + commands: 'Comandos', + system: 'Sistema', + data: 'Dados', + mission: 'Missão', + navigation: 'Navegação', + audio: 'Áudio', + camera: 'Câmera', + safety: 'Segurança', + export: 'Exportação', + refresh: 'Atualizar', + exportButton: 'Exportar', + deleteAll: 'Excluir Tudo', + clearAll: 'Limpar Tudo', + testLog: 'Registro de Teste', + createTestLogTitle: 'Criar uma entrada de auditoria de teste', + dateAndTime: 'Data e Hora', + type: 'Tipo', + action: 'Ação', + details: 'Detalhes', + noAuditLogsFound: 'Nenhum registro de auditoria encontrado', + viewDetails: 'Ver detalhes', + eventsLabel: 'Eventos de {type}', + deleteAllTitle: 'Excluir Todos os Registros de Auditoria', + deleteAllConfirm: 'Tem certeza que deseja excluir todos os {count} registros de auditoria? Esta ação não pode ser desfeita.', + clearAllTitle: 'Limpar Todos os Registros de Auditoria', + clearAllConfirm: 'Tem certeza que deseja limpar todos os {count} registros de auditoria? Esta ação não pode ser desfeita.', + deleting: 'Excluindo...', + dateCsv: 'Data', + timeCsv: 'Hora', + eventTypeCsv: 'Tipo de Evento', + actionCsv: 'Ação', + robotCsv: 'Robô', + robotIdCsv: 'ID do Robô', + ipAddressCsv: 'Endereço IP', + userAgentCsv: 'Agente do Usuário', + detailsCsv: 'Detalhes', + testAuditMessage: 'Entrada de auditoria de teste', + }, + aiDetections: { + recentTitle: 'Detecções Recentes de IA', + failedToLoad: 'Falha ao carregar detecções', + showingRecent: 'Mostrando {count} recentes', + autoRefreshEnabled: 'Atualização automática ativada', + autoRefreshDisabled: 'Atualização automática desativada', + live: 'Ao Vivo', + paused: 'Pausado', + noDetectionsYet: 'Nenhuma detecção ainda', + viewAllDetections: 'Ver Todas as Detecções', + settingsTitle: 'Configurações de Upload de Detecções', + settingsDescription: 'Configure quais detecções são salvas no banco de dados', + loadingCurrentSettings: 'Carregando configurações atuais...', + couldNotLoadSettings: 'Não foi possível carregar as configurações atuais: {error}', + retry: 'Tentar novamente', + robotNotConnectedSettings: 'O robô não está conectado. Conecte para carregar e aplicar configurações.', + minimumConfidence: 'Confiança Mínima', + minimumConfidenceDescription: 'Somente detecções com confiança acima deste limite serão salvas', + objectFilter: 'Filtro de Objetos', + allObjects: 'Todos os objetos', + selectedCount: '{count} selecionados', + selectAll: 'Selecionar Tudo', + clear: 'Limpar', + searchObjects: 'Buscar objetos...', + moreObjects: '+{count} mais', + noObjectsMatch: 'Nenhum objeto corresponde à busca', + allObjectsWillBeSaved: 'Todos os objetos detectados serão salvos. Selecione objetos específicos para filtrar.', + onlySelectedObjectsSaved: 'Somente os objetos selecionados serão salvos no banco de dados', + settingsApplied: 'Configurações aplicadas com sucesso!', + cancel: 'Cancelar', + applying: 'Aplicando...', + applySettings: 'Aplicar Configurações', + failedToFetchSettings: 'Falha ao buscar configurações', + uploadSettings: 'Configurações de upload', + settings: 'Configurações', + time: 'Tempo', + images: 'Imagens', + confidence: 'Confiança', + allTime: 'Todo o período', + lastHour: 'Última hora', + last6Hours: 'Últimas 6 horas', + last24Hours: 'Últimas 24 horas', + last7Days: 'Últimos 7 dias', + all: 'Todos', + withImages: 'Com imagens', + withoutImages: 'Sem imagens', + anyConfidence: 'Qualquer confiança', + high: 'Alta', + medium: 'Média', + low: 'Baixa', + clearFilters: 'Limpar filtros', + disableAutoRefresh: 'Desativar atualização automática', + enableAutoRefresh: 'Ativar atualização automática a cada 2 segundos', + realtime: 'Tempo real', + of: 'de', + detection: 'detecção', + detections: 'detecções', + deleteDetectionsTitle: 'Excluir {count} {detectionLabel}', + delete: 'Excluir', + moonDreamReady: 'Pronto para interagir com MoonDream. Selecione um modo e envie para começar.', + moonDreamCaption: 'Legenda', + moonDreamCaptionDescription: 'Gerar uma descrição da imagem', + moonDreamQueryNoReasoning: 'Consulta - Sem Raciocínio', + moonDreamQueryNoReasoningDescription: 'Faça uma pergunta sobre a imagem', + moonDreamQueryWithReasoning: 'Consulta - Com Raciocínio', + moonDreamQueryWithReasoningDescription: 'Faça uma pergunta com raciocínio detalhado', + moonDreamDetect: 'Detectar', + moonDreamDetectDescription: 'Detectar objetos específicos na imagem', + moonDreamPoint: 'Apontar', + moonDreamPointDescription: 'Apontar locais específicos na imagem', + moonDreamQuestionPlaceholder: 'Faça uma pergunta sobre a imagem...', + moonDreamReasoningPlaceholder: 'Faça uma pergunta para análise detalhada...', + moonDreamDetectPlaceholder: 'Descreva o que detectar...', + moonDreamPointPlaceholder: 'Descreva para onde apontar...', + moonDreamQuestionLabel: 'Pergunta', + moonDreamDescriptionLabel: 'Descrição', + moonDreamAnswerLabel: 'Resposta', + moonDreamFailedCaption: 'Falha ao gerar legenda', + moonDreamEnterQuestion: 'Digite uma pergunta', + moonDreamFailedQuestion: 'Falha ao responder pergunta', + moonDreamFailedReasoning: 'Falha ao responder pergunta com raciocínio', + moonDreamEnterDescription: 'Digite uma descrição', + moonDreamDetectResult: '{count} {objectLabel} encontrado(s) para "{input}". {overlayMessage}', + moonDreamObjectSingular: 'objeto', + moonDreamObjectPlural: 'objetos', + moonDreamBoundingBoxesShown: 'Caixas delimitadoras roxas exibidas no stream ao vivo.', + moonDreamFailedDetect: 'Falha ao detectar objetos', + moonDreamPointResult: '{count} {pointLabel} identificado(s) para "{input}". {overlayMessage}', + moonDreamPointSingular: 'ponto', + moonDreamPointPlural: 'pontos', + moonDreamRedMarkersShown: 'Marcadores vermelhos exibidos no stream ao vivo.', + moonDreamFailedPoint: 'Falha ao identificar pontos', + moonDreamUnexpectedError: 'Ocorreu um erro inesperado. Tente novamente.', + moonDreamOverlaysCleared: 'Sobreposições removidas do stream ao vivo.', + moonDreamClearOverlaysTitle: 'Limpar todas as caixas delimitadoras e pontos do stream ao vivo', + moonDreamClear: 'Limpar', + moonDreamAllOverlaysCleared: 'Todas as sobreposições foram removidas do stream ao vivo.', + moonDreamClearAllOverlaysTitle: 'Limpar todas as sobreposições', + moonDreamClearAll: 'Limpar Tudo', + moonDreamResponse: 'Resposta do MoonDream', + moonDreamProcessing: 'Processando...', + moonDreamSend: 'Enviar', + moonDreamGenerating: 'Gerando...', + moonDreamGenerateCaption: 'Gerar Legenda', + moonDreamBoxSingular: 'caixa', + moonDreamBoxPlural: 'caixas', + moonDreamMode: 'Modo', + moonDreamInput: 'Entrada', + }, help: { pageTitle: 'Central de Ajuda', pageSubtitle: 'Obtenha suporte, acesse documentação e conheça as diretrizes de segurança', @@ -377,6 +1032,7 @@ export const ptTranslations = { pageTitle: 'Monitor de Saúde', connected: 'Conectado', connecting: 'Conectando...', + connect: 'Conectar', disconnected: 'Desconectado', loadingTitle: 'Carregando Dados de Saúde', loadingDescription: 'Aguardando dados de diagnóstico...', @@ -446,8 +1102,96 @@ export const ptTranslations = { node: 'Nó', state: 'Estado', actions: 'Ações', + start: 'Iniciar', + stop: 'Parar', + reset: 'Redefinir', + close: 'Fechar', + viewModuleId: 'Ver ID do módulo', + moduleDetails: 'Detalhes do Módulo', + moreValues: '+{count} valores adicionais', + powerRails: 'Trilhos de Energia', // Diagnostics robotDiagnostics: 'Diagnósticos', + waitingForData: 'Aguardando dados...', + rebooting: 'Reiniciando...', + rebootSystem: 'Reiniciar Sistema', + wifi: 'WiFi', + cellular: '4G/Celular', + hotspot: 'Hotspot', + offline: 'Offline', + noNetwork: 'Sem rede', + measuring: 'Medindo...', + excellent: 'Excelente', + good: 'Bom', + fair: 'Regular', + poor: 'Ruim', + incomingData: 'Dados recebidos', + outgoingData: 'Dados enviados', + individualCores: 'Núcleos Individuais', + waitingForCpuData: 'Aguardando dados da CPU...', + error: 'Erro', + errors: 'Erros', + warning: 'Aviso', + warnings: 'Avisos', + allSystemsOk: 'Todos os Sistemas OK', + activeIssues: 'Problemas Ativos', + systemsOperatingNormally: '{count} sistemas operando normalmente', + connectToRobotToViewDiagnostics: 'Conecte ao robô para ver diagnósticos', + confirmSystemReboot: 'Confirmar Reinicialização do Sistema', + confirmSystemRebootDescription: 'Tem certeza que deseja reiniciar o sistema? Isso desconectará temporariamente todos os serviços e o robô ficará indisponível por alguns minutos.', + rebootNow: 'Reiniciar Agora', + wifiSettings: 'Configurações de WiFi', + scanForNetworks: 'Procurar redes', + modeLabel: 'Modo: {mode}', + ipAddress: 'IP: {ip}', + activeStatus: 'Ativo', + wifiStateLabel: 'WiFi {state}', + enabled: 'Ativado', + disabled: 'Desativado', + connectToRobotToToggleWifi: 'Conecte ao robô para alternar o WiFi', + clickToDisableWifi: 'Clique para desativar o WiFi', + clickToEnableWifi: 'Clique para ativar o WiFi', + wifiDisabledHelp: 'O WiFi está desativado. Ative o WiFi para procurar redes e gerenciar conexões.', + availableNetworks: 'Redes Disponíveis', + scanError: 'Erro de busca', + noNetworksFound: 'Nenhuma rede encontrada', + savedNetworks: 'Redes Salvas ({count})', + refreshSavedNetworks: 'Atualizar redes salvas', + connectedTag: '(Conectado)', + connectToNetwork: 'Conectar a {networkName}', + forgetNetwork: 'Esquecer {networkName}', + noSavedNetworksFound: 'Nenhuma rede salva encontrada', + connectToSsid: 'Conectar a {ssid}', + password: 'Senha', + username: 'Usuário', + wifiPasswordPlaceholder: 'Digite a senha do WiFi', + usernamePlaceholder: 'Digite o usuário', + passwordPlaceholder: 'Digite a senha', + saveNetworkForAutomaticConnection: 'Salvar rede para conexão automática', + wifiRadioTitle: 'Rádio WiFi', + wifiEnabledSuccess: 'WiFi ativado com sucesso', + wifiDisabledSuccess: 'WiFi desativado com sucesso', + wifiToggleFailedHardware: 'Falha ao alternar WiFi - verifique o hardware WiFi do robô', + failedToEnableWifi: 'Falha ao ativar WiFi', + failedToDisableWifi: 'Falha ao desativar WiFi', + networkScanTitle: 'Busca de Redes', + foundNetwork: '{count} rede encontrada', + foundNetworks: '{count} redes encontradas', + noNetworksFoundScanMessage: 'Nenhuma rede encontrada. Verifique se o WiFi está ativado no robô.', + failedToScanNetworks: 'Falha ao procurar redes. Verifique a conexão com o robô.', + wifiConnectionTitle: 'Conexão WiFi', + connectedToNetwork: 'Conectado a {networkName}', + failedToConnectNetwork: 'Falha ao conectar a {networkName}', + failedToConnectSavedNetwork: 'Falha ao conectar à rede salva', + failedToConnectSavedNetworkMessage: 'Falha ao conectar a {networkName}. O perfil salvo pode estar desatualizado.', + notConnectedToRobot: 'Não conectado ao robô', + connectionTimeout: 'Tempo limite da conexão', + forgetNetworkConfirm: 'Tem certeza que deseja esquecer a rede "{networkName}"? Será necessário digitar a senha novamente para conectar.', + failedToForgetNetwork: 'Falha ao esquecer rede', + networkForgottenTitle: 'Rede Esquecida', + networkForgottenMessage: '{networkName} removida com sucesso das redes salvas', + forgetNetworkTitle: 'Esquecer Rede', + failedToForgetNetworkMessage: 'Falha ao esquecer {networkName}', }, labs: { pageTitle: 'Laboratório', @@ -462,6 +1206,7 @@ export const ptTranslations = { missions: 'Missões', ai: 'IA', soundboard: 'Mesa de Som', + weather: 'Clima', // Feature descriptions chatDescription: 'Interface de chat com IA para interações naturais com o robô.', mapEditDescription: 'Ferramentas avançadas de edição de mapa para personalizar rotas de navegação do robô.', @@ -472,6 +1217,7 @@ export const ptTranslations = { missionsDescription: 'Planejamento e execução de missões de navegação autônoma.', aiDescription: 'Capacidades avançadas de IA e gerenciamento de modelos para robótica.', soundboardDescription: 'Controle de áudio interativo com presets TTS e gerenciamento de clipes de som para reprodução no robô.', + weatherDescription: 'Visualizador simples do clima para condições atuais', // Badges alpha: 'Alfa', beta: 'Beta', @@ -487,6 +1233,49 @@ export const ptTranslations = { confirm: 'Confirmar', cancel: 'Cancelar', }, + soundClips: { + title: 'Clipes de Som', + record: 'Gravar', + add: 'Adicionar', + addClip: 'Adicionar Clipe', + recording: 'Gravando...', + recordingDefaultName: 'Gravação', + recordingReady: 'Gravação pronta', + ready: 'Pronto', + recordingNamePlaceholder: 'Nome da gravação...', + soundClipNamePlaceholder: 'Nome do clipe de som...', + clipNamePlaceholder: 'Nome do clipe', + saving: 'Salvando...', + save: 'Salvar', + cancel: 'Cancelar', + uploading: 'Enviando...', + upload: 'Enviar', + clipsCount: '{count} clipes', + loginRequired: 'Faça login para gerenciar clipes de som', + emptyTitle: 'Nenhum clipe de som ainda', + emptyDescription: 'Clique em "Gravar" ou "Adicionar Clipe" para começar', + emptyDescriptionShort: 'Clique em "Gravar" ou "Adicionar" para começar', + failedMicrophoneAccess: 'Falha ao acessar o microfone. Verifique suas permissões.', + fileSizeLimit: 'O arquivo deve ter menos de 1GB', + audioFileRequired: 'Envie um arquivo de áudio', + failedUploadRecording: 'Falha ao enviar gravação', + failedUploadSoundClip: 'Falha ao enviar clipe de som', + failedDeleteSoundClip: 'Falha ao excluir clipe de som', + failedUpdateSoundClip: 'Falha ao atualizar clipe de som', + failedToPlay: 'Falha ao reproduzir clipe de som', + deleteConfirm: 'Excluir "{clipName}"?', + playInBrowser: 'Reproduzir no navegador', + connectToRobotToPlay: 'Conecte ao robô para reproduzir no robô', + playOnRobot: 'Reproduzir "{clipName}" no robô', + clickToPlayOnRobot: 'Clique para reproduzir no robô', + }, + weather: { + unknownLocation: 'Local desconhecido', + locationUnavailable: 'Não foi possível obter a localização. Ative os serviços de localização.', + geolocationUnsupported: 'A geolocalização não é compatível com seu navegador.', + failedToFetch: 'Falha ao buscar dados meteorológicos. Tente novamente.', + attribution: 'Dados meteorológicos fornecidos pela API MET Norway. Localização determinada pela API de Geolocalização do navegador. Somente suas coordenadas são compartilhadas com serviços de clima para buscar condições locais. Os dados são atualizados a cada 5 minutos.', + }, fleet: { pageTitle: 'Gerenciamento de Frota', addRobot: 'Adicionar Robô', diff --git a/frontend/src/utils/translations/zh-CN.ts b/frontend/src/utils/translations/zh-CN.ts new file mode 100644 index 0000000..10bed9d --- /dev/null +++ b/frontend/src/utils/translations/zh-CN.ts @@ -0,0 +1,1292 @@ +import { enTranslations } from './en'; + +export const zhCNTranslations: typeof enTranslations = { + "sidebar": { + "fullScreen": "全屏", + "darkMode": "深色模式", + "lightMode": "浅色模式", + "language": "语言", + "notifications": "通知", + "joystick": "Joystick", + "user": "用户" + }, + "notifications": { + "title": "通知", + "markAllAsRead": "全部标为已读", + "noNotifications": "暂无通知", + "newNotification": "新通知" + }, + "userProfile": { + "profile": "个人资料", + "settings": "设置", + "logout": "退出登录" + }, + "robotControls": { + "status": "状态", + "connected": "已连接", + "disconnected": "未连接", + "connect": "连接", + "disconnect": "断开连接", + "restart": "重启", + "title": "按钮" + }, + "robotModes": { + "idle": "空闲", + "balanceStand": "平衡站立", + "obstacleAvoidance": "避障", + "emergency": "紧急模式", + "pose": "姿态", + "locomotion": "移动中", + "lieDown": "趴下", + "jointLock": "关节锁定", + "damping": "阻尼", + "recoveryStand": "恢复站立", + "sit": "坐下", + "frontFlip": "前空翻", + "frontJump": "前跳" + }, + "chat": { + "sendMessage": "发送", + "typeMessage": "输入消息...", + "connecting": "正在连接...", + "connected": "已连接", + "disconnected": "未连接", + "title": "聊天", + "tts": "TTS" + }, + "login": { + "title": "登录", + "username": "用户名", + "email": "邮箱", + "password": "密码", + "loginButton": "登录", + "rememberMe": "记住我", + "forgotPassword": "忘记密码?", + "signIn": "登录", + "createAccount": "创建账号", + "createAccountTitle": "创建你的账号", + "welcomeBack": "欢迎回来", + "name": "姓名", + "namePlaceholder": "输入姓名(可选)", + "emailPlaceholder": "输入邮箱", + "passwordPlaceholder": "输入密码", + "confirmPassword": "确认密码", + "confirmPasswordPlaceholder": "再次输入密码", + "createAccountButton": "创建账号", + "creatingAccount": "正在创建账号...", + "loggingIn": "正在登录...", + "verifyingCredentials": "正在验证凭据...", + "passwordStrength": "密码强度", + "passwordVeryWeak": "很弱", + "passwordWeak": "弱", + "passwordFair": "一般", + "passwordGood": "良好", + "passwordStrong": "强", + "passwordMinLength": "密码至少需要 8 个字符", + "passwordsDoNotMatch": "两次输入的密码不一致", + "passwordTooWeak": "请选择更强的密码", + "signupSuccessTitle": "账号已创建!", + "signupSuccessMessage": "登录前请检查邮箱并确认你的账号。", + "emailAlreadyExists": "这个邮箱已注册账号。请尝试直接登录。", + "rateLimitExceeded": "尝试次数过多。请等待几分钟后再试。", + "signupError": "无法创建账号。请重试。", + "networkError": "连接错误。请检查网络后重试。", + "invalidCredentials": "邮箱或密码不正确。请重试。", + "emailNotConfirmed": "登录前请检查邮箱并确认你的账号。", + "tooManyLoginAttempts": "登录尝试次数过多。请等待几分钟后再试。", + "loginError": "无法登录。请检查凭据后重试。", + "loginSuccessRedirect": "登录成功!正在跳转到仪表盘...", + "selectLanguage": "选择语言", + "privacyPolicy": "隐私政策", + "termsOfService": "服务条款", + "acceptTerms": "我同意", + "acceptTermsLabel": "我同意", + "acceptPrivacyLabel": "我同意", + "and": "和", + "termsRequired": "你必须接受服务条款和隐私政策", + "lastUpdated": "最后更新", + "iUnderstand": "我明白" + }, + "robotOffline": { + "connectionError": "无法与机器人建立连接。", + "noPayload": "未挂载 payload。", + "robotDisconnected": "机器人已断开", + "connectToRobotMessage": "连接机器人以查看实时数据" + }, + "robotData": { + "title": "信息", + "robotState": "机器人状态", + "errorStatus": "错误状态", + "battery": "电量", + "speed": "速度" + }, + "robotCams": { + "title": "摄像头", + "initializingCamera": "正在初始化摄像头...", + "noCameraFound": "未找到摄像头", + "nothingConnected": "未挂载 payload。", + "live": "实时", + "mode": "模式", + "backCamera": "后置摄像头", + "thermal": "热成像", + "rgb": "RGB", + "overlay": "叠加层", + "noOverlay": "无叠加层", + "crosshair": "准星", + "grid": "网格", + "corners": "角线", + "enterFullscreen": "进入全屏", + "exitFullscreen": "退出全屏", + "waypointAr": "Waypoint AR", + "waypointArEnabled": "Waypoint AR 已启用", + "waypointArDisabled": "Waypoint AR 已关闭", + "pathAr": "Path AR", + "pathArEnabled": "Path AR 已启用", + "pathArDisabled": "Path AR 已关闭" + }, + "chatHeader": { + "robot": "机器人", + "online": "在线", + "offline": "离线" + }, + "connectionPopup": { + "title": "连接设置", + "statusConnected": "已连接", + "statusConnecting": "正在连接...", + "statusError": "连接失败", + "statusIdle": "未连接", + "ipAddress": "机器人 IP 地址", + "invalidIp": "请输入有效的 IP 地址", + "validIp": "有效的 IP 地址", + "buttonReconnect": "重新连接", + "buttonConnecting": "正在连接...", + "buttonConnect": "连接", + "connectedTo": "已连接到", + "notConnected": "未连接", + "noRobotConnected": "未连接机器人", + "manageRobotFleet": "管理机器人 Fleet", + "manageRobotFleetDescription": "添加、编辑或连接你的机器人", + "manageRobotConnections": "点击上方管理机器人连接" + }, + "robotHeader": { + "connected": "已连接", + "connecting": "正在连接...", + "disconnected": "未连接", + "connectionTitle": "配置连接" + }, + "actionButtons": { + "getUp": "起身", + "getDown": "趴下", + "lightOn": "开灯", + "lightOff": "关灯", + "antiCollisionOn": "开启防碰撞", + "antiCollisionOff": "关闭防碰撞", + "emergencyOn": "E-STOP", + "emergencyOff": "解除", + "poseOn": "开启观察模式", + "poseOff": "关闭观察模式", + "lock": "锁定", + "unlock": "解锁", + "hello": "打招呼", + "chat": "聊天", + "sit": "坐下", + "riseSit": "坐姿起身", + "stretch": "伸展", + "dance": "跳舞", + "stop": "停止" + }, + "myUI": { + "addWidget": "添加 Widget", + "layout": "布局", + "emptyDashboard": "你的仪表盘还是空的。", + "clickAddWidget": "点击“添加 Widget”向仪表盘添加组件。", + "snapToGrid": "吸附到网格", + "snapOnDrag": "拖动时吸附", + "hideGrid": "隐藏网格", + "showGrid": "显示网格", + "gridSize": "网格大小", + "toggleGrid": "切换网格", + "snapOnDragTooltip": "切换拖动吸附", + "widgetTitle": "Widget 标题", + "widgetName": "Widget 名称", + "joystickType": "Joystick 类型", + "leftJoystick": "左 Joystick(移动)", + "rightJoystick": "右 Joystick(旋转)", + "save": "保存", + "buttonAction": "按钮动作", + "topic": "Topic", + "gaugeName": "仪表名称", + "unit": "单位", + "minValue": "最小值", + "maxValue": "最大值", + "cameraSource": "数据源:", + "byTopic": "自定义 Topic", + "botbrain": "BotBrain", + "camcam": "CamCam", + "camera": "摄像头", + "frontalCamera": "前置", + "backCamera": "后置", + "rgbInfrared": "RGB/红外", + "audioTopic": "音频 Topic", + "saveToCloud": "保存到云端", + "saveToCloudTooltip": "将布局保存到云端", + "defaultLayouts": "默认布局", + "myLayouts": "我的布局", + "publicLayouts": "公开布局", + "loadingLayouts": "正在加载布局...", + "noSavedLayouts": "暂无已保存布局", + "noSavedLayoutsDescription": "保存当前布局后会显示在这里", + "loadingPublicLayouts": "正在加载公开布局...", + "noPublicLayouts": "暂无可用公开布局", + "public": "公开", + "updateLayoutTooltip": "用当前配置更新布局", + "editLayoutTooltip": "编辑布局详情", + "deleteLayoutTooltip": "删除布局", + "deleteLayoutConfirm": "确定要删除这个布局吗?", + "saveDashboardLayout": "保存仪表盘布局", + "layoutName": "布局名称", + "layoutNamePlaceholder": "我的自定义布局", + "description": "描述", + "descriptionOptional": "描述(可选)", + "descriptionPlaceholder": "简单说明这个布局", + "makePublic": "将此布局公开", + "publicLayoutDescription": "其他用户将可以使用这个布局", + "layoutNameRequired": "布局名称为必填项", + "cannotSaveEmpty": "不能保存空布局", + "saveLayout": "保存布局", + "cancel": "取消", + "saving": "正在保存...", + "editDashboardLayout": "编辑仪表盘布局", + "update": "更新", + "updating": "正在更新...", + "loginToSave": "请登录后保存布局", + "loginToUpdate": "请登录后更新布局", + "updateLayoutConfirm": "要用当前仪表盘配置更新这个布局吗?", + "setAsFavorite": "设为收藏", + "removeFavorite": "取消收藏", + "newLayoutTitle": "新布局", + "clearLayoutConfirm": "确定要清空当前布局吗?所有 Widget 都会被移除。", + "clear": "清空", + "deleteLayoutTitle": "删除布局", + "delete": "删除", + "updateLayoutTitle": "更新布局", + "recorder": "录像器", + "selectStreamsToRecord": "选择要录制的流", + "fileFormat": "文件格式", + "format": "格式", + "changeInSettingsToUpdate": "在设置中更改后生效", + "robotOffline": "机器人离线", + "recording": "正在录制", + "streamActive": "{count} 路流正在录制", + "streamsActive": "{count} 路流正在录制", + "readyToRecord": "准备录制", + "streamSelected": "已选择 {count} 路流", + "streamsSelected": "已选择 {count} 路流", + "selectedStreams": "已选流:", + "noStreamsSelected": "未选择流", + "processing": "正在处理...", + "stopAndSave": "停止并保存", + "startRecording": "开始录制", + "mainCamera": "主摄像头", + "thermalCamera": "热成像摄像头", + "rgbCamera": "RGB 摄像头", + "widgetCamera": "摄像头", + "widgetGauge": "仪表", + "widgetSidewaysGauge": "横向仪表", + "widget3DVisualization": "3D 可视化", + "widgetInformation": "信息", + "widgetChat": "聊天", + "widgetButton": "按钮", + "widgetButtonGroup": "按钮组", + "widgetJoystick": "Joystick", + "widgetAudioStream": "音频流", + "widgetMapView": "地图视图", + "addWidgetToDashboard": "向仪表盘添加 Widget", + "searchWidgets": "搜索 widgets...", + "noWidgetsFound": "没有找到匹配的 widget。", + "widgetAvailable": "{count} 个 widget 可用", + "widgetsAvailable": "{count} 个 widgets 可用", + "categoryAllWidgets": "全部 Widgets", + "categoryVisualization": "可视化", + "categoryControl": "控制", + "categoryMedia": "媒体", + "categoryInformation": "信息", + "categoryAiSmart": "AI 与智能", + "widgetCameraDescription": "来自机器人摄像头的实时视频流", + "widget3DVisualizationDescription": "机器人 3D 模型和环境", + "widgetMapViewDescription": "实时地图和导航视图", + "widgetJoystickDescription": "用于机器人控制的虚拟 Joystick", + "widgetButtonDescription": "自定义动作按钮", + "widgetButtonGroupDescription": "多个动作按钮", + "widgetDelivery": "配送", + "widgetDeliveryDescription": "配送管理控制", + "widgetMissions": "任务", + "widgetMissionsDescription": "管理并执行机器人任务", + "widgetAudioStreamDescription": "音频流控制", + "widgetMicrophone": "麦克风", + "widgetMicrophoneDescription": "语音输入和录制", + "widgetTTSPresets": "TTS 预设", + "widgetTTSPresetsDescription": "Text-to-speech 预设", + "widgetSoundClips": "声音片段", + "widgetSoundClipsDescription": "播放预录声音", + "widgetRecorder": "录制器", + "widgetRecorderDescription": "音频/视频录制", + "widgetGaugeDescription": "显示数值", + "widgetSidewaysGaugeDescription": "横向仪表显示", + "widgetInformationDescription": "显示机器人信息", + "widgetMapsManagement": "地图管理", + "widgetMapsManagementDescription": "管理已保存地图", + "widgetChatDescription": "聊天界面", + "widgetAIStream": "AI Stream", + "widgetAIStreamDescription": "AI 处理流", + "widgetRecentDetections": "最近检测", + "widgetRecentDetectionsDescription": "目标检测历史", + "layoutDefault": "默认", + "layoutCommandInput": "命令输入", + "layoutInformation": "信息", + "layoutImageVisualization": "图像可视化" + }, + "settings": { + "title": "设置", + "robotAddress": "默认机器人地址", + "robotAddressDescription": "输入机器人的 WebSocket 地址", + "robotModel": "3D 模型", + "robotModelDescription": "选择机器人的 3D 模型", + "invertJoystick": "反转 Joystick 左/右", + "invertJoystickDescription": "交换 Joystick 的左右控制", + "language": "语言", + "languageDescription": "选择偏好的语言", + "videoFormat": "视频录制格式", + "videoFormatDescription": "选择录制视频时使用的格式", + "overlayColor": "叠加层颜色", + "overlayColorDescription": "选择准星和其他摄像头叠加层的颜色", + "colorWhite": "白色", + "colorBlack": "黑色", + "colorRed": "红色", + "colorPurple": "紫色", + "colorBlue": "蓝色", + "colorGreen": "绿色", + "gamepadConfig": "手柄配置", + "gamepadConnected": "已连接", + "connectGamepadPrompt": "连接手柄并按任意按钮激活", + "configureButton": "配置按钮", + "assignAction": "分配动作", + "noAction": "无动作", + "movementActions": "移动动作", + "gestureActions": "姿态动作", + "systemActions": "系统动作", + "emergencyActions": "紧急动作", + "currentMappings": "当前按钮映射", + "notMapped": "未映射", + "resetToDefault": "恢复默认", + "clearAll": "全部清除", + "joystickVisualizationOnly": "仅可视化 Joystick", + "joystickVisualizationOnlyDescription": "只在可视化器中显示 Joystick 输入,不向机器人发送命令" + }, + "profile": { + "title": "个人资料", + "pageDescription": "管理你的账户设置和偏好", + "saveChanges": "保存更改", + "personalInformation": "个人信息", + "accountInformation": "账户信息", + "security": "安全", + "appearance": "外观", + "brandingOptions": "品牌选项", + "hideBotBotLogo": "隐藏 BotBot Logo", + "hideBotBotLogoDescription": "从导航栏移除 BotBot Logo", + "systemSettings": "系统设置", + "privacyAndLogging": "隐私与日志", + "auditLogging": "审计日志", + "auditLoggingDescription": "记录所有用户操作,用于安全和分析", + "robotConnection": "机器人连接", + "connectionTimeout": "连接超时", + "seconds": "秒", + "connectionTimeoutDescription": "等待机器人连接的超时时间(1-120 秒,默认 20 秒)", + "speedAndControl": "速度与控制", + "speedMode": "速度模式", + "speedLevel": "速度等级", + "speedModeBeginner": "入门", + "speedModeNormal": "正常", + "speedModeInsane": "极速", + "speedModeBeginnerDescription": "20% 速度 - 适合学习时安全使用", + "speedModeNormalDescription": "80% 速度 - 性能与安全均衡", + "speedModeInsaneDescription": "100% 速度 - 最大速度", + "enableInsaneModeTitle": "启用极速模式?", + "enableInsaneModeDescriptionPrefix": "这会将机器人速度设为最大速度的", + "enableInsaneModeDescriptionSuffix": "。", + "insaneModeRecommendation": "仅建议有经验的操作员在受控环境中使用。", + "enableInsane": "启用极速", + "avatarAlt": "头像", + "deleteAvatar": "删除", + "changeAvatar": "更换头像", + "uploadAvatar": "上传头像", + "uploadingAvatar": "正在上传...", + "avatarUploadHelp": "点击头像或按钮上传。最大 5MB。支持 JPEG、PNG、WebP 或 GIF。", + "databaseUnavailable": "database 连接不可用", + "avatarLoginRequired": "你必须登录后才能上传头像", + "invalidAvatarFile": "请选择有效图片文件(JPEG、PNG、WebP 或 GIF)", + "avatarFileSizeLimit": "文件大小必须小于 5MB", + "avatarUploadError": "上传头像失败,请重试。", + "deleteAvatarConfirm": "确定要删除你的头像吗?", + "deleteAvatarError": "删除头像失败,请重试。", + "name": "姓名", + "namePlaceholder": "输入你的姓名", + "nameDescription": "你的姓名或公司名称", + "userId": "用户 ID", + "userIdDescription": "你的唯一用户标识", + "email": "邮箱", + "emailDescription": "邮箱地址无法更改", + "changePassword": "修改密码", + "currentPassword": "当前密码", + "newPassword": "新密码", + "confirmPassword": "确认密码", + "passwordStrength": "密码强度", + "passwordVeryWeak": "很弱", + "passwordWeak": "弱", + "passwordFair": "一般", + "passwordGood": "良好", + "passwordStrong": "强", + "passwordFieldsRequired": "所有字段均为必填", + "passwordsDoNotMatch": "两次输入的密码不一致", + "passwordTooShort": "密码至少需要 6 个字符", + "incorrectPassword": "当前密码不正确", + "passwordUpdateError": "密码更新失败", + "passwordUpdated": "密码已成功更新!", + "updating": "正在更新...", + "updatePassword": "更新密码" + }, + "common": { + "save": "保存", + "saving": "正在保存...", + "saved": "已保存", + "cancel": "取消" + }, + "dashboard": { + "cockpit": "Cockpit", + "cockpitDescription": "控制并监控你的机器人", + "fleetManager": "Fleet 管理", + "fleetManagerDescription": "管理你的全部机器人", + "myUI": "我的 UI", + "myUIDescription": "自定义你的工作区", + "goodMorning": "早上好", + "goodAfternoon": "下午好", + "goodEvening": "晚上好", + "welcomeBackToBotBrain": "欢迎回到 BotBrain", + "user": "用户", + "userAvatar": "用户头像", + "active": "活跃", + "now": "现在", + "justNow": "刚刚", + "minutesAgo": "{count} 分钟前", + "hoursAgo": "{count} 小时前", + "daysAgo": "{count} 天前", + "actionsThisWeek": "本周 {count} 次操作", + "dayStreak": "连续 {count} 天", + "daysStreak": "连续 {count} 天", + "userFor": "已使用 {duration}", + "currentTime": "当前时间", + "todaysDate": "今天日期", + "today": "今天", + "year": "年", + "years": "年", + "month": "个月", + "months": "个月", + "week": "周", + "weeks": "周", + "day": "天", + "days": "天", + "fleetSize": "Fleet 规模", + "onlineNow": "当前在线", + "totalActions": "总操作数", + "activeDays": "活跃天数", + "thisMonth": "本月", + "userSince": "用户注册于", + "new": "新用户", + "newMember": "新用户", + "favoriteRobot": "常用机器人", + "mostUsed": "最常用", + "noFavoriteSet": "未设置常用机器人", + "offline": "离线", + "actions": "次操作", + "veryActive": "非常活跃", + "quiet": "较少操作", + "thisWeek": "本周", + "avgPerDay": "平均 {count}/天", + "peakHour": "高峰时段", + "mostActive": "最活跃", + "missions": "任务", + "created": "已创建", + "aiDetections": "AI 检测", + "confidenceAbbrev": "置信度", + "yoloActive": "YOLO 活跃", + "soundLibrary": "音频库", + "audioClips": "音频片段", + "storage": "存储", + "used": "已用", + "activityPattern": "活动模式", + "last30Days": "最近 30 天", + "total": "总计", + "dayAverage": "日均 {count}", + "less": "较少", + "more": "较多", + "activityCellTitle": "{day} {hour}:00 - {count} 次操作", + "sundayShort": "日", + "mondayShort": "一", + "tuesdayShort": "二", + "wednesdayShort": "三", + "thursdayShort": "四", + "fridayShort": "五", + "saturdayShort": "六", + "noRobotsConfiguredYet": "还没有配置机器人", + "robotFleetUsage": "机器人 Fleet 使用情况", + "robotCount": "{count} 个机器人", + "robotCountSingular": "{count} 个机器人", + "peak": "峰值", + "showLess": "收起", + "showMore": "再显示 {count} 个", + "never": "从未", + "yourRobots": "你的机器人", + "robot": "机器人", + "live": "实时", + "connected": "已连接", + "connecting": "正在连接...", + "connect": "连接", + "noRobotsYet": "还没有机器人", + "getStartedAddFirstRobot": "添加第一个机器人即可开始", + "addYourFirstRobot": "添加第一个机器人", + "yourRobotFleet": "你的机器人 Fleet", + "yourRobotFleetDescription": "管理并监控已连接的机器人", + "manageFleet": "管理 Fleet", + "addRobot": "添加机器人", + "connectNewDevice": "连接新设备", + "robotFleet": "机器人 Fleet", + "robotFleetDescription": "管理并连接整个机器人 Fleet,实时监控状态", + "quickAccess": "快速入口", + "quickAccessDescription": "你最常用的功能和工具", + "recentActivity": "最近活动", + "yourLatestActions": "你的最新操作", + "viewAll": "查看全部", + "noRecentActivity": "暂无最近活动", + "weeklyProgress": "每周进度", + "activityOverTheWeek": "本周活动情况", + "actionsPerformed": "已执行操作", + "actionPerformed": "已执行操作", + "totalCount": "总计 {count}", + "achievements": "成就", + "yourMilestones": "你的里程碑", + "unlocked": "已解锁", + "progress": "进度", + "mondayAbbrev": "周一", + "tuesdayAbbrev": "周二", + "wednesdayAbbrev": "周三", + "thursdayAbbrev": "周四", + "fridayAbbrev": "周五", + "saturdayAbbrev": "周六", + "sundayAbbrev": "周日", + "achievementFirstConnection": "首次连接", + "achievementFirstConnectionDescription": "连接你的第一个机器人", + "achievementFleetCommander": "Fleet 指挥官", + "achievementFleetCommanderDescription": "管理 5 个以上机器人", + "achievementPowerUser": "高级用户", + "achievementPowerUserDescription": "执行 1000 次以上操作", + "achievementGlobalOperator": "远程操作员", + "achievementGlobalOperatorDescription": "远程控制机器人" + }, + "maps": { + "pageTitle": "地图管理", + "pageDescription": "查看并管理机器人的导航地图", + "liveCamera": "实时摄像头", + "robotControls": "机器人控制", + "showJoysticks": "显示 Joystick", + "hideJoysticks": "隐藏 Joystick", + "hidePanel": "隐藏面板", + "showControlPanel": "显示控制面板", + "mapViewer": "地图查看器", + "currentMap": "当前地图", + "mapView": "地图视图", + "mappingControl": "建图控制", + "mapNamePlaceholder": "输入地图名称...", + "mappingLabel": "正在建图:{mapName}", + "stopMapping": "停止建图", + "startMapping": "开始建图", + "startNewMap": "开始新地图", + "cancel": "取消", + "availableMaps": "可用地图", + "refreshMaps": "刷新地图列表", + "connectToRobotToViewMaps": "连接机器人后查看地图", + "noMapsAvailable": "暂无可用地图", + "deleting": "正在删除...", + "loading": "正在加载...", + "active": "已激活", + "loadMap": "加载地图", + "load": "加载", + "delete": "删除", + "deleteMap": "删除地图", + "deleteMapTitle": "删除“{mapName}”?", + "deleteMapWarning": "此操作无法撤销。", + "mapLoadedTitle": "地图已加载", + "mapLoadedMessage": "已成功加载并激活地图:{mapName}", + "mapLoadedShortMessage": "已成功加载:{mapName}", + "failedToLoadMapTitle": "地图加载失败", + "failedToLoadMapMessage": "无法加载地图:{mapName}", + "failedToLoadMapShortMessage": "无法加载:{mapName}", + "failedToDeleteMapTitle": "地图删除失败", + "failedToDeleteMapMessage": "无法删除地图:{mapName}", + "setHomePositionTitle": "设置 Home 位置", + "setHomePositionMessage": "点击地图设置 Home 位置,然后拖动设置朝向", + "cancelSetHome": "取消设置 Home", + "setHomePositionTooltip": "设置 Home 位置", + "stopNavigation": "停止导航", + "cancelNavigationMode": "取消导航模式", + "navigateToLocation": "导航到位置", + "clearNavigationGoal": "清除导航目标", + "zoomIn": "放大", + "zoomOut": "缩小", + "resetView": "重置视图", + "centerOnRobot": "居中到机器人", + "stoppingNavigation": "正在停止导航...", + "stopNavigationClick": "停止导航 - 点击停止机器人", + "cancelGoalSetting": "取消设置目标", + "setNavigationGoal": "设置导航目标", + "hideNavigationPlan": "隐藏导航路径", + "showNavigationPlan": "显示导航路径", + "refreshMapData": "刷新地图数据", + "hideLocalCostmap": "隐藏 local costmap", + "showLocalCostmap": "显示 local costmap", + "failedToSetHomeTitle": "设置 Home 失败", + "failedToSetHomeMessage": "无法设置 Home 位置。请检查 ROS 连接。", + "loadingMap": "正在加载地图...", + "failedToLoadMapGeneric": "地图加载失败", + "useFallbackRenderer": "使用备用渲染器", + "noMapTopicsAvailable": "没有可用的地图 Topic。请确认 map_server 或 navigation 正在运行。", + "failedToLoadMapLibraries": "地图组件库加载失败", + "failedToCreateMapClient": "创建地图客户端失败", + "failedToInitializeMapViewer": "初始化地图查看器失败", + "navigationStoppedTitle": "导航已停止", + "navigationStoppedMessage": "机器人导航已取消", + "stopFailedTitle": "停止失败", + "stopFailedMessage": "停止导航失败,请重试。", + "stopErrorMessage": "停止导航时发生错误。", + "navigationStartedTitle": "导航已开始", + "navigationStartedMessage": "正在导航到目标位置", + "navigationFailedTitle": "导航失败", + "navigationFailedMessage": "启动导航失败。请检查 Nav2 是否正在运行。", + "failedParsePgm": "解析 PGM 文件失败。请确认它是有效的 P5 格式。", + "failedLoadPng": "加载 PNG 文件失败。请确认它是有效的 PNG 图像。", + "selectPgmOrPng": "请选择 PGM 或 PNG 文件。", + "failedParseYaml": "解析 YAML 文件失败。", + "uploadMapPgmPng": "上传 Map(PGM/PNG)", + "uploadYaml": "上传 YAML", + "saveMap": "保存 Map", + "unknown": "未知", + "showBaseMap": "显示基础地图", + "showCostmap": "显示 Costmap", + "costmap": "Costmap", + "baseMap": "基础地图", + "robotPosition": "机器人位置", + "walls": "墙体", + "obstacles": "障碍物", + "navigating": "正在导航...", + "navGoal": "导航目标", + "clickOnMapToSetNavigationGoal": "点击地图设置导航目标", + "missionProgress": "任务:{current}/{total}", + "goalSet": "目标已设置", + "navigatingToDestination": "正在导航到目标位置...", + "robot": "机器人", + "goal": "目标", + "mission": "任务", + "plan": "路径", + "setNavigationGoalInstructions": "点击设置导航目标 - 拖动设置朝向 - 按 ESC 取消", + "setHomePositionInstructions": "点击设置 Home 位置 - 拖动设置朝向 - 按 ESC 取消", + "settingHomePosition": "正在设置 Home 位置", + "position": "位置", + "mapConnected": "Map 已连接" + }, + "missions": { + "title": "任务", + "createNewMission": "新建任务", + "missionNamePlaceholder": "任务名称...", + "create": "创建", + "cancel": "取消", + "noMissionsYet": "还没有任务", + "createFirstMission": "创建第一个任务", + "active": "活动", + "paused": "已暂停", + "running": "运行中", + "created": "创建于", + "unknown": "未知", + "waypoint": "waypoint", + "waypoints": "waypoints", + "waypointShort": "wp", + "waypointsShort": "wps", + "edit": "编辑", + "delete": "删除", + "deleteMissionConfirm": "删除任务“{missionName}”?", + "missionEditor": "任务编辑器", + "missionTitle": "任务:{missionName}", + "missionRunning": "任务运行中", + "missionPaused": "任务已暂停", + "pending": "待执行", + "reached": "已到达", + "navPlan": "Nav Plan", + "toggleNavPlanOverlay": "切换导航路径叠加层", + "savingWaypoints": "正在保存 waypoints...", + "stop": "停止", + "start": "开始", + "resume": "继续", + "switchToCorrectMapFirst": "请先切换到正确的地图", + "resumeMission": "继续任务", + "startMissionTooltip": "开始任务", + "addWaypointsFirst": "请先添加 waypoints", + "noMissionSelected": "未选择任务", + "noMissionSelectedDescription": "从左侧面板选择任务,或创建新任务以开始规划 waypoints", + "openMissionList": "打开任务列表", + "cameraView": "摄像头视图", + "robotControls": "机器人控制", + "mapSwitchedTitle": "地图已切换", + "mapSwitchedMessage": "已加载地图:{mapName}", + "failedToSwitchMapTitle": "地图切换失败", + "failedToSwitchMapMessage": "无法加载任务地图", + "failedToLoadMissionsTitle": "任务加载失败", + "refreshPageMessage": "请尝试刷新页面", + "missionCreatedTitle": "任务已创建", + "missionCreatedMessage": "任务“{missionName}”已创建", + "failedToCreateMissionTitle": "任务创建失败", + "tryAgainMessage": "请重试", + "missionUpdatedTitle": "任务已更新", + "missionUpdatedMessage": "任务已重命名为“{missionName}”", + "failedToUpdateMissionTitle": "任务更新失败", + "missionDeletedTitle": "任务已删除", + "missionDeletedMessage": "任务已成功删除", + "failedToDeleteMissionTitle": "任务删除失败", + "savingWaypointsTitle": "正在保存 waypoints", + "savingWaypointsMessage": "请稍候,正在保存 waypoints...", + "noWaypointsFoundTitle": "未找到 waypoints", + "noWaypointsFoundMessage": "请添加 waypoints 并等待保存完成", + "missionStartedTitle": "任务已开始", + "missionStartedMessage": "任务“{missionName}”已激活,包含 {count} 个 waypoints", + "failedToStartMissionTitle": "任务启动失败", + "missionStoppedTitle": "任务已停止", + "missionStoppedMessage": "任务已停止", + "failedToStopMissionTitle": "任务停止失败", + "unknownError": "发生未知错误", + "failedToSaveWaypointsTitle": "waypoints 保存失败", + "changesMayNotHaveBeenSaved": "你的更改可能尚未保存", + "navigationCompleteTitle": "导航完成", + "navigationCompleteMessage": "任务已完成", + "navigationStartedTitle": "导航已开始", + "navigationFailedTitle": "导航失败", + "navigationErrorTitle": "导航错误", + "noWaypointsDefinedMessage": "未定义 waypoint。请先添加 waypoint。", + "nav2ActionUnavailableMessage": "Nav2 FollowWaypoints action server 不可用。请确认机器人上的 Nav2 正在运行。", + "navigatingThroughWaypoints": "正在通过 {count} 个 waypoints 导航", + "navigationCancelledTitle": "导航已取消", + "missionNavigationStopped": "任务导航已停止", + "cancelFailedTitle": "取消失败", + "cancelFailedMessage": "取消导航失败。请检查机器人连接。", + "cancelErrorTitle": "取消错误", + "cancelErrorMessage": "取消导航时发生错误", + "successfullyReachedAllWaypoints": "已成功到达所有 waypoints!", + "toggleEditMode": "切换编辑模式(E)", + "exitEdit": "退出编辑", + "clearAllWaypoints": "清除所有 waypoints", + "clear": "清除", + "waitingForMapDataFrom": "正在等待来自 {topic} 的地图数据", + "mayTake15Seconds": "这最多可能需要 15 秒", + "widgetName": "Widget 名称", + "widgetNamePlaceholder": "输入 Widget 名称", + "refreshMissions": "刷新任务", + "loading": "正在加载...", + "connectToRobotToViewMissions": "连接机器人后查看任务", + "loadMapToSeeMissions": "加载 Map 后查看任务", + "noMissionsForCurrentMap": "当前 Map 没有关联任务", + "stopMission": "停止任务", + "startMission": "开始任务", + "mapMismatchTitle": "Map 不匹配", + "mapMismatchCurrent": "任务创建于 {missionMapName},当前已加载 {currentMapName}", + "mapMismatchNoCurrentMap": "任务创建于 {missionMapName},但当前未加载 Map", + "switchMap": "切换 Map", + "retryLoadingMap": "重试加载 Map", + "mapServerRunningHelp": "请确认机器人上的 map server 正在运行", + "robotPosition": "机器人位置", + "navigatingProgress": "正在导航({current}/{total})", + "nav2Disconnected": "Nav2 未连接", + "navigationPlan": "导航路径", + "costmap": "Costmap" + }, + "auditLog": { + "pageTitle": "审计日志", + "pageDescription": "追踪应用中的所有活动和事件", + "advancedPageTitle": "高级审计系统", + "advancedPageDescription": "全面追踪并分析所有操作员活动", + "loading": "正在加载审计日志...", + "loadingData": "正在加载审计数据...", + "searchPlaceholder": "搜索日志...", + "totalEvents": "事件总数", + "eventsToday": "今日事件", + "activeRobots": "活跃机器人", + "criticalEvents": "关键事件", + "lastRange": "最近 {range}", + "allTime": "全部时间", + "daysRange": "{count} 天", + "sinceMidnight": "自午夜以来", + "mostActiveRobot": "最活跃:{robotName}", + "noRobotsActive": "暂无活跃机器人", + "safetyAndFailures": "安全与故障", + "analytics": "分析", + "timeline": "时间线", + "last24Hours": "最近 24 小时", + "last7Days": "最近 7 天", + "last30Days": "最近 30 天", + "last90Days": "最近 90 天", + "clear": "清空", + "eventDistributionByType": "按类型分布的事件", + "activityPattern24h": "24 小时活动模式", + "hourlyActivityTitle": "{hour}:00 - {count} 个事件", + "mostFrequentActions": "最常见动作", + "time": "时间", + "allEvents": "全部事件", + "authentication": "认证", + "robot": "机器人", + "commands": "命令", + "system": "系统", + "data": "数据", + "mission": "任务", + "navigation": "导航", + "audio": "音频", + "camera": "摄像头", + "safety": "安全", + "export": "导出", + "refresh": "刷新", + "exportButton": "导出", + "deleteAll": "全部删除", + "clearAll": "全部清空", + "testLog": "测试日志", + "createTestLogTitle": "创建一条测试审计日志", + "dateAndTime": "日期和时间", + "type": "类型", + "action": "动作", + "details": "详情", + "noAuditLogsFound": "未找到审计日志", + "viewDetails": "查看详情", + "eventsLabel": "{type} 事件", + "deleteAllTitle": "删除所有审计日志", + "deleteAllConfirm": "确定要删除全部 {count} 条审计日志吗?此操作无法撤销。", + "clearAllTitle": "清空所有审计日志", + "clearAllConfirm": "确定要清空全部 {count} 条审计日志吗?此操作无法撤销。", + "deleting": "正在删除...", + "dateCsv": "日期", + "timeCsv": "时间", + "eventTypeCsv": "事件类型", + "actionCsv": "动作", + "robotCsv": "机器人", + "robotIdCsv": "机器人 ID", + "ipAddressCsv": "IP 地址", + "userAgentCsv": "User Agent", + "detailsCsv": "详情", + "testAuditMessage": "测试审计日志条目" + }, + "aiDetections": { + "recentTitle": "最近 AI 检测", + "failedToLoad": "检测结果加载失败", + "showingRecent": "显示最近 {count} 条", + "autoRefreshEnabled": "自动刷新已启用", + "autoRefreshDisabled": "自动刷新已停用", + "live": "实时", + "paused": "已暂停", + "noDetectionsYet": "还没有检测结果", + "viewAllDetections": "查看全部检测", + "settingsTitle": "检测上传设置", + "settingsDescription": "配置哪些检测结果会保存到 database", + "loadingCurrentSettings": "正在加载当前设置...", + "couldNotLoadSettings": "无法加载当前设置:{error}", + "retry": "重试", + "robotNotConnectedSettings": "机器人未连接。连接后才能加载并应用设置。", + "minimumConfidence": "最低置信度", + "minimumConfidenceDescription": "只有置信度高于此阈值的检测结果才会保存", + "objectFilter": "对象过滤器", + "allObjects": "全部对象", + "selectedCount": "已选择 {count} 个", + "selectAll": "全选", + "clear": "清空", + "searchObjects": "搜索对象...", + "moreObjects": "+{count} 个", + "noObjectsMatch": "没有匹配的对象", + "allObjectsWillBeSaved": "所有检测到的对象都会保存。选择特定对象可进行过滤。", + "onlySelectedObjectsSaved": "只有选中的对象会保存到 database", + "settingsApplied": "设置已成功应用!", + "cancel": "取消", + "applying": "正在应用...", + "applySettings": "应用设置", + "failedToFetchSettings": "获取设置失败", + "uploadSettings": "上传设置", + "settings": "设置", + "time": "时间", + "images": "图片", + "confidence": "置信度", + "allTime": "全部时间", + "lastHour": "最近 1 小时", + "last6Hours": "最近 6 小时", + "last24Hours": "最近 24 小时", + "last7Days": "最近 7 天", + "all": "全部", + "withImages": "有图片", + "withoutImages": "无图片", + "anyConfidence": "任意置信度", + "high": "高", + "medium": "中", + "low": "低", + "clearFilters": "清除筛选", + "disableAutoRefresh": "关闭自动刷新", + "enableAutoRefresh": "每 2 秒自动刷新", + "realtime": "实时", + "of": "/", + "detection": "检测结果", + "detections": "检测结果", + "deleteDetectionsTitle": "删除 {count} 条{detectionLabel}", + "delete": "删除", + "moonDreamReady": "已准备好与 MoonDream 交互。选择模式并提交即可开始。", + "moonDreamCaption": "图像描述", + "moonDreamCaptionDescription": "生成图像描述", + "moonDreamQueryNoReasoning": "查询 - 无推理", + "moonDreamQueryNoReasoningDescription": "询问关于图像的问题", + "moonDreamQueryWithReasoning": "查询 - 带推理", + "moonDreamQueryWithReasoningDescription": "提出需要详细推理的问题", + "moonDreamDetect": "检测", + "moonDreamDetectDescription": "检测图像中的指定对象", + "moonDreamPoint": "指点", + "moonDreamPointDescription": "定位图像中的指定位置", + "moonDreamQuestionPlaceholder": "询问关于图像的问题...", + "moonDreamReasoningPlaceholder": "提出需要详细分析的问题...", + "moonDreamDetectPlaceholder": "描述要检测的内容...", + "moonDreamPointPlaceholder": "描述要指向的位置...", + "moonDreamQuestionLabel": "问题", + "moonDreamDescriptionLabel": "描述", + "moonDreamAnswerLabel": "答案", + "moonDreamFailedCaption": "生成图像描述失败", + "moonDreamEnterQuestion": "请输入问题", + "moonDreamFailedQuestion": "回答问题失败", + "moonDreamFailedReasoning": "带推理的问题回答失败", + "moonDreamEnterDescription": "请输入描述", + "moonDreamDetectResult": "找到 {count} 个匹配“{input}”的{objectLabel}。{overlayMessage}", + "moonDreamObjectSingular": "对象", + "moonDreamObjectPlural": "对象", + "moonDreamBoundingBoxesShown": "紫色边界框已显示在实时画面上。", + "moonDreamFailedDetect": "检测对象失败", + "moonDreamPointResult": "为“{input}”识别出 {count} 个{pointLabel}。{overlayMessage}", + "moonDreamPointSingular": "点", + "moonDreamPointPlural": "点", + "moonDreamRedMarkersShown": "红色标记已显示在实时画面上。", + "moonDreamFailedPoint": "识别点位失败", + "moonDreamUnexpectedError": "发生意外错误,请重试。", + "moonDreamOverlaysCleared": "已从实时画面清除叠加标记。", + "moonDreamClearOverlaysTitle": "清除实时画面上的所有边界框和点位", + "moonDreamClear": "清除", + "moonDreamAllOverlaysCleared": "已从实时画面清除所有叠加标记。", + "moonDreamClearAllOverlaysTitle": "清除所有叠加标记", + "moonDreamClearAll": "全部清除", + "moonDreamResponse": "MoonDream 响应", + "moonDreamProcessing": "正在处理...", + "moonDreamSend": "发送", + "moonDreamGenerating": "正在生成...", + "moonDreamGenerateCaption": "生成图像描述", + "moonDreamBoxSingular": "边界框", + "moonDreamBoxPlural": "边界框", + "moonDreamMode": "模式", + "moonDreamInput": "输入" + }, + "help": { + "pageTitle": "帮助中心", + "pageSubtitle": "获取支持、访问文档并了解安全指南", + "mySupportTitle": "我的支持", + "mySupportDescription": "你的专属支持代表可以帮助解答问题或处理故障。", + "mySupportAvailability": "周一至周五 9:00-18:00 BRT 可用", + "userManualTitle": "用户手册", + "userManualDescription": "访问 BotBrain 机器人的完整文档和指南。", + "userManualViewButton": "查看用户手册", + "userManualFormat": "PDF 格式 • 最后更新:2024 年 12 月", + "safetyTipsTitle": "通用安全提示", + "safetyTipsDescription": "请遵循这些基本指南,安全操作机器人。", + "safetyTip1Title": "保持安全距离", + "safetyTip1Description": "机器人运行时至少保持 1 米距离,避免碰撞。", + "safetyTip2Title": "检查工作状态", + "safetyTip2Description": "使用前确认机器人工作状态正常且没有错误指示。", + "safetyTip3Title": "在规格范围内使用", + "safetyTip3Description": "始终在制造商规格范围内并在监督下操作机器人。", + "additionalHelpTitle": "还需要帮助?", + "additionalHelpDescription": "找不到需要的信息?我们的支持团队随时可以帮助解答关于 BotBrain 的问题。", + "additionalHelpContactButton": "联系支持" + }, + "health": { + "pageTitle": "健康监控", + "connected": "已连接", + "connecting": "正在连接...", + "connect": "连接", + "disconnected": "未连接", + "loadingTitle": "正在加载健康数据", + "loadingDescription": "正在等待 diagnostics 数据...", + "notConnectedTitle": "未连接机器人", + "notConnectedDescription": "连接机器人以查看实时健康监控数据。", + "systemInformation": "系统信息", + "model": "型号", + "powerMode": "电源模式", + "uptime": "运行时间", + "jetpack": "Jetpack", + "serialNumber": "序列号", + "jetsonClocks": "Jetson Clocks", + "active": "启用", + "inactive": "未启用", + "cpuUsage": "CPU 使用率", + "core": "核心", + "noCpuData": "暂无 CPU 数据", + "gpuStatus": "GPU 状态", + "usage": "使用率", + "frequency": "频率", + "memory": "内存", + "ram": "RAM", + "swap": "Swap", + "temperature": "温度", + "cpu": "CPU", + "soc0": "SOC0", + "soc1": "SOC1", + "soc2": "SOC2", + "tj": "TJ", + "powerUsage": "功耗", + "avg": "平均", + "current": "当前", + "noPowerRailData": "暂无 power rail 数据", + "fanControl": "风扇控制", + "speed": "速度", + "mode": "模式", + "control": "控制", + "storage": "存储", + "used": "已用", + "percentUsed": "% 已用", + "connectivity": "连接", + "networkMode": "网络模式", + "latency": "延迟", + "download": "下载", + "upload": "上传", + "connectionQuality": "连接质量", + "mhz": "MHz", + "watts": "W", + "milliwatts": "mW", + "gigabytes": "GB", + "celsius": "°C", + "notAvailable": "N/A", + "stateMachineStatus": "状态机状态", + "status": "状态", + "id": "ID", + "node": "Node", + "state": "状态", + "actions": "操作", + "start": "启动", + "stop": "停止", + "reset": "重置", + "close": "关闭", + "viewModuleId": "查看模块 ID", + "moduleDetails": "模块详情", + "moreValues": "+{count} 个更多值", + "powerRails": "Power Rails", + "robotDiagnostics": "Diagnostics", + "waitingForData": "正在等待数据...", + "rebooting": "正在重启...", + "rebootSystem": "重启系统", + "wifi": "WiFi", + "cellular": "4G/蜂窝网络", + "hotspot": "Hotspot", + "offline": "离线", + "noNetwork": "无网络", + "measuring": "正在测量...", + "excellent": "优秀", + "good": "良好", + "fair": "一般", + "poor": "较差", + "incomingData": "入站数据", + "outgoingData": "出站数据", + "individualCores": "各 CPU 核心", + "waitingForCpuData": "正在等待 CPU 数据...", + "error": "错误", + "errors": "错误", + "warning": "警告", + "warnings": "警告", + "allSystemsOk": "所有系统正常", + "activeIssues": "当前问题", + "systemsOperatingNormally": "{count} 个系统运行正常", + "connectToRobotToViewDiagnostics": "连接机器人以查看 diagnostics", + "confirmSystemReboot": "确认重启系统", + "confirmSystemRebootDescription": "确定要重启系统吗?这会暂时断开所有服务,机器人将在几分钟内不可用。", + "rebootNow": "立即重启", + "wifiSettings": "WiFi 设置", + "scanForNetworks": "扫描网络", + "modeLabel": "模式:{mode}", + "ipAddress": "IP:{ip}", + "activeStatus": "活动", + "wifiStateLabel": "WiFi {state}", + "enabled": "已启用", + "disabled": "已禁用", + "connectToRobotToToggleWifi": "连接机器人后切换 WiFi", + "clickToDisableWifi": "点击禁用 WiFi", + "clickToEnableWifi": "点击启用 WiFi", + "wifiDisabledHelp": "WiFi 已禁用。启用 WiFi 后可扫描网络并管理连接。", + "availableNetworks": "可用网络", + "scanError": "扫描错误", + "noNetworksFound": "未找到网络", + "savedNetworks": "已保存网络({count})", + "refreshSavedNetworks": "刷新已保存网络", + "connectedTag": "(已连接)", + "connectToNetwork": "连接到 {networkName}", + "forgetNetwork": "忘记 {networkName}", + "noSavedNetworksFound": "未找到已保存网络", + "connectToSsid": "连接到 {ssid}", + "password": "密码", + "username": "用户名", + "wifiPasswordPlaceholder": "输入 WiFi 密码", + "usernamePlaceholder": "输入用户名", + "passwordPlaceholder": "输入密码", + "saveNetworkForAutomaticConnection": "保存网络用于自动连接", + "wifiRadioTitle": "WiFi Radio", + "wifiEnabledSuccess": "WiFi 已成功启用", + "wifiDisabledSuccess": "WiFi 已成功禁用", + "wifiToggleFailedHardware": "WiFi 切换失败,请检查机器人 WiFi 硬件", + "failedToEnableWifi": "启用 WiFi 失败", + "failedToDisableWifi": "禁用 WiFi 失败", + "networkScanTitle": "网络扫描", + "foundNetwork": "找到 {count} 个网络", + "foundNetworks": "找到 {count} 个网络", + "noNetworksFoundScanMessage": "未找到网络。请确认机器人已启用 WiFi。", + "failedToScanNetworks": "扫描网络失败。请检查机器人连接。", + "wifiConnectionTitle": "WiFi 连接", + "connectedToNetwork": "已连接到 {networkName}", + "failedToConnectNetwork": "无法连接到 {networkName}", + "failedToConnectSavedNetwork": "无法连接到已保存网络", + "failedToConnectSavedNetworkMessage": "无法连接到 {networkName}。已保存配置可能已过期。", + "notConnectedToRobot": "未连接机器人", + "connectionTimeout": "连接超时", + "forgetNetworkConfirm": "确定要忘记网络“{networkName}”吗?再次连接时需要重新输入密码。", + "failedToForgetNetwork": "忘记网络失败", + "networkForgottenTitle": "网络已忘记", + "networkForgottenMessage": "已从已保存网络中移除 {networkName}", + "forgetNetworkTitle": "忘记网络", + "failedToForgetNetworkMessage": "无法忘记 {networkName}" + }, + "labs": { + "pageTitle": "实验室", + "pageDescription": "欢迎来到实验室,这里用于在生产前测试实验性功能。", + "chat": "聊天", + "mapEdit": "地图编辑", + "charts": "图表", + "auditLog": "审计日志", + "blockProgramming": "积木编程", + "help": "帮助", + "missions": "任务", + "ai": "AI", + "soundboard": "音效板", + "weather": "天气", + "chatDescription": "用于自然机器人交互的 AI 聊天界面。", + "mapEditDescription": "用于定制机器人导航路径的高级地图编辑工具。", + "chartsDescription": "实时数据可视化和分析仪表盘。", + "auditLogDescription": "完整审计轨迹,记录所有用户操作和系统事件。", + "blockProgrammingDescription": "用于直观机器人控制和自动化的可视化编程界面。", + "helpDescription": "完整文档和支持资源。", + "missionsDescription": "自主导航任务规划与执行。", + "aiDescription": "面向机器人的高级 AI 能力和模型管理。", + "soundboardDescription": "带 TTS 预设和声音片段管理的交互式音频控制,用于机器人音频播放。", + "weatherDescription": "查看当前天气情况的简单天气面板。", + "alpha": "Alpha", + "beta": "Beta", + "alphaFeatures": "Alpha 功能", + "alphaNotice": "非常早期的概念验证。这些功能限制很多,可能存在明显问题,也可能无法按预期工作。请自行承担风险,并预期频繁变更或移除。", + "betaFeatures": "Beta 功能", + "betaNotice": "已有部分可用能力,但仍可能不稳定或存在问题。虽然比 alpha 功能更可靠,但仍处于活跃开发中,可能发生变化。" + }, + "confirmationDialog": { + "title": "确定吗?", + "message": "你即将执行:{action}", + "confirm": "确认", + "cancel": "取消" + }, + "soundClips": { + "title": "声音片段", + "record": "录制", + "add": "添加", + "addClip": "添加片段", + "recording": "正在录制...", + "recordingDefaultName": "录制", + "recordingReady": "录制就绪", + "ready": "就绪", + "recordingNamePlaceholder": "录制名称...", + "soundClipNamePlaceholder": "声音片段名称...", + "clipNamePlaceholder": "片段名称", + "saving": "正在保存...", + "save": "保存", + "cancel": "取消", + "uploading": "正在上传...", + "upload": "上传", + "clipsCount": "{count} 个片段", + "loginRequired": "请登录后管理声音片段", + "emptyTitle": "还没有声音片段", + "emptyDescription": "点击“录制”或“添加片段”开始", + "emptyDescriptionShort": "点击“录制”或“添加”开始", + "failedMicrophoneAccess": "无法访问麦克风。请检查浏览器权限。", + "fileSizeLimit": "文件大小必须小于 1GB", + "audioFileRequired": "请上传音频文件", + "failedUploadRecording": "上传录制失败", + "failedUploadSoundClip": "上传声音片段失败", + "failedDeleteSoundClip": "删除声音片段失败", + "failedUpdateSoundClip": "更新声音片段失败", + "failedToPlay": "声音片段播放失败", + "deleteConfirm": "删除“{clipName}”?", + "playInBrowser": "在浏览器中播放", + "connectToRobotToPlay": "连接机器人后可在机器人上播放", + "playOnRobot": "在机器人上播放“{clipName}”", + "clickToPlayOnRobot": "点击在机器人上播放" + }, + "weather": { + "unknownLocation": "未知位置", + "locationUnavailable": "无法获取位置。请启用定位服务。", + "geolocationUnsupported": "你的浏览器不支持 Geolocation。", + "failedToFetch": "获取天气数据失败,请重试。", + "attribution": "天气数据由 MET Norway API 提供。位置通过浏览器 Geolocation API 获取。只会向天气服务共享你的坐标以获取本地天气,数据每 5 分钟更新一次。" + }, + "fleet": { + "pageTitle": "Fleet 管理", + "addRobot": "添加机器人", + "addNewRobot": "添加新机器人", + "editRobot": "编辑机器人", + "noRobotsTitle": "你的 Fleet 中还没有机器人", + "noRobotsDescription": "添加第一台机器人即可开始", + "robotName": "机器人名称", + "robotNamePlaceholder": "输入机器人名称", + "address": "地址", + "addressPlaceholder": "例如:192.168.1.95", + "addressHelpText": "使用与当前机器人连接系统相同的格式", + "key": "Key(可选)", + "keyPlaceholder": "安全 Key(未来功能)", + "keyHelpText": "安全 Key 将在未来更新中实现", + "robotType": "机器人类型", + "connected": "已连接", + "connecting": "正在连接...", + "disconnected": "未连接", + "connect": "连接", + "disconnect": "断开连接", + "switchTo": "切换到", + "setAsFavorite": "设为收藏", + "editRobotTooltip": "编辑机器人", + "deleteRobotTooltip": "删除机器人", + "deleteRobotTitle": "删除机器人?", + "deleteRobotMessage": "确定要删除", + "deleteRobotWarning": "此操作无法撤销。", + "type": "类型", + "favoriteRobot": "收藏机器人", + "cancel": "取消", + "delete": "删除", + "deleting": "正在删除...", + "update": "更新", + "saving": "正在保存...", + "checkingAuth": "正在检查认证...", + "loadingRobots": "正在加载机器人...", + "refresh": "刷新", + "checking": "正在检查..." + } +};