keyed the dock, header and shell chrome (555 -> 487)

Layout reaches zero. The dock, header, user menu, theme switcher, spotlight,
shortcuts modal, screen-error fallback, rescan and bug report.

Three things worth knowing:

CORE_DOCK_ITEMS now carries labelKey instead of label, and one useDockItems() hook
resolves the composed list. DashboardLayout, Spotlight and DockSettings each spread
the same two sources before, so each would have had to resolve labels itself.
Plugin tiles keep the name from their own manifest — a plugin's strings are not ours.

Spotlight resolves labels INSIDE the hook, so rank() still matches what the reader
can see rather than a dot-path. Its to: '/settings/...' literals are untouched, which
is what docs:check parses — verified still 20 offered, 0 missing.

The rescan toast said 'N skill(s)'. That is English giving up on a plural, so it is
now a real plural entry per noun and reads correctly at 0, 1 and 2 in both locales.

ThemeSwitcher used t as its theme loop variable; renamed to entry.
This commit is contained in:
2026-08-23 04:40:39 +00:00
parent 6d8709c05a
commit 926034750f
17 changed files with 685 additions and 179 deletions
@@ -1,16 +1,18 @@
import { Bug } from 'lucide-react';
import { useBugReport } from './use-bug-report';
import { useTranslation } from '@/lib/i18n';
import { BugReportDialog } from './BugReportDialog';
export const BugReportButton = () => {
const bugReport = useBugReport();
const { t } = useTranslation();
return (
<>
<button
onClick={bugReport.handleOpen}
className="flex h-8 w-8 items-center justify-center rounded-full bg-destructive text-destructive-foreground transition-transform hover:scale-110"
aria-label="Report a bug"
aria-label={t('bugReport.label')}
>
<Bug size={16} />
</button>
@@ -3,6 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Loader2 } from 'lucide-react';
import { useTranslation } from '@/lib/i18n';
type BugReportDialogProps = {
open: boolean;
@@ -21,6 +22,7 @@ export const BugReportDialog = ({
onClose,
onSubmit,
}: BugReportDialogProps) => {
const { t, tUi } = useTranslation();
const [description, setDescription] = useState('');
const previewUrl = useMemo(() => (screenshot ? URL.createObjectURL(screenshot) : null), [screenshot]);
@@ -36,7 +38,7 @@ export const BugReportDialog = ({
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Report a Bug</DialogTitle>
<DialogTitle>{t('bugReport.title')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
@@ -46,34 +48,32 @@ export const BugReportDialog = ({
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : previewUrl ? (
<img src={previewUrl} alt="Screenshot" className="max-h-48 w-full object-contain" />
<img src={previewUrl} alt={t('bugReport.screenshot')} className="max-h-48 w-full object-contain" />
) : (
<div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
No screenshot captured
{t('bugReport.noScreenshot')}
</div>
)}
</div>
<Textarea
placeholder="Describe what went wrong..."
placeholder={t('bugReport.describePlaceholder')}
value={description}
onChange={(ev) => setDescription(ev.target.value)}
rows={4}
autoFocus
/>
<p className="text-xs text-muted-foreground">
Console logs, browser info, and recent API errors are collected automatically.
</p>
<p className="text-xs text-muted-foreground">{t('bugReport.collectedNote')}</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={submitting}>
Cancel
{tUi('button.cancel')}
</Button>
<Button onClick={handleSubmit} disabled={!canSubmit}>
{submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Submit
{submitting ? <Loader2 className="me-2 h-4 w-4 animate-spin" /> : null}
{t('bugReport.submit')}
</Button>
</DialogFooter>
</DialogContent>
@@ -6,6 +6,7 @@ import { useGlobal } from 'hooks/useGlobal';
import type { ErrorDetails } from 'hooks/useClient';
import { getConsoleBuffer } from '@/lib/console-buffer';
import { toast } from 'sonner';
import { useTranslation } from '@/lib/i18n';
export const useBugReport = () => {
const [open, setOpen] = useState(false);
@@ -14,6 +15,7 @@ export const useBugReport = () => {
const [submitting, setSubmitting] = useState(false);
const client = useClient();
const { user } = useAuth();
const { t } = useTranslation();
const [apiError] = useGlobal<ErrorDetails | null>('API_ERROR', null);
const handleOpen = useCallback(async () => {
@@ -57,15 +59,15 @@ export const useBugReport = () => {
}
await client.post('/bug-report', formData);
toast.success('Bug report submitted');
toast.success(t('bugReport.submitted'));
handleClose();
} catch {
toast.error('Failed to submit bug report');
toast.error(t('bugReport.failed'));
} finally {
setSubmitting(false);
}
},
[screenshot, client, user, apiError, handleClose],
[screenshot, client, user, apiError, handleClose, t],
);
return { open, capturing, submitting, screenshot, handleOpen, handleClose, handleSubmit };