feat(i18n): adicionar suporte a internacionalização com react-i18next

Implementa infraestrutura de i18n com suporte inicial a English e
Português (Brasil). Seletor de idioma em Settings > System > Preferences.

- Instala react-i18next e i18next
- Cria arquivos de tradução (en, pt-BR) para common, home, settings, layout
- Traduz layout (sidebar, header, footer), home e settings/system
- Persiste idioma via localStorage + KVStore (ui.language)
- DiskAlertBanner integrado com i18n

Closes #486
This commit is contained in:
LuisMIguelFurlanettoSousa 2026-03-23 11:16:01 -03:00
parent 67fc9290d8
commit 2d08533b7e
16 changed files with 568 additions and 128 deletions

View File

@ -1,6 +1,7 @@
/// <reference path="../../adonisrc.ts" />
/// <reference path="../../config/inertia.ts" />
import '~/lib/i18n'
import '../css/app.css'
import { createRoot } from 'react-dom/client'
import { createInertiaApp } from '@inertiajs/react'

View File

@ -1,4 +1,5 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePage } from '@inertiajs/react'
import { UsePageProps } from '../../types/system'
import ThemeToggle from '~/components/ThemeToggle'
@ -6,6 +7,7 @@ import { IconBug } from '@tabler/icons-react'
import DebugInfoModal from './DebugInfoModal'
export default function Footer() {
const { t } = useTranslation('layout')
const { appVersion } = usePage().props as unknown as UsePageProps
const [debugModalOpen, setDebugModalOpen] = useState(false)
@ -13,7 +15,7 @@ export default function Footer() {
<footer>
<div className="flex items-center justify-center gap-3 border-t border-border-subtle py-4">
<p className="text-sm/6 text-text-secondary">
Project N.O.M.A.D. Command Center v{appVersion}
{t('footer.version', { version: appVersion })}
</p>
<span className="text-gray-300">|</span>
<button
@ -21,7 +23,7 @@ export default function Footer() {
className="text-sm/6 text-gray-500 hover:text-desert-green flex items-center gap-1 cursor-pointer"
>
<IconBug className="size-3.5" />
Debug Info
{t('footer.debugInfo')}
</button>
<ThemeToggle />
</div>

View File

@ -12,43 +12,45 @@ import {
IconZoom
} from '@tabler/icons-react'
import { usePage } from '@inertiajs/react'
import { useTranslation } from 'react-i18next'
import StyledSidebar from '~/components/StyledSidebar'
import { getServiceLink } from '~/lib/navigation'
import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
import { SERVICE_NAMES } from '../../constants/service_names'
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const { t } = useTranslation('layout')
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const aiAssistantInstallStatus = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
const navigation = [
...(aiAssistantInstallStatus.isInstalled ? [{ name: aiAssistantName, href: '/settings/models', icon: IconWand, current: false }] : []),
{ name: 'Apps', href: '/settings/apps', icon: IconTerminal2, current: false },
{ name: 'Benchmark', href: '/settings/benchmark', icon: IconChartBar, current: false },
{ name: 'Content Explorer', href: '/settings/zim/remote-explorer', icon: IconZoom, current: false },
{ name: 'Content Manager', href: '/settings/zim', icon: IconFolder, current: false },
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
{ name: t('sidebar.apps'), href: '/settings/apps', icon: IconTerminal2, current: false },
{ name: t('sidebar.benchmark'), href: '/settings/benchmark', icon: IconChartBar, current: false },
{ name: t('sidebar.contentExplorer'), href: '/settings/zim/remote-explorer', icon: IconZoom, current: false },
{ name: t('sidebar.contentManager'), href: '/settings/zim', icon: IconFolder, current: false },
{ name: t('sidebar.mapsManager'), href: '/settings/maps', icon: IconMapRoute, current: false },
{
name: 'Service Logs & Metrics',
name: t('sidebar.serviceLogs'),
href: getServiceLink('9999'),
icon: IconDashboard,
current: false,
target: '_blank',
},
{
name: 'Check for Updates',
name: t('sidebar.checkUpdates'),
href: '/settings/update',
icon: IconArrowBigUpLines,
current: false,
},
{ name: 'System', href: '/settings/system', icon: IconSettings, current: false },
{ name: 'Support the Project', href: '/settings/support', icon: IconHeart, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
{ name: t('sidebar.system'), href: '/settings/system', icon: IconSettings, current: false },
{ name: t('sidebar.support'), href: '/settings/support', icon: IconHeart, current: false },
{ name: t('sidebar.legal'), href: '/settings/legal', icon: IconGavel, current: false },
]
return (
<div className="min-h-screen flex flex-row bg-surface-secondary/90">
<StyledSidebar title="Settings" items={navigation} />
<StyledSidebar title={t('sidebar.settings')} items={navigation} />
{children}
</div>
)

46
admin/inertia/lib/i18n.ts Normal file
View File

@ -0,0 +1,46 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
// Import all translation files
import enCommon from '~/locales/en/common.json'
import enHome from '~/locales/en/home.json'
import enSettings from '~/locales/en/settings.json'
import enLayout from '~/locales/en/layout.json'
import ptBRCommon from '~/locales/pt-BR/common.json'
import ptBRHome from '~/locales/pt-BR/home.json'
import ptBRSettings from '~/locales/pt-BR/settings.json'
import ptBRLayout from '~/locales/pt-BR/layout.json'
const savedLanguage = (() => {
try {
return localStorage.getItem('nomad:language') || 'en'
} catch {
return 'en'
}
})()
i18n.use(initReactI18next).init({
resources: {
en: {
common: enCommon,
home: enHome,
settings: enSettings,
layout: enLayout,
},
'pt-BR': {
common: ptBRCommon,
home: ptBRHome,
settings: ptBRSettings,
layout: ptBRLayout,
},
},
lng: savedLanguage,
fallbackLng: 'en',
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
})
export default i18n

View File

@ -0,0 +1,29 @@
{
"buttons": {
"save": "Save",
"cancel": "Cancel",
"confirm": "Confirm",
"goToSettings": "Go to Settings",
"reinstall": "Reinstall"
},
"alerts": {
"updateAvailable": "An update is available for Project N.O.M.A.D.!",
"diskWarningTitle": "Disk Space Running Low",
"diskCriticalTitle": "Disk Space Critically Low",
"diskMessage": "Disk \"{{diskName}}\" is {{usage}}% full. Free up space to avoid issues with downloads and services.",
"highMemoryTitle": "Very High Memory Usage Detected",
"highMemoryMessage": "System memory usage exceeds 90%. Performance degradation may occur.",
"gpuNotAccessibleTitle": "GPU Not Accessible to AI Assistant",
"gpuNotAccessibleMessage": "Your system has an NVIDIA GPU, but the AI Assistant can't access it. AI is running on CPU only, which is significantly slower.",
"fixReinstallAI": "Fix: Reinstall AI Assistant",
"reinstallAIConfirmTitle": "Reinstall AI Assistant?",
"reinstallAIConfirmMessage": "This will recreate the AI Assistant container with GPU support enabled. Your downloaded models will be preserved. The service will be briefly unavailable during reinstall.",
"reinstallSuccess": "AI Assistant is being reinstalled with GPU support. This page will reload shortly.",
"reinstallFailed": "Failed to reinstall: {{error}}"
},
"language": {
"label": "Language",
"en": "English",
"pt-BR": "Português (Brasil)"
}
}

View File

@ -0,0 +1,24 @@
{
"title": "Command Center",
"easySetup": {
"label": "Easy Setup",
"description": "Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!",
"badge": "Start here!"
},
"installApps": {
"label": "Install Apps",
"description": "Not seeing your favorite app? Install it here!"
},
"docs": {
"label": "Docs",
"description": "Read Project N.O.M.A.D. manuals and guides"
},
"settingsCard": {
"label": "Settings",
"description": "Configure your N.O.M.A.D. settings"
},
"maps": {
"label": "Maps",
"description": "View offline maps"
}
}

View File

@ -0,0 +1,25 @@
{
"header": {
"commandCenter": "Command Center"
},
"nav": {
"backToHome": "Back to Home"
},
"footer": {
"version": "Project N.O.M.A.D. Command Center v{{version}}",
"debugInfo": "Debug Info"
},
"sidebar": {
"settings": "Settings",
"apps": "Apps",
"benchmark": "Benchmark",
"contentExplorer": "Content Explorer",
"contentManager": "Content Manager",
"mapsManager": "Maps Manager",
"serviceLogs": "Service Logs & Metrics",
"checkUpdates": "Check for Updates",
"system": "System",
"support": "Support the Project",
"legal": "Legal Notices"
}
}

View File

@ -0,0 +1,57 @@
{
"system": {
"title": "System Information",
"subtitle": "Real-time monitoring and diagnostics",
"lastUpdated": "Last updated: {{time}}",
"refreshing": "Refreshing data every 30 seconds"
},
"sections": {
"resourceUsage": "Resource Usage",
"systemDetails": "System Details",
"memoryAllocation": "Memory Allocation",
"storageDevices": "Storage Devices",
"systemStatus": "System Status",
"preferences": "Preferences"
},
"labels": {
"cpuUsage": "CPU Usage",
"memoryUsage": "Memory Usage",
"swapUsage": "Swap Usage",
"cores": "{{count}} cores",
"utilized": "{{percent}}% Utilized",
"totalRam": "Total RAM",
"usedRam": "Used RAM",
"availableRam": "Available RAM",
"systemUptime": "System Uptime",
"cpuCores": "CPU Cores",
"storageDevicesCount": "Storage Devices",
"noStorageDetected": "No storage devices detected"
},
"os": {
"distribution": "Distribution",
"kernelVersion": "Kernel Version",
"architecture": "Architecture",
"hostname": "Hostname",
"platform": "Platform"
},
"cpu": {
"manufacturer": "Manufacturer",
"brand": "Brand",
"cores": "Cores",
"physicalCores": "Physical Cores",
"virtualization": "Virtualization",
"enabled": "Enabled",
"disabled": "Disabled"
},
"gpu": {
"title": "Graphics",
"model": "Model",
"vendor": "Vendor",
"vram": "VRAM"
},
"storage": {
"normal": "Normal",
"warningHigh": "Warning - Usage High",
"criticalFull": "Critical - Disk Almost Full"
}
}

View File

@ -0,0 +1,29 @@
{
"buttons": {
"save": "Salvar",
"cancel": "Cancelar",
"confirm": "Confirmar",
"goToSettings": "Ir para Configurações",
"reinstall": "Reinstalar"
},
"alerts": {
"updateAvailable": "Uma atualização está disponível para o Project N.O.M.A.D.!",
"diskWarningTitle": "Espaço em disco ficando baixo",
"diskCriticalTitle": "Espaço em disco criticamente baixo",
"diskMessage": "O disco \"{{diskName}}\" está {{usage}}% cheio. Libere espaço para evitar problemas com downloads e serviços.",
"highMemoryTitle": "Uso de memória muito alto detectado",
"highMemoryMessage": "O uso de memória do sistema excede 90%. Pode haver degradação de desempenho.",
"gpuNotAccessibleTitle": "GPU não acessível pelo Assistente de IA",
"gpuNotAccessibleMessage": "Seu sistema possui uma GPU NVIDIA, mas o Assistente de IA não consegue acessá-la. A IA está rodando apenas na CPU, o que é significativamente mais lento.",
"fixReinstallAI": "Corrigir: Reinstalar Assistente de IA",
"reinstallAIConfirmTitle": "Reinstalar Assistente de IA?",
"reinstallAIConfirmMessage": "Isso recriará o contêiner do Assistente de IA com suporte a GPU habilitado. Seus modelos baixados serão preservados. O serviço ficará brevemente indisponível durante a reinstalação.",
"reinstallSuccess": "O Assistente de IA está sendo reinstalado com suporte a GPU. Esta página será recarregada em breve.",
"reinstallFailed": "Falha ao reinstalar: {{error}}"
},
"language": {
"label": "Idioma",
"en": "English",
"pt-BR": "Português (Brasil)"
}
}

View File

@ -0,0 +1,24 @@
{
"title": "Central de Comando",
"easySetup": {
"label": "Configuração Rápida",
"description": "Não sabe por onde começar? Use o assistente de configuração para configurar seu N.O.M.A.D. rapidamente!",
"badge": "Comece aqui!"
},
"installApps": {
"label": "Instalar Apps",
"description": "Não encontra seu app favorito? Instale aqui!"
},
"docs": {
"label": "Documentação",
"description": "Leia os manuais e guias do Project N.O.M.A.D."
},
"settingsCard": {
"label": "Configurações",
"description": "Configure as opções do seu N.O.M.A.D."
},
"maps": {
"label": "Mapas",
"description": "Visualizar mapas offline"
}
}

View File

@ -0,0 +1,25 @@
{
"header": {
"commandCenter": "Central de Comando"
},
"nav": {
"backToHome": "Voltar ao Início"
},
"footer": {
"version": "Project N.O.M.A.D. Central de Comando v{{version}}",
"debugInfo": "Informações de Depuração"
},
"sidebar": {
"settings": "Configurações",
"apps": "Aplicativos",
"benchmark": "Benchmark",
"contentExplorer": "Explorador de Conteúdo",
"contentManager": "Gerenciador de Conteúdo",
"mapsManager": "Gerenciador de Mapas",
"serviceLogs": "Logs e Métricas de Serviços",
"checkUpdates": "Verificar Atualizações",
"system": "Sistema",
"support": "Apoiar o Projeto",
"legal": "Avisos Legais"
}
}

View File

@ -0,0 +1,57 @@
{
"system": {
"title": "Informações do Sistema",
"subtitle": "Monitoramento e diagnósticos em tempo real",
"lastUpdated": "Última atualização: {{time}}",
"refreshing": "Atualizando dados a cada 30 segundos"
},
"sections": {
"resourceUsage": "Uso de Recursos",
"systemDetails": "Detalhes do Sistema",
"memoryAllocation": "Alocação de Memória",
"storageDevices": "Dispositivos de Armazenamento",
"systemStatus": "Status do Sistema",
"preferences": "Preferências"
},
"labels": {
"cpuUsage": "Uso de CPU",
"memoryUsage": "Uso de Memória",
"swapUsage": "Uso de Swap",
"cores": "{{count}} núcleos",
"utilized": "{{percent}}% Utilizado",
"totalRam": "RAM Total",
"usedRam": "RAM Usada",
"availableRam": "RAM Disponível",
"systemUptime": "Tempo Ativo",
"cpuCores": "Núcleos de CPU",
"storageDevicesCount": "Dispositivos de Armazenamento",
"noStorageDetected": "Nenhum dispositivo de armazenamento detectado"
},
"os": {
"distribution": "Distribuição",
"kernelVersion": "Versão do Kernel",
"architecture": "Arquitetura",
"hostname": "Nome do Host",
"platform": "Plataforma"
},
"cpu": {
"manufacturer": "Fabricante",
"brand": "Modelo",
"cores": "Núcleos",
"physicalCores": "Núcleos Físicos",
"virtualization": "Virtualização",
"enabled": "Habilitado",
"disabled": "Desabilitado"
},
"gpu": {
"title": "Placa de Vídeo",
"model": "Modelo",
"vendor": "Fabricante",
"vram": "VRAM"
},
"storage": {
"normal": "Normal",
"warningHigh": "Alerta - Uso Elevado",
"criticalFull": "Crítico - Disco Quase Cheio"
}
}

View File

@ -7,6 +7,7 @@ import {
IconWifiOff,
} from '@tabler/icons-react'
import { Head, usePage } from '@inertiajs/react'
import { useTranslation } from 'react-i18next'
import AppLayout from '~/layouts/AppLayout'
import { getServiceLink } from '~/lib/navigation'
import { ServiceSlim } from '../../types/services'
@ -16,63 +17,6 @@ import { useSystemSetting } from '~/hooks/useSystemSetting'
import Alert from '~/components/Alert'
import { SERVICE_NAMES } from '../../constants/service_names'
// Maps is a Core Capability (display_order: 4)
const MAPS_ITEM = {
label: 'Maps',
to: '/maps',
target: '',
description: 'View offline maps',
icon: <IconMapRoute size={48} />,
installed: true,
displayOrder: 4,
poweredBy: null,
}
// System items shown after all apps
const SYSTEM_ITEMS = [
{
label: 'Easy Setup',
to: '/easy-setup',
target: '',
description:
'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!',
icon: <IconBolt size={48} />,
installed: true,
displayOrder: 50,
poweredBy: null,
},
{
label: 'Install Apps',
to: '/settings/apps',
target: '',
description: 'Not seeing your favorite app? Install it here!',
icon: <IconPlus size={48} />,
installed: true,
displayOrder: 51,
poweredBy: null,
},
{
label: 'Docs',
to: '/docs/home',
target: '',
description: 'Read Project N.O.M.A.D. manuals and guides',
icon: <IconHelp size={48} />,
installed: true,
displayOrder: 52,
poweredBy: null,
},
{
label: 'Settings',
to: '/settings/system',
target: '',
description: 'Configure your N.O.M.A.D. settings',
icon: <IconSettings size={48} />,
installed: true,
displayOrder: 53,
poweredBy: null,
},
]
interface DashboardItem {
label: string
to: string
@ -89,6 +33,8 @@ export default function Home(props: {
services: ServiceSlim[]
}
}) {
const { t } = useTranslation('home')
const { t: tCommon } = useTranslation('common')
const items: DashboardItem[] = []
const updateInfo = useUpdateAvailable();
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
@ -99,6 +45,62 @@ export default function Home(props: {
})
const shouldHighlightEasySetup = easySetupVisited?.value ? String(easySetupVisited.value) !== 'true' : false
// Maps is a Core Capability (display_order: 4)
const MAPS_ITEM: DashboardItem = {
label: t('maps.label'),
to: '/maps',
target: '',
description: t('maps.description'),
icon: <IconMapRoute size={48} />,
installed: true,
displayOrder: 4,
poweredBy: null,
}
// System items shown after all apps
const SYSTEM_ITEMS: DashboardItem[] = [
{
label: t('easySetup.label'),
to: '/easy-setup',
target: '',
description: t('easySetup.description'),
icon: <IconBolt size={48} />,
installed: true,
displayOrder: 50,
poweredBy: null,
},
{
label: t('installApps.label'),
to: '/settings/apps',
target: '',
description: t('installApps.description'),
icon: <IconPlus size={48} />,
installed: true,
displayOrder: 51,
poweredBy: null,
},
{
label: t('docs.label'),
to: '/docs/home',
target: '',
description: t('docs.description'),
icon: <IconHelp size={48} />,
installed: true,
displayOrder: 52,
poweredBy: null,
},
{
label: t('settingsCard.label'),
to: '/settings/system',
target: '',
description: t('settingsCard.description'),
icon: <IconSettings size={48} />,
installed: true,
displayOrder: 53,
poweredBy: null,
},
]
// Add installed services (non-dependency services only)
props.system.services
.filter((service) => service.installed && service.ui_location)
@ -133,18 +135,18 @@ export default function Home(props: {
return (
<AppLayout>
<Head title="Command Center" />
<Head title={t('title')} />
{
updateInfo?.updateAvailable && (
<div className='flex justify-center items-center p-4 w-full'>
<Alert
title="An update is available for Project N.O.M.A.D.!"
title={tCommon('alerts.updateAvailable')}
type="info-inverted"
variant="solid"
className="w-full"
buttonProps={{
variant: 'primary',
children: 'Go to Settings',
children: tCommon('buttons.goToSettings'),
icon: 'IconSettings',
onClick: () => {
window.location.href = '/settings/update'
@ -156,7 +158,7 @@ export default function Home(props: {
}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{items.map((item) => {
const isEasySetup = item.label === 'Easy Setup'
const isEasySetup = item.to === '/easy-setup'
const shouldHighlight = isEasySetup && shouldHighlightEasySetup
return (
@ -169,7 +171,7 @@ export default function Home(props: {
style={{ animationDuration: '1.5s' }}
></span>
<span className="relative inline-flex items-center rounded-full px-2.5 py-1 bg-desert-orange-light text-xs font-semibold text-white shadow-sm">
Start here!
{t('easySetup.badge')}
</span>
</span>
)}

View File

@ -1,5 +1,7 @@
import { useState } from 'react'
import { useState, useCallback } from 'react'
import { Head } from '@inertiajs/react'
import { useTranslation } from 'react-i18next'
import i18n from 'i18next'
import SettingsLayout from '~/layouts/SettingsLayout'
import { SystemInformationResponse } from '../../../types/system'
import { formatBytes } from '~/lib/util'
@ -19,6 +21,8 @@ import { IconCpu, IconDatabase, IconServer, IconDeviceDesktop, IconComponents }
export default function SettingsPage(props: {
system: { info: SystemInformationResponse | undefined }
}) {
const { t } = useTranslation('settings')
const { t: tCommon } = useTranslation('common')
const { data: info } = useSystemInfo({
initialData: props.system.info,
})
@ -44,7 +48,7 @@ export default function SettingsPage(props: {
const handleForceReinstallOllama = () => {
openModal(
<StyledModal
title="Reinstall AI Assistant?"
title={tCommon('alerts.reinstallAIConfirmTitle')}
onConfirm={async () => {
closeAllModals()
setReinstalling(true)
@ -54,14 +58,14 @@ export default function SettingsPage(props: {
throw new Error(response?.message || 'Force reinstall failed')
}
addNotification({
message: 'AI Assistant is being reinstalled with GPU support. This page will reload shortly.',
message: tCommon('alerts.reinstallSuccess'),
type: 'success',
})
try { localStorage.removeItem('nomad:gpu-banner-dismissed') } catch {}
setTimeout(() => window.location.reload(), 5000)
} catch (error) {
addNotification({
message: `Failed to reinstall: ${error instanceof Error ? error.message : 'Unknown error'}`,
message: tCommon('alerts.reinstallFailed', { error: error instanceof Error ? error.message : 'Unknown error' }),
type: 'error',
})
setReinstalling(false)
@ -69,13 +73,11 @@ export default function SettingsPage(props: {
}}
onCancel={closeAllModals}
open={true}
confirmText="Reinstall"
cancelText="Cancel"
confirmText={tCommon('buttons.reinstall')}
cancelText={tCommon('buttons.cancel')}
>
<p className="text-text-primary">
This will recreate the AI Assistant container with GPU support enabled.
Your downloaded models will be preserved. The service will be briefly
unavailable during reinstall.
{tCommon('alerts.reinstallAIConfirmMessage')}
</p>
</StyledModal>,
'gpu-health-force-reinstall-modal'
@ -110,22 +112,22 @@ export default function SettingsPage(props: {
return (
<SettingsLayout>
<Head title="System Information" />
<Head title={t('system.title')} />
<div className="xl:pl-72 w-full">
<main className="px-6 lg:px-12 py-6 lg:py-8">
<div className="mb-8">
<h1 className="text-4xl font-bold text-desert-green mb-2">System Information</h1>
<h1 className="text-4xl font-bold text-desert-green mb-2">{t('system.title')}</h1>
<p className="text-desert-stone-dark">
Real-time monitoring and diagnostics Last updated: {new Date().toLocaleString()}
Refreshing data every 30 seconds
{t('system.subtitle')} {t('system.lastUpdated', { time: new Date().toLocaleString() })}
{' '}{t('system.refreshing')}
</p>
</div>
{Number(memoryUsagePercent) > 90 && (
<div className="mb-6">
<Alert
type="error"
title="Very High Memory Usage Detected"
message="System memory usage exceeds 90%. Performance degradation may occur."
title={tCommon('alerts.highMemoryTitle')}
message={tCommon('alerts.highMemoryMessage')}
variant="bordered"
/>
</div>
@ -133,24 +135,24 @@ export default function SettingsPage(props: {
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
Resource Usage
{t('sections.resourceUsage')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<CircularGauge
value={info?.currentLoad.currentLoad || 0}
label="CPU Usage"
label={t('labels.cpuUsage')}
size="lg"
variant="cpu"
subtext={`${info?.cpu.cores || 0} cores`}
subtext={t('labels.cores', { count: info?.cpu.cores || 0 })}
icon={<IconCpu className="w-8 h-8" />}
/>
</div>
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<CircularGauge
value={Number(memoryUsagePercent)}
label="Memory Usage"
label={t('labels.memoryUsage')}
size="lg"
variant="memory"
subtext={`${formatBytes(memoryUsed)} / ${formatBytes(info?.mem.total || 0)}`}
@ -160,7 +162,7 @@ export default function SettingsPage(props: {
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<CircularGauge
value={Number(swapUsagePercent)}
label="Swap Usage"
label={t('labels.swapUsage')}
size="lg"
variant="disk"
subtext={`${formatBytes(info?.mem.swapused || 0)} / ${formatBytes(info?.mem.swaptotal || 0)}`}
@ -172,7 +174,7 @@ export default function SettingsPage(props: {
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
System Details
{t('sections.systemDetails')}
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
@ -181,11 +183,11 @@ export default function SettingsPage(props: {
icon={<IconDeviceDesktop className="w-6 h-6" />}
variant="elevated"
data={[
{ label: 'Distribution', value: info?.os.distro },
{ label: 'Kernel Version', value: info?.os.kernel },
{ label: 'Architecture', value: info?.os.arch },
{ label: 'Hostname', value: info?.os.hostname },
{ label: 'Platform', value: info?.os.platform },
{ label: t('os.distribution'), value: info?.os.distro },
{ label: t('os.kernelVersion'), value: info?.os.kernel },
{ label: t('os.architecture'), value: info?.os.arch },
{ label: t('os.hostname'), value: info?.os.hostname },
{ label: t('os.platform'), value: info?.os.platform },
]}
/>
<InfoCard
@ -193,13 +195,13 @@ export default function SettingsPage(props: {
icon={<IconCpu className="w-6 h-6" />}
variant="elevated"
data={[
{ label: 'Manufacturer', value: info?.cpu.manufacturer },
{ label: 'Brand', value: info?.cpu.brand },
{ label: 'Cores', value: info?.cpu.cores },
{ label: 'Physical Cores', value: info?.cpu.physicalCores },
{ label: t('cpu.manufacturer'), value: info?.cpu.manufacturer },
{ label: t('cpu.brand'), value: info?.cpu.brand },
{ label: t('cpu.cores'), value: info?.cpu.cores },
{ label: t('cpu.physicalCores'), value: info?.cpu.physicalCores },
{
label: 'Virtualization',
value: info?.cpu.virtualization ? 'Enabled' : 'Disabled',
label: t('cpu.virtualization'),
value: info?.cpu.virtualization ? t('cpu.enabled') : t('cpu.disabled'),
},
]}
/>
@ -208,12 +210,12 @@ export default function SettingsPage(props: {
<Alert
type="warning"
variant="bordered"
title="GPU Not Accessible to AI Assistant"
message="Your system has an NVIDIA GPU, but the AI Assistant can't access it. AI is running on CPU only, which is significantly slower."
title={tCommon('alerts.gpuNotAccessibleTitle')}
message={tCommon('alerts.gpuNotAccessibleMessage')}
dismissible={true}
onDismiss={handleDismissGpuBanner}
buttonProps={{
children: 'Fix: Reinstall AI Assistant',
children: tCommon('alerts.fixReinstallAI'),
icon: 'IconRefresh',
variant: 'action',
size: 'sm',
@ -226,15 +228,15 @@ export default function SettingsPage(props: {
)}
{info?.graphics?.controllers && info.graphics.controllers.length > 0 && (
<InfoCard
title="Graphics"
title={t('gpu.title')}
icon={<IconComponents className="w-6 h-6" />}
variant="elevated"
data={info.graphics.controllers.map((gpu, i) => {
const prefix = info.graphics.controllers.length > 1 ? `GPU ${i + 1} ` : ''
return [
{ label: `${prefix}Model`, value: gpu.model },
{ label: `${prefix}Vendor`, value: gpu.vendor },
{ label: `${prefix}VRAM`, value: gpu.vram ? `${gpu.vram} MB` : 'N/A' },
{ label: `${prefix}${t('gpu.model')}`, value: gpu.model },
{ label: `${prefix}${t('gpu.vendor')}`, value: gpu.vendor },
{ label: `${prefix}${t('gpu.vram')}`, value: gpu.vram ? `${gpu.vram} MB` : 'N/A' },
]
}).flat()}
/>
@ -244,7 +246,7 @@ export default function SettingsPage(props: {
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
Memory Allocation
{t('sections.memoryAllocation')}
</h2>
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
@ -253,7 +255,7 @@ export default function SettingsPage(props: {
{formatBytes(info?.mem.total || 0)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Total RAM
{t('labels.totalRam')}
</div>
</div>
<div className="text-center">
@ -261,7 +263,7 @@ export default function SettingsPage(props: {
{formatBytes(memoryUsed)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Used RAM
{t('labels.usedRam')}
</div>
</div>
<div className="text-center">
@ -269,7 +271,7 @@ export default function SettingsPage(props: {
{formatBytes(info?.mem.available || 0)}
</div>
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
Available RAM
{t('labels.availableRam')}
</div>
</div>
</div>
@ -280,7 +282,7 @@ export default function SettingsPage(props: {
></div>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-sm font-bold text-white drop-shadow-md z-10">
{memoryUsagePercent}% Utilized
{t('labels.utilized', { percent: memoryUsagePercent })}
</span>
</div>
</div>
@ -289,7 +291,7 @@ export default function SettingsPage(props: {
<section className="mb-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
Storage Devices
{t('sections.storageDevices')}
</h2>
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
@ -299,17 +301,17 @@ export default function SettingsPage(props: {
progressiveBarColor={true}
statuses={[
{
label: 'Normal',
label: t('storage.normal'),
min_threshold: 0,
color_class: 'bg-desert-olive',
},
{
label: 'Warning - Usage High',
label: t('storage.warningHigh'),
min_threshold: 75,
color_class: 'bg-desert-orange',
},
{
label: 'Critical - Disk Almost Full',
label: t('storage.criticalFull'),
min_threshold: 90,
color_class: 'bg-desert-red',
},
@ -317,7 +319,7 @@ export default function SettingsPage(props: {
/>
) : (
<div className="text-center text-desert-stone-dark py-8">
No storage devices detected
{t('labels.noStorageDetected')}
</div>
)}
</div>
@ -325,12 +327,38 @@ export default function SettingsPage(props: {
<section>
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
System Status
{t('sections.systemStatus')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatusCard title="System Uptime" value={uptimeDisplay} />
<StatusCard title="CPU Cores" value={info?.cpu.cores || 0} />
<StatusCard title="Storage Devices" value={storageItems.length} />
<StatusCard title={t('labels.systemUptime')} value={uptimeDisplay} />
<StatusCard title={t('labels.cpuCores')} value={info?.cpu.cores || 0} />
<StatusCard title={t('labels.storageDevicesCount')} value={storageItems.length} />
</div>
</section>
<section className="mt-12">
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
<div className="w-1 h-6 bg-desert-green" />
{t('sections.preferences')}
</h2>
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-text-primary">{tCommon('language.label')}</h3>
</div>
<select
value={i18n.language}
onChange={(e) => {
const lang = e.target.value
i18n.changeLanguage(lang)
try { localStorage.setItem('nomad:language', lang) } catch {}
api.updateSetting('ui.language', lang).catch(() => {})
}}
className="bg-surface-primary border border-border-default rounded-lg px-4 py-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-desert-green"
>
<option value="en">{tCommon('language.en')}</option>
<option value="pt-BR">{tCommon('language.pt-BR')}</option>
</select>
</div>
</div>
</section>
</main>

View File

@ -46,6 +46,7 @@
"edge.js": "^6.2.1",
"fast-xml-parser": "^5.5.6",
"fuse.js": "^7.1.0",
"i18next": "^25.10.5",
"luxon": "^3.6.1",
"maplibre-gl": "^4.7.1",
"mysql2": "^3.14.1",
@ -58,6 +59,7 @@
"react": "^19.1.0",
"react-adonis-transmit": "^1.0.1",
"react-dom": "^19.1.0",
"react-i18next": "^16.6.2",
"react-map-gl": "^8.1.0",
"react-markdown": "^10.1.0",
"reflect-metadata": "^0.2.2",
@ -1092,6 +1094,15 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@ -9646,6 +9657,15 @@
],
"license": "MIT"
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@ -9749,6 +9769,37 @@
"node": ">=18.18.0"
}
},
"node_modules/i18next": {
"version": "25.10.5",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.5.tgz",
"integrity": "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@ -13776,6 +13827,33 @@
"react": "^19.2.4"
}
},
"node_modules/react-i18next": {
"version": "16.6.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.2.tgz",
"integrity": "sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -16234,6 +16312,15 @@
"vite": "^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",

View File

@ -98,6 +98,7 @@
"edge.js": "^6.2.1",
"fast-xml-parser": "^5.5.6",
"fuse.js": "^7.1.0",
"i18next": "^25.10.5",
"luxon": "^3.6.1",
"maplibre-gl": "^4.7.1",
"mysql2": "^3.14.1",
@ -110,6 +111,7 @@
"react": "^19.1.0",
"react-adonis-transmit": "^1.0.1",
"react-dom": "^19.1.0",
"react-i18next": "^16.6.2",
"react-map-gl": "^8.1.0",
"react-markdown": "^10.1.0",
"reflect-metadata": "^0.2.2",