Translate pages
This commit is contained in:
parent
a7aaf5effa
commit
96402228e3
49 changed files with 1458 additions and 926 deletions
|
|
@ -50,27 +50,31 @@ function buildSummary(
|
|||
travelTimeFilters: AiTravelTimeFilter[],
|
||||
matchCount: number
|
||||
): string {
|
||||
const i18n = require('../i18n').default as { t: (key: string, opts?: Record<string, unknown>) => string };
|
||||
const { ts } = require('../i18n/server') as { ts: (v: string) => string };
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const [name, value] of Object.entries(filters)) {
|
||||
// Skip Listing status — shown via the mode selector UI
|
||||
if (name === 'Listing status') continue;
|
||||
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'number') {
|
||||
parts.push(name);
|
||||
parts.push(ts(name));
|
||||
} else if (Array.isArray(value)) {
|
||||
parts.push(`${name}: ${(value as string[]).join(', ')}`);
|
||||
parts.push(`${ts(name)}: ${(value as string[]).map((v) => ts(v)).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tt of travelTimeFilters) {
|
||||
const bounds =
|
||||
tt.max !== undefined ? `< ${tt.max} min` : tt.min !== undefined ? `> ${tt.min} min` : '';
|
||||
parts.push(`${tt.mode} to ${tt.label} ${bounds}`.trim());
|
||||
tt.max !== undefined
|
||||
? i18n.t('format.lessThanMin', { max: tt.max })
|
||||
: tt.min !== undefined
|
||||
? i18n.t('format.moreThanMin', { min: tt.min })
|
||||
: '';
|
||||
parts.push(i18n.t('format.toDestination', { mode: tt.mode, label: tt.label, bounds }).trim());
|
||||
}
|
||||
|
||||
if (parts.length === 0) return 'No filters set';
|
||||
const countStr = matchCount.toLocaleString();
|
||||
return `${countStr} properties match · Set ${parts.length} filter${parts.length > 1 ? 's' : ''}: ${parts.join(', ')}`;
|
||||
if (parts.length === 0) return i18n.t('format.noFiltersSet');
|
||||
return `${i18n.t('format.propertiesMatch', { count: matchCount.toLocaleString() })} \u00B7 ${i18n.t('format.setFilters', { count: parts.length, list: parts.join(', ') })}`;
|
||||
}
|
||||
|
||||
export function useAiFilters(): UseAiFiltersResult {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CarIcon, BicycleIcon, WalkingIcon, TransitIcon } from '../components/ui/icons';
|
||||
|
||||
export type TransportMode = 'car' | 'bicycle' | 'walking' | 'transit';
|
||||
|
|
@ -27,6 +28,24 @@ export const MODE_ICONS: Record<TransportMode, ComponentType<{ className?: strin
|
|||
transit: TransitIcon,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook returning translated mode labels and descriptions.
|
||||
*/
|
||||
export function useTranslatedModes() {
|
||||
const { t } = useTranslation();
|
||||
const label = useCallback(
|
||||
(mode: TransportMode): string =>
|
||||
({ car: t('travel.modeCar'), bicycle: t('travel.modeBicycle'), walking: t('travel.modeWalking'), transit: t('travel.modeTransit') })[mode],
|
||||
[t]
|
||||
);
|
||||
const desc = useCallback(
|
||||
(mode: TransportMode): string =>
|
||||
({ car: t('travel.modeCarDesc'), bicycle: t('travel.modeBicycleDesc'), walking: t('travel.modeWalkingDesc'), transit: t('travel.modeTransitDesc') })[mode],
|
||||
[t]
|
||||
);
|
||||
return { label, desc };
|
||||
}
|
||||
|
||||
export interface TravelTimeEntry {
|
||||
mode: TransportMode;
|
||||
slug: string;
|
||||
|
|
|
|||
|
|
@ -1,65 +1,22 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Step, CallBackProps } from 'react-joyride';
|
||||
import { ACTIONS, EVENTS, STATUS } from 'react-joyride';
|
||||
|
||||
const STORAGE_KEY = 'tutorial_completed';
|
||||
|
||||
const STEPS: Step[] = [
|
||||
{
|
||||
target: '[data-tutorial="filters"]',
|
||||
title: 'Tell the map what matters',
|
||||
content:
|
||||
'Set your budget, commute limit, school quality, crime threshold. Whatever matters to you. Only areas that qualify stay lit. Use the eye icon to colour by any feature.',
|
||||
placement: 'right',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="ai-filters"]',
|
||||
title: 'Or just describe it',
|
||||
content:
|
||||
'Type what you want in plain English, like "quiet area near good schools under \u00A3400k", and we\u2019ll set up the filters for you.',
|
||||
placement: 'right',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="map"]',
|
||||
title: 'Explore what\u2019s out there',
|
||||
content:
|
||||
'Pan and zoom across England. Click any coloured area to see crime, schools, prices, broadband, noise, and more about that neighbourhood.',
|
||||
placement: 'bottom',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="search"]',
|
||||
title: 'Jump to a location',
|
||||
content: 'Search for any place or postcode to fly straight there.',
|
||||
placement: 'bottom',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="right-pane"]',
|
||||
title: 'Dig into the details',
|
||||
content:
|
||||
'See area statistics, histograms, and individual property records: prices, floor area, energy ratings, and more.',
|
||||
placement: 'left',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="poi-button"]',
|
||||
title: 'What\u2019s nearby?',
|
||||
content:
|
||||
'Toggle schools, shops, stations, parks, and restaurants on the map to see what\u2019s within reach.',
|
||||
placement: 'left',
|
||||
disableBeacon: true,
|
||||
styles: {
|
||||
tooltip: {
|
||||
transform: 'translateY(-50px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked = false) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const steps: Step[] = useMemo(() => [
|
||||
{ target: '[data-tutorial="filters"]', title: t('tutorial.step1Title'), content: t('tutorial.step1Content'), placement: 'right' as const, disableBeacon: true },
|
||||
{ target: '[data-tutorial="ai-filters"]', title: t('tutorial.step2Title'), content: t('tutorial.step2Content'), placement: 'right' as const, disableBeacon: true },
|
||||
{ target: '[data-tutorial="map"]', title: t('tutorial.step3Title'), content: t('tutorial.step3Content'), placement: 'bottom' as const, disableBeacon: true },
|
||||
{ target: '[data-tutorial="search"]', title: t('tutorial.step4Title'), content: t('tutorial.step4Content'), placement: 'bottom' as const, disableBeacon: true },
|
||||
{ target: '[data-tutorial="right-pane"]', title: t('tutorial.step5Title'), content: t('tutorial.step5Content'), placement: 'left' as const, disableBeacon: true },
|
||||
{ target: '[data-tutorial="poi-button"]', title: t('tutorial.step6Title'), content: t('tutorial.step6Content'), placement: 'left' as const, disableBeacon: true, styles: { tooltip: { transform: 'translateY(-50px)' } } },
|
||||
], [t]);
|
||||
|
||||
const [run, setRun] = useState(() => {
|
||||
if (isMobile) return false;
|
||||
return !localStorage.getItem(STORAGE_KEY);
|
||||
|
|
@ -88,7 +45,7 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked
|
|||
|
||||
return useMemo(
|
||||
() => ({
|
||||
steps: STEPS,
|
||||
steps,
|
||||
run: shouldRun,
|
||||
handleCallback,
|
||||
resetTutorial,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue