From 5de82acaabfc0ab0b3e7d5a8bcd771ebaead98ce Mon Sep 17 00:00:00 2001 From: ClaraTschamon Date: Wed, 8 Apr 2026 16:55:18 +0200 Subject: [PATCH] feat: add project name translations --- .../ProjectGridCard/ProjectGridCard.test.tsx | 11 ++- .../ProjectGridCard/ProjectGridCard.tsx | 5 +- .../ProjectGridCardV2.test.tsx | 11 ++- .../ProjectGridCardV2/ProjectGridCardV2.tsx | 5 +- src/models/fpm/FPMProject.ts | 8 +- src/slices/ProjectsGrid/ProjectsGrid.test.tsx | 11 ++- .../ProjectsGridV2/ProjectsGridV2.test.tsx | 11 ++- src/slices/ProjectsMap/ProjectsMap.tsx | 88 ++++++++++++------- src/slices/TextWithCard/TextWithCard.test.tsx | 11 ++- src/test/integrationMocks/fpmProjectMock.ts | 12 +++ src/utils/getProjectTranslation.ts | 13 +++ 11 files changed, 144 insertions(+), 42 deletions(-) create mode 100644 src/utils/getProjectTranslation.ts diff --git a/src/components/ProjectGridCard/ProjectGridCard.test.tsx b/src/components/ProjectGridCard/ProjectGridCard.test.tsx index f1ac154d..2da52ca9 100644 --- a/src/components/ProjectGridCard/ProjectGridCard.test.tsx +++ b/src/components/ProjectGridCard/ProjectGridCard.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from '../../test/testUtils'; import { ProjectGridCardProps } from './ProjectGridCard'; import ProjectGridCard from '.'; import fpmProjectMock from '../../test/integrationMocks/fpmProjectMock'; +import getProjectTranslation from '../../utils/getProjectTranslation'; import { strapiMediaMock } from '../../test/strapiMocks/strapiMedia'; import React from 'react'; import messagesEn from '../../components/CreditsAvailableBadge/messages.en'; @@ -23,7 +24,15 @@ describe('The ProjectGridCard component', () => { it('displays the project card', () => { setup(); - expect(screen.getByText(fpmProjectMock.title)).toBeInTheDocument(); + expect( + screen.getByText( + getProjectTranslation( + fpmProjectMock.nameTranslations, + 'en', + fpmProjectMock.title + ) + ) + ).toBeInTheDocument(); }); it('displays the project thumbnail', () => { diff --git a/src/components/ProjectGridCard/ProjectGridCard.tsx b/src/components/ProjectGridCard/ProjectGridCard.tsx index ee4e5c08..8581d755 100644 --- a/src/components/ProjectGridCard/ProjectGridCard.tsx +++ b/src/components/ProjectGridCard/ProjectGridCard.tsx @@ -7,6 +7,7 @@ import { FORMAT_AS_HECTARE_CONFIG } from '../../constants/formatter'; import CreditsAvailableBadge from '../../components/CreditsAvailableBadge'; import CertificationBadge from '../../components/CertificationBadge'; import { IntlContext } from '../ContextProvider'; +import getProjectTranslation from '../../utils/getProjectTranslation'; export interface ProjectGridCardProps { project: PortfolioProject; @@ -15,7 +16,7 @@ export interface ProjectGridCardProps { export const ProjectGridCard = ({ project, }: ProjectGridCardProps): React.JSX.Element => { - const { formatNumber } = useContext(IntlContext); + const { formatNumber, locale } = useContext(IntlContext); return ( @@ -34,7 +35,7 @@ export const ProjectGridCard = ({ )} - {project.friendlyName || project.title} + {getProjectTranslation(project.nameTranslations, locale, project.title)} diff --git a/src/components/ProjectGridCardV2/ProjectGridCardV2.test.tsx b/src/components/ProjectGridCardV2/ProjectGridCardV2.test.tsx index 4ce56032..f836fa94 100644 --- a/src/components/ProjectGridCardV2/ProjectGridCardV2.test.tsx +++ b/src/components/ProjectGridCardV2/ProjectGridCardV2.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from '../../test/testUtils'; import { ProjectGridCardV2Props } from './ProjectGridCardV2'; import ProjectGridCardV2 from '.'; import fpmProjectMock from '../../test/integrationMocks/fpmProjectMock'; +import getProjectTranslation from '../../utils/getProjectTranslation'; import { strapiMediaMock } from '../../test/strapiMocks/strapiMedia'; import messagesEnCertificationBadge from '../CertificationBadge/messages.en'; import messagesEnCreditsAvailableBadge from '../CreditsAvailableBadge/messages.en'; @@ -23,7 +24,15 @@ describe('The ProjectGridCardV2 component', () => { it('displays the project title', () => { setup(); - expect(screen.getByText(fpmProjectMock.title)).toBeInTheDocument(); + expect( + screen.getByText( + getProjectTranslation( + fpmProjectMock.nameTranslations, + 'en', + fpmProjectMock.title + ) + ) + ).toBeInTheDocument(); }); it('displays the project thumbnail', () => { diff --git a/src/components/ProjectGridCardV2/ProjectGridCardV2.tsx b/src/components/ProjectGridCardV2/ProjectGridCardV2.tsx index 2bbb3b36..791f16f6 100644 --- a/src/components/ProjectGridCardV2/ProjectGridCardV2.tsx +++ b/src/components/ProjectGridCardV2/ProjectGridCardV2.tsx @@ -9,6 +9,7 @@ import { IntlContext } from '../ContextProvider'; import CreditsAvailableBadge from '../CreditsAvailableBadge'; import CertificationBadge from '../CertificationBadge'; import getCountryFlag from '../../utils/getCountryFlag'; +import getProjectTranslation from '../../utils/getProjectTranslation'; export interface ProjectGridCardV2Props { project: PortfolioProject; @@ -32,7 +33,7 @@ const getProjectTypeLabel = ( export const ProjectGridCardV2 = ({ project, }: ProjectGridCardV2Props): React.JSX.Element => { - const { formatNumber, formatMessage } = useContext(IntlContext); + const { formatNumber, formatMessage, locale } = useContext(IntlContext); return ( @@ -84,7 +85,7 @@ export const ProjectGridCardV2 = ({ {/* Content Section */} - {project.friendlyName || project.title} + {getProjectTranslation(project.nameTranslations, locale, project.title)} diff --git a/src/models/fpm/FPMProject.ts b/src/models/fpm/FPMProject.ts index a0d81b6d..bf652a2a 100644 --- a/src/models/fpm/FPMProject.ts +++ b/src/models/fpm/FPMProject.ts @@ -7,11 +7,17 @@ export enum CreditAvailability { SOON_CREDITS_AVAILABLE = 'soon_credits_available', } +export interface FPMProjectNameTranslation { + id: string; + language: string; + value: string; +} + interface FPMProject { id: string; title: string; description?: string; - friendlyName?: string; + nameTranslations?: FPMProjectNameTranslation[]; isPublic?: boolean; geom?: { diff --git a/src/slices/ProjectsGrid/ProjectsGrid.test.tsx b/src/slices/ProjectsGrid/ProjectsGrid.test.tsx index b1133c23..c020346c 100644 --- a/src/slices/ProjectsGrid/ProjectsGrid.test.tsx +++ b/src/slices/ProjectsGrid/ProjectsGrid.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '../../test/testUtils'; import ProjectsGrid from '.'; import { ProjectsGridProps } from './ProjectsGrid'; import fpmProjectMock from '../../test/integrationMocks/fpmProjectMock'; +import getProjectTranslation from '../../utils/getProjectTranslation'; import { strapiMediaMock } from '../../test/strapiMocks/strapiMedia'; import { strapiProjectMock } from '../../test/strapiMocks/strapiProject'; @@ -29,7 +30,15 @@ describe('The ProjectsGrid component', () => { it('displays the project cards', () => { setup(); - expect(screen.getByText(fpmProjectMock.title)).toBeInTheDocument(); + expect( + screen.getByText( + getProjectTranslation( + fpmProjectMock.nameTranslations, + 'en', + fpmProjectMock.title + ) + ) + ).toBeInTheDocument(); }); it('links to the portfolio', () => { diff --git a/src/slices/ProjectsGridV2/ProjectsGridV2.test.tsx b/src/slices/ProjectsGridV2/ProjectsGridV2.test.tsx index dc089cd3..46fba179 100644 --- a/src/slices/ProjectsGridV2/ProjectsGridV2.test.tsx +++ b/src/slices/ProjectsGridV2/ProjectsGridV2.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '../../test/testUtils'; import { ProjectsGridV2Props } from './ProjectsGridV2'; import fpmProjectMock from '../../test/integrationMocks/fpmProjectMock'; +import getProjectTranslation from '../../utils/getProjectTranslation'; import { strapiMediaMock } from '../../test/strapiMocks/strapiMedia'; import { strapiProjectMock } from '../../test/strapiMocks/strapiProject'; import ProjectsGridV2 from '.'; @@ -28,7 +29,15 @@ describe('The ProjectsGridV2 component', () => { it('displays the project cards', () => { setup(); - expect(screen.getByText(fpmProjectMock.title)).toBeInTheDocument(); + expect( + screen.getByText( + getProjectTranslation( + fpmProjectMock.nameTranslations, + 'en', + fpmProjectMock.title + ) + ) + ).toBeInTheDocument(); }); it('links to the portfolio', () => { diff --git a/src/slices/ProjectsMap/ProjectsMap.tsx b/src/slices/ProjectsMap/ProjectsMap.tsx index 9ed09af0..53353fef 100644 --- a/src/slices/ProjectsMap/ProjectsMap.tsx +++ b/src/slices/ProjectsMap/ProjectsMap.tsx @@ -21,9 +21,13 @@ import debounce from 'lodash/debounce'; import getFpmProjectsByBbox from '../../integrations/strapi/getFpmProjectsByBbox'; import getStrapiProjects from '../../integrations/strapi/getStrapiProjects'; import mergeProjectData from '../../utils/mergeProjectData'; -import { CreditAvailability } from '../../models/fpm/FPMProject'; +import { + CreditAvailability, + FPMProjectNameTranslation, +} from '../../models/fpm/FPMProject'; import IStrapiData from '../../models/strapi/IStrapiData'; import StrapiProject from '../../models/strapi/StrapiProject'; +import getProjectTranslation from '../../utils/getProjectTranslation'; const projectPinImage = 'https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.0.2/assets/fill/map-pin-fill.svg'; @@ -288,12 +292,28 @@ export const ProjectsMap: React.FC = ({ const coordinates = (e.features[0].geometry as any).coordinates.slice(); const { title, + nameTranslations: nameTranslationsRaw, projectDeveloper, slug, portfolioHost, creditAvailability, } = e.features[0].properties; + let parsedNameTranslations: FPMProjectNameTranslation[] | undefined; + try { + parsedNameTranslations = nameTranslationsRaw + ? JSON.parse(nameTranslationsRaw) + : undefined; + } catch { + parsedNameTranslations = undefined; + } + + const displayName = getProjectTranslation( + parsedNameTranslations, + locale, + title + ); + // Calculate if popup would go off screen at the top const point = map.current!.project(coordinates); const popupHeight = 150; // Approximate height of popup @@ -366,7 +386,7 @@ export const ProjectsMap: React.FC = ({ const description = `
${badge} -

${title}

+

${displayName}

${developer}

${button}
@@ -437,9 +457,9 @@ export const ProjectsMap: React.FC = ({ const [west, south, east, north] = bbox.split(',').map(Number); const bounds = new mapboxgl.LngLatBounds([west, south], [east, north]); const center = bounds.getCenter(); - + initialCenter = [center.lng, center.lat]; - + // Use the slice.defaultZoomLevel if provided (e.g., 8), otherwise default to 6 initialZoom = slice.defaultZoomLevel ?? 6; } @@ -457,8 +477,12 @@ export const ProjectsMap: React.FC = ({ map.current.addControl(new mapboxgl.NavigationControl(), 'top-right'); map.current.on('load', () => { - if(map.current?.getLayer('road-number-shield')) { - map.current?.setLayoutProperty('road-number-shield', 'visibility', 'none'); + if (map.current?.getLayer('road-number-shield')) { + map.current?.setLayoutProperty( + 'road-number-shield', + 'visibility', + 'none' + ); } setIsMapReady(true); }); @@ -476,7 +500,6 @@ export const ProjectsMap: React.FC = ({ slice.minZoomLevel, ]); - useEffect(() => { if (!map.current || !isMapReady) return; const currentMap = map.current; @@ -531,19 +554,20 @@ export const ProjectsMap: React.FC = ({ duration: 1500, }); - // Update bbox for this location - const buffer = 1; - const bbox = `${userLoc.lon - buffer},${userLoc.lat - buffer},${ - userLoc.lon + buffer - },${userLoc.lat + buffer}`; - initialBboxRef.current = bbox; - fetchProjectsData(bbox); + // Update bbox for this location + const buffer = 1; + const bbox = `${userLoc.lon - buffer},${userLoc.lat - buffer},${ + userLoc.lon + buffer + },${userLoc.lat + buffer}`; + initialBboxRef.current = bbox; + fetchProjectsData(bbox); + } + }, + () => { + // Permission denied or error - already have fallback data loaded + // No need to re-fetch since we already loaded FALLBACK_BBOX data } - }, - () => { - // Permission denied or error - already have fallback data loaded - // No need to re-fetch since we already loaded FALLBACK_BBOX data - }); + ); } else { // Geolocation not supported or disabled - use fallback initialBboxRef.current = FALLBACK_BBOX; @@ -566,12 +590,12 @@ export const ProjectsMap: React.FC = ({ return ( <> - + ); } @@ -601,13 +625,13 @@ export const ProjectsMap: React.FC = ({ )} - + ); diff --git a/src/slices/TextWithCard/TextWithCard.test.tsx b/src/slices/TextWithCard/TextWithCard.test.tsx index 8a6344a8..958efc19 100644 --- a/src/slices/TextWithCard/TextWithCard.test.tsx +++ b/src/slices/TextWithCard/TextWithCard.test.tsx @@ -5,6 +5,7 @@ import TextWithCard from '.'; import { TextWithCardProps } from './TextWithCard'; import { strapiProjectMock } from '../../test/strapiMocks/strapiProject'; import portfolioProjectMock from '../../test/mocks/portfolioProjectMock'; +import getProjectTranslation from '../../utils/getProjectTranslation'; const defaultProps: TextWithCardProps = { projects: [], @@ -83,7 +84,15 @@ describe('The TextWithCard component', () => { slice: { ...defaultProps.slice, project: { data: strapiProjectMock } }, }); - expect(screen.getByText(portfolioProjectMock.title)).toBeInTheDocument(); + expect( + screen.getByText( + getProjectTranslation( + portfolioProjectMock.nameTranslations, + 'en', + portfolioProjectMock.title + ) + ) + ).toBeInTheDocument(); }); const cardPositions = ['left', 'right']; diff --git a/src/test/integrationMocks/fpmProjectMock.ts b/src/test/integrationMocks/fpmProjectMock.ts index 4ac93e10..daf2f896 100644 --- a/src/test/integrationMocks/fpmProjectMock.ts +++ b/src/test/integrationMocks/fpmProjectMock.ts @@ -3,6 +3,18 @@ import FPMProject, { CreditAvailability } from '../../models/fpm/FPMProject'; const fpmProjectMock: FPMProject = { id: '1', title: 'Project 1', + nameTranslations: [ + { + id: 'name-translation-1', + language: 'en', + value: 'Project 1 Display Name English', + }, + { + id: 'name-translation-2', + language: 'de', + value: 'Project 1 Display Name German', + }, + ], geom: { type: 'Point', coordinates: [10.036542145100883, 47.42636837845707], diff --git a/src/utils/getProjectTranslation.ts b/src/utils/getProjectTranslation.ts new file mode 100644 index 00000000..1572c9b6 --- /dev/null +++ b/src/utils/getProjectTranslation.ts @@ -0,0 +1,13 @@ +import { FPMProjectNameTranslation } from '../models/fpm/FPMProject'; + +const getProjectTranslation = ( + nameTranslations: FPMProjectNameTranslation[] | undefined, + locale: string, + fallback: string +): string => { + if (!nameTranslations || nameTranslations.length === 0) return fallback; + const match = nameTranslations.find((t) => t.language === locale); + return match?.value || fallback; +}; + +export default getProjectTranslation;