diff --git a/admin/inertia/app/app.tsx b/admin/inertia/app/app.tsx index b71ab64..3359703 100644 --- a/admin/inertia/app/app.tsx +++ b/admin/inertia/app/app.tsx @@ -13,6 +13,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import NotificationsProvider from '~/providers/NotificationProvider' import { ThemeProvider } from '~/providers/ThemeProvider' import { UsePageProps } from '../../types/system' +import '../i18n' const appName = import.meta.env.VITE_APP_NAME || 'Project N.O.M.A.D.' const queryClient = new QueryClient() @@ -40,11 +41,16 @@ createInertiaApp({ createRoot(el).render( - + - {showDevtools && } + {showDevtools && ( + + )} diff --git a/admin/inertia/components/LanguageSwitcher.tsx b/admin/inertia/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..a80d880 --- /dev/null +++ b/admin/inertia/components/LanguageSwitcher.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from 'react-i18next' + +const languages = [ + { code: 'en', name: 'English', flag: '🇺🇸' }, + { code: 'zh', name: '中文', flag: '🇨🇳' }, +] + +export default function LanguageSwitcher() { + const { i18n } = useTranslation() + + return ( +
+ Language: +
+ {languages.map((lang) => ( + + ))} +
+
+ ) +} diff --git a/admin/inertia/i18n/index.ts b/admin/inertia/i18n/index.ts new file mode 100644 index 0000000..9fa4f6b --- /dev/null +++ b/admin/inertia/i18n/index.ts @@ -0,0 +1,29 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' + +import en from './locales/en.json' +import zh from './locales/zh.json' + +const resources = { + en: { translation: en }, + zh: { translation: zh }, +} + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources, + fallbackLng: 'en', + supportedLngs: ['en', 'zh'], + interpolation: { + escapeValue: false, + }, + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + }, + }) + +export default i18n diff --git a/admin/inertia/i18n/locales/en.json b/admin/inertia/i18n/locales/en.json new file mode 100644 index 0000000..bf3ada5 --- /dev/null +++ b/admin/inertia/i18n/locales/en.json @@ -0,0 +1,144 @@ +{ + "common": { + "home": "Home", + "settings": "Settings", + "docs": "Docs", + "chat": "Chat", + "maps": "Maps", + "search": "Search", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "install": "Install", + "uninstall": "Uninstall", + "start": "Start", + "stop": "Stop", + "restart": "Restart", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "warning": "Warning", + "info": "Info", + "confirm": "Confirm", + "yes": "Yes", + "no": "No", + "enabled": "Enabled", + "disabled": "Disabled", + "unknown": "Unknown", + "back": "Back", + "next": "Next", + "previous": "Previous", + "close": "Close", + "submit": "Submit", + "reset": "Reset" + }, + "home": { + "title": "Command Center", + "subtitle": "Your offline knowledge and education hub", + "updateAvailable": "An update is available for Project N.O.M.A.D.!", + "goToSettings": "Go to Settings", + "startHere": "Start here!", + "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": { + "maps": "Maps", + "easySetup": "Easy Setup", + "installApps": "Install Apps", + "docs": "Docs", + "settings": "Settings" + }, + "maps": { + "title": "Offline Maps", + "viewOffline": "View offline maps", + "search": "Search locations...", + "noResults": "No results found", + "loadingMap": "Loading map...", + "offline": "Offline map - no internet required" + }, + "chat": { + "title": "AI Assistant", + "placeholder": "Ask me anything...", + "thinking": "Thinking...", + "noMessages": "Start a conversation with the AI assistant", + "uploadDocument": "Upload Document", + "knowledgeBase": "Knowledge Base" + }, + "settings": { + "title": "Settings", + "system": "System", + "apps": "Apps", + "models": "Models", + "maps": "Maps", + "mapsManager": "Maps Manager", + "benchmark": "Benchmark", + "update": "Update", + "checkUpdates": "Check for Updates", + "legal": "Legal", + "support": "Support", + "supportProject": "Support the Project", + "contentExplorer": "Content Explorer", + "contentManager": "Content Manager", + "serviceLogs": "Service Logs & Metrics" + }, + "system": { + "title": "System Settings", + "hostname": "Hostname", + "version": "Version", + "uptime": "Uptime", + "storage": "Storage", + "memory": "Memory", + "cpu": "CPU", + "network": "Network", + "theme": "Theme", + "language": "Language", + "darkMode": "Dark Mode", + "lightMode": "Light Mode" + }, + "apps": { + "title": "Install Apps", + "installed": "Installed", + "notInstalled": "Not Installed", + "installing": "Installing...", + "noApps": "No apps available" + }, + "models": { + "title": "AI Models", + "download": "Download", + "downloading": "Downloading...", + "downloaded": "Downloaded", + "delete": "Delete Model", + "noModels": "No models installed" + }, + "easySetup": { + "title": "Easy Setup", + "welcome": "Welcome to Project N.O.M.A.D.!", + "getStarted": "Let's get you started with the basics.", + "next": "Next", + "skip": "Skip", + "complete": "Complete" + }, + "docs": { + "title": "Documentation", + "searchDocs": "Search documentation...", + "noResults": "No documentation found" + }, + "about": { + "title": "About Project N.O.M.A.D.", + "description": "Project N.O.M.A.D. (Node for Offline Media, Archives, and Data) is an offline-first knowledge and education server.", + "version": "Version", + "license": "License", + "github": "GitHub", + "website": "Website" + }, + "errors": { + "notFound": "Page Not Found", + "serverError": "Server Error", + "goHome": "Go to Home", + "tryAgain": "Try Again" + } +} \ No newline at end of file diff --git a/admin/inertia/i18n/locales/zh.json b/admin/inertia/i18n/locales/zh.json new file mode 100644 index 0000000..23b7d49 --- /dev/null +++ b/admin/inertia/i18n/locales/zh.json @@ -0,0 +1,144 @@ +{ + "common": { + "home": "首页", + "settings": "设置", + "docs": "文档", + "chat": "聊天", + "maps": "地图", + "search": "搜索", + "save": "保存", + "cancel": "取消", + "delete": "删除", + "edit": "编辑", + "install": "安装", + "uninstall": "卸载", + "start": "启动", + "stop": "停止", + "restart": "重启", + "loading": "加载中...", + "error": "错误", + "success": "成功", + "warning": "警告", + "info": "信息", + "confirm": "确认", + "yes": "是", + "no": "否", + "enabled": "已启用", + "disabled": "已禁用", + "unknown": "未知", + "back": "返回", + "next": "下一步", + "previous": "上一步", + "close": "关闭", + "submit": "提交", + "reset": "重置" + }, + "home": { + "title": "控制中心", + "subtitle": "您的离线知识与教育中心", + "updateAvailable": "Project N.O.M.A.D. 有可用更新!", + "goToSettings": "前往设置", + "startHere": "从这里开始!", + "poweredBy": "技术支持", + "easySetupDesc": "不确定从哪里开始?使用设置向导快速配置您的 N.O.M.A.D.!", + "installAppsDesc": "没有看到您想要的应用?在这里安装!", + "docsDesc": "阅读 Project N.O.M.A.D. 手册和指南", + "settingsDesc": "配置您的 N.O.M.A.D. 设置" + }, + "menu": { + "maps": "地图", + "easySetup": "快速设置", + "installApps": "安装应用", + "docs": "文档", + "settings": "设置" + }, + "maps": { + "title": "离线地图", + "viewOffline": "查看离线地图", + "search": "搜索位置...", + "noResults": "未找到结果", + "loadingMap": "加载地图中...", + "offline": "离线地图 - 无需互联网" + }, + "chat": { + "title": "AI 助手", + "placeholder": "问我任何问题...", + "thinking": "思考中...", + "noMessages": "开始与 AI 助手对话", + "uploadDocument": "上传文档", + "knowledgeBase": "知识库" + }, + "settings": { + "title": "设置", + "system": "系统", + "apps": "应用", + "models": "模型", + "maps": "地图", + "mapsManager": "地图管理器", + "benchmark": "基准测试", + "update": "更新", + "checkUpdates": "检查更新", + "legal": "法律", + "support": "支持", + "supportProject": "支持项目", + "contentExplorer": "内容浏览器", + "contentManager": "内容管理器", + "serviceLogs": "服务日志和指标" + }, + "system": { + "title": "系统设置", + "hostname": "主机名", + "version": "版本", + "uptime": "运行时间", + "storage": "存储", + "memory": "内存", + "cpu": "处理器", + "network": "网络", + "theme": "主题", + "language": "语言", + "darkMode": "深色模式", + "lightMode": "浅色模式" + }, + "apps": { + "title": "安装应用", + "installed": "已安装", + "notInstalled": "未安装", + "installing": "安装中...", + "noApps": "没有可用的应用" + }, + "models": { + "title": "AI 模型", + "download": "下载", + "downloading": "下载中...", + "downloaded": "已下载", + "delete": "删除模型", + "noModels": "没有已安装的模型" + }, + "easySetup": { + "title": "快速设置", + "welcome": "欢迎使用 Project N.O.M.A.D.!", + "getStarted": "让我们从基础设置开始。", + "next": "下一步", + "skip": "跳过", + "complete": "完成" + }, + "docs": { + "title": "文档", + "searchDocs": "搜索文档...", + "noResults": "未找到文档" + }, + "about": { + "title": "关于 Project N.O.M.A.D.", + "description": "Project N.O.M.A.D.(离线媒体、档案和数据节点)是一个离线优先的知识与教育服务器。", + "version": "版本", + "license": "许可证", + "github": "GitHub", + "website": "网站" + }, + "errors": { + "notFound": "页面未找到", + "serverError": "服务器错误", + "goHome": "返回首页", + "tryAgain": "重试" + } +} \ No newline at end of file diff --git a/admin/inertia/layouts/SettingsLayout.tsx b/admin/inertia/layouts/SettingsLayout.tsx index 0ecad83..855a9ab 100644 --- a/admin/inertia/layouts/SettingsLayout.tsx +++ b/admin/inertia/layouts/SettingsLayout.tsx @@ -9,47 +9,67 @@ import { IconSettings, IconTerminal2, IconWand, - IconZoom + IconZoom, } from '@tabler/icons-react' import { usePage } from '@inertiajs/react' import StyledSidebar from '~/components/StyledSidebar' import { getServiceLink } from '~/lib/navigation' import useServiceInstalledStatus from '~/hooks/useServiceInstalledStatus' 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 }) { + const { t } = useTranslation() 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 }, + ...(aiAssistantInstallStatus.isInstalled + ? [{ name: aiAssistantName, href: '/settings/models', icon: IconWand, current: false }] + : []), + { name: t('settings.apps'), href: '/settings/apps', icon: IconTerminal2, 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'), icon: IconDashboard, current: false, target: '_blank', }, { - name: 'Check for Updates', + name: t('settings.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('settings.system'), href: '/settings/system', icon: IconSettings, current: false }, + { name: t('settings.support'), href: '/settings/support', icon: IconHeart, current: false }, + { name: t('settings.legal'), href: '/settings/legal', icon: IconGavel, current: false }, ] return (
- - {children} + +
+
+ +
+ {children} +
) } diff --git a/admin/inertia/pages/home.tsx b/admin/inertia/pages/home.tsx index 1feebb2..76a78b5 100644 --- a/admin/inertia/pages/home.tsx +++ b/admin/inertia/pages/home.tsx @@ -15,57 +15,57 @@ import { useUpdateAvailable } from '~/hooks/useUpdateAvailable' import { useSystemSetting } from '~/hooks/useSystemSetting' import Alert from '~/components/Alert' import { SERVICE_NAMES } from '../../constants/service_names' +import { useTranslation } from 'react-i18next' // Maps is a Core Capability (display_order: 4) -const MAPS_ITEM = { - label: 'Maps', +const getMapsItem = (t: (key: string) => string) => ({ + label: t('menu.maps'), to: '/maps', target: '', - description: 'View offline maps', + description: t('maps.viewOffline'), icon: , installed: true, displayOrder: 4, poweredBy: null, -} +}) // 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', target: '', - description: - 'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!', + description: t('home.easySetupDesc'), icon: , installed: true, displayOrder: 50, poweredBy: null, }, { - label: 'Install Apps', + label: t('menu.installApps'), to: '/settings/apps', target: '', - description: 'Not seeing your favorite app? Install it here!', + description: t('home.installAppsDesc'), icon: , installed: true, displayOrder: 51, poweredBy: null, }, { - label: 'Docs', + label: t('menu.docs'), to: '/docs/home', target: '', - description: 'Read Project N.O.M.A.D. manuals and guides', + description: t('home.docsDesc'), icon: , installed: true, displayOrder: 52, poweredBy: null, }, { - label: 'Settings', + label: t('menu.settings'), to: '/settings/system', target: '', - description: 'Configure your N.O.M.A.D. settings', + description: t('home.settingsDesc'), icon: , installed: true, displayOrder: 53, @@ -89,15 +89,18 @@ export default function Home(props: { services: ServiceSlim[] } }) { + const { t } = useTranslation() const items: DashboardItem[] = [] - const updateInfo = useUpdateAvailable(); + const updateInfo = useUpdateAvailable() const { aiAssistantName } = usePage<{ aiAssistantName: string }>().props // Check if user has visited Easy Setup 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) props.system.services @@ -105,7 +108,10 @@ export default function Home(props: { .forEach((service) => { items.push({ // 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) : '#', target: '_blank', description: @@ -123,38 +129,36 @@ export default function Home(props: { }) // Add Maps as a Core Capability - items.push(MAPS_ITEM) + items.push(getMapsItem(t)) // Add system items - items.push(...SYSTEM_ITEMS) + items.push(...getSystemItems(t)) // Sort all items by display order items.sort((a, b) => a.displayOrder - b.displayOrder) return ( - - { - updateInfo?.updateAvailable && ( -
- router.visit('/settings/update'), - }} - /> -
- ) - } + + {updateInfo?.updateAvailable && ( +
+ router.visit('/settings/update'), + }} + /> +
+ )}
{items.map((item) => { - const isEasySetup = item.label === 'Easy Setup' + const isEasySetup = item.label === t('menu.easySetup') const shouldHighlight = isEasySetup && shouldHighlightEasySetup const tileContent = ( @@ -166,13 +170,17 @@ export default function Home(props: { style={{ animationDuration: '1.5s' }} > - Start here! + {t('home.startHere')} )}
{item.icon}

{item.label}

- {item.poweredBy &&

Powered by {item.poweredBy}

} + {item.poweredBy && ( +

+ {t('home.poweredBy')} {item.poweredBy} +

+ )}

{item.description}

) diff --git a/admin/package-lock.json b/admin/package-lock.json index d34cc88..37d1d69 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -47,6 +47,8 @@ "edge.js": "^6.2.1", "fast-xml-parser": "^5.5.7", "fuse.js": "^7.1.0", + "i18next": "^26.0.3", + "i18next-browser-languagedetector": "^8.2.1", "jszip": "^3.10.1", "luxon": "^3.6.1", "maplibre-gl": "^4.7.1", @@ -61,6 +63,7 @@ "react": "^19.1.0", "react-adonis-transmit": "^1.0.1", "react-dom": "^19.1.0", + "react-i18next": "^17.0.2", "react-map-gl": "^8.1.0", "react-markdown": "^10.1.0", "reflect-metadata": "^0.2.2", @@ -1096,6 +1099,14 @@ "@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==", + "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", @@ -9814,6 +9825,14 @@ ], "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==", + "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", @@ -9917,6 +9936,44 @@ "node": ">=18.18.0" } }, + "node_modules/i18next": { + "version": "26.0.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz", + "integrity": "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==", + "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" + } + ], + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -14049,6 +14106,32 @@ "react": "^19.2.4" } }, + "node_modules/react-i18next": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz", + "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "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", @@ -16513,6 +16596,14 @@ "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==", + "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 ab229f4..e6931f1 100644 --- a/admin/package.json +++ b/admin/package.json @@ -100,6 +100,8 @@ "edge.js": "^6.2.1", "fast-xml-parser": "^5.5.7", "fuse.js": "^7.1.0", + "i18next": "^26.0.3", + "i18next-browser-languagedetector": "^8.2.1", "jszip": "^3.10.1", "luxon": "^3.6.1", "maplibre-gl": "^4.7.1", @@ -114,6 +116,7 @@ "react": "^19.1.0", "react-adonis-transmit": "^1.0.1", "react-dom": "^19.1.0", + "react-i18next": "^17.0.2", "react-map-gl": "^8.1.0", "react-markdown": "^10.1.0", "reflect-metadata": "^0.2.2",