feat(i18n): Integrate language switcher and translate home/settings pages

- Add useTranslation hook to SettingsLayout
- Replace hardcoded menu items with translation keys
- Add LanguageSwitcher component to settings header
- Add i18n support to home page (Command Center)
- Translate Maps, Easy Setup, Install Apps, Docs, Settings menu items
- Update Alert components to use translation keys
- Add new translation keys for menu items and descriptions

Translation keys added:
- home.easySetupDesc, home.installAppsDesc, home.docsDesc, home.settingsDesc
- maps.viewOffline
- settings.contentExplorer, settings.contentManager, settings.mapsManager
- settings.checkUpdates, settings.supportProject, settings.serviceLogs

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-04 10:28:52 +08:00
parent a8f6fe353b
commit 56f49fe892
4 changed files with 110 additions and 60 deletions

View File

@ -39,7 +39,11 @@
"updateAvailable": "An update is available for Project N.O.M.A.D.!", "updateAvailable": "An update is available for Project N.O.M.A.D.!",
"goToSettings": "Go to Settings", "goToSettings": "Go to Settings",
"startHere": "Start here!", "startHere": "Start here!",
"poweredBy": "Powered by" "poweredBy": "Powered by",
"easySetupDesc": "Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!",
"installAppsDesc": "Not seeing your favorite app? Install it here!",
"docsDesc": "Read Project N.O.M.A.D. manuals and guides",
"settingsDesc": "Configure your N.O.M.A.D. settings"
}, },
"menu": { "menu": {
"maps": "Maps", "maps": "Maps",
@ -50,6 +54,7 @@
}, },
"maps": { "maps": {
"title": "Offline Maps", "title": "Offline Maps",
"viewOffline": "View offline maps",
"search": "Search locations...", "search": "Search locations...",
"noResults": "No results found", "noResults": "No results found",
"loadingMap": "Loading map...", "loadingMap": "Loading map...",
@ -69,10 +74,16 @@
"apps": "Apps", "apps": "Apps",
"models": "Models", "models": "Models",
"maps": "Maps", "maps": "Maps",
"mapsManager": "Maps Manager",
"benchmark": "Benchmark", "benchmark": "Benchmark",
"update": "Update", "update": "Update",
"checkUpdates": "Check for Updates",
"legal": "Legal", "legal": "Legal",
"support": "Support" "support": "Support",
"supportProject": "Support the Project",
"contentExplorer": "Content Explorer",
"contentManager": "Content Manager",
"serviceLogs": "Service Logs & Metrics"
}, },
"system": { "system": {
"title": "System Settings", "title": "System Settings",

View File

@ -39,7 +39,11 @@
"updateAvailable": "Project N.O.M.A.D. 有可用更新!", "updateAvailable": "Project N.O.M.A.D. 有可用更新!",
"goToSettings": "前往设置", "goToSettings": "前往设置",
"startHere": "从这里开始!", "startHere": "从这里开始!",
"poweredBy": "技术支持" "poweredBy": "技术支持",
"easySetupDesc": "不确定从哪里开始?使用设置向导快速配置您的 N.O.M.A.D.",
"installAppsDesc": "没有看到您想要的应用?在这里安装!",
"docsDesc": "阅读 Project N.O.M.A.D. 手册和指南",
"settingsDesc": "配置您的 N.O.M.A.D. 设置"
}, },
"menu": { "menu": {
"maps": "地图", "maps": "地图",
@ -50,6 +54,7 @@
}, },
"maps": { "maps": {
"title": "离线地图", "title": "离线地图",
"viewOffline": "查看离线地图",
"search": "搜索位置...", "search": "搜索位置...",
"noResults": "未找到结果", "noResults": "未找到结果",
"loadingMap": "加载地图中...", "loadingMap": "加载地图中...",
@ -69,10 +74,16 @@
"apps": "应用", "apps": "应用",
"models": "模型", "models": "模型",
"maps": "地图", "maps": "地图",
"mapsManager": "地图管理器",
"benchmark": "基准测试", "benchmark": "基准测试",
"update": "更新", "update": "更新",
"checkUpdates": "检查更新",
"legal": "法律", "legal": "法律",
"support": "支持" "support": "支持",
"supportProject": "支持项目",
"contentExplorer": "内容浏览器",
"contentManager": "内容管理器",
"serviceLogs": "服务日志和指标"
}, },
"system": { "system": {
"title": "系统设置", "title": "系统设置",

View File

@ -9,47 +9,67 @@ import {
IconSettings, IconSettings,
IconTerminal2, IconTerminal2,
IconWand, IconWand,
IconZoom IconZoom,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import { usePage } from '@inertiajs/react' import { usePage } from '@inertiajs/react'
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'
import LanguageSwitcher from '~/components/LanguageSwitcher'
import { useTranslation } from 'react-i18next'
export default function SettingsLayout({ children }: { children: React.ReactNode }) { export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const { t } = useTranslation()
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: 'Apps', href: '/settings/apps', icon: IconTerminal2, current: false }, ? [{ name: aiAssistantName, href: '/settings/models', icon: IconWand, current: false }]
{ name: 'Benchmark', href: '/settings/benchmark', icon: IconChartBar, current: false }, : []),
{ name: 'Content Explorer', href: '/settings/zim/remote-explorer', icon: IconZoom, current: false }, { name: t('settings.apps'), href: '/settings/apps', icon: IconTerminal2, current: false },
{ name: 'Content Manager', href: '/settings/zim', icon: IconFolder, current: false },
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
{ {
name: 'Service Logs & Metrics', name: t('settings.benchmark'),
href: '/settings/benchmark',
icon: IconChartBar,
current: false,
},
{
name: t('settings.contentExplorer'),
href: '/settings/zim/remote-explorer',
icon: IconZoom,
current: false,
},
{ name: t('settings.contentManager'), href: '/settings/zim', icon: IconFolder, current: false },
{ name: t('settings.mapsManager'), href: '/settings/maps', icon: IconMapRoute, current: false },
{
name: t('settings.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('settings.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('settings.system'), href: '/settings/system', icon: IconSettings, current: false },
{ name: 'Support the Project', href: '/settings/support', icon: IconHeart, current: false }, { name: t('settings.support'), href: '/settings/support', icon: IconHeart, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false }, { name: t('settings.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('settings.title')} items={navigation} />
{children} <div className="flex-1 flex flex-col">
<div className="p-4 border-b border-desert-stone-light bg-desert-white">
<LanguageSwitcher />
</div>
{children}
</div>
</div> </div>
) )
} }

View File

@ -15,57 +15,57 @@ import { useUpdateAvailable } from '~/hooks/useUpdateAvailable'
import { useSystemSetting } from '~/hooks/useSystemSetting' 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'
import { useTranslation } from 'react-i18next'
// Maps is a Core Capability (display_order: 4) // Maps is a Core Capability (display_order: 4)
const MAPS_ITEM = { const getMapsItem = (t: (key: string) => string) => ({
label: 'Maps', label: t('menu.maps'),
to: '/maps', to: '/maps',
target: '', target: '',
description: 'View offline maps', description: t('maps.viewOffline'),
icon: <IconMapRoute size={48} />, icon: <IconMapRoute size={48} />,
installed: true, installed: true,
displayOrder: 4, displayOrder: 4,
poweredBy: null, poweredBy: null,
} })
// System items shown after all apps // System items shown after all apps
const SYSTEM_ITEMS = [ const getSystemItems = (t: (key: string) => string) => [
{ {
label: 'Easy Setup', label: t('menu.easySetup'),
to: '/easy-setup', to: '/easy-setup',
target: '', target: '',
description: description: t('home.easySetupDesc'),
'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!',
icon: <IconBolt size={48} />, icon: <IconBolt size={48} />,
installed: true, installed: true,
displayOrder: 50, displayOrder: 50,
poweredBy: null, poweredBy: null,
}, },
{ {
label: 'Install Apps', label: t('menu.installApps'),
to: '/settings/apps', to: '/settings/apps',
target: '', target: '',
description: 'Not seeing your favorite app? Install it here!', description: t('home.installAppsDesc'),
icon: <IconPlus size={48} />, icon: <IconPlus size={48} />,
installed: true, installed: true,
displayOrder: 51, displayOrder: 51,
poweredBy: null, poweredBy: null,
}, },
{ {
label: 'Docs', label: t('menu.docs'),
to: '/docs/home', to: '/docs/home',
target: '', target: '',
description: 'Read Project N.O.M.A.D. manuals and guides', description: t('home.docsDesc'),
icon: <IconHelp size={48} />, icon: <IconHelp size={48} />,
installed: true, installed: true,
displayOrder: 52, displayOrder: 52,
poweredBy: null, poweredBy: null,
}, },
{ {
label: 'Settings', label: t('menu.settings'),
to: '/settings/system', to: '/settings/system',
target: '', target: '',
description: 'Configure your N.O.M.A.D. settings', description: t('home.settingsDesc'),
icon: <IconSettings size={48} />, icon: <IconSettings size={48} />,
installed: true, installed: true,
displayOrder: 53, displayOrder: 53,
@ -89,15 +89,18 @@ export default function Home(props: {
services: ServiceSlim[] services: ServiceSlim[]
} }
}) { }) {
const { t } = useTranslation()
const items: DashboardItem[] = [] const items: DashboardItem[] = []
const updateInfo = useUpdateAvailable(); const updateInfo = useUpdateAvailable()
const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props
// Check if user has visited Easy Setup // Check if user has visited Easy Setup
const { data: easySetupVisited } = useSystemSetting({ const { data: easySetupVisited } = useSystemSetting({
key: 'ui.hasVisitedEasySetup' key: 'ui.hasVisitedEasySetup',
}) })
const shouldHighlightEasySetup = easySetupVisited?.value ? String(easySetupVisited.value) !== 'true' : false const shouldHighlightEasySetup = easySetupVisited?.value
? String(easySetupVisited.value) !== 'true'
: false
// Add installed services (non-dependency services only) // Add installed services (non-dependency services only)
props.system.services props.system.services
@ -105,7 +108,10 @@ export default function Home(props: {
.forEach((service) => { .forEach((service) => {
items.push({ items.push({
// Inject custom AI Assistant name if this is the chat service // Inject custom AI Assistant name if this is the chat service
label: service.service_name === SERVICE_NAMES.OLLAMA && aiAssistantName ? aiAssistantName : (service.friendly_name || service.service_name), label:
service.service_name === SERVICE_NAMES.OLLAMA && aiAssistantName
? aiAssistantName
: service.friendly_name || service.service_name,
to: service.ui_location ? getServiceLink(service.ui_location) : '#', to: service.ui_location ? getServiceLink(service.ui_location) : '#',
target: '_blank', target: '_blank',
description: description:
@ -123,38 +129,36 @@ export default function Home(props: {
}) })
// Add Maps as a Core Capability // Add Maps as a Core Capability
items.push(MAPS_ITEM) items.push(getMapsItem(t))
// Add system items // Add system items
items.push(...SYSTEM_ITEMS) items.push(...getSystemItems(t))
// Sort all items by display order // Sort all items by display order
items.sort((a, b) => a.displayOrder - b.displayOrder) items.sort((a, b) => a.displayOrder - b.displayOrder)
return ( return (
<AppLayout> <AppLayout>
<Head title="Command Center" /> <Head title={t('home.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={t('home.updateAvailable')}
title="An update is available for Project N.O.M.A.D.!" type="info-inverted"
type="info-inverted" variant="solid"
variant="solid" className="w-full"
className="w-full" buttonProps={{
buttonProps={{ variant: 'primary',
variant: 'primary', children: t('home.goToSettings'),
children: 'Go to Settings', icon: 'IconSettings',
icon: 'IconSettings', onClick: () => router.visit('/settings/update'),
onClick: () => router.visit('/settings/update'), }}
}} />
/> </div>
</div> )}
)
}
<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.label === t('menu.easySetup')
const shouldHighlight = isEasySetup && shouldHighlightEasySetup const shouldHighlight = isEasySetup && shouldHighlightEasySetup
const tileContent = ( const tileContent = (
@ -166,13 +170,17 @@ 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('home.startHere')}
</span> </span>
</span> </span>
)} )}
<div className="flex items-center justify-center mb-2">{item.icon}</div> <div className="flex items-center justify-center mb-2">{item.icon}</div>
<h3 className="font-bold text-2xl">{item.label}</h3> <h3 className="font-bold text-2xl">{item.label}</h3>
{item.poweredBy && <p className="text-sm opacity-80">Powered by {item.poweredBy}</p>} {item.poweredBy && (
<p className="text-sm opacity-80">
{t('home.poweredBy')} {item.poweredBy}
</p>
)}
<p className="xl:text-lg mt-2">{item.description}</p> <p className="xl:text-lg mt-2">{item.description}</p>
</div> </div>
) )