Skip to content

Commit e63cab3

Browse files
authored
Add translations (#11)
1 parent bfe0bf5 commit e63cab3

15 files changed

Lines changed: 717 additions & 69 deletions

File tree

src/JoinSVG.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useI18n } from './i18n'
2+
13
const width = 112.5
24
const height = 75
35
const centerY = height / 2
@@ -12,10 +14,11 @@ const strokeWidth = 1.5
1214

1315
// Simple Venn-style SVGs for JOIN types
1416
export const InnerJoinSVG = () => {
17+
const { t } = useI18n()
1518
// Fill only the intersection using SVG mask, always red
1619
return (
1720
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
18-
<title>Inner join</title>
21+
<title>{t.innerJoinTitle}</title>
1922
<defs>
2023
<clipPath id="clip1">
2124
<circle cx={centerX1} cy={centerY} r={radius} />
@@ -52,9 +55,10 @@ export const InnerJoinSVG = () => {
5255
}
5356

5457
export const LeftJoinSVG = () => {
58+
const { t } = useI18n()
5559
return (
5660
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
57-
<title>Left join</title>
61+
<title>{t.leftJoinTitle}</title>
5862
<circle cx={centerX1} cy={centerY} r={radius} fill={fillColor} />
5963
{/* Borders always on top */}
6064
<circle
@@ -78,9 +82,10 @@ export const LeftJoinSVG = () => {
7882
}
7983

8084
export const RightJoinSVG = () => {
85+
const { t } = useI18n()
8186
return (
8287
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
83-
<title>Right join</title>
88+
<title>{t.rightJoinTitle}</title>
8489
<circle cx={centerX2} cy={centerY} r={radius} fill={fillColor} />
8590
{/* Borders always on top */}
8691
<circle
@@ -104,9 +109,10 @@ export const RightJoinSVG = () => {
104109
}
105110

106111
export const OuterJoinSVG = () => {
112+
const { t } = useI18n()
107113
return (
108114
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
109-
<title>Outer join</title>
115+
<title>{t.outerJoinTitle}</title>
110116
<circle cx={centerX1} cy={centerY} r={radius} fill={fillColor} />
111117
<circle cx={centerX2} cy={centerY} r={radius} fill={fillColor} />
112118
{/* Borders always on top */}
@@ -131,9 +137,10 @@ export const OuterJoinSVG = () => {
131137
}
132138

133139
export const LeftAntiJoinSVG = () => {
140+
const { t } = useI18n()
134141
return (
135142
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
136-
<title>Left anti join</title>
143+
<title>{t.leftAntiJoinTitle}</title>
137144
<defs>
138145
<mask id="leftAntiMask">
139146
<rect width={width} height={height} fill="white" />

src/JoinsApp.tsx

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useState } from 'preact/hooks'
2-
import { SQL_INFO } from './constants'
2+
import { SQL_QUERIES } from './constants'
3+
import { LOCALES, type Locale, useI18n } from './i18n'
4+
import type { Translations } from './i18n/types'
35
import {
46
InnerJoinSVG,
57
LeftAntiJoinSVG,
@@ -10,13 +12,29 @@ import {
1012
import Tables from './Tables'
1113
import type { JoinType } from './types'
1214

15+
/**
16+
* Get the description for a join type from translations
17+
*/
18+
function getJoinDescription(t: Translations, join: JoinType): string {
19+
const descriptionMap: Record<JoinType, keyof Translations> = {
20+
inner: 'innerJoinDesc',
21+
left: 'leftJoinDesc',
22+
leftanti: 'leftAntiJoinDesc',
23+
right: 'rightJoinDesc',
24+
outer: 'outerJoinDesc',
25+
}
26+
return t[descriptionMap[join]]
27+
}
28+
1329
/**
1430
* Main application component for Visual JOIN
1531
* Allows users to select different join types and see the results visually
1632
*/
1733
function JoinsApp() {
34+
const { t, locale, setLocale } = useI18n()
1835
const [currentJoin, setCurrentJoin] = useState<JoinType>('inner')
1936
const [showDesc, setShowDesc] = useState(false)
37+
const [showLangMenu, setShowLangMenu] = useState(false)
2038

2139
/**
2240
* Generates className for join buttons with active state
@@ -29,17 +47,58 @@ function JoinsApp() {
2947

3048
const selectJoin = (join: JoinType) => {
3149
setCurrentJoin(join)
32-
setShowDesc(false)
3350
}
3451

3552
return (
3653
<>
54+
{/* Language Switcher */}
55+
<div className="language-switcher">
56+
<button
57+
type="button"
58+
className="button-reset language-toggle"
59+
onClick={() => setShowLangMenu(!showLangMenu)}
60+
aria-label={t.language}
61+
aria-expanded={showLangMenu}
62+
>
63+
<svg
64+
xmlns="http://www.w3.org/2000/svg"
65+
viewBox="0 0 24 24"
66+
fill="none"
67+
stroke="currentColor"
68+
stroke-width="2"
69+
stroke-linecap="round"
70+
stroke-linejoin="round"
71+
aria-hidden="true"
72+
>
73+
<circle cx="12" cy="12" r="10" />
74+
<path d="M2 12h20" />
75+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
76+
</svg>
77+
</button>
78+
{showLangMenu && (
79+
<ul className="language-menu">
80+
{Object.entries(LOCALES).map(([code, name]) => (
81+
<li key={code}>
82+
<button
83+
type="button"
84+
className={`button-reset language-option ${locale === code ? 'is-active' : ''}`}
85+
onClick={() => {
86+
setLocale(code as Locale)
87+
setShowLangMenu(false)
88+
}}
89+
>
90+
{name}
91+
</button>
92+
</li>
93+
))}
94+
</ul>
95+
)}
96+
</div>
97+
3798
{/* Header */}
3899
<div className="header">
39-
<h1>Visual JOIN</h1>
40-
<span>
41-
Understand how joins work by interacting and see it visually
42-
</span>
100+
<h1>{t.title}</h1>
101+
<span>{t.subtitle}</span>
43102
</div>
44103

45104
<div className="content">
@@ -50,16 +109,16 @@ function JoinsApp() {
50109
className={currentJoinClass('inner')}
51110
onClick={() => selectJoin('inner')}
52111
>
53-
<h2>INNER JOIN</h2>
54-
<div className="subtitle">(or JOIN)</div>
112+
<h2>{t.innerJoin}</h2>
113+
<div className="subtitle">{t.orJoin}</div>
55114
<InnerJoinSVG />
56115
</button>
57116
<button
58117
type="button"
59118
className={currentJoinClass('left')}
60119
onClick={() => selectJoin('left')}
61120
>
62-
<h2>LEFT JOIN</h2>
121+
<h2>{t.leftJoin}</h2>
63122
<div className="subtitle">&nbsp;</div>
64123
<LeftJoinSVG />
65124
</button>
@@ -68,16 +127,16 @@ function JoinsApp() {
68127
className={currentJoinClass('leftanti')}
69128
onClick={() => selectJoin('leftanti')}
70129
>
71-
<h2>LEFT ANTI JOIN</h2>
72-
<div className="subtitle">(with WHERE IS NULL)</div>
130+
<h2>{t.leftAntiJoin}</h2>
131+
<div className="subtitle">{t.withWhereIsNull}</div>
73132
<LeftAntiJoinSVG />
74133
</button>
75134
<button
76135
type="button"
77136
className={currentJoinClass('right')}
78137
onClick={() => selectJoin('right')}
79138
>
80-
<h2>RIGHT JOIN</h2>
139+
<h2>{t.rightJoin}</h2>
81140
<div className="subtitle">&nbsp;</div>
82141
<RightJoinSVG />
83142
</button>
@@ -86,23 +145,25 @@ function JoinsApp() {
86145
className={currentJoinClass('outer')}
87146
onClick={() => selectJoin('outer')}
88147
>
89-
<h2>OUTER JOIN</h2>
90-
<div className="subtitle">(with UNION)</div>
148+
<h2>{t.outerJoin}</h2>
149+
<div className="subtitle">{t.withUnion}</div>
91150
<OuterJoinSVG />
92151
</button>
93152
</div>
94153

95154
{/* SQL Query and Description */}
96155
<div className="sql-container">
97-
<div className="sql">{SQL_INFO[currentJoin].query}</div>
156+
<div className="sql">{SQL_QUERIES[currentJoin]}</div>
98157
<button
99158
type="button"
100159
className="show-desc"
101160
onClick={() => setShowDesc(!showDesc)}
102161
>
103-
{showDesc ? 'Hide description »' : 'Description »'}
162+
{showDesc ? `${t.hideDescription} »` : `${t.description} »`}
104163
</button>
105-
{showDesc && <div className="desc">{SQL_INFO[currentJoin].desc}</div>}
164+
{showDesc && (
165+
<div className="desc">{getJoinDescription(t, currentJoin)}</div>
166+
)}
106167
</div>
107168

108169
{/* Tables */}

src/ModalAdd.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { JSX } from 'preact'
22
import { useState } from 'preact/hooks'
33
import { MODAL_TYPES, type ModalType } from './constants'
4+
import { useI18n } from './i18n'
45
import type { Like, User } from './types'
56

67
export default function ModalAdd({
@@ -16,6 +17,7 @@ export default function ModalAdd({
1617
closeModal: () => void
1718
defaultId: number
1819
}) {
20+
const { t } = useI18n()
1921
const [addId, setAddId] = useState(String(defaultId))
2022
const [addName, setAddName] = useState('')
2123

@@ -49,7 +51,7 @@ export default function ModalAdd({
4951
step="1"
5052
value={addId}
5153
onInput={(e) => setAddId(e.currentTarget.value)}
52-
placeholder="ID"
54+
placeholder={t.id}
5355
className="input-sm"
5456
/>
5557
<input
@@ -58,10 +60,10 @@ export default function ModalAdd({
5860
pattern=".*\S.*"
5961
value={addName}
6062
onInput={(e) => setAddName(e.currentTarget.value)}
61-
placeholder={modalType === MODAL_TYPES.LIKES ? 'Like' : 'Name'}
63+
placeholder={modalType === MODAL_TYPES.LIKES ? t.like : t.name}
6264
/>
6365
<button className="button" type="submit">
64-
Add
66+
{t.add}
6567
</button>
6668
</form>
6769
</div>

src/Tables.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MODAL_TYPES,
66
type ModalType,
77
} from './constants'
8+
import { useI18n } from './i18n'
89
import ModalAdd from './ModalAdd'
910
import type { JoinType, Like, User } from './types'
1011
import getJoins from './utils/getJoins'
@@ -14,6 +15,7 @@ import getJoins from './utils/getJoins'
1415
* Allows interactive adding/removing of data to see how join results change
1516
*/
1617
export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
18+
const { t } = useI18n()
1719
const [users, setUsers] = useState<User[]>(INITIAL_USERS)
1820
const [likes, setLikes] = useState<Like[]>(INITIAL_LIKES)
1921
const [modalType, setModalType] = useState<ModalType | null>(null)
@@ -52,12 +54,12 @@ export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
5254
<div className="tables">
5355
{/* Users */}
5456
<div className="tables-col">
55-
<h3>Users</h3>
57+
<h3>{t.users}</h3>
5658
<table>
5759
<thead>
5860
<tr>
59-
<th>ID</th>
60-
<th className="user">Name</th>
61+
<th>{t.id}</th>
62+
<th className="user">{t.name}</th>
6163
<th>&nbsp;</th>
6264
</tr>
6365
</thead>
@@ -68,7 +70,7 @@ export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
6870
<td>{user.name}</td>
6971
<td width="1">
7072
<button
71-
aria-label="Remove user"
73+
aria-label={t.removeUser}
7274
type="button"
7375
className="button danger"
7476
onClick={() => removeItem(MODAL_TYPES.USERS, user.uuid)}
@@ -81,23 +83,23 @@ export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
8183
</tbody>
8284
</table>
8385
<button
84-
aria-label="Add user"
86+
aria-label={t.addUser}
8587
className="button"
8688
type="button"
8789
onClick={() => setModalType(MODAL_TYPES.USERS)}
8890
>
89-
Add
91+
{t.add}
9092
</button>
9193
</div>
9294

9395
{/* Join */}
9496
<div className="tables-col">
95-
<h3>JOIN</h3>
97+
<h3>{t.join}</h3>
9698
<table>
9799
<thead>
98100
<tr>
99-
<th className="user">Name</th>
100-
<th className="like">Like</th>
101+
<th className="user">{t.name}</th>
102+
<th className="like">{t.like}</th>
101103
</tr>
102104
</thead>
103105
<tbody>
@@ -117,12 +119,12 @@ export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
117119

118120
{/* Likes */}
119121
<div className="tables-col">
120-
<h3>Likes</h3>
122+
<h3>{t.likes}</h3>
121123
<table>
122124
<thead>
123125
<tr>
124-
<th>User ID</th>
125-
<th className="like">Like</th>
126+
<th>{t.userId}</th>
127+
<th className="like">{t.like}</th>
126128
</tr>
127129
</thead>
128130
<tbody>
@@ -132,7 +134,7 @@ export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
132134
<td>{like.like}</td>
133135
<td width="1">
134136
<button
135-
aria-label="Remove like"
137+
aria-label={t.removeLike}
136138
type="button"
137139
className="button danger"
138140
onClick={() => removeItem(MODAL_TYPES.LIKES, like.uuid)}
@@ -145,12 +147,12 @@ export default function Tables({ currentJoin }: { currentJoin: JoinType }) {
145147
</tbody>
146148
</table>
147149
<button
148-
aria-label="Add like"
150+
aria-label={t.addLike}
149151
className="button"
150152
type="button"
151153
onClick={() => setModalType(MODAL_TYPES.LIKES)}
152154
>
153-
Add
155+
{t.add}
154156
</button>
155157
</div>
156158
</div>

0 commit comments

Comments
 (0)