mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 18:56:17 +01:00
migrate to lingui
This commit is contained in:
@@ -10,11 +10,9 @@ import { useState } from "react"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import clsx from "clsx"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
|
||||
export default function ConfigYaml() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [configContent, setConfigContent] = useState<string>("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
@@ -27,7 +25,7 @@ export default function ConfigYaml() {
|
||||
setConfigContent(config)
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error",
|
||||
title: t`Error`,
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
})
|
||||
@@ -43,23 +41,37 @@ export default function ConfigYaml() {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.yaml_config.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.subtitle")}</p>
|
||||
<h3 className="text-xl font-medium mb-2">
|
||||
<Trans>YAML Configuration</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Export your current systems configuration.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed my-1">
|
||||
{t("settings.yaml_config.des_1")} <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code>{" "}
|
||||
{t("settings.yaml_config.des_2")}
|
||||
<Trans>
|
||||
Systems may be managed in a <code className="bg-muted rounded-sm px-1 text-primary">config.yml</code> file
|
||||
inside your data directory.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>
|
||||
On each restart, systems in the database will be updated to match the systems defined in the file.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.yaml_config.des_3")}</p>
|
||||
<Alert className="my-4 border-destructive text-destructive w-auto table md:pe-6">
|
||||
<AlertCircleIcon className="h-4 w-4 stroke-destructive" />
|
||||
<AlertTitle>{t("settings.yaml_config.alert.title")}</AlertTitle>
|
||||
<AlertTitle>
|
||||
<Trans>Caution - potential data loss</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
{t("settings.yaml_config.alert.des_1")} <code>config.yml</code> {t("settings.yaml_config.alert.des_2")}
|
||||
<Trans>
|
||||
Existing systems not defined in <code>config.yml</code> will be deleted. Please make regular backups.
|
||||
</Trans>
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -78,7 +90,7 @@ export default function ConfigYaml() {
|
||||
<Separator className="my-5" />
|
||||
<Button type="button" className="mt-2 flex items-center gap-1" onClick={fetchConfig} disabled={isLoading}>
|
||||
<ButtonIcon className={clsx("h-4 w-4 me-0.5", isLoading && "animate-spin")} />
|
||||
{t("settings.export_configuration")}
|
||||
<Trans>Export configuration</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,20 +6,16 @@ import { Separator } from "@/components/ui/separator"
|
||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import { useState, useEffect } from "react"
|
||||
// import { Input } from '@/components/ui/input'
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useState } from "react"
|
||||
import { Trans } from "@lingui/macro"
|
||||
import languages from "../../../lib/languages.json"
|
||||
import { setLang } from "@/lib/i18n"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import { useLingui } from "@lingui/react"
|
||||
// import { setLang } from "@/lib/i18n"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = i18n.language
|
||||
}, [i18n.language])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { i18n } = useLingui()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
@@ -33,8 +29,12 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.general.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.general.subtitle")}</p>
|
||||
<h3 className="text-xl font-medium mb-2">
|
||||
<Trans>General</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Change general application options.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
@@ -42,25 +42,22 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium flex items-center gap-2">
|
||||
<LanguagesIcon className="h-4 w-4" />
|
||||
{t("settings.general.language.title")}
|
||||
<Trans>Language</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t("settings.general.language.subtitle_1")}{" "}
|
||||
<a
|
||||
href="https://hosted.weblate.org/engage/beszel/"
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Weblate
|
||||
</a>{" "}
|
||||
{t("settings.general.language.subtitle_2")}
|
||||
<Trans>
|
||||
Want to help us make our translations even better? Check out{" "}
|
||||
<a href="https://crowdin.com/project/beszel" className="link" target="_blank" rel="noopener noreferrer">
|
||||
Crowdin
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="lang">
|
||||
{t("settings.general.language.preferred_language")}
|
||||
<Trans>Preferred Language</Trans>
|
||||
</Label>
|
||||
<Select value={i18n.language} onValueChange={(lang: string) => setLang(lang)}>
|
||||
<Select value={i18n.locale} onValueChange={(lang: string) => dynamicActivate(lang)}>
|
||||
<SelectTrigger id="lang">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -77,13 +74,15 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.general.chart_options.title")}</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
<Trans>Chart options</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t("settings.general.chart_options.subtitle")}
|
||||
<Trans>Adjust display options for charts.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
{t("settings.general.chart_options.default_time_period")}
|
||||
<Trans>Default time period</Trans>
|
||||
</Label>
|
||||
<Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
|
||||
<SelectTrigger id="chartTime">
|
||||
@@ -98,13 +97,13 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
{t("settings.general.chart_options.default_time_period_des")}
|
||||
<Trans>Sets the default time range for charts when a system is viewed.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
{t("settings.save_settings")}
|
||||
<Trans>Save Settings</Trans>
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,8 @@ import { UserSettings } from "@/types.js"
|
||||
import General from "./general.tsx"
|
||||
import Notifications from "./notifications.tsx"
|
||||
import ConfigYaml from "./config-yaml.tsx"
|
||||
import { isAdmin } from "@/lib/utils.ts"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { t } from "i18next"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
try {
|
||||
@@ -31,47 +30,45 @@ export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
})
|
||||
$userSettings.set(updatedSettings.settings)
|
||||
toast({
|
||||
title: t("settings.saved"),
|
||||
description: t("settings.saved_des"),
|
||||
title: t`Settings saved`,
|
||||
description: t`Your user settings have been updated.`,
|
||||
})
|
||||
} catch (e) {
|
||||
// console.error('update settings', e)
|
||||
toast({
|
||||
title: t("settings.failed_to_save"),
|
||||
description: t("settings.check_logs"),
|
||||
title: t`Failed to save settings`,
|
||||
description: t`Check logs for more details.`,
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default function SettingsLayout() {
|
||||
const { t } = useTranslation()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: t("settings.general.title"),
|
||||
title: _(t({ message: `General`, comment: "Context: General settings" })),
|
||||
href: "/settings/general",
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t("settings.notifications.title"),
|
||||
title: t`Notifications`,
|
||||
href: "/settings/notifications",
|
||||
icon: BellIcon,
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin()) {
|
||||
sidebarNavItems.push({
|
||||
title: t("settings.yaml_config.short_title"),
|
||||
{
|
||||
title: t`YAML Config`,
|
||||
href: "/settings/config",
|
||||
icon: FileSlidersIcon,
|
||||
})
|
||||
}
|
||||
admin: true,
|
||||
},
|
||||
]
|
||||
|
||||
const page = useStore($router)
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Settings / Beszel"
|
||||
document.title = t`Settings` + " / Beszel"
|
||||
// redirect to account page if no page is specified
|
||||
if (page?.path === "/settings") {
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
@@ -81,8 +78,12 @@ export default function SettingsLayout() {
|
||||
return (
|
||||
<Card className="pt-5 px-4 pb-8 sm:pt-6 sm:px-7">
|
||||
<CardHeader className="p-0">
|
||||
<CardTitle className="mb-1">{t("settings.settings")}</CardTitle>
|
||||
<CardDescription>{t("settings.subtitle")}</CardDescription>
|
||||
<CardTitle className="mb-1">
|
||||
<Trans>Settings</Trans>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans>Manage display and notification preferences.</Trans>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Separator className="hidden md:block my-5" />
|
||||
|
||||
@@ -12,8 +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"
|
||||
import { t } from "i18next"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
url: string
|
||||
@@ -27,8 +26,6 @@ const NotificationSchema = v.object({
|
||||
})
|
||||
|
||||
const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSettings }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [webhooks, setWebhooks] = useState(userSettings.webhooks ?? [])
|
||||
const [emails, setEmails] = useState<string[]>(userSettings.emails ?? [])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -62,7 +59,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
await saveSettings(parsedData)
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: t("settings.failed_to_save"),
|
||||
title: t`Failed to save settings`,
|
||||
description: e.message,
|
||||
variant: "destructive",
|
||||
})
|
||||
@@ -73,51 +70,67 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">{t("settings.notifications.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">{t("settings.notifications.subtitle_1")}</p>
|
||||
<h3 className="text-xl font-medium mb-2">
|
||||
<Trans>Notifications</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Configure how you receive alert notifications.</Trans>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1.5 leading-relaxed">
|
||||
{t("settings.notifications.subtitle_2")} <BellIcon className="inline h-4 w-4" />{" "}
|
||||
{t("settings.notifications.subtitle_3")}
|
||||
<Trans>
|
||||
Looking instead for where to create alerts? Click the bell <BellIcon className="inline h-4 w-4" /> icons in
|
||||
the systems table.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.email.title")}</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
<Trans>Email notifications</Trans>
|
||||
</h3>
|
||||
{isAdmin() && (
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t("settings.notifications.email.please")}{" "}
|
||||
<a href="/_/#/settings/mail" className="link" target="_blank">
|
||||
{t("settings.notifications.email.configure_an_SMTP_server")}
|
||||
</a>{" "}
|
||||
{t("settings.notifications.email.to_ensure_alerts_are_delivered")}{" "}
|
||||
<Trans>
|
||||
Please{" "}
|
||||
<a href="/_/#/settings/mail" className="link" target="_blank">
|
||||
configure an SMTP server
|
||||
</a>{" "}
|
||||
to ensure alerts are delivered.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Label className="block" htmlFor="email">
|
||||
{t("settings.notifications.email.to_emails")}
|
||||
<Trans>To email(s)</Trans>
|
||||
</Label>
|
||||
<InputTags
|
||||
value={emails}
|
||||
onChange={setEmails}
|
||||
placeholder={t("settings.notifications.email.enter_email_address")}
|
||||
placeholder={t`Enter email address...`}
|
||||
className="w-full"
|
||||
type="email"
|
||||
id="email"
|
||||
/>
|
||||
<p className="text-[0.8rem] text-muted-foreground">{t("settings.notifications.email.des")}</p>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
<Trans>Save address using enter key or comma. Leave blank to disable email notifications.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="mb-1 text-lg font-medium">{t("settings.notifications.webhook.title")}</h3>
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
<Trans>Webhook / Push notifications</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{t("settings.notifications.webhook.des_1")}{" "}
|
||||
<a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
|
||||
Shoutrrr
|
||||
</a>{" "}
|
||||
{t("settings.notifications.webhook.des_2")}
|
||||
<Trans>
|
||||
Beszel uses{" "}
|
||||
<a href="https://containrrr.dev/shoutrrr/services/overview/" target="_blank" className="link">
|
||||
Shoutrrr
|
||||
</a>{" "}
|
||||
to integrate with popular notification services.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
{webhooks.length > 0 && (
|
||||
@@ -140,7 +153,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
onClick={addWebhook}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 -ms-0.5" />
|
||||
{t("settings.notifications.webhook.add")} URL
|
||||
<Trans>Add URL</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -151,7 +164,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
{t("settings.save_settings")}
|
||||
<Trans>Save Settings</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,13 +179,13 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
const res = await pb.send("/api/beszel/send-test-notification", { url })
|
||||
if ("err" in res && !res.err) {
|
||||
toast({
|
||||
title: t("settings.notifications.webhook.test_sent"),
|
||||
description: t("settings.notifications.webhook.test_sent_des"),
|
||||
title: t`Test notification sent`,
|
||||
description: t`Check your notification service`,
|
||||
})
|
||||
} else {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: res.err ?? "Failed to send test notification",
|
||||
title: t`Error`,
|
||||
description: res.err ?? t`Failed to send test notification`,
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
@@ -195,7 +208,9 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
|
||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<span>
|
||||
{t("settings.notifications.webhook.test")} <span className="hidden sm:inline">URL</span>
|
||||
<Trans>
|
||||
Test <span className="hidden sm:inline">URL</span>
|
||||
</Trans>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn, isAdmin } from "@/lib/utils"
|
||||
import { buttonVariants } from "../../ui/button"
|
||||
import { $router, Link, navigate } from "../../router"
|
||||
import { useStore } from "@nanostores/react"
|
||||
@@ -11,6 +11,7 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
href: string
|
||||
title: string
|
||||
icon?: React.FC<React.SVGProps<SVGSVGElement>>
|
||||
admin?: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -23,17 +24,20 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
<div className="md:hidden">
|
||||
<Select onValueChange={(value: string) => navigate(value)} value={page?.path}>
|
||||
<SelectTrigger className="w-full my-3.5">
|
||||
<SelectValue placeholder="Select a page" />
|
||||
<SelectValue placeholder="Select page" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.href} value={item.href}>
|
||||
<span className="flex items-center gap-2">
|
||||
{item.icon && <item.icon className="h-4 w-4" />}
|
||||
{item.title}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{items.map((item) => {
|
||||
if (item.admin && !isAdmin()) return null
|
||||
return (
|
||||
<SelectItem key={item.href} value={item.href}>
|
||||
<span className="flex items-center gap-2">
|
||||
{item.icon && <item.icon className="h-4 w-4" />}
|
||||
{item.title}
|
||||
</span>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Separator />
|
||||
|
||||
Reference in New Issue
Block a user