From 376e8d46213fd6cb2db92cec80e2a8b0d28302e9 Mon Sep 17 00:00:00 2001 From: Arsfy Date: Mon, 28 Oct 2024 13:37:21 +0800 Subject: [PATCH] ctrl k & i18n --- beszel/site/bun.lockb | Bin 155650 -> 158101 bytes beszel/site/package.json | 3 + beszel/site/src/components/add-system.tsx | 48 ++++----- .../src/components/alerts/alert-button.tsx | 15 +-- beszel/site/src/components/lang-toggle.tsx | 42 ++++++++ beszel/site/src/components/mode-toggle.tsx | 10 +- beszel/site/src/components/routes/home.tsx | 24 +++-- .../components/routes/settings/general.tsx | 45 +++++---- .../src/components/routes/settings/layout.tsx | 49 ++++----- .../routes/settings/notifications.tsx | 13 ++- beszel/site/src/components/routes/system.tsx | 10 +- .../systems-table/systems-table.tsx | 37 +++---- beszel/site/src/lib/i18n.ts | 22 ++++ beszel/site/src/lib/languages.json | 30 ++++++ beszel/site/src/locales/en/translation.json | 94 ++++++++++++++++++ beszel/site/src/locales/es/translation.json | 94 ++++++++++++++++++ beszel/site/src/main.tsx | 19 ++-- 17 files changed, 441 insertions(+), 114 deletions(-) create mode 100644 beszel/site/src/components/lang-toggle.tsx create mode 100644 beszel/site/src/lib/i18n.ts create mode 100644 beszel/site/src/lib/languages.json create mode 100644 beszel/site/src/locales/en/translation.json create mode 100644 beszel/site/src/locales/es/translation.json diff --git a/beszel/site/bun.lockb b/beszel/site/bun.lockb index 32b1d2dd37edb3530fd1baeb1969ecebd49a9520..d316e9006091c17741f49c2edfa7ad8b0dd36331 100755 GIT binary patch delta 29286 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 delta 27879 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�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 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 = () => {