Skip to content

Commit f2554e2

Browse files
committed
feat(i18n): localize UI components and error messages, add shared translation keys
1 parent 6093615 commit f2554e2

File tree

18 files changed

+231
-48
lines changed

18 files changed

+231
-48
lines changed

packages/ui-vite/src/components/Layout.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,24 @@ import { PageTransition } from './shared/PageTransition';
88
import { BackToTop } from './shared/BackToTop';
99
import { Button } from '@leanspec/ui-components';
1010
import { useProject } from '../contexts';
11+
import { useTranslation } from 'react-i18next';
1112

1213
function KeyboardShortcutsHelp({ onClose }: { onClose: () => void }) {
14+
const { t } = useTranslation('common');
1315
const shortcuts = [
14-
{ key: 'h', description: 'Go to dashboard (home)' },
15-
{ key: 'g', description: 'Go to specs list' },
16-
{ key: 's', description: 'Go to stats' },
17-
{ key: 'd', description: 'Go to dependencies' },
18-
{ key: ',', description: 'Go to settings' },
19-
{ key: '/', description: 'Focus search' },
20-
{ key: '⌘ + K', description: 'Open quick search' },
16+
{ key: 'h', description: t('keyboardShortcuts.items.dashboard') },
17+
{ key: 'g', description: t('keyboardShortcuts.items.specs') },
18+
{ key: 's', description: t('keyboardShortcuts.items.stats') },
19+
{ key: 'd', description: t('keyboardShortcuts.items.dependencies') },
20+
{ key: ',', description: t('keyboardShortcuts.items.settings') },
21+
{ key: '/', description: t('keyboardShortcuts.items.search') },
22+
{ key: '⌘ + K', description: t('keyboardShortcuts.items.quickSearch') },
2123
];
2224

2325
return (
2426
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
2527
<div className="bg-background border rounded-lg shadow-lg p-6 max-w-md w-full mx-4" onClick={e => e.stopPropagation()}>
26-
<h3 className="text-lg font-medium mb-4">Keyboard Shortcuts</h3>
28+
<h3 className="text-lg font-medium mb-4">{t('keyboardShortcuts.title')}</h3>
2729
<div className="space-y-2">
2830
{shortcuts.map((s) => (
2931
<div key={s.key} className="flex items-center justify-between">
@@ -38,7 +40,7 @@ function KeyboardShortcutsHelp({ onClose }: { onClose: () => void }) {
3840
size="sm"
3941
className="mt-4 w-full"
4042
>
41-
Close
43+
{t('actions.close')}
4244
</Button>
4345
</div>
4446
</div>

packages/ui-vite/src/components/Navigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function Breadcrumb({ basePath }: { basePath: string }) {
7878
case 'specs': {
7979
const searchParams = new URLSearchParams(parsed.query || '');
8080
const view = searchParams.get('view');
81-
const viewLabel = view === 'board' ? 'Board' : 'List';
81+
const viewLabel = view === 'board' ? t('specsPage.views.board') : t('specsPage.views.list');
8282
items = [{ label: homeLabel, to: basePath }, { label: `${specsLabel} (${viewLabel})` }];
8383
break;
8484
}

packages/ui-vite/src/components/context/ContextClient.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function ContextClient({ projectRoot }: ContextClientProps) {
4949
void loadFile(list[0].path, true);
5050
}
5151
} catch (err) {
52-
const message = err instanceof Error ? err.message : 'Failed to load context files';
52+
const message = err instanceof Error ? err.message : t('contextPage.errors.list');
5353
setError(message);
5454
} finally {
5555
setLoadingList(false);
@@ -68,7 +68,7 @@ export function ContextClient({ projectRoot }: ContextClientProps) {
6868
const detail = await api.getContextFile(path);
6969
setFileCache((prev) => ({ ...prev, [path]: detail }));
7070
} catch (err) {
71-
const message = err instanceof Error ? err.message : 'Failed to load file';
71+
const message = err instanceof Error ? err.message : t('contextPage.errors.file');
7272
setFileError(message);
7373
} finally {
7474
setLoadingFile(false);

packages/ui-vite/src/components/context/ContextFileDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function ContextFileDetail({ file, projectRoot, onBack }: ContextFileDeta
7878
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
7979
<Badge variant="outline" className="text-xs flex items-center gap-1">
8080
<Type className="h-3 w-3" />
81-
{file.fileType || 'text'}
81+
{file.fileType || t('contextPage.detail.defaultFileType')}
8282
</Badge>
8383
<Badge variant="outline" className="text-xs flex items-center gap-1">
8484
<Layers className="h-3 w-3" />

packages/ui-vite/src/components/shared/BackToTop.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { useEffect, useState } from 'react';
22
import { ArrowUp } from 'lucide-react';
33
import { Button } from '@leanspec/ui-components';
4+
import { useTranslation } from 'react-i18next';
45

56
export function BackToTop() {
67
const [isVisible, setIsVisible] = useState(false);
8+
const { t } = useTranslation('common');
79

810
useEffect(() => {
911
const toggleVisibility = () => {
@@ -29,7 +31,7 @@ export function BackToTop() {
2931
onClick={scrollToTop}
3032
size="icon"
3133
className="fixed bottom-6 right-6 h-12 w-12 rounded-full shadow-lg z-40 hover:scale-110 transition-transform"
32-
aria-label="Back to top"
34+
aria-label={t('actions.backToTop')}
3335
>
3436
<ArrowUp className="h-5 w-5" />
3537
</Button>

packages/ui-vite/src/components/shared/ColorPicker.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react';
22
import { Button, Popover, PopoverContent, PopoverTrigger } from '@leanspec/ui-components';
33
import { cn } from '../../lib/utils';
4+
import { useTranslation } from 'react-i18next';
45

56
const PROJECT_COLORS = [
67
'#ef4444',
@@ -25,6 +26,7 @@ interface ColorPickerProps {
2526

2627
export function ColorPicker({ value, onChange, disabled }: ColorPickerProps) {
2728
const [open, setOpen] = useState(false);
29+
const { t } = useTranslation('common');
2830

2931
return (
3032
<Popover open={open} onOpenChange={setOpen}>
@@ -34,7 +36,7 @@ export function ColorPicker({ value, onChange, disabled }: ColorPickerProps) {
3436
size="sm"
3537
className="h-8 w-8 p-0"
3638
disabled={disabled}
37-
aria-label="Pick color"
39+
aria-label={t('colorPicker.pickColor')}
3840
>
3941
<div
4042
className="h-4 w-4 rounded-full border"
@@ -56,7 +58,7 @@ export function ColorPicker({ value, onChange, disabled }: ColorPickerProps) {
5658
onChange(color);
5759
setOpen(false);
5860
}}
59-
aria-label={`Select color ${color}`}
61+
aria-label={t('colorPicker.selectColor', { color })}
6062
/>
6163
))}
6264
</div>

packages/ui-vite/src/components/shared/ErrorBoundary.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, type ErrorInfo, type ReactNode } from 'react';
22
import { AlertTriangle } from 'lucide-react';
33
import { Button } from '@leanspec/ui-components';
44
import { EmptyState } from './EmptyState';
5+
import { useTranslation } from 'react-i18next';
56

67
interface Props {
78
children: ReactNode;
@@ -15,7 +16,14 @@ interface State {
1516
error?: Error;
1617
}
1718

18-
export class ErrorBoundary extends Component<Props, State> {
19+
interface TranslatedProps extends Props {
20+
fallbackTitle: string;
21+
fallbackMessage: string;
22+
retryLabel: string;
23+
reloadLabel: string;
24+
}
25+
26+
class ErrorBoundaryInner extends Component<TranslatedProps, State> {
1927
state: State = { hasError: false };
2028

2129
static getDerivedStateFromError(error: Error): State {
@@ -36,16 +44,18 @@ export class ErrorBoundary extends Component<Props, State> {
3644
return (
3745
<EmptyState
3846
icon={AlertTriangle}
39-
title={this.props.title || 'Something went wrong'}
40-
description={this.props.message || this.state.error?.message || 'The page failed to render.'}
47+
title={this.props.title || this.props.fallbackTitle}
48+
description={
49+
this.props.message || this.state.error?.message || this.props.fallbackMessage
50+
}
4151
tone="error"
4252
actions={(
4353
<>
4454
<Button size="sm" onClick={this.resetBoundary}>
45-
Retry
55+
{this.props.retryLabel}
4656
</Button>
4757
<Button size="sm" variant="outline" onClick={() => window.location.reload()}>
48-
Reload page
58+
{this.props.reloadLabel}
4959
</Button>
5060
</>
5161
)}
@@ -56,3 +66,17 @@ export class ErrorBoundary extends Component<Props, State> {
5666
return this.props.children;
5767
}
5868
}
69+
70+
export function ErrorBoundary(props: Props) {
71+
const { t } = useTranslation(['common', 'errors']);
72+
73+
return (
74+
<ErrorBoundaryInner
75+
fallbackTitle={t('pageError.title', { ns: 'errors' })}
76+
fallbackMessage={t('pageError.description', { ns: 'errors' })}
77+
retryLabel={t('actions.retry', { ns: 'common' })}
78+
reloadLabel={t('actions.refresh', { ns: 'common' })}
79+
{...props}
80+
/>
81+
);
82+
}

packages/ui-vite/src/components/spec-detail/SubSpecTabs.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import GithubSlugger from 'github-slugger';
77
import { BookOpen, CheckSquare, Code, FileText, GitBranch, Map, Palette, TestTube, Wrench } from 'lucide-react';
88
import { Card, Button, Separator, cn } from '@leanspec/ui-components';
99
import { MermaidDiagram } from '../MermaidDiagram';
10+
import { useTranslation } from 'react-i18next';
1011

1112
export interface SubSpec {
1213
name: string;
@@ -88,6 +89,7 @@ interface SubSpecTabsProps {
8889
export function SubSpecTabs({ mainContent, subSpecs = [] }: SubSpecTabsProps) {
8990
const [activeTab, setActiveTab] = useState('readme');
9091
const markdownComponents = useMarkdownComponents();
92+
const { t } = useTranslation('common');
9193

9294
const hasSubSpecs = subSpecs.length > 0;
9395
const overviewCardVisible = hasSubSpecs && subSpecs.length > 2 && activeTab === 'readme';
@@ -116,9 +118,9 @@ export function SubSpecTabs({ mainContent, subSpecs = [] }: SubSpecTabsProps) {
116118
<div className="flex items-start gap-3">
117119
<BookOpen className="h-5 w-5 text-primary mt-0.5" />
118120
<div className="space-y-2">
119-
<h4 className="font-semibold text-sm">This spec has multiple sections</h4>
121+
<h4 className="font-semibold text-sm">{t('subSpecTabs.multiSectionTitle')}</h4>
120122
<p className="text-sm text-muted-foreground">
121-
Use the tabs below to navigate between the main overview and detailed sections.
123+
{t('subSpecTabs.multiSectionDescription')}
122124
</p>
123125
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
124126
{subSpecs.map((subSpec) => {
@@ -142,7 +144,7 @@ export function SubSpecTabs({ mainContent, subSpecs = [] }: SubSpecTabsProps) {
142144
)}
143145

144146
<div className="border-b flex flex-wrap gap-2">
145-
{renderTabButton('readme', 'Overview', FileText)}
147+
{renderTabButton('readme', t('specDetail.tabs.overview'), FileText)}
146148
{subSpecs.map((subSpec) => {
147149
const value = subSpec.name.toLowerCase();
148150
const Icon = subSpec.iconName ? ICON_MAP[subSpec.iconName] : undefined;

packages/ui-vite/src/components/spec-detail/TableOfContents.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
cn,
1010
} from '@leanspec/ui-components';
1111
import { extractHeadings, type HeadingItem } from '../../lib/markdown-utils';
12+
import { useTranslation } from 'react-i18next';
1213

1314
function scrollToHeading(id: string) {
1415
const element = document.getElementById(id);
@@ -57,18 +58,22 @@ interface TableOfContentsProps {
5758
}
5859

5960
export function TableOfContentsSidebar({ content }: TableOfContentsProps) {
61+
const { t } = useTranslation('common');
6062
const headings = useMemo(() => extractHeadings(content), [content]);
6163
if (headings.length === 0) return null;
6264

6365
return (
6466
<div className="py-2">
65-
<h4 className="mb-4 text-sm font-semibold leading-none tracking-tight px-2">On this page</h4>
67+
<h4 className="mb-4 text-sm font-semibold leading-none tracking-tight px-2">
68+
{t('tableOfContents.onThisPage')}
69+
</h4>
6670
<TOCList headings={headings} onHeadingClick={scrollToHeading} />
6771
</div>
6872
);
6973
}
7074

7175
export function TableOfContents({ content }: TableOfContentsProps) {
76+
const { t } = useTranslation('common');
7277
const [open, setOpen] = useState(false);
7378
const headings = useMemo(() => extractHeadings(content), [content]);
7479

@@ -87,13 +92,13 @@ export function TableOfContents({ content }: TableOfContentsProps) {
8792
aria-expanded={open}
8893
onClick={() => setOpen(true)}
8994
className="fixed bottom-24 right-6 h-12 w-12 rounded-full shadow-lg z-40 hover:scale-110 transition-transform"
90-
aria-label="Table of contents"
95+
aria-label={t('tableOfContents.open')}
9196
>
9297
<List className="h-5 w-5" />
9398
</Button>
9499
<DialogContent className="max-w-md max-h-[80vh] overflow-y-auto">
95100
<DialogHeader>
96-
<DialogTitle>Table of Contents</DialogTitle>
101+
<DialogTitle>{t('tableOfContents.title')}</DialogTitle>
97102
</DialogHeader>
98103
<TOCList headings={headings} onHeadingClick={handleHeadingClick} />
99104
</DialogContent>

packages/ui-vite/src/locales/en/common.json

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"retry": "Retry",
5656
"next": "Next",
5757
"loading": "Loading...",
58-
"search": "Search"
58+
"search": "Search",
59+
"backToTop": "Back to top"
5960
},
6061
"spec": {
6162
"spec": "Spec",
@@ -102,6 +103,31 @@
102103
"searchPlaceholder": "Search projects...",
103104
"noProject": "No project found."
104105
},
106+
"keyboardShortcuts": {
107+
"title": "Keyboard Shortcuts",
108+
"items": {
109+
"dashboard": "Go to dashboard (home)",
110+
"specs": "Go to specs list",
111+
"stats": "Go to stats",
112+
"dependencies": "Go to dependencies",
113+
"settings": "Go to settings",
114+
"search": "Focus search",
115+
"quickSearch": "Open quick search"
116+
}
117+
},
118+
"tableOfContents": {
119+
"title": "Table of Contents",
120+
"open": "Open table of contents",
121+
"onThisPage": "On this page"
122+
},
123+
"subSpecTabs": {
124+
"multiSectionTitle": "This spec has multiple sections",
125+
"multiSectionDescription": "Use the tabs below to navigate between the main overview and detailed sections."
126+
},
127+
"colorPicker": {
128+
"pickColor": "Pick color",
129+
"selectColor": "Select color {{color}}"
130+
},
105131
"createProject": {
106132
"title": "Add Project",
107133
"descriptionPicker": "Browse and select the project directory.",
@@ -371,10 +397,15 @@
371397
"openInEditor": "Open in Editor",
372398
"copy": "Copy",
373399
"copySuccess": "Copied!",
400+
"defaultFileType": "text",
374401
"lines": "{{count}} lines",
375402
"modified": "Modified {{date}}",
376403
"size": "{{size}} KB",
377404
"selectFile": "Select a file to view its content"
405+
},
406+
"errors": {
407+
"list": "Failed to load context files",
408+
"file": "Failed to load file"
378409
}
379410
},
380411
"specTimeline": {

0 commit comments

Comments
 (0)