- Нет действующей подписки. Пожалуйста,{' '}
+ {content.no_active_subscription}{' '}
) : (
<>
{isPremium && isAutoRenewal === false && (
-
)}
{isPremium && isAutoRenewal === false && (
}
>
)}
{!isPremium && isPremium !== null && (
-
)}
>
)}
diff --git a/apps/client/src/components/JobCard.jsx b/apps/client/src/components/JobCard.jsx
index b2de977..9182139 100755
--- a/apps/client/src/components/JobCard.jsx
+++ b/apps/client/src/components/JobCard.jsx
@@ -1,16 +1,18 @@
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
-import { useTranslation } from "react-i18next";
+import { useIntlayer } from "react-intlayer";
import { useState } from "react";
import useFetchCities from '../hooks/useFetchCities';
+import useFetchCategories from '../hooks/useFetchCategories';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
import { ImageModal } from './ui';
const JobCard = ({ job }) => {
- const { t } = useTranslation();
+ const content = useIntlayer("jobCard");
const navigate = useNavigate();
- const { cities, loading } = useFetchCities();
+ const { cities, loading: citiesLoading } = useFetchCities();
+ const { categories, loading: categoriesLoading } = useFetchCategories();
const [showImageModal, setShowImageModal] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
@@ -21,6 +23,13 @@ const JobCard = ({ job }) => {
cityLabel = city?.label || city?.name || null;
}
+ // Получаем название категории на нужном языке
+ let categoryLabel = null;
+ if (job.categoryId && Array.isArray(categories)) {
+ const category = categories.find(c => c.value === job.categoryId || c.id === job.categoryId);
+ categoryLabel = category?.label || category?.name || null;
+ }
+
const handleImageClick = (e) => {
e.stopPropagation(); // Prevent card click when clicking image
setShowImageModal(true);
@@ -61,40 +70,46 @@ const JobCard = ({ job }) => {
{/* Плашка Премиум */}
{job.user?.isPremium && (
{job.title}
- {job.category?.label && (
+ {(job.categoryId || job.category?.label) && (
- {job.category.label}
+ {categoriesLoading ? (
+
+ ) : (
+
+ {categoryLabel || job.category?.label || content.notSpecified.value}
+
+ )}
)}
- {t("salary_per_hour_card")}{" "}
- {job.salary || "Не указано"}
+ {content.salaryPerHourCard.value}{" "}
+ {job.salary || content.notSpecified.value}
- {t("location_card")}{" "}
- {loading ? (
+ {content.locationCard.value}{" "}
+ {citiesLoading ? (
) : (
- cityLabel || "Не указано"
+ cityLabel || content.notSpecified.value
)}
-
{job.description || "Описание отсутствует"}
+
{job.description || content.descriptionMissing.value}
{typeof job.shuttle === 'boolean' && (
- {t("shuttle") || "Подвозка"}: {job.shuttle ? t("yes") || "да" : t("no") || "нет"}
+ {content.shuttle.value}: {job.shuttle ? content.yes.value : content.no.value}
)}
{typeof job.meals === 'boolean' && (
- {t("meals") || "Питание"}: {job.meals ? t("yes") || "да" : t("no") || "нет"}
+ {content.meals.value}: {job.meals ? content.yes.value : content.no.value}
)}
-
{t("phone_number_card")} {job.phone || "Не указан"}
+
{content.phoneNumberCard.value} {job.phone || content.phoneNotSpecified.value}
{/* Image displayed under phone number in mini size */}
diff --git a/apps/client/src/components/JobListing.jsx b/apps/client/src/components/JobListing.jsx
index 780fbd1..c4ec486 100755
--- a/apps/client/src/components/JobListing.jsx
+++ b/apps/client/src/components/JobListing.jsx
@@ -3,18 +3,19 @@ import useJobs from '../hooks/useJobs';
import JobList from './JobList';
import PaginationControl from './PaginationControl';
import CityDropdown from './ui/city-dropwdown';
-import { useTranslation } from "react-i18next";
+import { useIntlayer } from "react-intlayer";
import { Helmet } from 'react-helmet-async';
import JobFilterModal from './ui/JobFilterModal';
import useFilterStore from '../store/filterStore';
const JobListing = () => {
- const { t, ready } = useTranslation();
+ const content = useIntlayer("jobListing");
+ const commonContent = useIntlayer("common");
const { filters, setFilters } = useFilterStore();
- // Ждем загрузки переводов
- const defaultCity = ready ? t('choose_city_dashboard') : 'Выбрать город';
- const defaultTitle = ready ? t('latest_jobs') : 'Последние вакансии';
+ // Use Intlayer content instead of i18next
+ const defaultCity = content.chooseCityDashboard.value;
+ const defaultTitle = content.latestJobs.value;
const [currentPage, setCurrentPage] = useState(1);
const [selectedCity, setSelectedCity] = useState({ value: null, label: defaultCity });
@@ -41,13 +42,13 @@ const JobListing = () => {
// Генерация SEO-friendly заголовка
const pageTitle = selectedCity.value
- ? `${t('jobs_in', { city: selectedCity.label })} - WorkNow`
+ ? `${content.jobsIn.value.replace('{{city}}', selectedCity.label)} - WorkNow`
: `${defaultTitle} | WorkNow`;
// Генерация динамического описания страницы
const pageDescription = selectedCity.value
- ? `${t('find_jobs_in', { city: selectedCity.label })}. ${t('new_vacancies_from_employers')}.`
- : `${t('job_search_platform')} - ${t('find_latest_jobs')}.`;
+ ? `${content.findJobsIn.value.replace('{{city}}', selectedCity.label)}. ${content.newVacanciesFromEmployers.value}.`
+ : `${content.jobSearchPlatform.value} - ${content.findLatestJobs.value}.`;
// Формирование динамического URL для SEO
const pageUrl = selectedCity.value
@@ -72,7 +73,7 @@ const JobListing = () => {
{JSON.stringify({
"@context": "https://schema.org",
"@type": "JobPosting",
- "title": selectedCity.value ? `Работа в ${selectedCity.label}` : "Работа в Израиле",
+ "title": selectedCity.value ? content.jobPostingTitle.value.replace('{{city}}', selectedCity.label) : content.jobPostingTitleDefault.value,
"description": pageDescription,
"datePosted": new Date().toISOString(),
"employmentType": "Full-time",
@@ -85,7 +86,7 @@ const JobListing = () => {
"@type": "Place",
"address": {
"@type": "PostalAddress",
- "addressLocality": selectedCity.value ? selectedCity.label : "Израиль",
+ "addressLocality": selectedCity.value ? selectedCity.label : content.jobLocationDefault.value,
"addressCountry": "IL"
}
}
@@ -107,7 +108,7 @@ const JobListing = () => {
onClick={() => setFilterOpen(true)}
>
- {t('board_settings')}
+ {content.boardSettings.value}
{
- const { t, i18n } = useTranslation();
+ const content = useIntlayer("navbar");
+ const { locale } = useLocale();
+ const { changeLanguage, isLoading, clearLanguagePreference } = useLanguageManager();
const location = useLocation();
- // Используем отдельные селекторы для language и changeLanguage
- const language = useLanguageStore((state) => state.language);
- const changeLanguage = useLanguageStore((state) => state.changeLanguage);
-
const [isExpanded, setIsExpanded] = useState(false);
// Close mobile navbar when route changes
@@ -35,11 +28,18 @@ const Navbar = () => {
setIsExpanded(false);
}, [location.pathname]);
- // Обработчик смены языка
- const handleLanguageChange = (lang) => {
- changeLanguage(lang); // Обновляем Zustand хранилище
- i18n.changeLanguage(lang); // Обновляем i18n
- };
+ // Expose functions to window for testing
+ useEffect(() => {
+ window.resetLanguageDetection = clearLanguagePreference;
+ window.testLanguageLoading = () => {
+ console.log('🧪 Language Loading Test');
+ console.log('Current locale:', locale);
+ console.log('Is loading:', isLoading);
+ console.log('Available languages:', ['ru', 'en', 'he', 'ar', 'uk']);
+ console.log('Cookie value:', document.cookie);
+ console.log('LocalStorage value:', localStorage.getItem('worknow-language'));
+ };
+ }, [clearLanguagePreference, locale, isLoading]);
return (
<>
@@ -56,19 +56,19 @@ const Navbar = () => {
-
- {t("vacancies")}
+ {content.vacancies.value}
/
-
- {t("seekers")}
+ {content.seekers.value}
/
-
- {t("jobs")}
+ {content.jobs.value}
{/* Dropdown Support */}
@@ -81,7 +81,7 @@ const Navbar = () => {
data-bs-toggle="dropdown"
aria-expanded="false"
>
- {t("support")}
+ {content.support.value}
-
@@ -91,17 +91,17 @@ const Navbar = () => {
target="_blank"
rel="noopener noreferrer"
>
- {t("rules")}
+ {content.rules.value}
-
- {t("technical_support")}
+ {content.technicalSupport.value}
-
- Выставление счетов
+ {content.billing.value}
@@ -122,32 +122,52 @@ const Navbar = () => {
aria-expanded="false"
>
- {language === "en" ? "English" : language === "uk" ? "Українська" : language === "he" ? "עברית" : language === "ar" ? "العربية" : "Русский"}
+ {locale === "en" ? content.languageNames.en.value : locale === "he" ? content.languageNames.he.value : locale === "ar" ? content.languageNames.ar.value : locale === "uk" ? content.languageNames.uk.value : content.languageNames.ru.value}
-
-
-
- handleLanguageChange("ru")} className="dropdown-item">
- Русский
+ changeLanguage("ru")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.ru.value}
-
- handleLanguageChange("uk")} className="dropdown-item">
- Українська
+ changeLanguage("he")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.he.value}
-
- handleLanguageChange("he")} className="dropdown-item">
- עברית
+ changeLanguage("ar")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.ar.value}
-
- handleLanguageChange("ar")} className="dropdown-item">
- العربية
+ changeLanguage("uk")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.uk.value}
@@ -158,7 +178,7 @@ const Navbar = () => {
- {t("signin")}
+ {content.signIn.value}
@@ -192,17 +212,17 @@ const Navbar = () => {
-
- {t("vacancies")}
+ {content.vacancies.value}
-
- {t("seekers")}
+ {content.seekers.value}
-
- {t("jobs")}
+ {content.jobs.value}
{/* Dropdown Support */}
@@ -214,7 +234,7 @@ const Navbar = () => {
data-bs-toggle="dropdown"
aria-expanded="false"
>
- {t("support")}
+ {content.support.value}
-
@@ -224,12 +244,12 @@ const Navbar = () => {
target="_blank"
rel="noopener noreferrer"
>
- {t("rules")}
+ {content.rules.value}
-
- {t("technical_support")}
+ {content.technicalSupport.value}
@@ -246,23 +266,53 @@ const Navbar = () => {
aria-expanded="false"
>
- {language === "en" ? "English" : language === "uk" ? "Українська" : language === "he" ? "עברית" : language === "ar" ? "العربية" : "Русский"}
+ {locale === "en" ? content.languageNames.en.value : locale === "he" ? content.languageNames.he.value : locale === "ar" ? content.languageNames.ar.value : locale === "uk" ? content.languageNames.uk.value : content.languageNames.ru.value}
-
- handleLanguageChange("en") } className="dropdown-item">English
+ changeLanguage("en")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.en.value}
+
-
- handleLanguageChange("ru") } className="dropdown-item">Русский
+ changeLanguage("ru")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.ru.value}
+
-
- handleLanguageChange("uk") } className="dropdown-item">Українська
+ changeLanguage("he")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.he.value}
+
-
- handleLanguageChange("he") } className="dropdown-item">עברית
+ changeLanguage("ar")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.ar.value}
+
-
- handleLanguageChange("ar") } className="dropdown-item">العربية
+ changeLanguage("uk")}
+ className="dropdown-item"
+ disabled={isLoading}
+ >
+ {isLoading ? '⏳' : ''} {content.languageNames.uk.value}
+
@@ -270,7 +320,7 @@ const Navbar = () => {
- {t("signin")}
+ {content.signIn.value}
diff --git a/apps/client/src/components/PremiumPage.jsx b/apps/client/src/components/PremiumPage.jsx
index 490df5f..981b51e 100644
--- a/apps/client/src/components/PremiumPage.jsx
+++ b/apps/client/src/components/PremiumPage.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { useUser, useClerk } from "@clerk/clerk-react";
import axios from "axios";
import { useUserSync } from "../hooks/useUserSync.js";
-import { useTranslation } from "react-i18next";
+import { useIntlayer } from "react-intlayer";
import { useLoadingProgress } from '../hooks/useLoadingProgress';
import { useGoogleAnalytics } from '../hooks/useGoogleAnalytics.js';
@@ -14,7 +14,7 @@ const PremiumPage = () => {
const [loading, setLoading] = useState(false);
const { dbUser, loading: userLoading, error: userError, refreshUser } = useUserSync();
const { startLoadingWithProgress, completeLoading } = useLoadingProgress();
- const { t } = useTranslation();
+ const content = useIntlayer("premiumPage");
const { trackPremiumSubscription, trackButtonClick, trackError } = useGoogleAnalytics();
// Text carousel state
@@ -23,10 +23,10 @@ const PremiumPage = () => {
// Different variations of the pricing title
const titleVariations = [
- t('pricing_title'),
- t('pricing_effective'),
- t('pricing_convenient'),
- t('pricing_trust')
+ content.pricingTitle.value,
+ content.pricingEffective.value,
+ content.pricingConvenient.value,
+ content.pricingTrust.value
];
// Text carousel effect - change title every 3 seconds
@@ -57,12 +57,12 @@ const PremiumPage = () => {
price: 0,
period: "/mo",
features: [
- { textKey: "pricing_free_up_to_5_ads", icon: "📝", color: "text-primary" },
- { textKey: "pricing_free_daily_boost", icon: "📈", color: "text-success" },
- { textKey: "pricing_free_basic_support", icon: "💬", color: "text-info" },
+ { text: content.pricingFreeUpTo5Ads.value, icon: "📝", color: "text-primary" },
+ { text: content.pricingFreeDailyBoost.value, icon: "📈", color: "text-success" },
+ { text: content.pricingFreeBasicSupport.value, icon: "💬", color: "text-info" },
],
button: {
- textKey: "pricing_free_use_free",
+ text: content.pricingFreeUseFree.value,
variant: "outline-primary",
action: (navigate) => navigate("/create-new-advertisement")
}
@@ -72,15 +72,15 @@ const PremiumPage = () => {
price: 99,
period: "/mo",
features: [
- { textKey: "pricing_pro_up_to_10_ads", icon: "📝", color: "text-primary" },
- { textKey: "pricing_pro_unlimited_seeker_data", icon: "👥", color: "text-success" },
- { textKey: "pricing_pro_top_jobs", icon: "⭐", color: "text-warning" },
- { textKey: "pricing_pro_color_highlighting", icon: "🎨", color: "text-info" },
- { textKey: "pricing_pro_advanced_filters", icon: "🔍", color: "text-primary" },
- { textKey: "pricing_pro_priority_support", icon: "🚀", color: "text-danger" },
+ { text: content.pricingProUpTo10Ads.value, icon: "📝", color: "text-primary" },
+ { text: content.pricingProUnlimitedSeekerData.value, icon: "👥", color: "text-success" },
+ { text: content.pricingProTopJobs.value, icon: "⭐", color: "text-warning" },
+ { text: content.pricingProColorHighlighting.value, icon: "🎨", color: "text-info" },
+ { text: content.pricingProAdvancedFilters.value, icon: "🔍", color: "text-primary" },
+ { text: content.pricingProPrioritySupport.value, icon: "🚀", color: "text-danger" },
],
button: {
- textKey: "pricing_pro_buy_premium",
+ text: content.pricingProBuyPremium.value,
variant: "primary",
priceId: "price_1Qt5J0COLiDbHvw1IQNl90uU"
},
@@ -91,17 +91,17 @@ const PremiumPage = () => {
price: 200, // Base price for new users
period: "/mo",
features: [
- { textKey: "pricing_deluxe_all_from_pro", icon: "✨", color: "text-warning" },
- { textKey: "pricing_deluxe_personal_manager", icon: "👨💼", color: "text-primary" },
- { textKey: "pricing_deluxe_facebook_autoposting", icon: "⚡", color: "text-info" },
- { textKey: "pricing_deluxe_facebook_promotion", icon: "📢", color: "text-success" },
- { textKey: "pricing_deluxe_personal_mailing", icon: "📧", color: "text-primary" },
- { textKey: "pricing_deluxe_telegram_publications", icon: "🔥", color: "text-info" },
- { textKey: "pricing_deluxe_personal_candidate_selection", icon: "🎯", color: "text-warning" },
- { textKey: "pricing_deluxe_interview_organization", icon: "🤝", color: "text-success" },
+ { text: content.pricingDeluxeAllFromPro.value, icon: "✨", color: "text-warning" },
+ { text: content.pricingDeluxePersonalManager.value, icon: "👨💼", color: "text-primary" },
+ { text: content.pricingDeluxeFacebookAutoposting.value, icon: "⚡", color: "text-info" },
+ { text: content.pricingDeluxeFacebookPromotion.value, icon: "📢", color: "text-success" },
+ { text: content.pricingDeluxePersonalMailing.value, icon: "📧", color: "text-primary" },
+ { text: content.pricingDeluxeTelegramPublications.value, icon: "🔥", color: "text-info" },
+ { text: content.pricingDeluxePersonalCandidateSelection.value, icon: "🎯", color: "text-warning" },
+ { text: content.pricingDeluxeInterviewOrganization.value, icon: "🤝", color: "text-success" },
],
button: {
- textKey: "pricing_deluxe_buy_deluxe",
+ text: content.pricingDeluxeBuyDeluxe.value,
variant: "primary",
priceId: "price_1RfHjiCOLiDbHvw1repgIbnK" // Price ID for 200 ILS
},
@@ -175,11 +175,11 @@ const PremiumPage = () => {
});
if (error.response?.status === 404) {
- alert("Пользователь не найден. Попробуйте войти заново.");
+ alert(content.userNotFound.value);
} else if (error.response?.data?.error) {
alert(`Ошибка оплаты: ${error.response.data.error}`);
} else {
- alert("Ошибка оплаты. Попробуйте позже.");
+ alert(content.paymentError.value);
}
} finally {
setLoading(false);
@@ -215,7 +215,7 @@ const PremiumPage = () => {
margin: '0 auto',
padding: '0 20px'
}}>
- {t('pricing_description')}
+ {content.pricingDescription.value}