From 859d7318cee5a6b1d333d3195d5e438c1ca60d5a Mon Sep 17 00:00:00 2001 From: tetiana zorii Date: Tue, 20 Jan 2026 22:51:11 -0500 Subject: [PATCH 1/2] feat(i18n): localize quiz anti-cheat, header and blog filters - Add quiz anti-cheat toast messages translations (ua/en/pl) - Translate header navigation buttons - Localize blog filter buttons (All/Tech/Career/Insights/News) - Add blog search placeholder translations - Ensure all user-facing messages are localized --- .../components/blog/BlogCategoryLinks.tsx | 15 ++++++++++++-- frontend/components/blog/BlogFilters.tsx | 14 +++++++++++-- frontend/components/blog/BlogHeaderSearch.tsx | 6 ++++-- frontend/components/header/AppMobileMenu.tsx | 10 ++++++---- frontend/components/header/UnifiedHeader.tsx | 8 +++++--- .../components/shared/LanguageSwitcher.tsx | 14 +++++++++---- frontend/hooks/useAntiCheat.ts | 14 +++++++------ frontend/lib/navigation.ts | 14 ++++++------- frontend/messages/en.json | 16 +++++++++++++++ frontend/messages/pl.json | 16 +++++++++++++++ frontend/messages/uk.json | 20 +++++++++++++++++-- 11 files changed, 115 insertions(+), 32 deletions(-) diff --git a/frontend/components/blog/BlogCategoryLinks.tsx b/frontend/components/blog/BlogCategoryLinks.tsx index 01b3f458..c5636c6c 100644 --- a/frontend/components/blog/BlogCategoryLinks.tsx +++ b/frontend/components/blog/BlogCategoryLinks.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useTranslations } from 'next-intl'; import { Link, usePathname } from '@/i18n/routing'; import { cn } from '@/lib/utils'; @@ -21,7 +22,17 @@ export function BlogCategoryLinks({ linkClassName, onNavigate, }: BlogCategoryLinksProps) { + const t = useTranslations('blog'); + const tNav = useTranslations('navigation'); const pathname = usePathname(); + + // Helper function to get translated category label + const getCategoryLabel = (categoryName: string): string => { + const key = categoryName.toLowerCase(); + const translationKey = `categories.${key}` as const; + const translated = t.raw(translationKey); + return typeof translated === 'string' ? translated : categoryName; + }; const baseLink = linkClassName || 'rounded-md px-3 py-2 text-sm font-medium transition-colors ' + @@ -53,7 +64,7 @@ export function BlogCategoryLinks({ isActive ? 'bg-muted text-foreground' : 'text-muted-foreground' )} > - {category.displayTitle} + {getCategoryLabel(category.displayTitle)} ); })} @@ -66,7 +77,7 @@ export function BlogCategoryLinks({ pathname === '/' ? 'bg-muted text-foreground' : 'text-muted-foreground' )} > - Home + {tNav('home')} ); diff --git a/frontend/components/blog/BlogFilters.tsx b/frontend/components/blog/BlogFilters.tsx index 7dfeeb42..549e518d 100644 --- a/frontend/components/blog/BlogFilters.tsx +++ b/frontend/components/blog/BlogFilters.tsx @@ -133,6 +133,16 @@ export default function BlogFilters({ setSelectedAuthor(null); setSelectedCategory(null); }; + + // Helper function to get translated category label + const getCategoryLabel = (categoryName: string): string => { + const key = categoryName.toLowerCase(); + const translationKey = `categories.${key}` as const; + // Try to get translation, fallback to original name + const translated = t.raw(translationKey); + return typeof translated === 'string' ? translated : categoryName; + }; + const allCategories = useMemo(() => { if (categories.length) { return categories @@ -343,7 +353,7 @@ export default function BlogFilters({ : 'rounded-full border border-gray-300 bg-white px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800' } > - All + {t('all')} {allCategories.map(category => ( ))} diff --git a/frontend/components/blog/BlogHeaderSearch.tsx b/frontend/components/blog/BlogHeaderSearch.tsx index 002c3329..f6e26406 100644 --- a/frontend/components/blog/BlogHeaderSearch.tsx +++ b/frontend/components/blog/BlogHeaderSearch.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Search } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { useRouter } from '@/i18n/routing'; type PostSearchItem = { @@ -36,6 +37,7 @@ function extractSnippet(body: PostSearchItem['body'], query: string) { const SEARCH_ENDPOINT = '/api/blog-search'; export function BlogHeaderSearch() { + const t = useTranslations('blog'); const [open, setOpen] = useState(false); const [value, setValue] = useState(''); const [items, setItems] = useState([]); @@ -156,7 +158,7 @@ export function BlogHeaderSearch() { onKeyDown={event => { if (event.key === 'Escape') setOpen(false); }} - placeholder="What're we looking for ?" + placeholder={t('searchPlaceholder')} className="w-full bg-transparent text-sm text-foreground outline-none" style={{ fontFamily: 'Lato, system-ui, -apple-system, sans-serif' }} /> @@ -200,7 +202,7 @@ export function BlogHeaderSearch() { )} {value && !results.length && !isLoading && (
- No matches + {t('noMatches')}
)} diff --git a/frontend/components/header/AppMobileMenu.tsx b/frontend/components/header/AppMobileMenu.tsx index 74fa8aea..90629de5 100644 --- a/frontend/components/header/AppMobileMenu.tsx +++ b/frontend/components/header/AppMobileMenu.tsx @@ -2,6 +2,7 @@ import { Menu, X } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/routing'; import { SITE_LINKS } from '@/lib/navigation'; @@ -24,6 +25,7 @@ export function AppMobileMenu({ showAdminLink = false, blogCategories = [], }: Props) { + const t = useTranslations('navigation'); const [open, setOpen] = useState(false); const close = () => setOpen(false); @@ -79,7 +81,7 @@ export function AppMobileMenu({ onClick={close} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" > - Home + {t('home')} ) : null} @@ -98,7 +100,7 @@ export function AppMobileMenu({ onClick={close} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" > - {link.label} + {'labelKey' in link ? t(link.labelKey) : link.label} )) )} @@ -122,7 +124,7 @@ export function AppMobileMenu({ onClick={close} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" > - Dashboard + {t('dashboard')} {showAdminLink ? ( @@ -147,7 +149,7 @@ export function AppMobileMenu({ onClick={close} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" > - Log in + {t('login')} )} diff --git a/frontend/components/header/UnifiedHeader.tsx b/frontend/components/header/UnifiedHeader.tsx index d69970d8..b952b34a 100644 --- a/frontend/components/header/UnifiedHeader.tsx +++ b/frontend/components/header/UnifiedHeader.tsx @@ -1,5 +1,6 @@ 'use client'; import { LogIn, Settings, User } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/routing'; import { SITE_LINKS } from '@/lib/navigation'; @@ -28,10 +29,11 @@ export function UnifiedHeader({ showAdminLink = false, blogCategories = [], }: UnifiedHeaderProps) { + const t = useTranslations('navigation'); const isShop = variant === 'shop'; const isBlog = variant === 'blog'; const brandHref = isShop ? '/shop' : isBlog ? '/blog' : '/'; - const brandBadge = isShop ? 'Shop' : isBlog ? 'Blog' : ''; + const brandBadge = isShop ? t('shop') : isBlog ? t('blog') : ''; return (
@@ -69,7 +71,7 @@ export function UnifiedHeader({ href={link.href} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" > - {link.label} + {t(link.labelKey)} ))} @@ -109,7 +111,7 @@ export function UnifiedHeader({ className="inline-flex items-center gap-2 rounded-md bg-secondary px-3 py-2 text-sm font-medium text-foreground transition-colors hover:opacity-90" > - Log in + {t('login')} ) : ( diff --git a/frontend/components/shared/LanguageSwitcher.tsx b/frontend/components/shared/LanguageSwitcher.tsx index 0f67a143..94da2407 100644 --- a/frontend/components/shared/LanguageSwitcher.tsx +++ b/frontend/components/shared/LanguageSwitcher.tsx @@ -5,6 +5,12 @@ import { useParams, usePathname,useSearchParams } from 'next/navigation'; import { locales, type Locale } from '@/i18n/config'; import { Link } from '@/i18n/routing'; +const localeLabels: Record = { + uk: 'UA', + en: 'EN', + pl: 'PL', +}; + export default function LanguageSwitcher() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -37,9 +43,9 @@ export default function LanguageSwitcher() {
diff --git a/frontend/hooks/useAntiCheat.ts b/frontend/hooks/useAntiCheat.ts index 29c7109b..cb416c60 100644 --- a/frontend/hooks/useAntiCheat.ts +++ b/frontend/hooks/useAntiCheat.ts @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; export type AntiCheatViolation = { @@ -9,6 +10,7 @@ export type AntiCheatViolation = { }; export function useAntiCheat(isActive: boolean = true) { + const t = useTranslations('quiz.antiCheat'); const [violations, setViolations] = useState([]); const [isTabActive, setIsTabActive] = useState(true); const [showWarning, setShowWarning] = useState(false); @@ -25,14 +27,14 @@ export function useAntiCheat(isActive: boolean = true) { setViolations(prev => [...prev, violation]); setShowWarning(true); - const messages = { - copy: '⚠️ Копіювання заборонено під час квізу', - paste: '⚠️ Вставка заборонена під час квізу', - 'context-menu': '⚠️ Контекстне меню заборонено під час квізу', - 'tab-switch': '⚠️ Перехід на іншу вкладку зафіксовано', + const messageKey: Record = { + copy: 'copy', + paste: 'paste', + 'context-menu': 'contextMenu', + 'tab-switch': 'tabSwitch', }; - toast.warning(messages[type], { + toast.warning(t(messageKey[type]), { duration: 3000, }); diff --git a/frontend/lib/navigation.ts b/frontend/lib/navigation.ts index 0fd999ae..194d458b 100644 --- a/frontend/lib/navigation.ts +++ b/frontend/lib/navigation.ts @@ -1,9 +1,9 @@ export const SITE_LINKS = [ - { href: '/q&a', label: 'Q&A' }, - { href: '/quizzes', label: 'Quizzes' }, - { href: '/leaderboard', label: 'Leaderboard' }, - { href: '/blog', label: 'Blog' }, - { href: '/about', label: 'About' }, - { href: '/contacts', label: 'Contacts' }, - { href: '/shop', label: 'Shop' }, + { href: '/q&a', labelKey: 'qa' }, + { href: '/quizzes', labelKey: 'quizzes' }, + { href: '/leaderboard', labelKey: 'leaderboard' }, + { href: '/blog', labelKey: 'blog' }, + { href: '/about', labelKey: 'about' }, + { href: '/contacts', labelKey: 'contacts' }, + { href: '/shop', labelKey: 'shop' }, ] as const; diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 467de75d..86560fa2 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -18,6 +18,7 @@ "about": "About", "contacts": "Contacts", "blog": "Blog", + "shop": "Shop", "login": "Log in", "logout": "Log out", "signup": "Sign up", @@ -164,6 +165,12 @@ "message": "Your progress will not be saved.", "confirm": "Exit", "cancel": "Continue" + }, + "antiCheat": { + "copy": "Copying is not allowed during the quiz", + "paste": "Pasting is not allowed during the quiz", + "contextMenu": "Context menu is not allowed during the quiz", + "tabSwitch": "Tab switch detected" } }, "blog": { @@ -171,7 +178,16 @@ "subtitle": "Explore the latest articles in Tech, News, and Career Growth, covering trends, ideas, and practical insights.", "metaTitle": "Blog | DevLovers", "metaDescription": "Explore the latest articles and insights", + "all": "All", + "noMatches": "No matches", "searchPlaceholder": "Search...", + "categories": { + "tech": "Tech", + "career": "Career", + "insights": "Insights", + "news": "News", + "growth": "Career" + }, "removeTag": "Remove tag", "add": "Add", "clear": "Clear", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 58f5ba87..0f014205 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -18,6 +18,7 @@ "about": "O nas", "contacts": "Kontakt", "blog": "Blog", + "shop": "Sklep", "login": "Zaloguj się", "logout": "Wyloguj się", "signup": "Zarejestruj się", @@ -164,6 +165,12 @@ "message": "Twój postęp nie zostanie zapisany.", "confirm": "Wyjdź", "cancel": "Kontynuuj" + }, + "antiCheat": { + "copy": "Kopiowanie jest zabronione podczas quizu", + "paste": "Wklejanie jest zabronione podczas quizu", + "contextMenu": "Menu kontekstowe jest zabronione podczas quizu", + "tabSwitch": "Wykryto przełączenie karty" } }, "blog": { @@ -171,7 +178,16 @@ "subtitle": "Poznaj najnowsze artykuły z Tech, News i Career Growth — trendy, idee i praktyczne wskazówki.", "metaTitle": "Blog | DevLovers", "metaDescription": "Odkryj najnowsze artykuły i spostrzeżenia", + "all": "Wszystkie", + "noMatches": "Brak wyników", "searchPlaceholder": "Szukaj...", + "categories": { + "tech": "Technologia", + "career": "Kariera", + "insights": "Spostrzeżenia", + "news": "Aktualności", + "growth": "Kariera" + }, "removeTag": "Usuń tag", "add": "Dodaj", "clear": "Wyczyść", diff --git a/frontend/messages/uk.json b/frontend/messages/uk.json index 9887cf6e..e5cb16be 100644 --- a/frontend/messages/uk.json +++ b/frontend/messages/uk.json @@ -18,6 +18,7 @@ "about": "Про нас", "contacts": "Контакти", "blog": "Блог", + "shop": "Магазин", "login": "Увійти", "logout": "Вийти", "signup": "Зареєструватися", @@ -25,8 +26,8 @@ }, "homepage": { "title": "DevLovers", - "subtitle": "Пройди співбесіду, ніби ти вже Senior", - "description": "Практикуй типові питання, поглиблюй знання та проходь квізи перед співбесідами на Junior, Middle або Senior позиції.", + "subtitle": "Платформа для підготовки до технічних співбесід", + "description": "Опануйте frontend, backend та full-stack-розробку за допомогою наших питань для співбесід, квізів і навчальних матеріалів.", "cta": "Стартуємо" }, "qa": { @@ -164,6 +165,12 @@ "message": "Ваш прогрес не буде збережено.", "confirm": "Вийти", "cancel": "Продовжити" + }, + "antiCheat": { + "copy": "Копіювання заборонено під час квізу", + "paste": "Вставка заборонена під час квізу", + "contextMenu": "Контекстне меню заборонено під час квізу", + "tabSwitch": "Перехід на іншу вкладку зафіксовано" } }, "blog": { @@ -171,7 +178,16 @@ "subtitle": "Досліджуйте найсвіжіші статті про Tech, News та Career Growth із трендами, ідеями й практичними інсайтами.", "metaTitle": "Блог | DevLovers", "metaDescription": "Досліджуйте останні статті та корисні матеріали", + "all": "Усі", + "noMatches": "Нічого не знайдено", "searchPlaceholder": "Пошук...", + "categories": { + "tech": "Технології", + "career": "Кар'єра", + "insights": "Інсайти", + "news": "Новини", + "growth": "Кар'єра" + }, "removeTag": "Видалити тег", "add": "Додати", "clear": "Очистити", From 84fbe3882932cc0fb21b9c08339f77b49bba6753 Mon Sep 17 00:00:00 2001 From: tetiana zorii Date: Tue, 20 Jan 2026 23:30:03 -0500 Subject: [PATCH 2/2] fix(i18n): resolve code review issues - Fix blog category fallback using t.has() instead of typeof check - Add useCallback to useAntiCheat with proper dependencies - Ensure translations update on locale switch --- .../components/blog/BlogCategoryLinks.tsx | 13 +++-- frontend/components/blog/BlogFilters.tsx | 14 +++-- frontend/hooks/useAntiCheat.ts | 57 ++++++++++--------- frontend/messages/en.json | 12 +++- frontend/messages/pl.json | 12 +++- frontend/messages/uk.json | 12 +++- 6 files changed, 81 insertions(+), 39 deletions(-) diff --git a/frontend/components/blog/BlogCategoryLinks.tsx b/frontend/components/blog/BlogCategoryLinks.tsx index c5636c6c..a18f39de 100644 --- a/frontend/components/blog/BlogCategoryLinks.tsx +++ b/frontend/components/blog/BlogCategoryLinks.tsx @@ -28,10 +28,15 @@ export function BlogCategoryLinks({ // Helper function to get translated category label const getCategoryLabel = (categoryName: string): string => { - const key = categoryName.toLowerCase(); - const translationKey = `categories.${key}` as const; - const translated = t.raw(translationKey); - return typeof translated === 'string' ? translated : categoryName; + const key = categoryName.toLowerCase() as 'tech' | 'career' | 'insights' | 'news' | 'growth'; + const categoryTranslations: Record = { + tech: t('categories.tech'), + career: t('categories.career'), + insights: t('categories.insights'), + news: t('categories.news'), + growth: t('categories.growth'), + }; + return categoryTranslations[key] || categoryName; }; const baseLink = linkClassName || diff --git a/frontend/components/blog/BlogFilters.tsx b/frontend/components/blog/BlogFilters.tsx index 549e518d..dc1fb2a4 100644 --- a/frontend/components/blog/BlogFilters.tsx +++ b/frontend/components/blog/BlogFilters.tsx @@ -136,11 +136,15 @@ export default function BlogFilters({ // Helper function to get translated category label const getCategoryLabel = (categoryName: string): string => { - const key = categoryName.toLowerCase(); - const translationKey = `categories.${key}` as const; - // Try to get translation, fallback to original name - const translated = t.raw(translationKey); - return typeof translated === 'string' ? translated : categoryName; + const key = categoryName.toLowerCase() as 'tech' | 'career' | 'insights' | 'news' | 'growth'; + const categoryTranslations: Record = { + tech: t('categories.tech'), + career: t('categories.career'), + insights: t('categories.insights'), + news: t('categories.news'), + growth: t('categories.growth'), + }; + return categoryTranslations[key] || categoryName; }; const allCategories = useMemo(() => { diff --git a/frontend/hooks/useAntiCheat.ts b/frontend/hooks/useAntiCheat.ts index cb416c60..2922d000 100644 --- a/frontend/hooks/useAntiCheat.ts +++ b/frontend/hooks/useAntiCheat.ts @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; @@ -9,6 +9,13 @@ export type AntiCheatViolation = { timestamp: Date; }; +const messageKey: Record = { + copy: 'copy', + paste: 'paste', + 'context-menu': 'contextMenu', + 'tab-switch': 'tabSwitch', +}; + export function useAntiCheat(isActive: boolean = true) { const t = useTranslations('quiz.antiCheat'); const [violations, setViolations] = useState([]); @@ -16,35 +23,31 @@ export function useAntiCheat(isActive: boolean = true) { const [showWarning, setShowWarning] = useState(false); const warningTimeoutRef = useRef(null); - const addViolation = (type: AntiCheatViolation['type']) => { - if (!isActive) return; - - const violation: AntiCheatViolation = { - type, - timestamp: new Date(), - }; + const addViolation = useCallback( + (type: AntiCheatViolation['type']) => { + if (!isActive) return; - setViolations(prev => [...prev, violation]); - setShowWarning(true); + const violation: AntiCheatViolation = { + type, + timestamp: new Date(), + }; - const messageKey: Record = { - copy: 'copy', - paste: 'paste', - 'context-menu': 'contextMenu', - 'tab-switch': 'tabSwitch', - }; + setViolations(prev => [...prev, violation]); + setShowWarning(true); - toast.warning(t(messageKey[type]), { - duration: 3000, - }); + toast.warning(t(messageKey[type]), { + duration: 3000, + }); - if (warningTimeoutRef.current) { - clearTimeout(warningTimeoutRef.current); - } - warningTimeoutRef.current = setTimeout(() => { - setShowWarning(false); - }, 3000); - }; + if (warningTimeoutRef.current) { + clearTimeout(warningTimeoutRef.current); + } + warningTimeoutRef.current = setTimeout(() => { + setShowWarning(false); + }, 3000); + }, + [isActive, t] + ); useEffect(() => { if (!isActive) return; @@ -88,7 +91,7 @@ export function useAntiCheat(isActive: boolean = true) { clearTimeout(warningTimeoutRef.current); } }; - }, [isActive]); + }, [isActive, addViolation]); const resetViolations = () => { setViolations([]); diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 86560fa2..3a6aaee9 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -9,6 +9,16 @@ "close": "Close", "submit": "Submit" }, + "aria": { + "primaryNav": "Primary navigation", + "dashboard": "Dashboard", + "shopAdmin": "Shop admin", + "blogCategories": "Blog categories", + "searchBlog": "Search blog", + "toggleMenu": "Toggle menu", + "closeMenu": "Close menu", + "logout": "Log out" + }, "navigation": { "home": "Home", "topics": "Topics", @@ -186,7 +196,7 @@ "career": "Career", "insights": "Insights", "news": "News", - "growth": "Career" + "growth": "Growth" }, "removeTag": "Remove tag", "add": "Add", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 0f014205..7c71e627 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -9,6 +9,16 @@ "close": "Zamknij", "submit": "Wyślij" }, + "aria": { + "primaryNav": "Nawigacja główna", + "dashboard": "Panel", + "shopAdmin": "Panel administracyjny sklepu", + "blogCategories": "Kategorie bloga", + "searchBlog": "Szukaj w blogu", + "toggleMenu": "Przełącz menu", + "closeMenu": "Zamknij menu", + "logout": "Wyloguj się" + }, "navigation": { "home": "Strona główna", "topics": "Tematy", @@ -186,7 +196,7 @@ "career": "Kariera", "insights": "Spostrzeżenia", "news": "Aktualności", - "growth": "Kariera" + "growth": "Wzrost" }, "removeTag": "Usuń tag", "add": "Dodaj", diff --git a/frontend/messages/uk.json b/frontend/messages/uk.json index e5cb16be..9337b17b 100644 --- a/frontend/messages/uk.json +++ b/frontend/messages/uk.json @@ -9,6 +9,16 @@ "close": "Закрити", "submit": "Надіслати" }, + "aria": { + "primaryNav": "Головна навігація", + "dashboard": "Панель керування", + "shopAdmin": "Адміністрування магазину", + "blogCategories": "Категорії блогу", + "searchBlog": "Пошук у блозі", + "toggleMenu": "Відкрити/закрити меню", + "closeMenu": "Закрити меню", + "logout": "Вийти" + }, "navigation": { "home": "Головна", "topics": "Теми", @@ -186,7 +196,7 @@ "career": "Кар'єра", "insights": "Інсайти", "news": "Новини", - "growth": "Кар'єра" + "growth": "Зростання" }, "removeTag": "Видалити тег", "add": "Додати",