Skip to content

Commit 9ade51d

Browse files
authored
feat(i18n): add translations for blog categories, and UI components (#253)
- Translate blog category labels in mobile menu, cards, and filters - Add CTA hover variants translations - Add aria-label translations for theme toggle, cart, search, GitHub star - Update translation files for EN, UK, PL locales
1 parent 2bcafdd commit 9ade51d

13 files changed

Lines changed: 129 additions & 30 deletions

File tree

frontend/components/blog/BlogCard.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ export default function BlogCard({
2222
disableHoverColor?: boolean;
2323
}) {
2424
const t = useTranslations('blog');
25+
26+
const getCategoryLabel = (categoryName: string): string => {
27+
const key = categoryName.toLowerCase() as
28+
| 'tech'
29+
| 'career'
30+
| 'insights'
31+
| 'news'
32+
| 'growth';
33+
const categoryTranslations: Record<string, string> = {
34+
tech: t('categories.tech'),
35+
career: t('categories.career'),
36+
insights: t('categories.insights'),
37+
news: t('categories.news'),
38+
growth: t('categories.growth'),
39+
};
40+
return categoryTranslations[key] || categoryName;
41+
};
42+
2543
const excerpt =
2644
(post.body ?? [])
2745
.filter((b): b is PortableTextBlock => b._type === 'block')
@@ -34,8 +52,9 @@ export default function BlogCard({
3452
() => formatBlogDate(post.publishedAt),
3553
[post.publishedAt]
3654
);
37-
const categoryLabel =
55+
const rawCategory =
3856
post.categories?.[0] === 'Growth' ? 'Career' : post.categories?.[0];
57+
const categoryLabel = rawCategory ? getCategoryLabel(rawCategory) : undefined;
3958

4059
return (
4160
<article

frontend/components/blog/BlogFilters.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ export default function BlogFilters({
502502
<div className="relative flex flex-col h-full pt-8">
503503
{featuredPost.categories?.[0] && (
504504
<div className="absolute top-0 left-0 text-xs font-bold uppercase tracking-[0.2em] text-[var(--accent-primary)]">
505-
{featuredPost.categories[0]}
505+
{getCategoryLabel(featuredPost.categories[0] === 'Growth' ? 'Career' : featuredPost.categories[0])}
506506
</div>
507507
)}
508508
<div className="my-auto">

frontend/components/blog/BlogHeaderSearch.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function normalizeSearchText(value: string) {
4242

4343
export function BlogHeaderSearch() {
4444
const t = useTranslations('blog');
45+
const tAria = useTranslations('aria');
4546
const locale = useLocale();
4647
const [open, setOpen] = useState(false);
4748
const [value, setValue] = useState('');
@@ -158,7 +159,7 @@ export function BlogHeaderSearch() {
158159
})
159160
}
160161
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
161-
aria-label="Search blog"
162+
aria-label={tAria('searchBlog')}
162163
>
163164
<Search className="h-4 w-4" aria-hidden="true" />
164165
</button>

frontend/components/header/AppMobileMenu.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,25 @@ export function AppMobileMenu({
3030
const tMobileMenu = useTranslations('mobileMenu');
3131
const tCategories = useTranslations('shop.catalog.categories');
3232
const tProducts = useTranslations('shop.products');
33+
const tBlog = useTranslations('blog');
3334
const pathname = usePathname();
35+
36+
const getBlogCategoryLabel = (categoryName: string): string => {
37+
const key = categoryName.toLowerCase() as
38+
| 'tech'
39+
| 'career'
40+
| 'insights'
41+
| 'news'
42+
| 'growth';
43+
const categoryTranslations: Record<string, string> = {
44+
tech: tBlog('categories.tech'),
45+
career: tBlog('categories.career'),
46+
insights: tBlog('categories.insights'),
47+
news: tBlog('categories.news'),
48+
growth: tBlog('categories.growth'),
49+
};
50+
return categoryTranslations[key] || categoryName;
51+
};
3452
const searchParams = useSearchParams();
3553
const currentCategory = searchParams.get('category');
3654
const [open, setOpen] = useState(false);
@@ -176,14 +194,15 @@ export function AppMobileMenu({
176194
const slug = slugify(category.title || '');
177195
const href = `/blog/category/${slug}`;
178196
const isActive = pathname === href;
197+
const displayTitle = category.title === 'Growth' ? 'Career' : category.title;
179198
return (
180199
<Link
181200
key={category._id}
182201
href={href}
183202
onClick={close}
184203
className={linkClass(isActive)}
185204
>
186-
{category.title}
205+
{getBlogCategoryLabel(displayTitle)}
187206
</Link>
188207
);
189208
})}

frontend/components/header/DesktopActions.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function DesktopActions({
2222
showAdminLink = false,
2323
}: DesktopActionsProps) {
2424
const t = useTranslations('navigation');
25+
const tAria = useTranslations('aria');
2526
const isShop = variant === 'shop';
2627
const isBlog = variant === 'blog';
2728

@@ -32,7 +33,7 @@ export function DesktopActions({
3233
variant="icon"
3334
href="/dashboard"
3435
icon={User}
35-
label="Dashboard"
36+
label={tAria('dashboard')}
3637
/>
3738
)}
3839

@@ -41,7 +42,7 @@ export function DesktopActions({
4142
variant="icon"
4243
href="/shop/admin"
4344
icon={Settings}
44-
label="Shop admin"
45+
label={tAria('shopAdmin')}
4546
/>
4647
)}
4748

frontend/components/home/InteractiveCTAButton.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ export function InteractiveCTAButton() {
1212
const [variantIndex, setVariantIndex] = React.useState(1);
1313
const [isHovered, setIsHovered] = React.useState(false);
1414

15-
// Тимчасово для тестування - цифри замість текстів
1615
const textVariants = [
17-
`${t('cta')} 1`,
18-
`${t('cta')} 2`,
19-
`${t('cta')} 3`,
20-
`${t('cta')} 4`,
21-
`${t('cta')} 5`,
16+
t('ctaVariants.1'),
17+
t('ctaVariants.2'),
18+
t('ctaVariants.3'),
19+
t('ctaVariants.4'),
20+
t('ctaVariants.5'),
21+
t('ctaVariants.6'),
22+
t('ctaVariants.7'),
23+
t('ctaVariants.8'),
2224
];
2325

2426
const defaultVariant = t('cta');
@@ -35,7 +37,6 @@ export function InteractiveCTAButton() {
3537
setVariantIndex(prev => (prev + 1) % textVariants.length);
3638
};
3739

38-
// Орбітальні частинки
3940
const particles = Array.from({ length: 12 }, (_, i) => ({
4041
id: i,
4142
angle: (i * 360) / 8,

frontend/components/quiz/QuizContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ export function QuizContainer({
501501
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
502502
/>
503503
</svg>
504-
Exit Quiz
504+
{tExit('exitButton')}
505505
</Button>
506506
</div>
507507
<QuizProgress

frontend/components/shared/GitHubStarButton.tsx

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

33
import { Star } from 'lucide-react';
4+
import { useTranslations } from 'next-intl';
45
import { useState, useEffect, useRef } from 'react';
56

67
interface GitHubStarButtonProps {
78
className?: string;
89
}
910

1011
export function GitHubStarButton({ className = '' }: GitHubStarButtonProps) {
12+
const t = useTranslations('aria');
1113
const [displayCount, setDisplayCount] = useState(0);
1214
const [finalCount, setFinalCount] = useState<number | null>(null);
1315
const githubUrl = 'https://github.com/DevLoversTeam/devlovers.net';
@@ -74,7 +76,7 @@ export function GitHubStarButton({ className = '' }: GitHubStarButtonProps) {
7476
href={githubUrl}
7577
target="_blank"
7678
rel="noopener noreferrer"
77-
aria-label={`Star on GitHub - ${displayCount} stars`}
79+
aria-label={t('starOnGithub', { count: displayCount })}
7880
className={`
7981
hidden lg:inline-flex
8082
group relative

frontend/components/shop/header/CartButton.tsx

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

33
import { ShoppingBag } from 'lucide-react';
4+
import { useTranslations } from 'next-intl';
45

56
import { useMounted } from '@/hooks/use-mounted';
67
import { HeaderButton } from '@/components/shared/HeaderButton';
@@ -10,18 +11,20 @@ import { useCart } from '../CartProvider';
1011
export function CartButton() {
1112
const { cart } = useCart();
1213
const mounted = useMounted();
14+
const t = useTranslations('aria');
1315

1416
const itemCount = mounted ? cart.summary.itemCount : 0;
1517
const showCount = itemCount > 0;
1618

1719
const badgeText = itemCount > 99 ? '99+' : itemCount;
20+
const label = showCount ? t('cartWithItems', { count: itemCount }) : t('cart');
1821

1922
return (
2023
<HeaderButton
2124
href="/shop/cart"
2225
variant="icon"
2326
icon={ShoppingBag}
24-
label={showCount ? `Cart, ${itemCount} items` : 'Cart'}
27+
label={label}
2528
badge={showCount ? badgeText : undefined}
2629
badgeClassName="bg-[color:var(--accent-primary)] text-white"
2730
/>

frontend/components/theme/ThemeToggle.tsx

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

33
import { useTheme } from 'next-themes';
4+
import { useTranslations } from 'next-intl';
45
import { Monitor, Sun, Moon } from 'lucide-react';
56
import { motion } from 'framer-motion';
67
import { useEffect, useState } from 'react';
78

89
const themes = [
9-
{ value: 'system', icon: Monitor, label: 'System theme' },
10-
{ value: 'light', icon: Sun, label: 'Light theme' },
11-
{ value: 'dark', icon: Moon, label: 'Dark theme' },
10+
{ value: 'system', icon: Monitor, labelKey: 'themeSystem' },
11+
{ value: 'light', icon: Sun, labelKey: 'themeLight' },
12+
{ value: 'dark', icon: Moon, labelKey: 'themeDark' },
1213
] as const;
1314

1415
export function ThemeToggle() {
1516
const { theme, setTheme } = useTheme();
17+
const t = useTranslations('aria');
1618
const [mounted, setMounted] = useState(false);
1719

1820
useEffect(() => {
@@ -36,11 +38,11 @@ export function ThemeToggle() {
3638

3739
return (
3840
<div className="flex h-9 items-center gap-1 rounded-full bg-neutral-100 p-1 dark:border dark:border-neutral-800 dark:bg-neutral-950">
39-
{themes.map(({ value, icon: Icon, label }) => (
41+
{themes.map(({ value, icon: Icon, labelKey }) => (
4042
<button
4143
key={value}
4244
onClick={() => setTheme(value)}
43-
aria-label={label}
45+
aria-label={t(labelKey)}
4446
className="theme-toggle-btn relative flex h-7 w-7 items-center justify-center rounded-full"
4547
>
4648
{theme === value && (

0 commit comments

Comments
 (0)