Implemented project-wide localization plumbing and converted the visible UI/app messages to translation-ready strings.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { AlertCircleIcon } from 'lucide-react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
|
||||
export default function AlertError({
|
||||
errors,
|
||||
@@ -8,10 +9,12 @@ export default function AlertError({
|
||||
errors: string[];
|
||||
title?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle>{title || 'Something went wrong.'}</AlertTitle>
|
||||
<AlertTitle>{t(title || 'Something went wrong.')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
{Array.from(new Set(errors)).map((error, index) => (
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
import { UserMenuContent } from '@/components/user-menu-content';
|
||||
import { useCurrentUrl } from '@/hooks/use-current-url';
|
||||
import { useInitials } from '@/hooks/use-initials';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { dashboard, home } from '@/routes';
|
||||
import { dashboard as adminDashboard } from '@/routes/admin';
|
||||
@@ -56,6 +57,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
||||
const { auth } = page.props;
|
||||
const getInitials = useInitials();
|
||||
const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl();
|
||||
const { t } = useTranslation();
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Live',
|
||||
@@ -108,7 +110,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
||||
className="flex h-full w-64 flex-col items-stretch justify-between bg-sidebar"
|
||||
>
|
||||
<SheetTitle className="sr-only">
|
||||
Navigation menu
|
||||
{t('Navigation menu')}
|
||||
</SheetTitle>
|
||||
<SheetHeader className="flex justify-start text-left">
|
||||
<AppLogoIcon className="h-6 w-6 fill-current text-black dark:text-white" />
|
||||
@@ -125,7 +127,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
||||
{item.icon && (
|
||||
<item.icon className="h-5 w-5" />
|
||||
)}
|
||||
<span>{item.title}</span>
|
||||
<span>{t(item.title)}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -168,7 +170,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
||||
{item.icon && (
|
||||
<item.icon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
</Link>
|
||||
{isCurrentUrl(item.href) && (
|
||||
<div className="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-primary"></div>
|
||||
@@ -199,13 +201,13 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
||||
className="group inline-flex h-9 w-9 items-center justify-center rounded-md bg-transparent p-0 text-sm font-medium text-accent-foreground ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Live directory
|
||||
{t('Live directory')}
|
||||
</span>
|
||||
<Radio className="size-5 opacity-80 group-hover:opacity-100" />
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Live directory</p>
|
||||
<p>{t('Live directory')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { Appearance } from '@/hooks/use-appearance';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function AppearanceToggleTab({
|
||||
@@ -10,6 +11,7 @@ export default function AppearanceToggleTab({
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) {
|
||||
const { appearance, updateAppearance } = useAppearance();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [
|
||||
{ value: 'light', icon: Sun, label: 'Light' },
|
||||
@@ -37,7 +39,7 @@ export default function AppearanceToggleTab({
|
||||
)}
|
||||
>
|
||||
<Icon className="-ml-1 h-4 w-4" />
|
||||
<span className="ml-1.5 text-sm">{label}</span>
|
||||
<span className="ml-1.5 text-sm">{t(label)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
|
||||
|
||||
export function Breadcrumbs({
|
||||
@@ -15,6 +16,8 @@ export function Breadcrumbs({
|
||||
}: {
|
||||
breadcrumbs: BreadcrumbItemType[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{breadcrumbs.length > 0 && (
|
||||
@@ -28,12 +31,12 @@ export function Breadcrumbs({
|
||||
<BreadcrumbItem>
|
||||
{isLast ? (
|
||||
<BreadcrumbPage>
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={item.href}>
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
|
||||
export default function DeleteUser() {
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -28,9 +30,11 @@ export default function DeleteUser() {
|
||||
/>
|
||||
<div className="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
|
||||
<div className="relative space-y-0.5 text-red-600 dark:text-red-100">
|
||||
<p className="font-medium">Warning</p>
|
||||
<p className="font-medium">{t('Warning')}</p>
|
||||
<p className="text-sm">
|
||||
Please proceed with caution, this cannot be undone.
|
||||
{t(
|
||||
'Please proceed with caution, this cannot be undone.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -45,13 +49,12 @@ export default function DeleteUser() {
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
Are you sure you want to delete your account?
|
||||
{t('Are you sure you want to delete your account?')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Once your account is deleted, all of its resources
|
||||
and data will also be permanently deleted. Please
|
||||
enter your password to confirm you would like to
|
||||
permanently delete your account.
|
||||
{t(
|
||||
'Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.',
|
||||
)}
|
||||
</DialogDescription>
|
||||
|
||||
<Form
|
||||
@@ -77,7 +80,7 @@ export default function DeleteUser() {
|
||||
id="password"
|
||||
name="password"
|
||||
ref={passwordInput}
|
||||
placeholder="Password"
|
||||
placeholder={t('Password')}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
|
||||
export default function Heading({
|
||||
title,
|
||||
description,
|
||||
@@ -7,6 +9,8 @@ export default function Heading({
|
||||
description?: string;
|
||||
variant?: 'default' | 'small';
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<header className={variant === 'small' ? '' : 'mb-8 space-y-0.5'}>
|
||||
<h2
|
||||
@@ -16,10 +20,12 @@ export default function Heading({
|
||||
: 'text-xl font-semibold tracking-tight'
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{t(title)}
|
||||
</h2>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(description)}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -7,25 +7,27 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { useCurrentUrl } from '@/hooks/use-current-url';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import type { NavItem } from '@/types';
|
||||
|
||||
export function NavMain({ items = [] }: { items: NavItem[] }) {
|
||||
const { isCurrentUrl } = useCurrentUrl();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SidebarGroup className="px-2 py-0">
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>{t('Platform')}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isCurrentUrl(item.href)}
|
||||
tooltip={{ children: item.title }}
|
||||
tooltip={{ children: t(item.title) }}
|
||||
>
|
||||
<Link href={item.href} prefetch>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<span>{t(item.title)}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Eye, EyeOff } from 'lucide-react';
|
||||
import type { ComponentProps, Ref } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function PasswordInput({
|
||||
@@ -10,6 +11,7 @@ export default function PasswordInput({
|
||||
...props
|
||||
}: Omit<ComponentProps<'input'>, 'type'> & { ref?: Ref<HTMLInputElement> }) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -23,7 +25,7 @@ export default function PasswordInput({
|
||||
type="button"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 text-muted-foreground hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring focus-visible:outline-none"
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
aria-label={t(showPassword ? 'Hide password' : 'Show password')}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import { regenerateRecoveryCodes } from '@/routes/two-factor';
|
||||
|
||||
type Props = {
|
||||
@@ -26,6 +27,7 @@ export default function TwoFactorRecoveryCodes({
|
||||
const [codesAreVisible, setCodesAreVisible] = useState<boolean>(false);
|
||||
const codesSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const canRegenerateCodes = recoveryCodesList.length > 0 && codesAreVisible;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleCodesVisibility = useCallback(async () => {
|
||||
if (!codesAreVisible && !recoveryCodesList.length) {
|
||||
@@ -57,11 +59,12 @@ export default function TwoFactorRecoveryCodes({
|
||||
<CardHeader>
|
||||
<CardTitle className="flex gap-3">
|
||||
<LockKeyhole className="size-4" aria-hidden="true" />
|
||||
2FA recovery codes
|
||||
{t('2FA recovery codes')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Recovery codes let you regain access if you lose your 2FA
|
||||
device. Store them in a secure password manager.
|
||||
{t(
|
||||
'Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.',
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -76,7 +79,9 @@ export default function TwoFactorRecoveryCodes({
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{codesAreVisible ? 'Hide' : 'View'} recovery codes
|
||||
{codesAreVisible
|
||||
? t('Hide recovery codes')
|
||||
: t('View recovery codes')}
|
||||
</Button>
|
||||
|
||||
{canRegenerateCodes && (
|
||||
@@ -92,7 +97,7 @@ export default function TwoFactorRecoveryCodes({
|
||||
disabled={processing}
|
||||
aria-describedby="regenerate-warning"
|
||||
>
|
||||
<RefreshCw /> Regenerate codes
|
||||
<RefreshCw /> {t('Regenerate codes')}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
@@ -112,7 +117,7 @@ export default function TwoFactorRecoveryCodes({
|
||||
ref={codesSectionRef}
|
||||
className="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm"
|
||||
role="list"
|
||||
aria-label="Recovery codes"
|
||||
aria-label={t('Recovery codes')}
|
||||
>
|
||||
{recoveryCodesList.length ? (
|
||||
recoveryCodesList.map((code, index) => (
|
||||
@@ -127,7 +132,9 @@ export default function TwoFactorRecoveryCodes({
|
||||
) : (
|
||||
<div
|
||||
className="space-y-2"
|
||||
aria-label="Loading recovery codes"
|
||||
aria-label={t(
|
||||
'Loading recovery codes',
|
||||
)}
|
||||
>
|
||||
{Array.from(
|
||||
{ length: 8 },
|
||||
@@ -145,13 +152,13 @@ export default function TwoFactorRecoveryCodes({
|
||||
|
||||
<div className="text-xs text-muted-foreground select-none">
|
||||
<p id="regenerate-warning">
|
||||
Each recovery code can be used once to
|
||||
access your account and will be removed
|
||||
after use. If you need more, click{' '}
|
||||
{t(
|
||||
'Each recovery code can be used once to access your account and will be removed after use. If you need more, click',
|
||||
)}{' '}
|
||||
<span className="font-bold">
|
||||
Regenerate codes
|
||||
{t('Regenerate codes')}
|
||||
</span>{' '}
|
||||
above.
|
||||
{t('above.')}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Spinner } from '@/components/ui/spinner';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { useClipboard } from '@/hooks/use-clipboard';
|
||||
import { OTP_MAX_LENGTH } from '@/hooks/use-two-factor-auth';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import { confirm } from '@/routes/two-factor';
|
||||
|
||||
function GridScanIcon() {
|
||||
@@ -64,6 +65,7 @@ function TwoFactorSetupStep({
|
||||
}) {
|
||||
const { resolvedAppearance } = useAppearance();
|
||||
const [copiedText, copy] = useClipboard();
|
||||
const { t } = useTranslation();
|
||||
const IconComponent = copiedText === manualSetupKey ? Check : Copy;
|
||||
|
||||
return (
|
||||
@@ -104,7 +106,7 @@ function TwoFactorSetupStep({
|
||||
<div className="relative flex w-full items-center justify-center">
|
||||
<div className="absolute inset-0 top-1/2 h-px w-full bg-border" />
|
||||
<span className="relative bg-card px-2 py-1">
|
||||
or, enter the code manually
|
||||
{t('or, enter the code manually')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -147,6 +149,7 @@ function TwoFactorVerificationStep({
|
||||
}) {
|
||||
const [code, setCode] = useState<string>('');
|
||||
const pinInputContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
@@ -210,7 +213,7 @@ function TwoFactorVerificationStep({
|
||||
onClick={onBack}
|
||||
disabled={processing}
|
||||
>
|
||||
Back
|
||||
{t('Back')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -219,7 +222,7 @@ function TwoFactorVerificationStep({
|
||||
processing || code.length < OTP_MAX_LENGTH
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,6 +257,7 @@ export default function TwoFactorSetupModal({
|
||||
}: Props) {
|
||||
const [showVerificationStep, setShowVerificationStep] =
|
||||
useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const modalConfig = useMemo<{
|
||||
title: string;
|
||||
@@ -262,29 +266,32 @@ export default function TwoFactorSetupModal({
|
||||
}>(() => {
|
||||
if (twoFactorEnabled) {
|
||||
return {
|
||||
title: 'Two-factor authentication enabled',
|
||||
description:
|
||||
title: t('Two-factor authentication enabled'),
|
||||
description: t(
|
||||
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
|
||||
buttonText: 'Close',
|
||||
),
|
||||
buttonText: t('Close'),
|
||||
};
|
||||
}
|
||||
|
||||
if (showVerificationStep) {
|
||||
return {
|
||||
title: 'Verify authentication code',
|
||||
description:
|
||||
title: t('Verify authentication code'),
|
||||
description: t(
|
||||
'Enter the 6-digit code from your authenticator app',
|
||||
buttonText: 'Continue',
|
||||
),
|
||||
buttonText: t('Continue'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Enable two-factor authentication',
|
||||
description:
|
||||
title: t('Enable two-factor authentication'),
|
||||
description: t(
|
||||
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
|
||||
buttonText: 'Continue',
|
||||
),
|
||||
buttonText: t('Continue'),
|
||||
};
|
||||
}, [twoFactorEnabled, showVerificationStep]);
|
||||
}, [showVerificationStep, t, twoFactorEnabled]);
|
||||
|
||||
const resetModalState = useCallback(() => {
|
||||
setShowVerificationStep(false);
|
||||
|
||||
@@ -2,10 +2,13 @@ import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { useTranslation } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <nav aria-label={t("breadcrumb")} data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
@@ -84,6 +87,8 @@ function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
@@ -93,7 +98,7 @@ function BreadcrumbEllipsis({
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
<span className="sr-only">{t("More")}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { useTranslatedChildren } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
@@ -39,19 +40,23 @@ function Button({
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const translatedChildren = useTranslatedChildren(children)
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{translatedChildren}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { useTranslation } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
@@ -49,6 +50,8 @@ function DialogContent({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
@@ -63,7 +66,7 @@ function DialogContent({
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t("Close")}</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import * as React from "react"
|
||||
|
||||
import { useTranslatedChildren } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const translatedChildren = useTranslatedChildren(children)
|
||||
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
@@ -15,7 +19,9 @@ function Label({
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{translatedChildren}
|
||||
</LabelPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { useTranslation } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
@@ -50,6 +51,8 @@ function SheetContent({
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
@@ -72,7 +75,7 @@ function SheetContent({
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t("Close")}</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { useTranslation } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
@@ -160,6 +161,7 @@ function Sidebar({
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
@@ -180,8 +182,10 @@ function Sidebar({
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
<SheetTitle>{t("Sidebar")}</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t("Displays the mobile sidebar.")}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
@@ -252,6 +256,7 @@ function SidebarTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar, isMobile, state } = useSidebar()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -267,22 +272,23 @@ function SidebarTrigger({
|
||||
{...props}
|
||||
>
|
||||
{isMobile || state === "collapsed" ? <PanelLeftOpenIcon /> : <PanelLeftCloseIcon />}
|
||||
<span className="sr-only">Toggle sidebar</span>
|
||||
<span className="sr-only">{t("Toggle sidebar")}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle sidebar"
|
||||
aria-label={t("Toggle sidebar")}
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle sidebar"
|
||||
title={t("Toggle sidebar")}
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Loader2Icon } from "lucide-react"
|
||||
|
||||
import { useTranslation } from "@/lib/translations"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
aria-label={t("Loading")}
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { UserInfo } from '@/components/user-info';
|
||||
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
import { logout } from '@/routes';
|
||||
import { edit } from '@/routes/profile';
|
||||
import type { User } from '@/types';
|
||||
@@ -18,6 +19,7 @@ type Props = {
|
||||
|
||||
export function UserMenuContent({ user }: Props) {
|
||||
const cleanup = useMobileNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogout = () => {
|
||||
cleanup();
|
||||
@@ -41,7 +43,7 @@ export function UserMenuContent({ user }: Props) {
|
||||
onClick={cleanup}
|
||||
>
|
||||
<Settings className="mr-2" />
|
||||
Settings
|
||||
{t('Settings')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
@@ -55,7 +57,7 @@ export function UserMenuContent({ user }: Props) {
|
||||
data-test="logout-button"
|
||||
>
|
||||
<LogOut className="mr-2" />
|
||||
Log out
|
||||
{t('Log out')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Hls from 'hls.js/light';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from '@/lib/translations';
|
||||
|
||||
export function VideoPlayer({
|
||||
src,
|
||||
@@ -9,6 +10,7 @@ export function VideoPlayer({
|
||||
title: string;
|
||||
}) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
@@ -39,7 +41,7 @@ export function VideoPlayer({
|
||||
if (!src) {
|
||||
return (
|
||||
<div className="flex aspect-video w-full items-center justify-center bg-neutral-950 text-sm text-neutral-300">
|
||||
Offline
|
||||
{t('Offline')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user