Skip to content

Commit 810f230

Browse files
ericyangpanclaude
andcommitted
refactor(i18n): migrate components to hierarchical namespace
Refactor all UI components to use hierarchical translation namespaces instead of flat structure. This improves code maintainability and follows the i18n architecture rules. - Controls: useTranslations('components.controls.*') - Navigation: useTranslations('components.navigation.*') - Product: useTranslations('components.product.*') - Sidebar: useTranslations('components.sidebar.*') - Common: useTranslations('components.common.*') Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6625b7f commit 810f230

28 files changed

+224
-255
lines changed

src/components/Footer.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function FooterLinkList({ title, links }: FooterLinkListProps) {
3535
}
3636

3737
export default function Footer() {
38-
const tComponents = useTranslations('components')
38+
const tComponent = useTranslations('components.common.footer')
3939
const tShared = useTranslations('shared')
4040

4141
// Define link arrays (static hrefs, only labels depend on translations)
@@ -81,8 +81,8 @@ export default function Footer() {
8181
{tShared('terms.aiCodingStack')}
8282
</span>
8383
<p className="text-sm pb-[var(--spacing-sm)] leading-[1.8] text-[var(--color-text-secondary)] font-light">
84-
{tComponents('footer.tagline')}
85-
<span className="block mt-[var(--spacing-sm)]">{tComponents('footer.openSource')}</span>
84+
{tComponent('tagline')}
85+
<span className="block mt-[var(--spacing-sm)]">{tComponent('openSource')}</span>
8686
</p>
8787
<div className="flex gap-[var(--spacing-xs)]">
8888
<ThemeSwitcher />
@@ -96,7 +96,7 @@ export default function Footer() {
9696
</div>
9797

9898
<div className="py-[var(--spacing-md)] text-center text-xs text-[var(--color-text-muted)]">
99-
{tComponents('footer.copyright')}
99+
{tComponent('copyright')}
100100
</div>
101101
</footer>
102102
)

src/components/Header.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function Header() {
3232
const [isMenuOpen, setIsMenuOpen] = useState(false)
3333
const [activeMegaMenu, setActiveMegaMenu] = useState<'aiCodingStack' | 'ranking' | null>(null)
3434
const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false)
35-
const tComponents = useTranslations('components')
35+
const tComponent = useTranslations('components.common.header')
3636

3737
// Menu items configuration - memoized to avoid recreation on each render
3838
const menuItems = useMemo<MenuItem[]>(
@@ -107,7 +107,7 @@ function Header() {
107107
aria-expanded={isActive}
108108
aria-haspopup="true"
109109
>
110-
{tComponents(`header.${item.translationKey}`)}
110+
{tComponent(`${item.translationKey}`)}
111111
</Link>
112112
{item.megaMenuType === 'aiCodingStack' && (
113113
<StackMegaMenu isOpen={isActive} onClose={handleMegaMenuClose} />
@@ -123,17 +123,17 @@ function Header() {
123123
<li key={item.href}>
124124
{item.isExternal ? (
125125
<a href={item.href} target="_blank" rel="noopener" className={DESKTOP_LINK_CLASSES}>
126-
{tComponents(`header.${item.translationKey}`)}
126+
{tComponent(`${item.translationKey}`)}
127127
</a>
128128
) : (
129129
<Link href={item.href} className={DESKTOP_LINK_CLASSES}>
130-
{tComponents(`header.${item.translationKey}`)}
130+
{tComponent(`${item.translationKey}`)}
131131
</Link>
132132
)}
133133
</li>
134134
)
135135
},
136-
[activeMegaMenu, handleMegaMenuOpen, handleMegaMenuClose, tComponents]
136+
[activeMegaMenu, handleMegaMenuOpen, handleMegaMenuClose, tComponent]
137137
)
138138

139139
// Render mobile menu item
@@ -142,22 +142,22 @@ function Header() {
142142
<li key={item.href}>
143143
{item.isExternal ? (
144144
<a href={item.href} target="_blank" rel="noopener" className={MOBILE_LINK_CLASSES}>
145-
{tComponents(`header.${item.translationKey}`)}
145+
{tComponent(`${item.translationKey}`)}
146146
</a>
147147
) : (
148148
<Link href={item.href} className={MOBILE_LINK_CLASSES} onClick={handleMenuClose}>
149-
{tComponents(`header.${item.translationKey}`)}
149+
{tComponent(`${item.translationKey}`)}
150150
</Link>
151151
)}
152152
</li>
153153
),
154-
[handleMenuClose, tComponents]
154+
[handleMenuClose, tComponent]
155155
)
156156

157157
// Memoized menu button label
158158
const menuButtonLabel = useMemo(
159-
() => (isMenuOpen ? tComponents('header.closeMenu') : tComponents('header.openMenu')),
160-
[isMenuOpen, tComponents]
159+
() => (isMenuOpen ? tComponent('closeMenu') : tComponent('openMenu')),
160+
[isMenuOpen, tComponent]
161161
)
162162

163163
return (
@@ -206,7 +206,7 @@ function Header() {
206206
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
207207
/>
208208
</svg>
209-
<span className="flex-1 text-left">{tComponents('header.searchPlaceholder')}</span>
209+
<span className="flex-1 text-left">{tComponent('searchPlaceholder')}</span>
210210
<kbd className="flex items-center gap-1 px-1.5 py-0.5 text-xs border border-[var(--color-border)]">
211211
<Command className="w-3 h-3" />
212212
<span>K</span>
@@ -221,7 +221,7 @@ function Header() {
221221
type="button"
222222
onClick={() => setIsSearchDialogOpen(true)}
223223
className="p-[var(--spacing-xs)] hover:bg-[var(--color-hover)] transition-colors"
224-
aria-label={tComponents('header.search')}
224+
aria-label={tComponent('search')}
225225
>
226226
<svg
227227
className="w-5 h-5"
@@ -244,7 +244,7 @@ function Header() {
244244
type="button"
245245
onClick={handleMenuToggle}
246246
className="p-[var(--spacing-xs)] hover:bg-[var(--color-hover)] transition-colors"
247-
aria-label={tComponents('header.toggleMenu')}
247+
aria-label={tComponent('toggleMenu')}
248248
>
249249
<svg
250250
className="w-6 h-6"

src/components/controls/CopyButton.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl'
44
import { useState } from 'react'
55

66
export default function CopyButton({ text }: { text: string }) {
7-
const tComponents = useTranslations('components')
7+
const tComponent = useTranslations('components.controls.copyButton')
88
const [copied, setCopied] = useState(false)
99

1010
const handleCopy = async () => {
@@ -26,12 +26,8 @@ export default function CopyButton({ text }: { text: string }) {
2626
? 'text-green-600'
2727
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
2828
}`}
29-
title={
30-
copied ? tComponents('copyButton.copied') : tComponents('copyButton.copyToClipboard')
31-
}
32-
aria-label={
33-
copied ? tComponents('copyButton.copied') : tComponents('copyButton.copyToClipboard')
34-
}
29+
title={copied ? tComponent('copied') : tComponent('copyToClipboard')}
30+
aria-label={copied ? tComponent('copied') : tComponent('copyToClipboard')}
3531
>
3632
{copied ? (
3733
<svg
@@ -45,7 +41,7 @@ export default function CopyButton({ text }: { text: string }) {
4541
strokeLinejoin="round"
4642
role="img"
4743
>
48-
<title>{tComponents('copyButton.copied')}</title>
44+
<title>{tComponent('copied')}</title>
4945
<polyline points="20 6 9 17 4 12"></polyline>
5046
</svg>
5147
) : (
@@ -60,7 +56,7 @@ export default function CopyButton({ text }: { text: string }) {
6056
strokeLinejoin="round"
6157
role="img"
6258
>
63-
<title>{tComponents('copyButton.copyToClipboard')}</title>
59+
<title>{tComponent('copyToClipboard')}</title>
6460
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
6561
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
6662
</svg>

src/components/controls/FilterSortBar.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,18 @@ export default function FilterSortBar({
2828
searchQuery = '',
2929
onSearchChange,
3030
}: FilterSortBarProps) {
31-
const tComponents = useTranslations('components')
31+
const tComponent = useTranslations('components.controls.filterSortBar')
3232
const [isSortOpen, setIsSortOpen] = useState(false)
3333
const sortRef = useRef<HTMLDivElement>(null)
3434

3535
const sortOptions = [
36-
{ value: 'default', label: tComponents('filterSortBar.sortDefault') },
37-
{ value: 'name-asc', label: tComponents('filterSortBar.sortNameAsc') },
38-
{ value: 'name-desc', label: tComponents('filterSortBar.sortNameDesc') },
36+
{ value: 'default', label: tComponent('sortDefault') },
37+
{ value: 'name-asc', label: tComponent('sortNameAsc') },
38+
{ value: 'name-desc', label: tComponent('sortNameDesc') },
3939
]
4040

4141
const currentSortLabel =
42-
sortOptions.find(opt => opt.value === sortOrder)?.label ||
43-
tComponents('filterSortBar.sortDefault')
42+
sortOptions.find(opt => opt.value === sortOrder)?.label || tComponent('sortDefault')
4443

4544
// Close dropdown when clicking outside
4645
useEffect(() => {
@@ -91,17 +90,15 @@ export default function FilterSortBar({
9190
type="text"
9291
value={searchQuery}
9392
onChange={e => onSearchChange(e.target.value)}
94-
placeholder={tComponents('filterSortBar.search')}
93+
placeholder={tComponent('search')}
9594
className="flex-1 px-[var(--spacing-sm)] py-1 text-sm border border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-border-strong)] transition-colors"
9695
/>
9796
</div>
9897
)}
9998

10099
{/* Sort Custom Dropdown */}
101100
<div className="flex items-center gap-[var(--spacing-xs)]">
102-
<span className="text-xs text-[var(--color-text-muted)]">
103-
{tComponents('filterSortBar.sort')}
104-
</span>
101+
<span className="text-xs text-[var(--color-text-muted)]">{tComponent('sort')}</span>
105102
<div className="relative" ref={sortRef}>
106103
<button
107104
type="button"
@@ -138,9 +135,7 @@ export default function FilterSortBar({
138135

139136
{/* License Filter Buttons */}
140137
<div className="flex items-center gap-[var(--spacing-xs)]">
141-
<span className="text-xs text-[var(--color-text-muted)]">
142-
{tComponents('filterSortBar.license')}
143-
</span>
138+
<span className="text-xs text-[var(--color-text-muted)]">{tComponent('license')}</span>
144139
<button
145140
type="button"
146141
onClick={() => toggleLicense('open-source')}
@@ -150,7 +145,7 @@ export default function FilterSortBar({
150145
: 'border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)]'
151146
}`}
152147
>
153-
{tComponents('filterSortBar.openSource')}
148+
{tComponent('openSource')}
154149
</button>
155150
<button
156151
type="button"
@@ -161,14 +156,14 @@ export default function FilterSortBar({
161156
: 'border-[var(--color-border)] bg-[var(--color-background)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)]'
162157
}`}
163158
>
164-
{tComponents('filterSortBar.proprietary')}
159+
{tComponent('proprietary')}
165160
</button>
166161
</div>
167162

168163
{/* Platform Filter Buttons */}
169164
<div className="flex items-center gap-[var(--spacing-xs)]">
170165
<span className="text-xs text-[var(--color-text-muted)]">
171-
{platformLabel || tComponents('filterSortBar.platform')}
166+
{platformLabel || tComponent('platform')}
172167
</span>
173168
{availablePlatforms.map(platform => (
174169
<button
@@ -193,7 +188,7 @@ export default function FilterSortBar({
193188
onClick={clearFilters}
194189
className="ml-auto px-[var(--spacing-sm)] py-1 text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors"
195190
>
196-
{tComponents('filterSortBar.clearFilters')}
191+
{tComponent('clearFilters')}
197192
</button>
198193
)}
199194
</div>

src/components/controls/LanguageSwitcher.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default function LanguageSwitcher() {
5858
const locale = useLocale() as Locale
5959
const router = useRouter()
6060
const pathname = usePathname()
61-
const tComponents = useTranslations('components')
61+
const tComponent = useTranslations('components.common.footer')
6262
const [isOpen, setIsOpen] = useState(false)
6363
const dropdownRef = useRef<HTMLDivElement>(null)
6464

@@ -120,8 +120,8 @@ export default function LanguageSwitcher() {
120120
type="button"
121121
onClick={() => setIsOpen(!isOpen)}
122122
className="footer-control-button"
123-
title={tComponents('footer.selectLanguage')}
124-
aria-label={tComponents('footer.selectLanguage')}
123+
title={tComponent('selectLanguage')}
124+
aria-label={tComponent('selectLanguage')}
125125
aria-expanded={isOpen}
126126
>
127127
<Languages className="footer-control-icon" />

src/components/controls/SearchDialog.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl'
55
import { useEffect, useState } from 'react'
66
import { createPortal } from 'react-dom'
77
import { useRouter } from '@/i18n/navigation'
8+
import { buildManifestPath } from '@/lib/manifest-registry'
89
import type { SearchResult } from '@/lib/search'
910
import { getAutocompleteSuggestions } from '@/lib/search'
1011

@@ -15,7 +16,8 @@ export interface SearchDialogProps {
1516
}
1617

1718
export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogProps) {
18-
const t = useTranslations()
19+
const tShared = useTranslations('shared')
20+
const tComponent = useTranslations('components.controls.searchDialog')
1921
const router = useRouter()
2022
const [query, setQuery] = useState('')
2123
const [suggestions, setSuggestions] = useState<SearchResult[]>([])
@@ -48,7 +50,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
4850
// Navigate to result
4951
const navigateToResult = (result: SearchResult) => {
5052
onClose()
51-
router.push(`/${result.category}/${result.id}`)
53+
router.push(buildManifestPath(result.category, result.id))
5254
}
5355

5456
// Navigate to search page
@@ -102,7 +104,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
102104
<Command.Input
103105
value={query}
104106
onValueChange={setQuery}
105-
placeholder={t('components.header.searchPlaceholder')}
107+
placeholder={tComponent('placeholder')}
106108
className="flex-1 bg-transparent text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none text-base"
107109
autoFocus
108110
/>
@@ -116,9 +118,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
116118
{/* Empty State */}
117119
<Command.Empty className="py-12 text-center">
118120
{query.trim() === '' ? (
119-
<div className="text-[var(--color-text-muted)] text-sm">
120-
{t('pages.search.placeholder')}
121-
</div>
121+
<div className="text-[var(--color-text-muted)] text-sm">{tComponent('empty')}</div>
122122
) : (
123123
<div className="text-center">
124124
<svg
@@ -136,7 +136,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
136136
/>
137137
</svg>
138138
<p className="text-sm text-[var(--color-text-muted)]">
139-
{t('pages.search.noResultsFor', { query })}
139+
{tComponent('noResultsFor', { query })}
140140
</p>
141141
</div>
142142
)}
@@ -163,8 +163,8 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
163163
</div>
164164
<div className="flex-shrink-0">
165165
<span className="inline-block px-2 py-0.5 text-xs border border-[var(--color-border)] text-[var(--color-text-muted)]">
166-
{t(
167-
`shared.categories.plural.${result.category === 'providers' ? 'modelProviders' : result.category}`
166+
{tShared(
167+
`categories.plural.${result.category === 'providers' ? 'modelProviders' : result.category}`
168168
)}
169169
</span>
170170
</div>
@@ -180,7 +180,7 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
180180
className="px-4 py-3 cursor-pointer transition-colors border-t border-[var(--color-border)] data-[selected=true]:bg-[var(--color-hover)] aria-selected:bg-[var(--color-hover)] text-[var(--color-text-secondary)]"
181181
>
182182
<div className="flex items-center gap-2 text-sm">
183-
<span>View all results for &quot;{query}&quot;</span>
183+
<span>{tComponent('viewAllResults', { query })}</span>
184184
<span className="ml-auto text-[var(--color-text-muted)]"></span>
185185
</div>
186186
</Command.Item>
@@ -199,13 +199,13 @@ export default function SearchDialog({ isOpen, onClose, locale }: SearchDialogPr
199199
<kbd className="px-1.5 py-0.5 border border-[var(--color-border)] bg-[var(--color-bg)]">
200200
201201
</kbd>
202-
<span>{t('pages.search.navigate')}</span>
202+
<span>{tComponent('navigate')}</span>
203203
</div>
204204
<div className="flex items-center gap-1.5">
205205
<kbd className="px-1.5 py-0.5 border border-[var(--color-border)] bg-[var(--color-bg)]">
206206
207207
</kbd>
208-
<span>{t('pages.search.select')}</span>
208+
<span>{tComponent('select')}</span>
209209
</div>
210210
</div>
211211
)}

src/components/controls/SearchInput.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export default function SearchInput({
1717
placeholder,
1818
onSearch,
1919
}: SearchInputProps) {
20-
const t = useTranslations()
20+
const tShared = useTranslations('shared')
21+
const tComponent = useTranslations('components.controls.searchInput')
2122
const router = useRouter()
2223
const [query, setQuery] = useState(initialQuery)
2324
const [suggestions, setSuggestions] = useState<SearchResult[]>([])
@@ -28,7 +29,7 @@ export default function SearchInput({
2829
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
2930
const hasUserInteracted = useRef(false)
3031

31-
const placeholderText = placeholder || t('components.header.searchPlaceholder')
32+
const placeholderText = placeholder || tComponent('placeholder')
3233

3334
// Debounce search suggestions
3435
useEffect(() => {
@@ -190,8 +191,8 @@ export default function SearchInput({
190191
</div>
191192
<div className="flex-shrink-0">
192193
<span className="inline-block px-2 py-0.5 text-xs border border-[var(--color-border)] text-[var(--color-text-muted)]">
193-
{t(
194-
`shared.categories.plural.${result.category === 'providers' ? 'modelProviders' : result.category}`
194+
{tShared(
195+
`categories.plural.${result.category === 'providers' ? 'modelProviders' : result.category}`
195196
)}
196197
</span>
197198
</div>

0 commit comments

Comments
 (0)