Skip to content

Commit 75a8c61

Browse files
Merge pull request #126 from DevLoversTeam/localize-main-pages
feat: add translations for blog, QA and quiz
2 parents 89c91ab + bb166b2 commit 75a8c61

13 files changed

Lines changed: 426 additions & 78 deletions

File tree

frontend/app/[locale]/quiz/[slug]/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getQuizBySlug, getQuizQuestionsRandomized } from '@/db/queries/quiz';
22
import { notFound } from 'next/navigation';
3+
import { getTranslations } from 'next-intl/server';
34
import { QuizContainer } from '@/components/quiz/QuizContainer';
45
import { PendingResultHandler } from '@/components/quiz/PendingResultHandler';
56

@@ -11,6 +12,7 @@ interface QuizPageProps {
1112

1213
export default async function QuizPage({ params }: QuizPageProps) {
1314
const { locale, slug } = await params;
15+
const t = await getTranslations({ locale, namespace: 'quiz.page' });
1416

1517
const user = await getCurrentUser();
1618

@@ -22,11 +24,11 @@ export default async function QuizPage({ params }: QuizPageProps) {
2224

2325
const seed = Date.now();
2426
const questions = await getQuizQuestionsRandomized(quiz.id, locale, seed);
25-
27+
2628
if (!questions.length) {
2729
return (
2830
<div className="min-h-screen flex items-center justify-center">
29-
<p className="text-gray-600">Немає питань для цього квізу</p>
31+
<p className="text-gray-600">{t('noQuestions')}</p>
3032
</div>
3133
);
3234
}
@@ -44,9 +46,9 @@ export default async function QuizPage({ params }: QuizPageProps) {
4446
</p>
4547
)}
4648
<div className="mt-4 flex gap-4 text-sm text-gray-500">
47-
<span>Питань: {quiz.questionsCount}</span>
49+
<span>{t('questionsLabel')}: {quiz.questionsCount}</span>
4850
<span>
49-
Час: {Math.floor((quiz.timeLimitSeconds ?? questions.length * 30) / 60)} хв
51+
{t('timeLabel')}: {Math.floor((quiz.timeLimitSeconds ?? questions.length * 30) / 60)} {t('minutes')}
5052
</span>
5153
</div>
5254
</div>

frontend/app/[locale]/quizzes/page.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getActiveQuizzes, getUserQuizzesProgress } from '@/db/queries/quiz';
22
import { getCurrentUser } from '@/lib/auth';
3+
import { getTranslations } from 'next-intl/server';
34
import QuizzesSection from '@/components/quiz/QuizzesSection';
45

56
type PageProps = { params: Promise<{ locale: string }> };
@@ -8,8 +9,9 @@ export const dynamic = 'force-dynamic';
89

910
export default async function QuizzesPage({ params }: PageProps) {
1011
const { locale } = await params;
12+
const t = await getTranslations({ locale, namespace: 'quiz.list' });
1113
const session = await getCurrentUser();
12-
14+
1315
const quizzes = await getActiveQuizzes(locale);
1416

1517
let userProgressMap: Record<string, any> = {};
@@ -22,9 +24,9 @@ export default async function QuizzesPage({ params }: PageProps) {
2224
if (!quizzes.length) {
2325
return (
2426
<div className="mx-auto max-w-5xl py-12">
25-
<h1 className="text-3xl font-bold mb-4">Quizzes</h1>
27+
<h1 className="text-3xl font-bold mb-4">{t('title')}</h1>
2628
<p className="text-gray-600 dark:text-gray-400">
27-
No quizzes available yet. Please check back soon.
29+
{t('noQuizzes')}
2830
</p>
2931
</div>
3032
);
@@ -34,11 +36,11 @@ export default async function QuizzesPage({ params }: PageProps) {
3436
<div className="mx-auto max-w-5xl py-12">
3537
<div className="mb-8">
3638
<p className="text-sm text-blue-600 dark:text-blue-400 font-semibold">
37-
Practice
39+
{t('practice')}
3840
</p>
39-
<h1 className="text-3xl font-bold">Quizzes</h1>
41+
<h1 className="text-3xl font-bold">{t('title')}</h1>
4042
<p className="text-gray-600 dark:text-gray-400">
41-
Choose a quiz to test your knowledge.
43+
{t('subtitle')}
4244
</p>
4345
</div>
4446

frontend/components/q&a/Pagination.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import { useTranslations } from 'next-intl';
34
import { cn } from '@/lib/utils';
45

56
interface PaginationProps {
@@ -13,6 +14,8 @@ export function Pagination({
1314
totalPages,
1415
onPageChange,
1516
}: PaginationProps) {
17+
const t = useTranslations('qa.pagination');
18+
1619
if (totalPages <= 1) return null;
1720

1821
const getPageNumbers = (): (number | 'ellipsis')[] => {
@@ -57,7 +60,7 @@ export function Pagination({
5760
return (
5861
<nav
5962
className="flex items-center justify-center gap-1 mt-8"
60-
aria-label="Пагінація"
63+
aria-label={t('label')}
6164
>
6265
<button
6366
onClick={() => onPageChange(currentPage - 1)}
@@ -69,9 +72,9 @@ export function Pagination({
6972
? 'text-gray-400 dark:text-gray-600 cursor-not-allowed'
7073
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
7174
)}
72-
aria-label="Попередня сторінка"
75+
aria-label={t('previousPage')}
7376
>
74-
Назад
77+
{t('previous')}
7578
</button>
7679

7780
<div className="flex items-center gap-1 mx-2">
@@ -94,7 +97,7 @@ export function Pagination({
9497
? 'bg-blue-600 text-white'
9598
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
9699
)}
97-
aria-label={`Сторінка ${page}`}
100+
aria-label={t('page', { page })}
98101
aria-current={page === currentPage ? 'page' : undefined}
99102
>
100103
{page}
@@ -113,9 +116,9 @@ export function Pagination({
113116
? 'text-gray-400 dark:text-gray-600 cursor-not-allowed'
114117
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
115118
)}
116-
aria-label="Наступна сторінка"
119+
aria-label={t('nextPage')}
117120
>
118-
Вперед
121+
{t('next')}
119122
</button>
120123
</nav>
121124
);

frontend/components/quiz/CountdownTimer.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useEffect, useState } from 'react';
4+
import { useTranslations } from 'next-intl';
45
import { cn } from '@/lib/utils';
56

67
interface CountdownTimerProps {
@@ -14,6 +15,7 @@ export function CountdownTimer({
1415
onTimeUp,
1516
isActive,
1617
}: CountdownTimerProps) {
18+
const t = useTranslations('quiz.timer');
1719
const [endTime] = useState(() => Date.now() + timeLimitSeconds * 1000);
1820
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
1921

@@ -79,7 +81,7 @@ export function CountdownTimer({
7981
percentage <= 10 && 'animate-pulse'
8082
)}>
8183
<div className="flex items-center justify-between mb-2">
82-
<span className="text-sm font-medium">Залишилось часу:</span>
84+
<span className="text-sm font-medium">{t('label')}</span>
8385
<span className="text-2xl font-bold font-mono">
8486
{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
8587
</span>
@@ -97,7 +99,7 @@ export function CountdownTimer({
9799

98100
{percentage <= 30 && (
99101
<p className="text-xs mt-2 font-medium">
100-
{percentage <= 10 ? '⚠️ Час майже закінчився!' : '⏰ Поспішайте!'}
102+
{percentage <= 10 ? t('almostDone') : t('hurryUp')}
101103
</p>
102104
)}
103105
</div>

frontend/components/quiz/QuizCard.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import { useTranslations } from 'next-intl';
34
import { Link } from '@/i18n/routing';
45
import { Badge } from '@/components/ui/badge';
56

@@ -21,16 +22,17 @@ interface QuizCardProps {
2122
}
2223

2324
export function QuizCard({ quiz, userProgress }: QuizCardProps) {
25+
const t = useTranslations('quiz.card');
2426
const percentage = userProgress && userProgress.totalQuestions > 0
2527
? Math.round((userProgress.bestScore / userProgress.totalQuestions) * 100)
2628
: 0;
2729

2830
return (
2931
<div className="rounded-xl border border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-5 shadow-sm hover:shadow-md transition-shadow">
3032
<div className="flex gap-2 mb-3">
31-
<Badge variant="blue">{quiz.categoryName ?? 'Uncategorized'}</Badge>
33+
<Badge variant="blue">{quiz.categoryName ?? t('uncategorized')}</Badge>
3234
{userProgress && (
33-
<Badge variant="success">✓ Completed</Badge>
35+
<Badge variant="success">{t('completed')}</Badge>
3436
)}
3537
</div>
3638
<h2 className="text-xl font-semibold mb-2">
@@ -42,16 +44,16 @@ export function QuizCard({ quiz, userProgress }: QuizCardProps) {
4244
</p>
4345
)}
4446
<div className="flex gap-3 text-xs text-gray-500 mb-3">
45-
<span>📝 {quiz.questionsCount} questions</span>
47+
<span>📝 {quiz.questionsCount} {t('questions')}</span>
4648
<span>
47-
⏱️ {Math.floor((quiz.timeLimitSeconds ?? quiz.questionsCount * 30) / 60)} min
49+
⏱️ {Math.floor((quiz.timeLimitSeconds ?? quiz.questionsCount * 30) / 60)} {t('min')}
4850
</span>
4951
</div>
5052
{userProgress && (
5153
<div className="mb-4">
5254
<div className="flex justify-between text-xs mb-1.5">
5355
<span className="text-gray-600 dark:text-gray-400">
54-
Best: {userProgress.bestScore}/{userProgress.totalQuestions}
56+
{t('best')} {userProgress.bestScore}/{userProgress.totalQuestions}
5557
</span>
5658
<span className="font-medium text-gray-900 dark:text-gray-100">
5759
{percentage}%
@@ -64,15 +66,15 @@ export function QuizCard({ quiz, userProgress }: QuizCardProps) {
6466
/>
6567
</div>
6668
<p className="text-xs text-gray-500 mt-1">
67-
{userProgress.attemptsCount} {userProgress.attemptsCount === 1 ? 'attempt' : 'attempts'}
69+
{userProgress.attemptsCount} {userProgress.attemptsCount === 1 ? t('attempt') : t('attempts')}
6870
</p>
6971
</div>
7072
)}
7173
<Link
7274
href={`/quiz/${quiz.slug}`}
7375
className="block w-full text-center rounded-lg bg-blue-600 text-white px-4 py-2.5 text-sm font-medium hover:bg-blue-500 transition-colors"
7476
>
75-
{userProgress ? 'Retake Quiz' : 'Start Quiz'}
77+
{userProgress ? t('retake') : t('start')}
7678
</Link>
7779
</div>
7880
);

frontend/components/quiz/QuizContainer.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useLocale } from 'next-intl';
3+
import { useLocale, useTranslations } from 'next-intl';
44
import { savePendingQuizResult } from '@/lib/guest-quiz';
55
import { useReducer, useTransition } from 'react';
66
import { useAntiCheat } from '@/hooks/useAntiCheat';
@@ -114,6 +114,7 @@ export function QuizContainer({
114114
timeLimitSeconds,
115115
onBackToTopics,
116116
}: QuizContainerProps) {
117+
const tRules = useTranslations('quiz.rules');
117118
const [isPending, startTransition] = useTransition();
118119
const [state, dispatch] = useReducer(quizReducer, {
119120
status: 'rules',
@@ -230,59 +231,56 @@ const locale = useLocale();
230231
return (
231232
<div className="max-w-2xl mx-auto space-y-6 p-6 rounded-xl border border-gray-200 dark:border-gray-800">
232233
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
233-
Правила проходження квізу
234+
{tRules('title')}
234235
</h2>
235236

236237
<div className="space-y-4 text-gray-700 dark:text-gray-300">
237238
<div className="flex gap-3">
238239
<span className="text-xl">📝</span>
239240
<div>
240-
<p className="font-medium">Загальні правила</p>
241+
<p className="font-medium">{tRules('general.title')}</p>
241242
<p className="text-sm text-gray-600 dark:text-gray-400">
242-
Відповідайте на питання чесно. Кожне питання має тільки одну
243-
правильну відповідь.
243+
{tRules('general.description')}
244244
</p>
245245
</div>
246246
</div>
247247

248248
<div className="flex gap-3">
249249
<span className="text-xl">🚫</span>
250250
<div>
251-
<p className="font-medium">Заборонено</p>
251+
<p className="font-medium">{tRules('forbidden.title')}</p>
252252
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
253-
<li>Копіювання та вставка тексту</li>
254-
<li>Використання контекстного меню (права кнопка миші)</li>
255-
<li>Переключення на інші вкладки або програми</li>
256-
<li>Використання сторонніх джерел інформації</li>
253+
<li>{tRules('forbidden.copyPaste')}</li>
254+
<li>{tRules('forbidden.contextMenu')}</li>
255+
<li>{tRules('forbidden.tabSwitch')}</li>
256+
<li>{tRules('forbidden.externalSources')}</li>
257257
</ul>
258258
</div>
259259
</div>
260260

261261
<div className="flex gap-3">
262262
<span className="text-xl">⚠️</span>
263263
<div>
264-
<p className="font-medium">Система контролю</p>
264+
<p className="font-medium">{tRules('control.title')}</p>
265265
<p className="text-sm text-gray-600 dark:text-gray-400">
266-
Порушення правил фіксуються автоматично. При 3+ порушеннях
267-
результат не зараховується до рейтингу.
266+
{tRules('control.description')}
268267
</p>
269268
</div>
270269
</div>
271270

272271
<div className="flex gap-3">
273272
<span className="text-xl">⏱️</span>
274273
<div>
275-
<p className="font-medium">Час проходження</p>
274+
<p className="font-medium">{tRules('time.title')}</p>
276275
<p className="text-sm text-gray-600 dark:text-gray-400">
277-
Мінімальний час: {totalQuestions * 3} секунд (по 3 секунди на
278-
питання). Занадто швидке проходження не зараховується.
276+
{tRules('time.description', { seconds: totalQuestions * 3 })}
279277
</p>
280278
</div>
281279
</div>
282280
</div>
283281

284282
<Button onClick={handleStart} className="w-full" size="lg">
285-
Почати квіз
283+
{tRules('startButton')}
286284
</Button>
287285
</div>
288286
);

frontend/components/quiz/QuizProgress.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import { useTranslations } from 'next-intl';
34
import { cn } from '@/lib/utils';
45

56
interface Answer {
@@ -46,13 +47,14 @@ function getVisibleIndices(current: number, total: number): (number | 'ellipsis'
4647
}
4748

4849
export function QuizProgress({ current, total, answers }: QuizProgressProps) {
50+
const t = useTranslations('quiz.progress');
4951
const visibleIndices = getVisibleIndices(current, total);
5052

5153
return (
5254
<div className="space-y-4">
5355
<div className="text-center">
5456
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
55-
Питання {current + 1} / {total}
57+
{t('label', { current: current + 1, total })}
5658
</span>
5759
</div>
5860

0 commit comments

Comments
 (0)