diff --git a/src/components/popular-categories/PopularCategories.tsx b/src/components/popular-categories/PopularCategories.tsx index 6f01e5f0e..1bc69a42a 100644 --- a/src/components/popular-categories/PopularCategories.tsx +++ b/src/components/popular-categories/PopularCategories.tsx @@ -23,6 +23,7 @@ import { } from '~/utils/helper-functions' import { CategoryInterface, ItemsWithCount, SortEnum } from '~/types' import { styles } from '~/components/popular-categories/PopularCategories.styles' +import { titleToCamel } from '~/utils/title-to-camel-case' interface PopularCategoriesProps { title: string @@ -68,7 +69,9 @@ const PopularCategories: FC = ({ iconColor={item.appearance.color} key={item._id} link={`${authRoutes.subjects.path}?categoryId=${item._id}`} - title={item.name} + title={t(`categories.${titleToCamel(item.name)}`, { + defaultValue: item.name + })} /> )), [response.items, oppositeRole, t] diff --git a/src/constants/translations/en/categories.json b/src/constants/translations/en/categories.json new file mode 100644 index 000000000..418159793 --- /dev/null +++ b/src/constants/translations/en/categories.json @@ -0,0 +1,19 @@ +{ + "computerScience": "Computer science", + "music": "Music", + "psychology": "Psychology", + "finances": "Finances", + "audit": "Audit", + "astronomy": "Astronomy", + "biology": "Biology", + "cooking": "Cooking", + "languages": "Languages", + "painting": "Painting", + "design": "Design", + "marketing": "Marketing", + "it": "IT", + "mathematics": "Mathematics", + "chemistry": "Chemistry", + "physics": "Physics", + "history": "History" +} \ No newline at end of file diff --git a/src/constants/translations/en/index.ts b/src/constants/translations/en/index.ts index 8aa95dbb3..d892ddc1d 100644 --- a/src/constants/translations/en/index.ts +++ b/src/constants/translations/en/index.ts @@ -45,6 +45,8 @@ import editProfilePage from './edit-profile.json' import quiz from './quiz.json' import bookmarkedOffers from './bookmarked-offers-page.json' import activeStudents from './active-students.json' +import categories from './categories.json' +import subjects from './subjects.json' const en = { translations: { @@ -94,7 +96,9 @@ const en = { editProfilePage, quiz, bookmarkedOffers, - activeStudents + activeStudents, + categories, + subjects } } diff --git a/src/constants/translations/en/subjects.json b/src/constants/translations/en/subjects.json new file mode 100644 index 000000000..1d47d6367 --- /dev/null +++ b/src/constants/translations/en/subjects.json @@ -0,0 +1,155 @@ +{ + "abstractPainting": "Abstract Painting", + "accounting": "Accounting", + "acrylicPainting": "Acrylic Painting", + "advertising": "Advertising", + "africanHistory": "African History", + "algebra": "Algebra", + "americanHistory": "American History", + "analyticalChemistry": "Analytical Chemistry", + "anatomy": "Anatomy", + "ancientHistory": "Ancient History", + "arabic": "Arabic", + "artificialIntelligence": "Artificial Intelligence", + "asianHistory": "Asian History", + "astrophysics": "Astrophysics", + "biochemistry": "Biochemistry", + "botany": "Botany", + "brandManagement": "Brand Management", + "calculus": "Calculus", + "cellBiology": "Cell Biology", + "chinese": "Chinese", + "classicalMechanics": "Classical Mechanics", + "clinicalPsychology": "Clinical Psychology", + "cognitivePsychology": "Cognitive Psychology", + "complianceAudit": "Compliance Audit", + "computerNetworks": "Computer Networks", + "consumerBehavior": "Consumer Behavior", + "contentMarketing": "Content Marketing", + "corporateFinance": "Corporate Finance", + "cosmology": "Cosmology", + "culturalHistory": "Cultural History", + "cybersecurity": "Cybersecurity", + "dataStructuresAndAlgorithms": "Data Structures and Algorithms", + "databaseManagementSystems": "Database Management Systems", + "designThinking": "Design Thinking", + "developmentalPsychology": "Developmental Psychology", + "differentialEquations": "Differential Equations", + "digitalMarketing": "Digital Marketing", + "discreteMathematics": "Discrete Mathematics", + "ecology": "Ecology", + "economics": "Economics", + "educationalPsychology": "Educational Psychology", + "electromagnetism": "Electromagnetism", + "english": "English", + "environmentalAudit": "Environmental Audit", + "environmentalChemistry": "Environmental Chemistry", + "ethnomusicology": "Ethnomusicology", + "europeanHistory": "European History", + "evolutionaryBiology": "Evolutionary Biology", + "experimentalPsychology": "Experimental Psychology", + "externalAudit": "External Audit", + "fashionDesign": "Fashion Design", + "financialAnalysis": "Financial Analysis", + "financialAudit": "Financial Audit", + "financialMarkets": "Financial Markets", + "financialPlanning": "Financial Planning", + "fluidDynamics": "Fluid Dynamics", + "forensicAudit": "Forensic Audit", + "forensicPsychology": "Forensic Psychology", + "french": "French", + "galacticAstronomy": "Galactic Astronomy", + "gameDevelopment": "Game Development", + "gastronomy": "Gastronomy", + "genetics": "Genetics", + "geometry": "Geometry", + "german": "German", + "graphicDesign": "Graphic Design", + "gravitationalAstronomy": "Gravitational Astronomy", + "guitar": "Guitar", + "healthPsychology": "Health Psychology", + "highEnergyAstrophysics": "High-energy Astrophysics", + "itAudit": "IT Audit", + "industrialDesign": "Industrial Design", + "industrialOrganizationalPsychology": "Industrial-Organizational Psychology", + "inorganicChemistry": "Inorganic Chemistry", + "interiorDesign": "Interior Design", + "internalAudit": "Internal Audit", + "internationalFinance": "International Finance", + "introductionToProgramming": "Introduction to Programming", + "investments": "Investments", + "italian": "Italian", + "japanese": "Japanese", + "jazzStudies": "Jazz Studies", + "landscapePainting": "Landscape Painting", + "linearAlgebra": "Linear Algebra", + "machineLearning": "Machine Learning", + "marketResearch": "Market Research", + "marketingAnalytics": "Marketing Analytics", + "mathematicalLogic": "Mathematical Logic", + "medicinalChemistry": "Medicinal Chemistry", + "medievalHistory": "Medieval History", + "microbiology": "Microbiology", + "militaryHistory": "Military History", + "mixedMediaPainting": "Mixed Media Painting", + "modernHistory": "Modern History", + "motionDesign": "Motion Design", + "musicComposition": "Music Composition", + "musicHistory": "Music History", + "musicProduction": "Music Production", + "musicTheory": "Music Theory", + "neuropsychology": "Neuropsychology", + "nuclearChemistry": "Nuclear Chemistry", + "nuclearPhysics": "Nuclear Physics", + "numberTheory": "Number Theory", + "observationalAstronomy": "Observational Astronomy", + "oilPainting": "Oil Painting", + "operatingSystems": "Operating Systems", + "operationalAudit": "Operational Audit", + "optics": "Optics", + "organicChemistry": "Organic Chemistry", + "particlePhysics": "Particle Physics", + "pastelPainting": "Pastel Painting", + "personalFinance": "Personal Finance", + "physicalChemistry": "Physical Chemistry", + "physiology": "Physiology", + "piano": "Piano", + "planetaryScience": "Planetary Science", + "polymerChemistry": "Polymer Chemistry", + "portraitPainting": "Portrait Painting", + "portuguese": "Portuguese", + "productDesign": "Product Design", + "publicRelations": "Public Relations", + "qualityAudit": "Quality Audit", + "quantumMechanics": "Quantum Mechanics", + "radioAstronomy": "Radio Astronomy", + "realisticPainting": "Realistic Painting", + "riskManagement": "Risk Management", + "salesManagement": "Sales Management", + "socialMediaMarketing": "Social Media Marketing", + "socialPsychology": "Social Psychology", + "softwareEngineering": "Software Engineering", + "solarAstronomy": "Solar Astronomy", + "solidStatePhysics": "Solid State Physics", + "spanish": "Spanish", + "statistics": "Statistics", + "stellarAstronomy": "Stellar Astronomy", + "stillLifePainting": "Still Life Painting", + "taxAudit": "Tax Audit", + "testing": "Testing", + "theoreticalChemistry": "Theoretical Chemistry", + "thermodynamics": "Thermodynamics", + "trigonometry": "Trigonometry", + "typography": "Typography", + "uiUxDesign": "UI/UX Design", + "ukrainian": "Ukrainian", + "userExperienceUxDesign": "User Experience (UX) Design", + "violin": "Violin", + "voiceTraining": "Voice Training", + "watercolorPainting": "Watercolor Painting", + "webDesign": "Web Design", + "webDevelopment": "Web Development", + "worldHistory": "World History", + "zoology": "Zoology" + } + \ No newline at end of file diff --git a/src/constants/translations/uk/categories.json b/src/constants/translations/uk/categories.json new file mode 100644 index 000000000..3306c17dd --- /dev/null +++ b/src/constants/translations/uk/categories.json @@ -0,0 +1,19 @@ +{ + "computerScience": "Комп'ютерні науки", + "music": "Музика", + "psychology": "Психологія", + "finances": "Фінанси", + "audit": "Аудит", + "astronomy": "Астрономія", + "biology": "Біологія", + "cooking": "Готування", + "languages": "Мови", + "painting": "Малювання", + "design": "Дизайн", + "marketing": "Маркетинг", + "it": "Програмування", + "mathematics": "Математика", + "chemistry": "Хімія", + "physics": "Фізика", + "history": "Історія" +} \ No newline at end of file diff --git a/src/constants/translations/uk/index.ts b/src/constants/translations/uk/index.ts index e16fde121..8e69dd88b 100644 --- a/src/constants/translations/uk/index.ts +++ b/src/constants/translations/uk/index.ts @@ -45,6 +45,8 @@ import userProfilePage from './user-profile-page.json' import quiz from './quiz.json' import bookmarkedOffers from './bookmarked-offers-page.json' import activeStudents from './active-students.json' +import categories from './categories.json' +import subjects from './subjects.json' const uk = { translations: { @@ -94,7 +96,9 @@ const uk = { userProfilePage, quiz, bookmarkedOffers, - activeStudents + activeStudents, + categories, + subjects } } diff --git a/src/constants/translations/uk/subjects.json b/src/constants/translations/uk/subjects.json new file mode 100644 index 000000000..f7c423185 --- /dev/null +++ b/src/constants/translations/uk/subjects.json @@ -0,0 +1,155 @@ +{ + "abstractPainting": "Абстрактний живопис", + "accounting": "Бухгалтерський облік", + "acrylicPainting": "Акриловий живопис", + "advertising": "Реклама", + "africanHistory": "Історія Африки", + "algebra": "Алгебра", + "americanHistory": "Історія Америки", + "analyticalChemistry": "Аналітична хімія", + "anatomy": "Анатомія", + "ancientHistory": "Стародавня історія", + "arabic": "Арабська мова", + "artificialIntelligence": "Штучний інтелект", + "asianHistory": "Історія Азії", + "astrophysics": "Астрофізика", + "biochemistry": "Біохімія", + "botany": "Ботаніка", + "brandManagement": "Управління брендом", + "calculus": "Математичний аналіз", + "cellBiology": "Клітинна біологія", + "chinese": "Китайська мова", + "classicalMechanics": "Класична механіка", + "clinicalPsychology": "Клінічна психологія", + "cognitivePsychology": "Когнітивна психологія", + "complianceAudit": "Аудит відповідності", + "computerNetworks": "Комп'ютерні мережі", + "consumerBehavior": "Поведінка споживачів", + "contentMarketing": "Контент-маркетинг", + "corporateFinance": "Корпоративні фінанси", + "cosmology": "Космологія", + "culturalHistory": "Культурна історія", + "cybersecurity": "Кібербезпека", + "dataStructuresAndAlgorithms": "Структури даних і алгоритми", + "databaseManagementSystems": "Системи управління базами даних", + "designThinking": "Дизайн-мислення", + "developmentalPsychology": "Психологія розвитку", + "differentialEquations": "Диференціальні рівняння", + "digitalMarketing": "Цифровий маркетинг", + "discreteMathematics": "Дискретна математика", + "ecology": "Екологія", + "economics": "Економіка", + "educationalPsychology": "Педагогічна психологія", + "electromagnetism": "Електромагнетизм", + "english": "Англійська мова", + "environmentalAudit": "Екологічний аудит", + "environmentalChemistry": "Екологічна хімія", + "ethnomusicology": "Етномузикологія", + "europeanHistory": "Історія Європи", + "evolutionaryBiology": "Еволюційна біологія", + "experimentalPsychology": "Експериментальна психологія", + "externalAudit": "Зовнішній аудит", + "fashionDesign": "Дизайн одягу", + "financialAnalysis": "Фінансовий аналіз", + "financialAudit": "Фінансовий аудит", + "financialMarkets": "Фінансові ринки", + "financialPlanning": "Фінансове планування", + "fluidDynamics": "Динаміка рідин", + "forensicAudit": "Судово-бухгалтерський аудит", + "forensicPsychology": "Судова психологія", + "french": "Французька мова", + "galacticAstronomy": "Галактична астрономія", + "gameDevelopment": "Розробка ігор", + "gastronomy": "Гастрономія", + "genetics": "Генетика", + "geometry": "Геометрія", + "german": "Німецька мова", + "graphicDesign": "Графічний дизайн", + "gravitationalAstronomy": "Гравітаційна астрономія", + "guitar": "Гітара", + "healthPsychology": "Психологія здоров'я", + "highEnergyAstrophysics": "Астрофізика високих енергій", + "itAudit": "IT-аудит", + "industrialDesign": "Промисловий дизайн", + "industrialOrganizationalPsychology": "Індустріально-організаційна психологія", + "inorganicChemistry": "Неорганічна хімія", + "interiorDesign": "Дизайн інтер'єру", + "internalAudit": "Внутрішній аудит", + "internationalFinance": "Міжнародні фінанси", + "introductionToProgramming": "Вступ до програмування", + "investments": "Інвестиції", + "italian": "Італійська мова", + "japanese": "Японська мова", + "jazzStudies": "Джазові студії", + "landscapePainting": "Пейзажний живопис", + "linearAlgebra": "Лінійна алгебра", + "machineLearning": "Машинне навчання", + "marketResearch": "Маркетингові дослідження", + "marketingAnalytics": "Маркетингова аналітика", + "mathematicalLogic": "Математична логіка", + "medicinalChemistry": "Медична хімія", + "medievalHistory": "Середньовічна історія", + "microbiology": "Мікробіологія", + "militaryHistory": "Військова історія", + "mixedMediaPainting": "Мішана техніка живопису", + "modernHistory": "Нова історія", + "motionDesign": "Дизайн анімації", + "musicComposition": "Музична композиція", + "musicHistory": "Історія музики", + "musicProduction": "Музичне продюсування", + "musicTheory": "Теорія музики", + "neuropsychology": "Нейропсихологія", + "nuclearChemistry": "Ядерна хімія", + "nuclearPhysics": "Ядерна фізика", + "numberTheory": "Теорія чисел", + "observationalAstronomy": "Спостережна астрономія", + "oilPainting": "Олійний живопис", + "operatingSystems": "Операційні системи", + "operationalAudit": "Операційний аудит", + "optics": "Оптика", + "organicChemistry": "Органічна хімія", + "particlePhysics": "Фізика частинок", + "pastelPainting": "Пастельний живопис", + "personalFinance": "Особисті фінанси", + "physicalChemistry": "Фізична хімія", + "physiology": "Фізіологія", + "piano": "Фортепіано", + "planetaryScience": "Планетологія", + "polymerChemistry": "Хімія полімерів", + "portraitPainting": "Портретний живопис", + "portuguese": "Португальська мова", + "productDesign": "Дизайн продукту", + "publicRelations": "Зв'язки з громадськістю", + "qualityAudit": "Аудит якості", + "quantumMechanics": "Квантова механіка", + "radioAstronomy": "Радіоастрономія", + "realisticPainting": "Реалістичний живопис", + "riskManagement": "Управління ризиками", + "salesManagement": "Управління продажами", + "socialMediaMarketing": "Маркетинг у соціальних мережах", + "socialPsychology": "Соціальна психологія", + "softwareEngineering": "Програмна інженерія", + "solarAstronomy": "Сонячна астрономія", + "solidStatePhysics": "Фізика твердого тіла", + "spanish": "Іспанська мова", + "statistics": "Статистика", + "stellarAstronomy": "Зоряна астрономія", + "stillLifePainting": "Натюрмортний живопис", + "taxAudit": "Податковий аудит", + "testing": "Тестування", + "theoreticalChemistry": "Теоретична хімія", + "thermodynamics": "Термодинаміка", + "trigonometry": "Тригонометрія", + "typography": "Типографіка", + "uiUxDesign": "Дизайн UI/UX", + "ukrainian": "Українська мова", + "userExperienceUxDesign": "Дизайн користувацького досвіду (UX)", + "violin": "Скрипка", + "voiceTraining": "Вокальні тренування", + "watercolorPainting": "Акварельний живопис", + "webDesign": "Вебдизайн", + "webDevelopment": "Веброзробка", + "worldHistory": "Всесвітня історія", + "zoology": "Зоологія" +} + \ No newline at end of file diff --git a/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx b/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx index 023b19627..06f7ab206 100644 --- a/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx +++ b/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx @@ -33,6 +33,8 @@ import { } from '~/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.constants' import { styles } from '~/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.styles' +import { fetchAndTranslateData } from '~/utils/fetch-and-translate-category' +import { titleToCamel } from '~/utils/title-to-camel-case' interface SubjectGroupProps { subject: Partial @@ -51,10 +53,13 @@ function SubjectGroup({ }: Readonly) { const { t } = useTranslation() - const getSubjectsNames = useCallback( - () => subjectService.getSubjectsNames(selectedCategory), - [selectedCategory] - ) + const getSubjectsNames = useCallback(() => { + return fetchAndTranslateData( + () => subjectService.getSubjectsNames(selectedCategory), + 'subjects', + t + ) + }, [selectedCategory, t]) const handleDisableOptions = (option: Partial) => { return disableOptions.some((subject) => subject._id === option._id) @@ -75,7 +80,7 @@ function SubjectGroup({ disabled={!selectedCategory} fullWidth getOptionDisabled={handleDisableOptions} - labelField='name' + labelField='displayName' onChange={(_, value) => handleChange(value!)} service={getSubjectsNames} textFieldProps={{ @@ -112,36 +117,38 @@ const AddProfessionalCategoryModal: FC = ({ const formSubmission = () => { const userRoleCategory = userRole as MainUserRole - const { category } = data - - // TODO: icon should be displayed accordingly to category + const { category, subjects } = data if (category.appearance === undefined) { category.appearance = { color: '#E3B21C', icon: 'ScienceRoundedIcon' } } - + const sanitizedCategory = { + ...category, + name: t(`categories.${titleToCamel(category.name)}`, { + lng: 'en', + defaultValue: category.name + }) + } + const sanitizedSubjects = subjects.map((subject) => ({ + ...subject, + name: t(`subjects.${titleToCamel(subject.name)}`, { + lng: 'en', + defaultValue: subject.name + }) + })) + const categoryData: UserMainSubject = { + ...data, + category: sanitizedCategory, + subjects: sanitizedSubjects, + _id: isEdit ? (initialValuesFromProps?._id ?? '') : crypto.randomUUID(), + isDeletionBlocked + } if (isEdit) { - const categoryToUpdate: UserMainSubject = { - _id: initialValuesFromProps?._id ?? '', - isDeletionBlocked, - ...data - } dispatch( - updateCategory({ - category: categoryToUpdate, - userRole: userRoleCategory - }) + updateCategory({ category: categoryData, userRole: userRoleCategory }) ) } else { - const categoryToAdd: UserMainSubject = { - _id: uuidv4(), - isDeletionBlocked, - ...data - } dispatch( - addCategory({ - category: categoryToAdd, - userRole: userRoleCategory - }) + addCategory({ category: categoryData, userRole: userRoleCategory }) ) } closeModal() @@ -208,7 +215,13 @@ const AddProfessionalCategoryModal: FC = ({ ) return isBlocked && isCurrent } - + const fetchTranslatedCategories = useCallback(() => { + return fetchAndTranslateData( + () => categoryService.getCategoriesNames(), + 'categories', + t + ) + }, [t]) const SubjectsGroup = data.subjects.map((subject, index) => ( >} @@ -239,9 +252,9 @@ const AddProfessionalCategoryModal: FC = ({ disabled={isDeletionBlocked} fullWidth getOptionDisabled={handleBlockOption} - labelField='name' + labelField='displayName' onChange={handleMainStudyCategoryChange} - service={categoryService.getCategoriesNames} + service={fetchTranslatedCategories} textFieldProps={{ label: `${t( 'editProfilePage.profile.professionalTab.mainStudyCategory' diff --git a/src/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.tsx b/src/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.tsx index 62f418420..e74532783 100644 --- a/src/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.tsx +++ b/src/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.tsx @@ -19,6 +19,7 @@ import { import useConfirm from '~/hooks/use-confirm' import { styles } from '~/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.styles' import { getValidatedHexColor } from '~/utils/get-validated-hex-color' +import { titleToCamel } from '~/utils/title-to-camel-case' interface ProfessionalCategoryProps { item: UserMainSubject @@ -86,7 +87,9 @@ const ProfessionalCategory: FC = ({ labelSx={styles.subjectChipLabel(categoryColor)} sx={styles.subjectChip(categoryColor)} > - {subject.name} + {t(`subjects.${titleToCamel(subject.name)}`, { + defaultValue: subject.name + })} )) @@ -130,7 +133,9 @@ const ProfessionalCategory: FC = ({ label={t('editProfilePage.profile.professionalTab.mainStudyCategory')} > - {item.category.name} + {t(`categories.${titleToCamel(item.category.name)}`, { + defaultValue: item.category.name + })} { const { t, i18n } = useTranslation() @@ -82,7 +83,9 @@ const Categories = () => { iconColor={item.appearance.color} key={item._id} link={`${authRoutes.subjects.path}?categoryId=${item._id}`} - title={item.name} + title={t(`categories.${titleToCamel(item.name)}`, { + defaultValue: item.name + })} /> ) }), diff --git a/src/pages/subjects/Subjects.tsx b/src/pages/subjects/Subjects.tsx index 879f214b9..e95c73ca3 100644 --- a/src/pages/subjects/Subjects.tsx +++ b/src/pages/subjects/Subjects.tsx @@ -28,7 +28,7 @@ import useBreakpoints from '~/hooks/use-breakpoints' import { getOpositeRole, getScreenBasedLimit } from '~/utils/helper-functions' import { mapArrayByField } from '~/utils/map-array-by-field' import { getSuffixes } from '~/utils/get-translation-suffixes' - +import { fetchAndTranslateData } from '~/utils/fetch-and-translate-category' import { CategoryNameInterface, SizeEnum, @@ -38,6 +38,7 @@ import { import { itemsLoadLimit } from '~/constants' import { authRoutes } from '~/router/constants/authRoutes' import { styles } from '~/pages/subjects/Subjects.styles' +import { titleToCamel } from '~/utils/title-to-camel-case' const Subjects = () => { const [match, setMatch] = useState('') @@ -108,7 +109,9 @@ const Subjects = () => { iconColor={item.category.appearance.color} key={item._id} link={`${authRoutes.findOffers.path}?categoryId=${categoryId}&subjectId=${item._id}`} - title={item.name} + title={t(`subjects.${titleToCamel(item.name)}`, { + defaultValue: item.name + })} /> ) }), @@ -130,13 +133,19 @@ const Subjects = () => { const category = response.find((option) => option._id === categoryId) setCategoryName(category?.name ?? '') } - + const fetchTranslatedCategories = useCallback(() => { + return fetchAndTranslateData( + () => categoryService.getCategoriesNames(), + 'categories', + t + ) + }, [t]) const autoCompleteCategories = ( { style={styles.titleWithDescription} title={t('subjectsPage.subjects.title', { category: categoryName + ? t(`categories.${titleToCamel(categoryName)}`, { + defaultValue: categoryName + }) + : categoryName })} /> diff --git a/src/types/common/interfaces/common.interfaces.ts b/src/types/common/interfaces/common.interfaces.ts index 447812d18..b20fc6fc3 100644 --- a/src/types/common/interfaces/common.interfaces.ts +++ b/src/types/common/interfaces/common.interfaces.ts @@ -45,6 +45,7 @@ export interface CategoryInterface { export interface CategoryNameInterface { _id: string name: string + displayName?: string } export interface SubjectInterface { @@ -54,6 +55,7 @@ export interface SubjectInterface { totalOffers: DataByRole createdAt: string updatedAt: string + displayName?: string } export interface SubjectNameInterface { diff --git a/src/utils/fetch-and-translate-category.tsx b/src/utils/fetch-and-translate-category.tsx new file mode 100644 index 000000000..31663fd9c --- /dev/null +++ b/src/utils/fetch-and-translate-category.tsx @@ -0,0 +1,28 @@ +import { AxiosHeaders, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import { titleToCamel } from './title-to-camel-case' + +export async function fetchAndTranslateData( + serviceMethod: () => Promise>, + translationKey: string, + t: (key: string, options?: { defaultValue: string }) => string +): Promise>> { + try { + const response = await serviceMethod() + const translatedData = response.data.map((item) => ({ + ...item, + displayName: t(`${translationKey}.${titleToCamel(item.name)}`, { + defaultValue: item.name + }) + })) + return { ...response, data: translatedData } + } catch (error: unknown) { + console.error(`Error fetching ${translationKey}:`, error) + return { + data: [], + status: 500, + statusText: 'Error', + headers: new AxiosHeaders(), + config: {} as InternalAxiosRequestConfig + } as AxiosResponse> + } +} diff --git a/src/utils/title-to-camel-case.ts b/src/utils/title-to-camel-case.ts new file mode 100644 index 000000000..c9ac94976 --- /dev/null +++ b/src/utils/title-to-camel-case.ts @@ -0,0 +1,10 @@ +export function titleToCamel(title: string): string { + return title + .replace(/[^a-zA-Z0-9\s]/g, '') + .toLowerCase() + .split(' ') + .map((word, index) => + index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1) + ) + .join('') +} diff --git a/tests/unit/components/popular-categories/PopularCategories.spec.jsx b/tests/unit/components/popular-categories/PopularCategories.spec.jsx index cd5293720..e4c92719c 100644 --- a/tests/unit/components/popular-categories/PopularCategories.spec.jsx +++ b/tests/unit/components/popular-categories/PopularCategories.spec.jsx @@ -2,7 +2,15 @@ import { screen, waitFor } from '@testing-library/react' import PopularCategories from '~/components/popular-categories/PopularCategories' import { URLs } from '~/constants/request' import { renderWithProviders, mockAxiosClient } from '~tests/test-utils' +import { useTranslation } from 'react-i18next' +import {titleToCamel} from '~/utils/title-to-camel-case' +const { t } = useTranslation() +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key, options) => options?.defaultValue || key.split('.').pop(), + }), +})); const items = [ { _id: '1', @@ -47,7 +55,7 @@ describe('PopularCategories', () => { }) it('render card correctly', async () => { - const card = await screen.findByText('Math') + const card = await screen.findByText(t(`categories.${titleToCamel('Math')}`, { defaultValue: 'Math' })) expect(card).toBeInTheDocument() }) diff --git a/tests/unit/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.spec.jsx b/tests/unit/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.spec.jsx index 04ce43b70..854265076 100644 --- a/tests/unit/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.spec.jsx +++ b/tests/unit/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.spec.jsx @@ -5,6 +5,9 @@ import { professionalSubjectTemplate } from '~/containers/edit-profile/professio import { mockAxiosClient } from '~tests/test-utils' import { URLs } from '~/constants/request' import { vi } from 'vitest' +import { useTranslation } from 'react-i18next' +import {titleToCamel} from '~/utils/title-to-camel-case' +const { t } = useTranslation() const mockCloseModal = vi.fn() const mockedBlockedCategory = [{ _id: '4', name: 'Music' }] @@ -12,6 +15,12 @@ const mockedBlockedCategory = [{ _id: '4', name: 'Music' }] const mockDispatch = vi.fn() const mockSelector = vi.fn() +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key, options) => options?.defaultValue || key, + }), +})); + vi.mock('react-redux', async () => { const actual = await vi.importActual('react-redux') return { @@ -65,8 +74,8 @@ describe('AddProfessionalCategoryModal without initial value', () => { const professionalSubjects = screen.getAllByLabelText( /editProfilePage.profile.professionalTab.subject/ ) - - await selectOption(categoryAutocomplete, 'Cooking') + + await selectOption(categoryAutocomplete, t(`categories.${titleToCamel('Cooking')}`, { defaultValue: 'Cooking' })); await act(async () => { fireEvent.change(professionalSubjects[0], { @@ -74,7 +83,10 @@ describe('AddProfessionalCategoryModal without initial value', () => { }) }) - expect(professionalSubjects[0].value).toBe('Updated Gastronomy') + expect(professionalSubjects[0].value).toBe('Updated Gastronomy'); + if (professionalSubjects.length > 1) { + expect(professionalSubjects[1].value).not.toBe('Updated Gastronomy'); + } }) it('should render SubjectGroup using template in (modal create mode)', async () => { @@ -107,8 +119,7 @@ describe('AddProfessionalCategoryModal without initial value', () => { const professionalSubjects = screen.getByLabelText( /editProfilePage.profile.professionalTab.subject/ ) - - await selectOption(categoryAutocomplete, 'Cooking') + await selectOption(categoryAutocomplete, t(`categories.${titleToCamel('Cooking')}`, { defaultValue: 'Cooking' })) await act(async () => { fireEvent.change(professionalSubjects, { @@ -129,7 +140,7 @@ describe('AddProfessionalCategoryModal without initial value', () => { const professionalSubjects = screen.getAllByLabelText( /editProfilePage.profile.professionalTab.subject/ ) - await selectOption(categoryAutocomplete, 'Cooking') + await selectOption(categoryAutocomplete, t(`categories.${titleToCamel('Cooking')}`, { defaultValue: 'Cooking' })) await act(async () => { fireEvent.change(professionalSubjects[0], { @@ -154,7 +165,7 @@ describe('AddProfessionalCategoryModal without initial value', () => { /editProfilePage.profile.professionalTab.mainStudyCategory/ ) - await selectOption(categoryAutocomplete, 'Cooking') + await selectOption(categoryAutocomplete, t(`categories.${titleToCamel('Cooking')}`, { defaultValue: 'Cooking' })) expect(submitButton).toBeDisabled() }) @@ -247,13 +258,13 @@ describe('AddProfessionalCategoryModal Subject Updates', () => { /editProfilePage.profile.professionalTab.subject/ ) - await selectOption(categoryAutocomplete, 'Cooking') + await selectOption(categoryAutocomplete,t(`categories.${titleToCamel('Cooking')}`, { defaultValue: 'Cooking' })); await act(async () => { fireEvent.change(professionalSubjects[0], { target: { value: 'Gastronomy' } }) }) - expect(screen.getByDisplayValue('Gastronomy')).toBeInTheDocument() + expect(screen.getByDisplayValue('Gastronomy')).toBeInTheDocument(); expect(professionalSubjects[1].value).toBe('Varenychky') }) @@ -284,10 +295,13 @@ describe('AddProfessionalCategoryModal Subject Updates', () => { const professionalSubjects = screen.getAllByLabelText( /editProfilePage.profile.professionalTab.subject/ ) - initialValues.subjects.forEach((subject, index) => { - const subjectElement = professionalSubjects[index] - expect(subjectElement.value).toMatch(new RegExp(subject.name, 'i')) + const translatedSubjectName = t(`subjects.${titleToCamel(subject.name)}`, { + defaultValue: subject.name + }) + + const subjectElement = professionalSubjects[index]; + expect(subjectElement.value).toMatch(new RegExp(translatedSubjectName, 'i')) }) }) }) diff --git a/tests/unit/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.spec.jsx b/tests/unit/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.spec.jsx index ee7771fb5..bc466b787 100644 --- a/tests/unit/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.spec.jsx +++ b/tests/unit/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory.spec.jsx @@ -1,10 +1,18 @@ import ProfessionalCategory from '~/containers/edit-profile/professional-info-tab/professional-category/ProfessionalCategory' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '~tests/test-utils' - +import { useTranslation } from 'react-i18next' +import {titleToCamel} from '~/utils/title-to-camel-case' +const { t } = useTranslation() const mockOpenProfessionalCategoryModal = vi.fn() const mockedHandleDelete = vi.fn() +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key, options) => options?.defaultValue || key, + }), +})) + const categoryWithSubjects = { _id: '648850c4fdc2d1a130c24aea', category: { @@ -77,15 +85,17 @@ describe('ProfessionalCategory', () => { it('should render subjects correctly', () => { renderProfessionalCategoryWithItem(categoryWithSubjects) - - const firstSubjectName = categoryWithSubjects.subjects[0].name + + const firstSubjectName = t(`subjects.${titleToCamel(categoryWithSubjects.subjects[0].name)}`, { + defaultValue: categoryWithSubjects.subjects[0].name, + }) const firstSubjectNameElement = screen.getByText(firstSubjectName) expect(firstSubjectNameElement).toBeInTheDocument() - + const subjectLabels = screen.getAllByText( /editProfilePage.profile.professionalTab.subject/ ) - + expect(subjectLabels).toHaveLength(categoryWithSubjects.subjects.length) }) diff --git a/tests/unit/pages/categories/Categories.spec.jsx b/tests/unit/pages/categories/Categories.spec.jsx index a2d867d40..c423ac559 100644 --- a/tests/unit/pages/categories/Categories.spec.jsx +++ b/tests/unit/pages/categories/Categories.spec.jsx @@ -4,6 +4,9 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '~tests/test-utils' import Categories from '~/pages/categories/Categories' import useLoadMore from '~/hooks/use-load-more' +import { useTranslation } from 'react-i18next' +import {titleToCamel} from '~/utils/title-to-camel-case' +const {t} = useTranslation(); const resetDataMock = vi.fn() const loadMoreMock = vi.fn() @@ -20,7 +23,9 @@ vi.mock('~/hooks/use-categories-names', () => ({ })) vi.mock('~/hooks/use-load-more') - +vi.mock('i18next', () => ({ + t: (key) => key, +})); describe('Categories page', () => { beforeAll(() => { useLoadMore.mockImplementation(() => ({ @@ -78,7 +83,7 @@ describe('Categories page', () => { expect(autocomplete.value).toBe('Music') - const categoryName = screen.getByText(/Music/) + const categoryName = screen.getByText(t(`categories.${titleToCamel('Music')}`, {defaultValue: 'Music'})) expect(categoryName).toBeInTheDocument() }) diff --git a/tests/unit/pages/subjects/Subjects.spec.jsx b/tests/unit/pages/subjects/Subjects.spec.jsx index acbe57941..0a2a12026 100644 --- a/tests/unit/pages/subjects/Subjects.spec.jsx +++ b/tests/unit/pages/subjects/Subjects.spec.jsx @@ -3,10 +3,11 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '~tests/test-utils' import Subjects from '~/pages/subjects/Subjects' import useLoadMore from '~/hooks/use-load-more' - +import { useTranslation } from 'react-i18next' const resetDataMock = vi.fn() const loadMoreMock = vi.fn() - +import {titleToCamel} from '~/utils/title-to-camel-case' +const {t} = useTranslation(); vi.mock('~/hooks/use-subjects-names', () => ({ __esModule: true, default: () => ({ @@ -18,6 +19,9 @@ vi.mock('~/hooks/use-subjects-names', () => ({ vi.mock('~/hooks/use-load-more') +vi.mock('i18next', () => ({ + t: (key) => key, +})); describe('Subjects page', () => { beforeAll(() => { useLoadMore.mockImplementation(() => ({ @@ -76,8 +80,7 @@ describe('Subjects page', () => { expect(autocomplete.value).toBe('Violin') - const subjectName = screen.getByText(/Violin/) - + const subjectName = screen.getByText(t(`subjects.${titleToCamel('Violin')}`, {defaultValue: 'Violin'})) expect(subjectName).toBeInTheDocument() }) })