Skip to content

Commit 7f70b56

Browse files
egorfedorovclaude
andcommitted
Add 12-language translations for all game UI text
New translation system: - src/data/game-i18n.ts: 200+ keys in 12 languages (RU, EN, ES, DE, FR, PT, ZH, JA, KO, TR, IT, AR) - src/hooks/useGameText.ts: G(key) hook with game-specific + common fallback All 10 games updated to use G() for static text: - 188 G() calls (fully translated to all 12 languages) - 45 remaining L() calls (dynamic strings with variables, EN fallback) Translation coverage: - Codenames, Imaginarium, Hat: 100% G() (zero L() remaining) - Quiz, Danetki: 1 L() each (game title only) - Mafia: 14 L() (narrator scripts with dynamic content) - Others: dynamic strings with player names/numbers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f645fab commit 7f70b56

12 files changed

Lines changed: 479 additions & 251 deletions

src/data/game-i18n.ts

Lines changed: 213 additions & 0 deletions
Large diffs are not rendered by default.

src/games/ActivityGame.tsx

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useMemo } from 'react'
22
import { Plus, Play, Check, SkipForward, RotateCcw, Pencil, MessageSquare, User, AlertCircle } from 'lucide-react'
33
import { useI18n } from '../i18n'
4+
import { useGameText } from '../hooks/useGameText'
45
import { useTimer } from '../hooks/useTimer'
56
import { activityWords } from '../data/words'
67
import clsx from 'clsx'
@@ -13,36 +14,37 @@ interface Team {
1314
score: number
1415
}
1516

16-
const modeConfig: Record<Mode, { icon: any; label: { ru: string; en: string }; color: string; desc: { ru: string; en: string } }> = {
17-
explain: {
18-
icon: MessageSquare,
19-
label: { ru: 'Объясни', en: 'Explain' },
20-
color: 'text-blue-400 bg-blue-400/10 border-blue-400/20',
21-
desc: { ru: 'Объясните слово, не используя однокоренные', en: 'Explain the word without using root words' },
22-
},
23-
draw: {
24-
icon: Pencil,
25-
label: { ru: 'Нарисуй', en: 'Draw' },
26-
color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20',
27-
desc: { ru: 'Нарисуйте слово — без букв и цифр', en: 'Draw the word — no letters or numbers' },
28-
},
29-
show: {
30-
icon: User,
31-
label: { ru: 'Покажи', en: 'Show' },
32-
color: 'text-amber-400 bg-amber-400/10 border-amber-400/20',
33-
desc: { ru: 'Покажите жестами — без слов и звуков', en: 'Show with gestures — no words or sounds' },
34-
},
35-
}
36-
37-
const suggestedScore = (count: number) => {
38-
if (count <= 2) return 30
39-
if (count === 3) return 40
40-
return 50
41-
}
42-
4317
export default function ActivityGame() {
4418
const { t, lang } = useI18n()
4519
const L = (ru: string, en: string) => lang === 'ru' ? ru : en
20+
const G = useGameText('activity')
21+
22+
const modeConfig: Record<Mode, { icon: any; labelKey: string; color: string; descKey: string }> = {
23+
explain: {
24+
icon: MessageSquare,
25+
labelKey: 'explain_mode',
26+
color: 'text-blue-400 bg-blue-400/10 border-blue-400/20',
27+
descKey: 'explain_desc',
28+
},
29+
draw: {
30+
icon: Pencil,
31+
labelKey: 'draw_mode',
32+
color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20',
33+
descKey: 'draw_desc',
34+
},
35+
show: {
36+
icon: User,
37+
labelKey: 'show_mode',
38+
color: 'text-amber-400 bg-amber-400/10 border-amber-400/20',
39+
descKey: 'show_desc',
40+
},
41+
}
42+
43+
const suggestedScore = (count: number) => {
44+
if (count <= 2) return 30
45+
if (count === 3) return 40
46+
return 50
47+
}
4648

4749
const [phase, setPhase] = useState<Phase>('setup')
4850
const [teams, setTeams] = useState<Team[]>([
@@ -138,14 +140,14 @@ export default function ActivityGame() {
138140
if (phase === 'setup') {
139141
return (
140142
<div className="space-y-6">
141-
<h2 className="text-2xl font-bold">{L('Активити', 'Activity')}</h2>
143+
<h2 className="text-2xl font-bold">{G('title')}</h2>
142144

143145
<div className="card p-4 text-sm text-text-secondary space-y-1">
144-
<p className="font-medium text-text mb-2">{L('Как играть:', 'How to play:')}</p>
145-
<p>1. <span className="text-blue-400">{L('Объясни', 'Explain')}</span>{L('описать слово, не используя однокоренные', 'describe the word without root words')}</p>
146-
<p>2. <span className="text-emerald-400">{L('Нарисуй', 'Draw')}</span>{L('нарисовать слово без букв и цифр', 'draw the word, no letters or numbers')}</p>
147-
<p>3. <span className="text-amber-400">{L('Покажи', 'Show')}</span>{L('показать жестами без слов и звуков', 'act out with gestures, no words or sounds')}</p>
148-
<p className="pt-1 text-text-muted">{L('Режим выбирается случайно каждый ход', 'Mode is chosen randomly each turn')}</p>
146+
<p className="font-medium text-text mb-2">{G('how_to_play')}</p>
147+
<p>1. <span className="text-blue-400">{G('explain_mode')}</span>{G('explain_no_root')}</p>
148+
<p>2. <span className="text-emerald-400">{G('draw_mode')}</span>{G('draw_no_letters')}</p>
149+
<p>3. <span className="text-amber-400">{G('show_mode')}</span>{G('show_no_words')}</p>
150+
<p className="pt-1 text-text-muted">{G('mode_random_each_turn')}</p>
149151
</div>
150152

151153
{/* Teams */}
@@ -176,21 +178,21 @@ export default function ActivityGame() {
176178

177179
<div className="card p-4">
178180
<label className="text-sm text-text-secondary block mb-2">
179-
{L('Цель', 'Target')}: <span className="text-text font-mono">{targetScore}</span> {t.game.points}
181+
{G('target')}: <span className="text-text font-mono">{targetScore}</span> {t.game.points}
180182
</label>
181183
<input type="range" min={10} max={50} step={5} value={targetScore}
182184
onChange={e => setTargetScore(Number(e.target.value))}
183185
className="w-full accent-accent" />
184186
</div>
185187

186188
<div className="card p-4 text-center">
187-
<p className="text-text-muted text-sm mb-1">{L('Ход команды', 'Team turn')}</p>
189+
<p className="text-text-muted text-sm mb-1">{G('team_turn')}</p>
188190
<p className="text-lg font-semibold text-accent">{teams[currentTeam].name}</p>
189191
</div>
190192

191193
<button onClick={drawCard} className="btn-primary w-full text-lg py-4 touch-manipulation">
192194
<Play size={20} />
193-
{L('Вытянуть карту', 'Draw Card')}
195+
{G('draw_card')}
194196
</button>
195197
</div>
196198
)
@@ -217,26 +219,26 @@ export default function ActivityGame() {
217219

218220
<div className={clsx('inline-flex items-center gap-2 px-4 py-2 rounded-full border text-sm font-medium', mc.color)}>
219221
<Icon size={16} />
220-
{L(mc.label.ru, mc.label.en)}
222+
{G(mc.labelKey)}
221223
</div>
222224

223225
<div className="card p-8 space-y-4">
224226
<p className="text-text-muted text-sm">
225227
{t.game.team}: <span className="text-accent font-medium">{teams[currentTeam].name}</span>
226228
</p>
227-
<p className="text-sm text-text-secondary mb-2">{L('Ваше слово:', 'Your word:')}</p>
229+
<p className="text-sm text-text-secondary mb-2">{G('your_word')}</p>
228230
<p className="text-3xl sm:text-4xl font-bold">{currentWord}</p>
229231
</div>
230232

231233
<div className="card p-4 text-sm text-text-muted space-y-1">
232234
<div className="flex items-start gap-2">
233235
<AlertCircle size={14} className="mt-0.5 text-amber-400 flex-shrink-0" />
234-
<p>{L(mc.desc.ru, mc.desc.en)}</p>
236+
<p>{G(mc.descKey)}</p>
235237
</div>
236238
</div>
237239

238240
<button onClick={startPlaying} className="btn-primary w-full text-lg py-5 touch-manipulation">
239-
<Play size={20} /> {L('Старт (60 сек)', 'Start (60s)')}
241+
<Play size={20} /> {G('start_60s')}
240242
</button>
241243
</div>
242244
)
@@ -255,7 +257,7 @@ export default function ActivityGame() {
255257
<div className="flex items-center justify-between">
256258
<div className={clsx('inline-flex items-center gap-1.5 px-3 py-1 rounded-full border text-xs font-medium', mc.color)}>
257259
<Icon size={12} />
258-
{L(mc.label.ru, mc.label.en)}
260+
{G(mc.labelKey)}
259261
</div>
260262
<span className="text-sm text-text-muted">+{turnScore}</span>
261263
</div>

src/games/AliasGame.tsx

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useMemo, useCallback } from 'react'
22
import { Plus, Minus, Play, RotateCcw, Users, AlertTriangle } from 'lucide-react'
33
import { useI18n } from '../i18n'
4+
import { useGameText } from '../hooks/useGameText'
45
import { useTimer } from '../hooks/useTimer'
56
import { aliasWords, aliasWordsEn } from '../data/words'
67
import clsx from 'clsx'
@@ -15,6 +16,7 @@ interface Team {
1516
export default function AliasGame() {
1617
const { t, lang } = useI18n()
1718
const L = (ru: string, en: string) => lang === 'ru' ? ru : en
19+
const G = useGameText()
1820

1921
const [phase, setPhase] = useState<Phase>('setup')
2022
const [teams, setTeams] = useState<Team[]>([
@@ -142,7 +144,7 @@ export default function AliasGame() {
142144
<h2 className="text-2xl font-bold">{L('Алиас', 'Alias')}</h2>
143145

144146
<div className="card p-4 text-sm text-text-secondary space-y-1">
145-
<p className="font-medium text-text mb-2">{L('Как играть:', 'How to play:')}</p>
147+
<p className="font-medium text-text mb-2">{G('how_to_play')}</p>
146148
<p>1. {L('Разделитесь на команды', 'Split into teams')}</p>
147149
<p>2. {L('Один объясняет слово — остальные угадывают', 'One explains — others guess')}</p>
148150
<p>3. {L('Нельзя: однокоренные слова, жесты, звуки', 'No root words, gestures, or sounds')}</p>
@@ -154,10 +156,7 @@ export default function AliasGame() {
154156
<div className="card p-3 border-amber-500/30 bg-amber-500/5 flex items-start gap-2 text-sm">
155157
<AlertTriangle size={18} className="text-amber-400 shrink-0 mt-0.5" />
156158
<p className="text-amber-300">
157-
{L(
158-
'Рекомендуется минимум 4 игрока (по 2 в команде). Сейчас: ' + totalPlayers,
159-
'At least 4 players recommended (2 per team). Currently: ' + totalPlayers
160-
)}
159+
{G('min_players_warning')} ({L('по 2 в команде', '2 per team')}). {L('Сейчас', 'Currently')}: {totalPlayers}
161160
</p>
162161
</div>
163162
)}
@@ -186,7 +185,7 @@ export default function AliasGame() {
186185
{/* Players per team */}
187186
<div className="flex items-center gap-2 ml-1 text-xs text-text-muted">
188187
<Users size={12} />
189-
<span>{L('Игроков:', 'Players:')}</span>
188+
<span>{G('players_label')}</span>
190189
<button onClick={() => updatePlayersPerTeam(i, -1)}
191190
className="w-5 h-5 rounded bg-bg-surface flex items-center justify-center hover:bg-border transition-colors">
192191
<Minus size={10} />
@@ -202,19 +201,19 @@ export default function AliasGame() {
202201
{/* Total players */}
203202
<div className="flex items-center justify-end gap-1.5 text-xs text-text-muted pt-1">
204203
<Users size={12} />
205-
<span>{L('Всего игроков:', 'Total players:')} <span className="font-mono text-text">{totalPlayers}</span></span>
204+
<span>{G('total_players')} <span className="font-mono text-text">{totalPlayers}</span></span>
206205
</div>
207206
</div>
208207

209208
{/* Recommended settings */}
210209
<div className="card p-3 bg-accent/5 border-accent/20 space-y-1.5 text-sm">
211210
<p className="font-medium text-accent text-xs uppercase tracking-wider">
212-
{L('Рекомендуемые настройки', 'Recommended settings')}
211+
{G('recommended_settings')}
213212
</p>
214213
<p className="text-text-secondary">
215-
{L('Цель', 'Target')}: <span className="text-text font-mono">{suggestTargetScore(teams.length)}</span> {t.game.points}
214+
{G('target')}: <span className="text-text font-mono">{suggestTargetScore(teams.length)}</span> {t.game.points}
216215
{' · '}
217-
{L('Таймер', 'Timer')}: <span className="text-text font-mono">{getRecommendedTimer()}</span> {L('сек', 'sec')}
216+
{L('Таймер', 'Timer')}: <span className="text-text font-mono">{getRecommendedTimer()}</span> {G('sec')}
218217
</p>
219218
<p className="text-text-muted text-xs">
220219
{L(
@@ -228,15 +227,15 @@ export default function AliasGame() {
228227
<div className="card p-4 space-y-4">
229228
<div>
230229
<label className="text-sm text-text-secondary block mb-2">
231-
{L('Цель', 'Target score')}: <span className="text-text font-mono">{targetScore}</span> {t.game.points}
230+
{G('target_score')}: <span className="text-text font-mono">{targetScore}</span> {t.game.points}
232231
</label>
233232
<input type="range" min={20} max={100} step={10} value={targetScore}
234233
onChange={e => setTargetScore(Number(e.target.value))}
235234
className="w-full accent-accent" />
236235
</div>
237236
<div>
238237
<label className="text-sm text-text-secondary block mb-2">
239-
{L('Время на ход', 'Turn time')}: <span className="text-text font-mono">{timerSeconds}</span> {L('сек', 'sec')}
238+
{G('turn_time')}: <span className="text-text font-mono">{timerSeconds}</span> {G('sec')}
240239
</label>
241240
<input type="range" min={30} max={120} step={10} value={timerSeconds}
242241
onChange={e => setTimerSeconds(Number(e.target.value))}
@@ -276,25 +275,22 @@ export default function AliasGame() {
276275
)}
277276

278277
<div className="card p-8 space-y-4">
279-
<p className="text-text-muted text-sm">{L('Ход команды', 'Current turn')}</p>
278+
<p className="text-text-muted text-sm">{G('current_turn')}</p>
280279
<p className="text-3xl font-bold text-accent">{teams[currentTeam].name}</p>
281280
<p className="text-text-secondary text-sm">
282-
{L(
283-
'Передайте телефон объясняющему игроку. У вас ' + timerSeconds + ' секунд.',
284-
'Pass the phone to the explainer. You have ' + timerSeconds + ' seconds.'
285-
)}
281+
{G('pass_phone')}. {L('У вас ' + timerSeconds + ' секунд.', 'You have ' + timerSeconds + ' seconds.')}
286282
</p>
287283
</div>
288284

289285
<div className="card p-4 text-sm text-text-muted space-y-1">
290-
<p>{L('Напоминание:', 'Reminder:')}</p>
291-
<p>&bull; {L('Нельзя использовать однокоренные слова', 'No root words allowed')}</p>
292-
<p>&bull; {L('Нельзя показывать жестами', 'No gestures allowed')}</p>
293-
<p>&bull; {L('Если сложно — лучше пропустить', 'If it\'s hard — better skip')}</p>
286+
<p>{G('reminder')}</p>
287+
<p>&bull; {G('no_root_words')}</p>
288+
<p>&bull; {G('no_gestures')}</p>
289+
<p>&bull; {G('better_skip')}</p>
294290
</div>
295291

296292
<button onClick={startTurn} className="btn-primary w-full text-lg py-4">
297-
<Play size={20} /> {L('Старт!', 'Start!')}
293+
<Play size={20} /> {G('start_btn')}
298294
</button>
299295
</div>
300296
)
@@ -370,7 +366,7 @@ export default function AliasGame() {
370366
{turnScore >= 0 ? `+${turnScore}` : turnScore}
371367
</p>
372368
<p className="text-text-muted text-sm mt-2">
373-
{L('Всего', 'Total')}: {teams[currentTeam].score}/{targetScore}
369+
{G('total')}: {teams[currentTeam].score}/{targetScore}
374370
</p>
375371
</div>
376372

@@ -392,8 +388,8 @@ export default function AliasGame() {
392388

393389
<button onClick={nextTurn} className="btn-primary w-full">
394390
{teams.some(t => t.score >= targetScore)
395-
? L('Результаты', 'Results')
396-
: L('Ход следующей команды', 'Next team\'s turn')}
391+
? G('results')
392+
: G('next_teams_turn')}
397393
</button>
398394
</div>
399395
)

0 commit comments

Comments
 (0)