This commit is contained in:
Luís Miguel 2026-03-27 07:14:54 -03:00 committed by GitHub
commit 06e6185641
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 573 additions and 130 deletions

View File

@ -1,3 +1,3 @@
import { KVStoreKey } from "../types/kv_store.js"; import { KVStoreKey } from "../types/kv_store.js";
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName']; export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName', 'ui.language'];

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Footer from '~/components/Footer' import Footer from '~/components/Footer'
import ChatButton from '~/components/chat/ChatButton' import ChatButton from '~/components/chat/ChatButton'
import ChatModal from '~/components/chat/ChatModal' import ChatModal from '~/components/chat/ChatModal'
@ -9,6 +10,7 @@ import { IconArrowLeft } from '@tabler/icons-react'
import classNames from 'classnames' import classNames from 'classnames'
export default function AppLayout({ children }: { children: React.ReactNode }) { export default function AppLayout({ children }: { children: React.ReactNode }) {
const { t } = useTranslation('layout')
const [isChatOpen, setIsChatOpen] = useState(false) const [isChatOpen, setIsChatOpen] = useState(false)
const aiAssistantInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const aiAssistantInstalled = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
@ -18,7 +20,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
window.location.pathname !== '/home' && ( window.location.pathname !== '/home' && (
<Link href="/home" className="absolute top-60 md:top-48 left-4 flex items-center"> <Link href="/home" className="absolute top-60 md:top-48 left-4 flex items-center">
<IconArrowLeft className="mr-2" size={24} /> <IconArrowLeft className="mr-2" size={24} />
<p className="text-lg text-text-secondary">Back to Home</p> <p className="text-lg text-text-secondary">{t('nav.backToHome')}</p>
</Link> </Link>
)} )}
<div <div
@ -26,7 +28,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
onClick={() => (window.location.href = '/home')} onClick={() => (window.location.href = '/home')}
> >
<img src="/project_nomad_logo.png" alt="Project Nomad Logo" className="h-40 w-40" /> <img src="/project_nomad_logo.png" alt="Project Nomad Logo" className="h-40 w-40" />
<h1 className="text-5xl font-bold text-desert-green">Command Center</h1> <h1 className="text-5xl font-bold text-desert-green">{t('header.commandCenter')}</h1>
</div> </div>
<hr className={ <hr className={
classNames( classNames(

View File

@ -12,43 +12,45 @@ import {
IconZoom IconZoom
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { usePage } from '@inertiajs/react' import { usePage } from '@inertiajs/react'
import { useTranslation } from 'react-i18next'
import StyledSidebar from '~/components/StyledSidebar' import StyledSidebar from '~/components/StyledSidebar'
import { getServiceLink } from '~/lib/navigation' import { getServiceLink } from '~/lib/navigation'
import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus' import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus'
import { SERVICE_NAMES } from '../../constants/service_names' import { SERVICE_NAMES } from '../../constants/service_names'
export default function SettingsLayout({ children }: { children: React.ReactNode }) { export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const { t } = useTranslation('layout')
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
const aiAssistantInstallStatus = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA) const aiAssistantInstallStatus = useServiceInstalledStatus(SERVICE_NAMES.OLLAMA)
const navigation = [ const navigation = [
...(aiAssistantInstallStatus.isInstalled ? [{ name: aiAssistantName, href: '/settings/models', icon: IconWand, current: false }] : []), ...(aiAssistantInstallStatus.isInstalled ? [{ name: aiAssistantName, href: '/settings/models', icon: IconWand, current: false }] : []),
{ name: 'Apps', href: '/settings/apps', icon: IconTerminal2, current: false }, { name: t('sidebar.apps'), href: '/settings/apps', icon: IconTerminal2, current: false },
{ name: 'Benchmark', href: '/settings/benchmark', icon: IconChartBar, current: false }, { name: t('sidebar.benchmark'), href: '/settings/benchmark', icon: IconChartBar, current: false },
{ name: 'Content Explorer', href: '/settings/zim/remote-explorer', icon: IconZoom, current: false }, { name: t('sidebar.contentExplorer'), href: '/settings/zim/remote-explorer', icon: IconZoom, current: false },
{ name: 'Content Manager', href: '/settings/zim', icon: IconFolder, current: false }, { name: t('sidebar.contentManager'), href: '/settings/zim', icon: IconFolder, current: false },
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false }, { name: t('sidebar.mapsManager'), href: '/settings/maps', icon: IconMapRoute, current: false },
{ {
name: 'Service Logs & Metrics', name: t('sidebar.serviceLogs'),
href: getServiceLink('9999'), href: getServiceLink('9999'),
icon: IconDashboard, icon: IconDashboard,
current: false, current: false,
target: '_blank', target: '_blank',
}, },
{ {
name: 'Check for Updates', name: t('sidebar.checkUpdates'),
href: '/settings/update', href: '/settings/update',
icon: IconArrowBigUpLines, icon: IconArrowBigUpLines,
current: false, current: false,
}, },
{ name: 'System', href: '/settings/system', icon: IconSettings, current: false }, { name: t('sidebar.system'), href: '/settings/system', icon: IconSettings, current: false },
{ name: 'Support the Project', href: '/settings/support', icon: IconHeart, current: false }, { name: t('sidebar.support'), href: '/settings/support', icon: IconHeart, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false }, { name: t('sidebar.legal'), href: '/settings/legal', icon: IconGavel, current: false },
] ]
return ( return (
<div className="min-h-screen flex flex-row bg-surface-secondary/90"> <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} {children}
</div> </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, IconWifiOff,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { Head, usePage } from '@inertiajs/react' import { Head, usePage } from '@inertiajs/react'
import { useTranslation } from 'react-i18next'
import AppLayout from '~/layouts/AppLayout' import AppLayout from '~/layouts/AppLayout'
import { getServiceLink } from '~/lib/navigation' import { getServiceLink } from '~/lib/navigation'
import { ServiceSlim } from '../../types/services' import { ServiceSlim } from '../../types/services'
@ -16,63 +17,6 @@ import { useSystemSetting } from '~/hooks/useSystemSetting'
import Alert from '~/components/Alert' import Alert from '~/components/Alert'
import { SERVICE_NAMES } from '../../constants/service_names' 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 { interface DashboardItem {
label: string label: string
to: string to: string
@ -89,6 +33,8 @@ export default function Home(props: {
services: ServiceSlim[] services: ServiceSlim[]
} }
}) { }) {
const { t } = useTranslation('home')
const { t: tCommon } = useTranslation('common')
const items: DashboardItem[] = [] const items: DashboardItem[] = []
const updateInfo = useUpdateAvailable(); const updateInfo = useUpdateAvailable();
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
@ -99,6 +45,62 @@ export default function Home(props: {
}) })
const shouldHighlightEasySetup = easySetupVisited?.value ? String(easySetupVisited.value) !== 'true' : false 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) // Add installed services (non-dependency services only)
props.system.services props.system.services
.filter((service) => service.installed && service.ui_location) .filter((service) => service.installed && service.ui_location)
@ -133,18 +135,18 @@ export default function Home(props: {
return ( return (
<AppLayout> <AppLayout>
<Head title="Command Center" /> <Head title={t('title')} />
{ {
updateInfo?.updateAvailable && ( updateInfo?.updateAvailable && (
<div className='flex justify-center items-center p-4 w-full'> <div className='flex justify-center items-center p-4 w-full'>
<Alert <Alert
title="An update is available for Project N.O.M.A.D.!" title={tCommon('alerts.updateAvailable')}
type="info-inverted" type="info-inverted"
variant="solid" variant="solid"
className="w-full" className="w-full"
buttonProps={{ buttonProps={{
variant: 'primary', variant: 'primary',
children: 'Go to Settings', children: tCommon('buttons.goToSettings'),
icon: 'IconSettings', icon: 'IconSettings',
onClick: () => { onClick: () => {
window.location.href = '/settings/update' 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"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{items.map((item) => { {items.map((item) => {
const isEasySetup = item.label === 'Easy Setup' const isEasySetup = item.to === '/easy-setup'
const shouldHighlight = isEasySetup && shouldHighlightEasySetup const shouldHighlight = isEasySetup && shouldHighlightEasySetup
return ( return (
@ -169,7 +171,7 @@ export default function Home(props: {
style={{ animationDuration: '1.5s' }} style={{ animationDuration: '1.5s' }}
></span> ></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"> <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>
</span> </span>
)} )}

View File

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

View File

@ -46,6 +46,7 @@
"edge.js": "^6.2.1", "edge.js": "^6.2.1",
"fast-xml-parser": "^5.5.6", "fast-xml-parser": "^5.5.6",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"i18next": "^25.10.5",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"maplibre-gl": "^4.7.1", "maplibre-gl": "^4.7.1",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
@ -58,6 +59,7 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-adonis-transmit": "^1.0.1", "react-adonis-transmit": "^1.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^16.6.2",
"react-map-gl": "^8.1.0", "react-map-gl": "^8.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
@ -1092,6 +1094,15 @@
"@babel/core": "^7.0.0-0" "@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": { "node_modules/@babel/template": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@ -9646,6 +9657,15 @@
], ],
"license": "MIT" "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": { "node_modules/html-url-attributes": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@ -9749,6 +9769,37 @@
"node": ">=18.18.0" "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": { "node_modules/iconv-lite": {
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@ -13776,6 +13827,33 @@
"react": "^19.2.4" "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": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "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" "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": { "node_modules/vt-pbf": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",

View File

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

View File

@ -10,6 +10,7 @@ export const KV_STORE_SCHEMA = {
'ui.theme': 'string', 'ui.theme': 'string',
'ai.assistantCustomName': 'string', 'ai.assistantCustomName': 'string',
'gpu.type': 'string', 'gpu.type': 'string',
'ui.language': 'string',
} as const } as const
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string