diff --git a/admin/inertia/app/app.tsx b/admin/inertia/app/app.tsx
index b71ab64..13a1d37 100644
--- a/admin/inertia/app/app.tsx
+++ b/admin/inertia/app/app.tsx
@@ -1,6 +1,7 @@
///
///
+import '~/lib/i18n'
import '../css/app.css'
import { createRoot } from 'react-dom/client'
import { createInertiaApp } from '@inertiajs/react'
diff --git a/admin/inertia/components/Footer.tsx b/admin/inertia/components/Footer.tsx
index e74c3a2..41b3426 100644
--- a/admin/inertia/components/Footer.tsx
+++ b/admin/inertia/components/Footer.tsx
@@ -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() {
{
updateInfo?.updateAvailable && (
{
window.location.href = '/settings/update'
@@ -156,7 +158,7 @@ export default function Home(props: {
}
{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' }}
>
- Start here!
+ {t('easySetup.badge')}
)}
diff --git a/admin/inertia/pages/settings/system.tsx b/admin/inertia/pages/settings/system.tsx
index 7b40088..06924c7 100644
--- a/admin/inertia/pages/settings/system.tsx
+++ b/admin/inertia/pages/settings/system.tsx
@@ -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(
{
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')}
>
- 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')}
,
'gpu-health-force-reinstall-modal'
@@ -110,22 +112,22 @@ export default function SettingsPage(props: {
return (
-
+
-
System Information
+
{t('system.title')}
- 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')}
{Number(memoryUsagePercent) > 90 && (
@@ -133,24 +135,24 @@ export default function SettingsPage(props: {
- Resource Usage
+ {t('sections.resourceUsage')}
}
/>
- System Details
+ {t('sections.systemDetails')}
@@ -181,11 +183,11 @@ export default function SettingsPage(props: {
icon={
}
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 },
]}
/>
}
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: {
0 && (
}
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: {
- Memory Allocation
+ {t('sections.memoryAllocation')}
@@ -253,7 +255,7 @@ export default function SettingsPage(props: {
{formatBytes(info?.mem.total || 0)}
- Total RAM
+ {t('labels.totalRam')}
@@ -261,7 +263,7 @@ export default function SettingsPage(props: {
{formatBytes(memoryUsed)}
- Used RAM
+ {t('labels.usedRam')}
@@ -269,7 +271,7 @@ export default function SettingsPage(props: {
{formatBytes(info?.mem.available || 0)}
- Available RAM
+ {t('labels.availableRam')}
@@ -280,7 +282,7 @@ export default function SettingsPage(props: {
>
- {memoryUsagePercent}% Utilized
+ {t('labels.utilized', { percent: memoryUsagePercent })}
@@ -289,7 +291,7 @@ export default function SettingsPage(props: {
- Storage Devices
+ {t('sections.storageDevices')}
@@ -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: {
/>
) : (
- No storage devices detected
+ {t('labels.noStorageDetected')}
)}
@@ -325,12 +327,38 @@ export default function SettingsPage(props: {
- System Status
+ {t('sections.systemStatus')}
-
-
-
+
+
+
+
+
+
+
+
+ {t('sections.preferences')}
+
+
+
+
+
{tCommon('language.label')}
+
+
{
+ 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"
+ >
+ {tCommon('language.en')}
+ {tCommon('language.pt-BR')}
+
+
diff --git a/admin/package-lock.json b/admin/package-lock.json
index ac4b5b9..86258da 100644
--- a/admin/package-lock.json
+++ b/admin/package-lock.json
@@ -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",
diff --git a/admin/package.json b/admin/package.json
index fc01737..33ce2e1 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -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",