diff --git a/beszel/site/bun.lockb b/beszel/site/bun.lockb index 32b1d2d..d316e90 100755 Binary files a/beszel/site/bun.lockb and b/beszel/site/bun.lockb differ diff --git a/beszel/site/package.json b/beszel/site/package.json index ef0ec63..0763443 100644 --- a/beszel/site/package.json +++ b/beszel/site/package.json @@ -31,11 +31,14 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "d3-time": "^3.1.0", + "i18next": "^23.16.4", + "i18next-browser-languagedetector": "^8.0.0", "lucide-react": "^0.452.0", "nanostores": "^0.11.3", "pocketbase": "^0.21.5", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^15.1.0", "recharts": "^2.13.0", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", diff --git a/beszel/site/src/components/add-system.tsx b/beszel/site/src/components/add-system.tsx index 56cd2d0..0662670 100644 --- a/beszel/site/src/components/add-system.tsx +++ b/beszel/site/src/components/add-system.tsx @@ -24,8 +24,11 @@ import { useState, useRef, MutableRefObject } from 'react' import { useStore } from '@nanostores/react' import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils' import { navigate } from './router' +import { useTranslation } from 'react-i18next' export function AddSystemButton({ className }: { className?: string }) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) const port = useRef() as MutableRefObject const publicKey = useStore($publicKey) @@ -74,41 +77,40 @@ export function AddSystemButton({ className }: { className?: string }) { className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')} > - Add System + {t('add')} {t('system')} - Add New System + {t('add_system.add_new_system')} Docker - Binary + {t('add_system.binary')} - The agent must be running on the system to connect. Copy the{' '} - docker-compose.yml for the agent - below. + {t('add_system.dialog_des_1')}{' '} + docker-compose.yml {t('add_system.dialog_des_2')}
-

Click to copy

+

{t('add_system.click_to_copy')}

@@ -154,34 +156,34 @@ export function AddSystemButton({ className }: { className?: string }) { variant={'ghost'} onClick={() => copyDockerCompose(port.current.value)} > - Copy docker compose + {t('copy')} docker compose - + - The agent must be running on the system to connect. Copy the{' '} - install command for the agent below. + {t('add_system.dialog_des_1')}{' '} + install command {t('add_system.dialog_des_2')}
-

Click to copy

+

{t('add_system.click_to_copy')}

@@ -227,14 +229,14 @@ export function AddSystemButton({ className }: { className?: string }) { variant={'ghost'} onClick={() => copyInstallCommand(port.current.value)} > - Copy linux command + {t('copy')} linux {t('add_system.command')} - + - + ) } diff --git a/beszel/site/src/components/alerts/alert-button.tsx b/beszel/site/src/components/alerts/alert-button.tsx index 929e69a..ecdc569 100644 --- a/beszel/site/src/components/alerts/alert-button.tsx +++ b/beszel/site/src/components/alerts/alert-button.tsx @@ -17,6 +17,7 @@ import { Link } from '../router' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Checkbox } from '../ui/checkbox' import { SystemAlert, SystemAlertGlobal } from './alerts-system' +import { useTranslation } from 'react-i18next' export default memo(function AlertsButton({ system }: { system: SystemRecord }) { const alerts = useStore($alerts) @@ -54,6 +55,8 @@ function TheContent({ }: { data: { system: SystemRecord; alerts: AlertRecord[]; systemAlerts: AlertRecord[] } }) { + const { t } = useTranslation() + const [overwriteExisting, setOverwriteExisting] = useState(false) const systems = $systems.get() @@ -69,13 +72,13 @@ function TheContent({ return ( <> - Alerts + {t('alerts.title')} - See{' '} + {t('alerts.subtitle_1')}{' '} - notification settings + {t('alerts.notification_settings')} {' '} - to configure how you receive alerts. + {t('alerts.subtitle_2')} @@ -86,7 +89,7 @@ function TheContent({ - All systems + {t('all_systems')} @@ -107,7 +110,7 @@ function TheContent({ checked={overwriteExisting} onCheckedChange={setOverwriteExisting} /> - Overwrite existing alerts + {t('alerts.overwrite_existing_alerts')}
{data.map((d) => ( diff --git a/beszel/site/src/components/lang-toggle.tsx b/beszel/site/src/components/lang-toggle.tsx new file mode 100644 index 0000000..46ffa80 --- /dev/null +++ b/beszel/site/src/components/lang-toggle.tsx @@ -0,0 +1,42 @@ +import { useEffect } from 'react' +import { GlobeIcon, Languages } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { useTranslation } from 'react-i18next' +import languages from '../lib/languages.json' + +export function LangToggle() { + const { i18n } = useTranslation(); + + useEffect(() => { + document.documentElement.lang = i18n.language; + }, [i18n.language]); + + return ( + + + + + + {languages.map(({ lang, label }) => ( + i18n.changeLanguage(lang)} + > + {label} + + ))} + + + ) +} diff --git a/beszel/site/src/components/mode-toggle.tsx b/beszel/site/src/components/mode-toggle.tsx index 35e1bc3..257c70c 100644 --- a/beszel/site/src/components/mode-toggle.tsx +++ b/beszel/site/src/components/mode-toggle.tsx @@ -8,8 +8,10 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useTheme } from '@/components/theme-provider' +import { useTranslation } from 'react-i18next' export function ModeToggle() { + const { t } = useTranslation() const { setTheme } = useTheme() return ( @@ -18,21 +20,21 @@ export function ModeToggle() { setTheme('light')}> - Light + {t('themes.light')} setTheme('dark')}> - Dark + {t('themes.dark')} setTheme('system')}> - System + {t('themes.system')} diff --git a/beszel/site/src/components/routes/home.tsx b/beszel/site/src/components/routes/home.tsx index 0a0f646..8bcca40 100644 --- a/beszel/site/src/components/routes/home.tsx +++ b/beszel/site/src/components/routes/home.tsx @@ -9,10 +9,15 @@ import { AlertRecord, SystemRecord } from '@/types' import { Input } from '../ui/input' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Link } from '../router' +import { useTranslation } from 'react-i18next' const SystemsTable = lazy(() => import('../systems-table/systems-table')) +const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + export default function () { + const { t } = useTranslation() + const hubVersion = useStore($hubVersion) const [filter, setFilter] = useState() const alerts = useStore($alerts) @@ -58,7 +63,7 @@ export default function () {
- Active Alerts + {t('home.active_alerts')}
@@ -76,8 +81,11 @@ export default function () { {alert.sysname} {info.name} - Exceeds {alert.value} - {info.unit} average in last {alert.min} min + {t('active_des', { + value: alert.value, + unit: info.unit, + minutes: alert.min + })}
- All Systems + {t('all_systems')} - Updated in real time. Press{' '} + {t('home.subtitle_1')}{' '} - K + {isMac ? '⌘' : "Ctrl"}K {' '} - to open the command palette. + {t('home.subtitle_2')}
setFilter(e.target.value)} className="w-full md:w-56 lg:w-80 ml-auto px-4" /> diff --git a/beszel/site/src/components/routes/settings/general.tsx b/beszel/site/src/components/routes/settings/general.tsx index 9cc331a..f16eca7 100644 --- a/beszel/site/src/components/routes/settings/general.tsx +++ b/beszel/site/src/components/routes/settings/general.tsx @@ -12,10 +12,18 @@ import { Separator } from '@/components/ui/separator' import { LoaderCircleIcon, SaveIcon } from 'lucide-react' import { UserSettings } from '@/types' import { saveSettings } from './layout' -import { useState } from 'react' +import { useState, useEffect } from 'react' // import { Input } from '@/components/ui/input' +import { useTranslation } from 'react-i18next' +import languages from '../../../lib/languages.json' export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) { + const { t, i18n } = useTranslation() + + useEffect(() => { + document.documentElement.lang = i18n.language; + }, [i18n.language]); + const [isLoading, setIsLoading] = useState(false) async function handleSubmit(e: React.FormEvent) { @@ -30,46 +38,49 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us return (
-

General

+

{t('settings.general.title')}

- Change general application options. + {t('settings.general.subtitle')}

- {/*
-

Language

+

{t('settings.general.language.title')}

- Internationalization will be added in a future release. Please see the{' '} + {t('settings.general.language.subtitle_1')}{' '} - discussion on GitHub + Crowdin {' '} - for more details. + {t('settings.general.language.subtitle_2')}

- i18n.changeLanguage(lang)}> - English + {languages.map((lang) => ( + + {lang.label} + + ))} -
*/} +
-

Chart options

+

{t('settings.general.chart_options.title')}

- Adjust display options for charts. + {t('settings.general.chart_options.subtitle')}

- Sets the default time range for charts when a system is viewed. + {t('settings.general.chart_options.default_time_period_des')}

@@ -102,7 +113,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us ) : ( )} - Save settings + {t('settings.save_settings')}
diff --git a/beszel/site/src/components/routes/settings/layout.tsx b/beszel/site/src/components/routes/settings/layout.tsx index 05d547b..29b3736 100644 --- a/beszel/site/src/components/routes/settings/layout.tsx +++ b/beszel/site/src/components/routes/settings/layout.tsx @@ -13,27 +13,7 @@ import General from './general.tsx' import Notifications from './notifications.tsx' import ConfigYaml from './config-yaml.tsx' import { isAdmin } from '@/lib/utils.ts' - -const sidebarNavItems = [ - { - title: 'General', - href: '/settings/general', - icon: SettingsIcon, - }, - { - title: 'Notifications', - href: '/settings/notifications', - icon: BellIcon, - }, -] - -if (isAdmin()) { - sidebarNavItems.push({ - title: 'YAML Config', - href: '/settings/config', - icon: FileSlidersIcon, - }) -} +import { useTranslation } from 'react-i18next' export async function saveSettings(newSettings: Partial) { try { @@ -64,6 +44,29 @@ export async function saveSettings(newSettings: Partial) { } export default function SettingsLayout() { + const { t } = useTranslation() + + const sidebarNavItems = [ + { + title: t('settings.general.title'), + href: '/settings/general', + icon: SettingsIcon, + }, + { + title: t('settings.notifications.title'), + href: '/settings/notifications', + icon: BellIcon, + }, + ] + + if (isAdmin()) { + sidebarNavItems.push({ + title: 'YAML Config', + href: '/settings/config', + icon: FileSlidersIcon, + }) + } + const page = useStore($router) useEffect(() => { @@ -77,8 +80,8 @@ export default function SettingsLayout() { return ( - Settings - Manage display and notification preferences. + {t('settings.settings')} + {t('settings.subtitle')} diff --git a/beszel/site/src/components/routes/settings/notifications.tsx b/beszel/site/src/components/routes/settings/notifications.tsx index 503ccef..372bbd6 100644 --- a/beszel/site/src/components/routes/settings/notifications.tsx +++ b/beszel/site/src/components/routes/settings/notifications.tsx @@ -12,6 +12,7 @@ import { UserSettings } from '@/types' import { saveSettings } from './layout' import * as v from 'valibot' import { isAdmin } from '@/lib/utils' +import { useTranslation } from 'react-i18next' interface ShoutrrrUrlCardProps { url: string @@ -25,6 +26,8 @@ const NotificationSchema = v.object({ }) const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSettings }) => { + const { t } = useTranslation() + const [webhooks, setWebhooks] = useState(userSettings.webhooks ?? []) const [emails, setEmails] = useState(userSettings.emails ?? []) const [isLoading, setIsLoading] = useState(false) @@ -69,13 +72,13 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting return (
-

Notifications

+

{t('settings.notifications.title')}

- Configure how you receive alert notifications. + {t('settings.notifications.subtitle_1')}

- Looking instead for where to create alerts? Click the bell{' '} - icons in the systems table. + {t('settings.notifications.subtitle_2')}{' '} + {t('settings.notifications.subtitle_3')}

@@ -161,7 +164,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting ) : ( )} - Save settings + {t('settings.save_settings')}
diff --git a/beszel/site/src/components/routes/system.tsx b/beszel/site/src/components/routes/system.tsx index 94b20e3..ff41b31 100644 --- a/beszel/site/src/components/routes/system.tsx +++ b/beszel/site/src/components/routes/system.tsx @@ -21,6 +21,7 @@ import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons' import { useIntersectionObserver } from '@/lib/use-intersection-observer' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { timeTicks } from 'd3-time' +import { useTranslation } from 'react-i18next' const AreaChartDefault = lazy(() => import('../charts/area-chart')) const ContainerChart = lazy(() => import('../charts/container-chart')) @@ -374,9 +375,8 @@ export default function SystemDetail({ name }: { name: string }) { : null} > ) => { @@ -534,7 +536,7 @@ function ContainerFilterBar() { return ( <> ) { const val = info.getValue() as number @@ -102,6 +103,8 @@ function sortableHeader( } export default function SystemsTable({ filter }: { filter?: string }) { + const { t } = useTranslation() + const data = useStore($systems) const hubVersion = useStore($hubVersion) const [sorting, setSorting] = useState([]) @@ -145,32 +148,32 @@ export default function SystemsTable({ filter }: { filter?: string }) { ) }, - header: ({ column }) => sortableHeader(column, 'System', ServerIcon), + header: ({ column }) => sortableHeader(column, t('systems_table.system'), ServerIcon), }, { accessorKey: 'info.cpu', invertSorting: true, cell: CellFormatter, - header: ({ column }) => sortableHeader(column, 'CPU', CpuIcon), + header: ({ column }) => sortableHeader(column, t('systems_table.cpu'), CpuIcon), }, { accessorKey: 'info.mp', invertSorting: true, cell: CellFormatter, - header: ({ column }) => sortableHeader(column, 'Memory', MemoryStickIcon), + header: ({ column }) => sortableHeader(column, t('systems_table.memory'), MemoryStickIcon), }, { accessorKey: 'info.dp', invertSorting: true, cell: CellFormatter, - header: ({ column }) => sortableHeader(column, 'Disk', HardDriveIcon), + header: ({ column }) => sortableHeader(column, t('systems_table.disk'), HardDriveIcon), }, { accessorFn: (originalRow) => originalRow.info.b || 0, id: 'n', invertSorting: true, size: 115, - header: ({ column }) => sortableHeader(column, 'Net', EthernetIcon), + header: ({ column }) => sortableHeader(column, t('systems_table.net'), EthernetIcon), cell: (info) => { const val = info.getValue() as number return ( @@ -184,7 +187,7 @@ export default function SystemsTable({ filter }: { filter?: string }) { accessorKey: 'info.v', invertSorting: true, size: 50, - header: ({ column }) => sortableHeader(column, 'Agent', WifiIcon, true), + header: ({ column }) => sortableHeader(column, t('systems_table.agent'), WifiIcon, true), cell: (info) => { const version = info.getValue() as string if (!version || !hubVersion) { @@ -217,7 +220,7 @@ export default function SystemsTable({ filter }: { filter?: string }) { @@ -233,44 +236,42 @@ export default function SystemsTable({ filter }: { filter?: string }) { {status === 'paused' ? ( <> - Resume + {t('systems_table.resume')} ) : ( <> - Pause + {t('systems_table.pause')} )} copyToClipboard(host)}> - Copy host + {t('systems_table.copy_host')} - Delete + {t('systems_table.delete')} - Are you sure you want to delete {name}? + {t('systems_table.delete_confirm', { name })} - This action cannot be undone. This will permanently delete all current records - for {name} from the - database. + {t('systems_table.delete_confirm_des_1')} {name} {t('systems_table.delete_confirm_des_2')} - Cancel + {t('cancel')} pb.collection('systems').delete(id)} > - Continue + {t('continue')} @@ -354,7 +355,7 @@ export default function SystemsTable({ filter }: { filter?: string }) { ) : ( - No systems found + {t('systems_table.no_systems_found')} )} diff --git a/beszel/site/src/lib/i18n.ts b/beszel/site/src/lib/i18n.ts new file mode 100644 index 0000000..3990a1f --- /dev/null +++ b/beszel/site/src/lib/i18n.ts @@ -0,0 +1,22 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import en from '../locales/en/translation.json'; +import es from '../locales/es/translation.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + es: { translation: es } + }, + fallbackLng: 'en', + interpolation: { + escapeValue: false + } + }); + +export { i18n }; \ No newline at end of file diff --git a/beszel/site/src/lib/languages.json b/beszel/site/src/lib/languages.json new file mode 100644 index 0000000..4cb11f0 --- /dev/null +++ b/beszel/site/src/lib/languages.json @@ -0,0 +1,30 @@ +[ + { + "lang": "en", + "label": "English" + }, + { + "lang": "es", + "label": "Español" + }, + { + "lang": "fr", + "label": "Français" + }, + { + "lang": "de", + "label": "Deutsch" + }, + { + "lang": "ru", + "label": "Русский" + }, + { + "lang": "zh-Hans", + "label": "简体中文" + }, + { + "lang": "zh-Hant", + "label": "繁體中文" + } +] \ No newline at end of file diff --git a/beszel/site/src/locales/en/translation.json b/beszel/site/src/locales/en/translation.json new file mode 100644 index 0000000..b689e9d --- /dev/null +++ b/beszel/site/src/locales/en/translation.json @@ -0,0 +1,94 @@ +{ + "all_systems": "All Systems", + "filter": "Filter...", + "copy": "Copy", + "add": "Add", + "system": "System", + "systems": "Systems", + "cancel": "Cancel", + "continue": "Continue", + "home": { + "active_alerts": "Active Alerts", + "active_des": "Exceeds {{value}}{{unit}} average in last {{minutes}} minutes", + "subtitle_1": "Updated in real time. Press", + "subtitle_2": "to open the command palette." + }, + "systems_table": { + "system": "System", + "memory": "Memory", + "cpu": "CPU", + "disk": "Disk", + "net": "Net", + "agent": "Agent", + "no_systems_found": "No systems found.", + "open_menu": "Open menu", + "resume": "Resume", + "pause": "Pause", + "copy_host": "Copy host", + "delete": "Delete", + "delete_confirm": "Are you sure you want to delete {{name}}?", + "delete_confirm_des_1": "This action cannot be undone. This will permanently delete all current records for", + "delete_confirm_des_2": "from the database." + }, + "alerts": { + "title": "Alerts", + "subtitle_1": "See", + "notification_settings": "notification settings", + "subtitle_2": "to configure how you receive alerts.", + "overwrite_existing_alerts": "Overwrite existing alerts" + }, + "settings": { + "settings": "Settings", + "subtitle": "Manage display and notification preferences.", + "save_settings": "Save Settings", + "general": { + "title": "General", + "subtitle": "Change general application options.", + "language": { + "title": "Language", + "subtitle_1": "Want to help us make our translations even better? Check out", + "subtitle_2": "for more details.", + "preferred_language": "Preferred Language" + }, + "chart_options": { + "title": "Chart options", + "subtitle": "Adjust display options for charts.", + "default_time_period": "Default time period", + "default_time_period_des": "Sets the default time range for charts when a system is viewed." + } + }, + "notifications": { + "title": "Notifications", + "subtitle_1": "Configure how you receive alert notifications.", + "subtitle_2": "Looking instead for where to create alerts? Click the bell", + "subtitle_3": "icons in the systems table." + }, + "language": "Language" + }, + "user_dm": { + "users": "Users", + "logs": "Logs", + "backups": "Backups", + "auth_providers": "Auth Providers", + "log_out": "Log Out" + }, + "themes": { + "toggle_theme": "Toggle theme", + "light": "Light", + "dark": "Dark", + "system": "System" + }, + "add_system": { + "add_new_system": "Add New System", + "binary": "Binary", + "dialog_des_1": "The agent must be running on the system to connect. Copy the", + "dialog_des_2": "for the agent below.", + "name": "Name", + "host_ip": "Host / IP", + "port": "Port", + "public_key": "Public Key", + "click_to_copy": "Click to copy", + "command": "command", + "add_system": "Add system" + } +} \ No newline at end of file diff --git a/beszel/site/src/locales/es/translation.json b/beszel/site/src/locales/es/translation.json new file mode 100644 index 0000000..63407ef --- /dev/null +++ b/beszel/site/src/locales/es/translation.json @@ -0,0 +1,94 @@ +{ + "all_systems": "Todos los sistemas", + "filter": "Filtrar...", + "copy": "Copiar", + "add": "Agregar", + "system": "Sistema", + "systems": "Sistemas", + "cancel": "Cancelar", + "continue": "Continuar", + "home": { + "active_alerts": "Alertas activas", + "active_des": "Excede el promedio de {{value}}{{unit}} en los últimos {{minutes}} minutos", + "subtitle_1": "Actualizado en tiempo real. Presione", + "subtitle_2": "para abrir la paleta de comandos." + }, + "systems_table": { + "system": "Sistema", + "memory": "Memoria", + "cpu": "CPU", + "disk": "Disco", + "net": "Red", + "agent": "Agente", + "no_systems_found": "No se encontraron sistemas.", + "open_menu": "Abrir menú", + "resume": "Reanudar", + "pause": "Pausar", + "copy_host": "Copiar host", + "delete": "Eliminar", + "delete_confirm": "¿Estás seguro de que quieres eliminar {{name}}?", + "delete_confirm_des_1": "Esta acción no se puede deshacer. Esto eliminará permanentemente todos los registros actuales para", + "delete_confirm_des_2": "de la base de datos." + }, + "alerts": { + "title": "Alertas", + "subtitle_1": "Ver", + "notification_settings": "configuración de notificaciones", + "subtitle_2": "para configurar cómo recibe las alertas.", + "overwrite_existing_alerts": "Sobrescribir alertas existentes" + }, + "settings": { + "settings": "Configuración", + "subtitle": "Administrar las preferencias de visualización y notificación.", + "save_settings": "Guardar configuración", + "general": { + "title": "General", + "subtitle": "Cambiar las opciones generales de la aplicación.", + "language": { + "title": "Idioma", + "subtitle_1": "¿Quieres ayudarnos a mejorar nuestras traducciones? Consulta", + "subtitle_2": "para más detalles.", + "preferred_language": "Idioma preferido" + }, + "chart_options": { + "title": "Opciones de gráfico", + "subtitle": "Ajustar las opciones de visualización para los gráficos.", + "default_time_period": "Periodo de tiempo predeterminado", + "default_time_period_des": "Establece el rango de tiempo predeterminado para los gráficos cuando se visualiza un sistema." + } + }, + "notifications": { + "title": "Notificaciones", + "subtitle_1": "Configure cómo recibe las notificaciones de alerta.", + "subtitle_2": "¿Busca dónde crear alertas? Haga clic en el icono de campana", + "subtitle_3": "en la tabla de sistemas." + }, + "language": "Idioma" + }, + "user_dm": { + "users": "Usuarios", + "logs": "Registros", + "backups": "Respaldos", + "auth_providers": "Proveedores de autenticación", + "log_out": "Cerrar sesión" + }, + "themes": { + "toggle_theme": "Alternar tema", + "light": "Claro", + "dark": "Oscuro", + "system": "Sistema" + }, + "add_system": { + "add_new_system": "Agregar nuevo sistema", + "binary": "Binario", + "dialog_des_1": "El agente debe estar ejecutándose en el sistema para conectarse. Copie el", + "dialog_des_2": "para el agente a continuación.", + "name": "Nombre", + "host_ip": "Host / IP", + "port": "Puerto", + "public_key": "Clave pública", + "click_to_copy": "Haga clic para copiar", + "command": "comando", + "add_system": "Agregar sistema" + } +} \ No newline at end of file diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx index 89b6204..91d4efd 100644 --- a/beszel/site/src/main.tsx +++ b/beszel/site/src/main.tsx @@ -11,6 +11,7 @@ import { $hubVersion, $copyContent, } from './lib/stores.ts' +import { LangToggle } from './components/lang-toggle.tsx' import { ModeToggle } from './components/mode-toggle.tsx' import { cn, @@ -48,6 +49,9 @@ import { $router, Link } from './components/router.tsx' import SystemDetail from './components/routes/system.tsx' import { AddSystemButton } from './components/add-system.tsx' +import './lib/i18n.ts' +import { useTranslation } from 'react-i18next' + // const ServerDetail = lazy(() => import('./components/routes/system.tsx')) const CommandPalette = lazy(() => import('./components/command-palette.tsx')) const LoginPage = lazy(() => import('./components/login/login.tsx')) @@ -111,6 +115,8 @@ const App = () => { } const Layout = () => { + const { t } = useTranslation() + const authenticated = useStore($authenticated) const copyContent = useStore($copyContent) @@ -131,6 +137,7 @@ const Layout = () => {