Skip to content

Commit 359cf00

Browse files
authored
Enhancement: Added i18n localization support. (#31)
1 parent 5dbe973 commit 359cf00

59 files changed

Lines changed: 1952 additions & 883 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/package-lock.json

Lines changed: 87 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
"test:watch": "vitest"
1212
},
1313
"dependencies": {
14+
"i18next": "^26.0.4",
1415
"react": "^18.3.1",
1516
"react-dom": "^18.3.1",
17+
"react-i18next": "^17.0.3",
1618
"react-icons": "^5.4.0",
1719
"react-router-dom": "^6.30.3"
1820
},

frontend/src/App.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useEffect } from 'react'
22
import { Routes, Route, Navigate, useLocation, useParams } from 'react-router-dom'
3+
import { useTranslation } from 'react-i18next'
34
import { useAuth } from './context/AuthContext'
45
import { FavoritesProvider } from './context/FavoritesContext'
56
import { UISettingsProvider } from './context/UISettingsContext'
@@ -23,10 +24,11 @@ import CampaignsView from './views/CampaignsView'
2324
import CampaignDetailView from './views/CampaignDetailView'
2425

2526
function LoadingScreen() {
27+
const { t } = useTranslation()
2628
return (
2729
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-deep)' }}>
2830
<div style={{ textAlign: 'center' }}>
29-
<h1 style={{ fontSize: 28, letterSpacing: '0.1em', marginBottom: 24 }}>GRIMOIRE</h1>
31+
<h1 style={{ fontSize: 28, letterSpacing: '0.1em', marginBottom: 24 }}>{t('app.name')}</h1>
3032
<Spinner size={28} />
3133
</div>
3234
</div>

frontend/src/components/DownloadArchiveModal.jsx

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
11
import { useState, useEffect, useRef } from 'react'
2+
import { useTranslation } from 'react-i18next'
23
import { LuX, LuDownload } from 'react-icons/lu'
34
import { mediaUrl } from '../api'
45

5-
const FORMATS = [
6-
{
7-
id: 'zip',
8-
label: 'ZIP',
9-
ext: '.zip',
10-
description: 'Compatible with all platforms. Best for sharing.',
11-
},
12-
{
13-
id: 'tar',
14-
label: 'TAR',
15-
ext: '.tar',
16-
description: 'Uncompressed archive. Preserves Unix permissions.',
17-
},
18-
{
19-
id: 'tar.gz',
20-
label: 'TAR.GZ',
21-
ext: '.tar.gz',
22-
description: 'Gzip-compressed tar. Standard on Linux / macOS.',
23-
},
24-
{
25-
id: 'tar.bz2',
26-
label: 'TAR.BZ2',
27-
ext: '.tar.bz2',
28-
description: 'Bzip2-compressed tar. Slightly better compression than gzip.',
29-
},
30-
]
31-
326
export default function DownloadArchiveModal({ title, params, onClose }) {
7+
const { t } = useTranslation()
338
const [fmt, setFmt] = useState('zip')
349
const firstRef = useRef(null)
3510

11+
const FORMATS = [
12+
{
13+
id: 'zip',
14+
label: t('download.zip.label'),
15+
ext: t('download.zip.ext'),
16+
description: t('download.zip.description'),
17+
},
18+
{
19+
id: 'tar',
20+
label: t('download.tar.label'),
21+
ext: t('download.tar.ext'),
22+
description: t('download.tar.description'),
23+
},
24+
{
25+
id: 'tar.gz',
26+
label: t('download.tarGz.label'),
27+
ext: t('download.tarGz.ext'),
28+
description: t('download.tarGz.description'),
29+
},
30+
{
31+
id: 'tar.bz2',
32+
label: t('download.tarBz2.label'),
33+
ext: t('download.tarBz2.ext'),
34+
description: t('download.tarBz2.description'),
35+
},
36+
]
37+
3638
useEffect(() => {
3739
firstRef.current?.focus()
3840
const onKey = (e) => { if (e.key === 'Escape') onClose() }
@@ -71,12 +73,12 @@ export default function DownloadArchiveModal({ title, params, onClose }) {
7173
}}>
7274
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
7375
<span id="dl-modal-title" style={{ fontSize: 15, fontWeight: 600 }}>
74-
Download Archive
76+
{t('download.title')}
7577
</span>
7678
<button
7779
onClick={onClose}
7880
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', display: 'flex', padding: 2 }}
79-
aria-label="Close"
81+
aria-label={t('common.close')}
8082
>
8183
<LuX size={16} />
8284
</button>
@@ -131,13 +133,13 @@ export default function DownloadArchiveModal({ title, params, onClose }) {
131133
onClick={onClose}
132134
style={{ padding: '7px 16px', borderRadius: 6, background: 'var(--bg-card)', border: '1px solid var(--border)', color: 'var(--text-dim)', fontSize: 14, cursor: 'pointer' }}
133135
>
134-
Cancel
136+
{t('download.cancel')}
135137
</button>
136138
<button
137139
onClick={handleDownload}
138140
style={{ padding: '7px 18px', borderRadius: 6, background: 'var(--gold-dim)', border: 'none', color: 'var(--bg-deep)', fontSize: 14, fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6 }}
139141
>
140-
<LuDownload size={14} /> Download
142+
<LuDownload size={14} /> {t('download.download')}
141143
</button>
142144
</div>
143145
</div>

frontend/src/components/FavoriteButton.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
import { useTranslation } from 'react-i18next'
12
import { LuHeart } from 'react-icons/lu'
23
import { useFavorites } from '../context/FavoritesContext'
34

45
export default function FavoriteButton({ type, id, style, cardHovered }) {
6+
const { t } = useTranslation()
57
const { isFavorite, toggleFavorite } = useFavorites()
68
const active = isFavorite(type, id)
79
const visible = cardHovered === undefined ? true : (active || cardHovered)
810

911
return (
1012
<button
1113
onClick={e => { e.stopPropagation(); toggleFavorite(type, id) }}
12-
aria-label={active ? 'Remove from favorites' : 'Add to favorites'}
14+
aria-label={active ? t('common.removeFromFavorites') : t('common.addToFavorites')}
1315
style={{
1416
position: 'absolute', top: 6, right: 6, zIndex: 3,
1517
width: 28, height: 28, borderRadius: '50%', border: 'none',

frontend/src/components/MobileSidebar.jsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useState } from 'react'
22
import { NavLink, useLocation } from 'react-router-dom'
3+
import { useTranslation } from 'react-i18next'
34
import { LuLibrary, LuMap, LuSearch, LuSettings, LuLogOut, LuUser, LuHeart, LuEllipsis, LuX, LuScroll } from 'react-icons/lu'
45

56
export default function MobileSidebar({ onLogout, uiSettings = {} }) {
7+
const { t } = useTranslation()
68
const [moreOpen, setMoreOpen] = useState(false)
79
const location = useLocation()
810
const { hide_maps, hide_tokens, hide_campaigns } = uiSettings
@@ -24,15 +26,15 @@ export default function MobileSidebar({ onLogout, uiSettings = {} }) {
2426
background: 'var(--bg-panel)', borderTop: '1px solid var(--border)',
2527
padding: '8px 0',
2628
}}>
27-
{!hide_maps && <MoreItem to="/maps" Icon={LuMap} label="Maps" onClick={() => setMoreOpen(false)} />}
28-
{!hide_tokens && <MoreItem to="/tokens" Icon={LuUser} label="Tokens" onClick={() => setMoreOpen(false)} />}
29-
<MoreItem to="/settings" Icon={LuSettings} label="Settings" onClick={() => setMoreOpen(false)} />
29+
{!hide_maps && <MoreItem to="/maps" Icon={LuMap} label={t('nav.maps')} onClick={() => setMoreOpen(false)} />}
30+
{!hide_tokens && <MoreItem to="/tokens" Icon={LuUser} label={t('nav.tokens')} onClick={() => setMoreOpen(false)} />}
31+
<MoreItem to="/settings" Icon={LuSettings} label={t('nav.settings')} onClick={() => setMoreOpen(false)} />
3032
<button
3133
onClick={() => { setMoreOpen(false); onLogout() }}
3234
style={{ ...moreItemStyle, width: '100%', border: 'none', cursor: 'pointer', color: 'var(--text-dim)' }}
3335
>
3436
<LuLogOut size={18} />
35-
<span>Log out</span>
37+
<span>{t('nav.logOut')}</span>
3638
</button>
3739
</div>
3840
</>
@@ -44,16 +46,16 @@ export default function MobileSidebar({ onLogout, uiSettings = {} }) {
4446
background: 'var(--bg-panel)', borderTop: '1px solid var(--border)',
4547
display: 'flex', justifyContent: 'space-around', padding: '8px 0',
4648
}}>
47-
<NavLink to="/library" end={false} style={({ isActive }) => mobileNavStyle(isActive)}><LuLibrary size={20} />Library</NavLink>
48-
<NavLink to="/search" end style={({ isActive }) => mobileNavStyle(isActive)}><LuSearch size={20} />Search</NavLink>
49-
<NavLink to="/favorites" end style={({ isActive }) => mobileNavStyle(isActive)}><LuHeart size={20} />Favorites</NavLink>
50-
{!hide_campaigns && <NavLink to="/campaigns" end style={({ isActive }) => mobileNavStyle(isActive)}><LuScroll size={20} />Campaigns</NavLink>}
49+
<NavLink to="/library" end={false} style={({ isActive }) => mobileNavStyle(isActive)}><LuLibrary size={20} />{t('nav.library')}</NavLink>
50+
<NavLink to="/search" end style={({ isActive }) => mobileNavStyle(isActive)}><LuSearch size={20} />{t('nav.search')}</NavLink>
51+
<NavLink to="/favorites" end style={({ isActive }) => mobileNavStyle(isActive)}><LuHeart size={20} />{t('nav.favorites')}</NavLink>
52+
{!hide_campaigns && <NavLink to="/campaigns" end style={({ isActive }) => mobileNavStyle(isActive)}><LuScroll size={20} />{t('nav.campaigns')}</NavLink>}
5153
<button
5254
onClick={() => setMoreOpen(o => !o)}
5355
style={mobileNavStyle(moreActive || moreOpen)}
5456
>
5557
{moreOpen ? <LuX size={20} /> : <LuEllipsis size={20} />}
56-
More
58+
{t('nav.more')}
5759
</button>
5860
</div>
5961
</>

0 commit comments

Comments
 (0)