Lots of improvements
Some checks failed
CI / Python (lint + test) (push) Failing after 1m39s
CI / Frontend (lint + typecheck) (push) Failing after 1m49s
CI / Rust (lint + test) (push) Failing after 1m50s
Build and publish Docker image / build-and-push (push) Failing after 3m9s

This commit is contained in:
Andras Schmelczer 2026-04-04 10:45:48 +01:00
parent 3853b5dce7
commit b94cf17d75
33 changed files with 2587 additions and 1866 deletions

View file

@ -139,8 +139,9 @@ export default function App() {
window.history.replaceState({}, '', newUrl);
trackEvent('Purchase');
setShowLicenseSuccess(true);
refreshAuth();
}
// Always refresh auth on startup to pick up server-side subscription changes
refreshAuth().catch(() => {});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const savedSearches = useSavedSearches(user?.id ?? null);
@ -419,6 +420,8 @@ export default function App() {
isPropertySaved={user ? savedProperties.isPropertySaved : undefined}
getSavedPropertyId={user ? savedProperties.getSavedPropertyId : undefined}
deferTutorial={showLicenseSuccess}
onSaveSearch={user ? savedSearches.saveSearch : undefined}
savingSearch={savedSearches.saving}
/>
)}
{showAuthModal && (

View file

@ -58,7 +58,7 @@ export default function HomePage({
}, []);
return (
<div className="flex-1 overflow-y-auto bg-warm-50 dark:bg-navy-950 relative">
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-warm-50 dark:bg-navy-950 relative">
<div className="relative" style={{ zIndex: 1 }}>
{/* Hero */}
<div className="relative overflow-hidden bg-gradient-to-b from-navy-950 via-navy-900 to-navy-900 dark:from-navy-950 dark:via-navy-900 dark:to-navy-950 min-h-[calc(100dvh-3rem)] flex flex-col">
@ -78,7 +78,7 @@ export default function HomePage({
<p className="text-lg text-warm-400 mb-8 max-w-xl">
{t('home.heroDescription')}
</p>
<div className="flex items-center gap-4 mb-10">
<div className="flex flex-wrap items-center gap-4 mb-10">
<button
onClick={() => {
trackEvent('CTA Click', { location: 'hero', label: 'explore_map' });
@ -118,7 +118,7 @@ export default function HomePage({
{t('home.seeTheDifference')}
</button>
</div>
<div className="flex gap-12 pt-3 border-t border-white/10">
<div className="flex flex-wrap gap-x-12 gap-y-4 pt-3 border-t border-white/10">
<div>
<div className="text-2xl md:text-3xl font-bold text-white">
<TickerValue text="13M" active={statsActive} />

View file

@ -2,7 +2,7 @@ import { Fragment, memo, useState, useMemo, useRef, useCallback, useEffect } fro
import { useTranslation } from 'react-i18next';
import { ts } from '../../i18n/server';
import { Slider } from '../ui/Slider';
import { ChevronIcon, LightbulbIcon } from '../ui/icons';
import { ChevronIcon, CloseIcon, LightbulbIcon, SpinnerIcon } from '../ui/icons';
import { PillToggle } from '../ui/PillToggle';
import { PillGroup } from '../ui/PillGroup';
@ -203,6 +203,9 @@ interface FiltersProps {
onUpgradeClick?: () => void;
onResetTutorial?: () => void;
filterImpacts?: Record<string, number>;
onClearAll: () => void;
onSaveSearch?: (name: string) => Promise<void>;
savingSearch?: boolean;
}
export default memo(function Filters({
@ -242,6 +245,9 @@ export default memo(function Filters({
onUpgradeClick,
onResetTutorial,
filterImpacts,
onClearAll,
onSaveSearch,
savingSearch,
}: FiltersProps) {
const { t } = useTranslation();
const modeRestrictions = useMemo(() => {
@ -416,6 +422,50 @@ export default memo(function Filters({
const badgeCount = enabledFeatureList.length + activeEntryCount;
const [showClearPopup, setShowClearPopup] = useState(false);
const [clearSaveName, setClearSaveName] = useState('');
const [clearSaveError, setClearSaveError] = useState<string | null>(null);
const handleClearAllClick = useCallback(() => {
if (badgeCount === 0) return;
if (onSaveSearch) {
setShowClearPopup(true);
setClearSaveName('');
setClearSaveError(null);
} else {
onClearAll();
}
}, [badgeCount, onSaveSearch, onClearAll]);
const handleSaveAndClear = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
if (!clearSaveName.trim() || savingSearch) return;
try {
await onSaveSearch!(clearSaveName.trim());
setShowClearPopup(false);
onClearAll();
} catch {
setClearSaveError(t('saveSearch.saving'));
}
},
[clearSaveName, savingSearch, onSaveSearch, onClearAll, t]
);
const handleClearWithoutSaving = useCallback(() => {
setShowClearPopup(false);
onClearAll();
}, [onClearAll]);
useEffect(() => {
if (!showClearPopup) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setShowClearPopup(false);
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [showClearPopup]);
return (
<div
ref={containerRef}
@ -424,8 +474,7 @@ export default memo(function Filters({
<div
className="flex flex-col min-h-0"
style={{
flexGrow: activeFilterCollapsed ? 0 : addFilterCollapsed ? 1 : 3,
flexShrink: activeFilterCollapsed ? 0 : 1,
flex: activeFilterCollapsed ? '0 0 auto' : addFilterCollapsed ? '1 1 0' : '3 1 0',
}}
>
<button
@ -442,10 +491,31 @@ export default memo(function Filters({
</span>
)}
</div>
<ChevronIcon
direction={activeFilterCollapsed ? 'down' : 'up'}
className="w-4 h-4 text-warm-400 dark:text-warm-500"
/>
<div className="flex items-center gap-2">
{badgeCount > 0 && (
<span
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
handleClearAllClick();
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
handleClearAllClick();
}
}}
className="text-xs text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-200 underline"
>
{t('filters.clearAll')}
</span>
)}
<ChevronIcon
direction={activeFilterCollapsed ? 'down' : 'up'}
className="w-4 h-4 text-warm-400 dark:text-warm-500"
/>
</div>
</button>
{!activeFilterCollapsed && <div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
@ -521,6 +591,7 @@ export default memo(function Filters({
onDragEnd={() => onTravelTimeDragEnd(index)}
onToggleBest={() => onTravelTimeToggleBest(index)}
onRemove={() => onTravelTimeRemoveEntry(index)}
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
/>
</div>
))}
@ -618,6 +689,7 @@ export default memo(function Filters({
onDragEnd={() => onTravelTimeDragEnd(index)}
onToggleBest={() => onTravelTimeToggleBest(index)}
onRemove={() => onTravelTimeRemoveEntry(index)}
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
/>
</div>
))}
@ -625,7 +697,7 @@ export default memo(function Filters({
data-filter-name={feature.name}
className={`space-y-0.5 px-2 py-1.5 rounded ${isActive ? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30' : isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
>
<div className="flex items-center justify-between gap-1">
<div className="relative z-10 flex items-center justify-between gap-1">
<FeatureLabel feature={feature} size="sm" className="min-w-0 shrink" hideIconOnMobile />
<FeatureActions
feature={feature}
@ -705,6 +777,7 @@ export default memo(function Filters({
onDragEnd={() => onTravelTimeDragEnd(index)}
onToggleBest={() => onTravelTimeToggleBest(index)}
onRemove={() => onTravelTimeRemoveEntry(index)}
filterImpact={filterImpacts?.[travelFieldKey(entry)]}
/>
</div>
))}
@ -715,8 +788,7 @@ export default memo(function Filters({
<div
className="flex flex-col min-h-0 border-t border-warm-200 dark:border-warm-700"
style={{
flexGrow: addFilterCollapsed ? 0 : activeFilterCollapsed ? 1 : 2,
flexShrink: addFilterCollapsed ? 0 : 1,
flex: addFilterCollapsed ? '0 0 auto' : activeFilterCollapsed ? '1 1 0' : '2 1 0',
}}
>
<button
@ -730,7 +802,7 @@ export default memo(function Filters({
/>
</button>
{!addFilterCollapsed && (
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col">
<div className="flex-1 min-h-0 overflow-y-auto">
<FeatureBrowser
availableFeatures={availableFeatures}
allFeatures={features}
@ -826,6 +898,63 @@ export default memo(function Filters({
onNavigateToSource={onNavigateToSource}
/>
)}
{showClearPopup && (
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={() => setShowClearPopup(false)}>
<div className="absolute inset-0 bg-black/50 dark:bg-black/70" />
<div
className="relative w-full max-w-sm mx-4 bg-white dark:bg-warm-900 rounded-lg shadow-xl border border-warm-200 dark:border-warm-700"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between px-5 pt-5 pb-3">
<h2 className="text-lg font-semibold text-navy-950 dark:text-white">
{t('filters.clearAllTitle')}
</h2>
<button
onClick={() => setShowClearPopup(false)}
className="text-warm-400 hover:text-warm-700 dark:text-warm-400 dark:hover:text-warm-200"
>
<CloseIcon className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSaveAndClear} className="p-5 pt-2 space-y-4">
<p className="text-sm text-warm-600 dark:text-warm-400">
{t('filters.clearAllSavePrompt')}
</p>
<div>
<input
type="text"
value={clearSaveName}
onChange={(e) => setClearSaveName(e.target.value)}
className="w-full px-3 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 text-navy-950 dark:text-white placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-2 ring-teal-400 dark:ring-teal-500"
placeholder={t('saveSearch.namePlaceholder')}
autoFocus
/>
</div>
{clearSaveError && (
<p className="text-sm text-red-600 dark:text-red-300">{clearSaveError}</p>
)}
<div className="flex gap-3 justify-end">
<button
type="button"
onClick={handleClearWithoutSaving}
className="px-4 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 text-warm-700 dark:text-warm-300 hover:bg-warm-50 dark:hover:bg-warm-700"
>
{t('filters.clearWithoutSaving')}
</button>
<button
type="submit"
disabled={!clearSaveName.trim() || savingSearch}
className="flex items-center gap-2 px-4 py-2 text-sm rounded bg-teal-600 text-white font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-wait"
>
{savingSearch && <SpinnerIcon className="w-4 h-4 animate-spin" />}
{savingSearch ? t('saveSearch.saving') : t('filters.saveAndClear')}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
});

View file

@ -1,9 +1,11 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import type { PostcodeGeometry } from '../../types';
import { authHeaders } from '../../lib/api';
import { useIsMobile } from '../../hooks/useIsMobile';
import { useLocationSearch, type SearchResult } from '../../hooks/useLocationSearch';
import { PlaceSearchInput } from '../ui/PlaceSearchInput';
import { LocateIcon } from '../ui/icons/LocateIcon';
import { SearchIcon } from '../ui/icons/SearchIcon';
export interface SearchedLocation {
@ -14,6 +16,7 @@ export interface SearchedLocation {
const ZOOM_FOR_TYPE: Record<string, number> = {
city: 10,
borough: 12,
outcode: 14,
town: 13,
suburb: 14,
quarter: 14,
@ -35,6 +38,7 @@ export default function LocationSearch({
onLocationSearched?: (postcode: SearchedLocation | null) => void;
onMouseEnter?: () => void;
}) {
const { t } = useTranslation();
const search = useLocationSearch();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@ -80,7 +84,7 @@ export default function LocationSearch({
try {
const res = await fetch(`/api/postcode/${encodeURIComponent(result.label)}`, authHeaders());
if (!res.ok) {
setError('Postcode not found');
setError(t('locationSearch.postcodeNotFound'));
return;
}
const json: {
@ -94,7 +98,7 @@ export default function LocationSearch({
search.clear();
if (isMobile) setExpanded(false);
} catch {
setError('Lookup failed');
setError(t('locationSearch.lookupFailed'));
} finally {
setLoading(false);
}
@ -102,17 +106,71 @@ export default function LocationSearch({
[onFlyTo, onLocationSearched, isMobile, search]
);
// Mobile collapsed state: just a search icon button
const [locating, setLocating] = useState(false);
const locateUser = useCallback(async () => {
if (!navigator.geolocation) {
setError(t('locationSearch.geolocationUnsupported'));
return;
}
setError(null);
setLocating(true);
search.close();
try {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
});
});
const { latitude, longitude } = position.coords;
const res = await fetch(
`/api/nearest-postcode?lat=${latitude}&lng=${longitude}`,
authHeaders()
);
if (!res.ok) {
setError(t('locationSearch.lookupFailed'));
return;
}
const json: {
postcode: string;
latitude: number;
longitude: number;
geometry: PostcodeGeometry;
} = await res.json();
onFlyTo(json.latitude, json.longitude, 16);
onLocationSearched?.({ postcode: json.postcode, geometry: json.geometry });
search.clear();
if (isMobile) setExpanded(false);
} catch {
setError(t('locationSearch.geolocationFailed'));
} finally {
setLocating(false);
}
}, [onFlyTo, onLocationSearched, isMobile, search, t]);
// Mobile collapsed state: search icon + locate button
if (isMobile && !expanded) {
return (
<button
type="button"
onClick={() => setExpanded(true)}
className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg pointer-events-auto"
aria-label="Search places or postcodes"
>
<SearchIcon className="w-5 h-5 text-warm-600 dark:text-warm-300" />
</button>
<div className="flex gap-2 pointer-events-auto">
<button
type="button"
onClick={() => setExpanded(true)}
className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg"
aria-label={t('locationSearch.searchLabel')}
>
<SearchIcon className="w-5 h-5 text-warm-600 dark:text-warm-300" />
</button>
<button
type="button"
onClick={locateUser}
disabled={locating}
className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg text-warm-600 dark:text-warm-300 hover:text-teal-600 dark:hover:text-teal-400 disabled:opacity-50"
aria-label={t('locationSearch.locateMe')}
>
<LocateIcon className={`w-5 h-5 ${locating ? 'animate-pulse' : ''}`} />
</button>
</div>
);
}
@ -129,12 +187,22 @@ export default function LocationSearch({
search={search}
onSelect={selectResult}
loading={loading}
placeholder="Search places or postcodes..."
placeholder={t('locationSearch.placeholder')}
size="sm"
inputClassName="px-2 py-2 text-sm w-56 border-none outline-none bg-transparent text-warm-700 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500"
inputRef={inputRef}
onInputChange={() => setError(null)}
/>
<button
type="button"
onClick={locateUser}
disabled={locating}
className="p-2 mr-0.5 rounded hover:bg-warm-100 dark:hover:bg-warm-700 text-warm-400 dark:text-warm-500 hover:text-teal-600 dark:hover:text-teal-400 disabled:opacity-50"
aria-label={t('locationSearch.locateMe')}
title={t('locationSearch.locateMe')}
>
<LocateIcon className={`w-4 h-4 ${locating ? 'animate-pulse' : ''}`} />
</button>
</div>
{error && (

View file

@ -30,6 +30,7 @@ import { CloseIcon } from '../ui/icons/CloseIcon';
import type { FeatureFilters } from '../../types';
import { useDeckLayers } from '../../hooks/useDeckLayers';
import { useTranslatedModes, type TravelTimeEntry } from '../../hooks/useTravelTime';
import { ts } from '../../i18n/server';
interface MapProps {
data: HexagonData[];
@ -59,6 +60,7 @@ interface MapProps {
hideLegend?: boolean;
travelTimeEntries?: TravelTimeEntry[];
densityLabel?: string;
totalCount?: number;
}
const EMPTY_TRAVEL_ENTRIES: TravelTimeEntry[] = [];
@ -115,11 +117,13 @@ export default memo(function Map({
bounds: viewportBounds,
hideLegend = false,
travelTimeEntries = EMPTY_TRAVEL_ENTRIES,
densityLabel = 'Number of properties',
densityLabel: densityLabelProp,
totalCount: totalCountProp,
}: MapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const modes = useTranslatedModes();
const densityLabel = densityLabelProp ?? t('mapLegend.numberOfProperties');
const [internalViewState, setInternalViewState] = useState<ViewState>(
initialViewState || INITIAL_VIEW_STATE
);
@ -288,8 +292,8 @@ export default memo(function Map({
<MapLegend
featureLabel={
viewSource === 'eye'
? `Previewing \u201c${colorFeatureMeta.name}\u201d`
: colorFeatureMeta.name
? t('mapLegend.previewing', { name: ts(colorFeatureMeta.name) })
: ts(colorFeatureMeta.name)
}
range={colorRange}
showCancel={viewSource === 'eye'}
@ -311,7 +315,7 @@ export default memo(function Map({
: [countRange.min, countRange.max]
}
totalCount={
usePostcodeView ? postcodeCountRange.total : countRange.total
totalCountProp ?? (usePostcodeView ? postcodeCountRange.total : countRange.total)
}
showCancel={false}
onCancel={onCancelPin}

View file

@ -1,4 +1,5 @@
import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Slider } from '../ui/Slider';
import { IconButton } from '../ui/IconButton';
import { PillToggle } from '../ui/PillToggle';
@ -8,9 +9,9 @@ import { TravelTimeInfoPopup } from '../ui/TravelTimeInfoPopup';
import { CloseIcon } from '../ui/icons/CloseIcon';
import { EyeIcon } from '../ui/icons/EyeIcon';
import { InfoIcon } from '../ui/icons/InfoIcon';
import { formatFilterValue } from '../../lib/format';
import { formatFilterValue, formatNumber } from '../../lib/format';
import { useTravelDestinations } from '../../hooks/useTravelDestinations';
import { MODE_LABELS, MODE_ICONS, type TransportMode } from '../../hooks/useTravelTime';
import { MODE_ICONS, useTranslatedModes, type TransportMode } from '../../hooks/useTravelTime';
interface TravelTimeCardProps {
mode: TransportMode;
@ -29,6 +30,7 @@ interface TravelTimeCardProps {
onDragEnd: () => void;
onToggleBest: () => void;
onRemove: () => void;
filterImpact?: number;
}
export function TravelTimeCard({
@ -48,7 +50,10 @@ export function TravelTimeCard({
onDragEnd,
onToggleBest,
onRemove,
filterImpact,
}: TravelTimeCardProps) {
const { t } = useTranslation();
const modes = useTranslatedModes();
const { destinations, loading: destinationsLoading } = useTravelDestinations(mode);
const [showInfo, setShowInfo] = useState(false);
const [showBestInfo, setShowBestInfo] = useState(false);
@ -75,23 +80,23 @@ export function TravelTimeCard({
<div className="flex items-center gap-1.5">
<ModeIcon className="w-4 h-4 text-teal-600 dark:text-teal-400" />
<span className="text-sm font-medium text-navy-950 dark:text-warm-100">
Travel Time ({MODE_LABELS[mode]})
{t('travel.travelTime', { mode: modes.label(mode) })}
</span>
</div>
<div className="flex items-center gap-0.5">
<IconButton onClick={() => setShowInfo(true)} title="Feature info">
<IconButton onClick={() => setShowInfo(true)} title={t('filters.featureInfo')}>
<InfoIcon className="w-3.5 h-3.5" />
</IconButton>
{slug && (
<IconButton
onClick={onTogglePin}
active={isPinned}
title={isPinned ? 'Stop previewing' : 'Preview on map'}
title={isPinned ? t('travel.stopPreviewing') : t('travel.previewOnMap')}
>
<EyeIcon className="w-3.5 h-3.5" filled={isPinned} />
</IconButton>
)}
<IconButton onClick={() => onRemove()} title="Remove travel time">
<IconButton onClick={() => onRemove()} title={t('travel.removeTravelTime')}>
<CloseIcon className="w-3.5 h-3.5" />
</IconButton>
</div>
@ -104,14 +109,14 @@ export function TravelTimeCard({
onSelect={handleDestinationSelect}
value={label || undefined}
onClear={() => onSetDestination('', '', 0, 0)}
placeholder="Select destination..."
placeholder={t('travel.selectDestination')}
/>
{/* Best-case toggle — transit only, shown when destination is set */}
{slug && mode === 'transit' && (
<div className="flex items-center gap-1.5">
<PillToggle label="Best case" active={useBest} onClick={onToggleBest} size="xs" />
<IconButton onClick={() => setShowBestInfo(true)} title="What is best case?">
<PillToggle label={t('travel.bestCase')} active={useBest} onClick={onToggleBest} size="xs" />
<IconButton onClick={() => setShowBestInfo(true)} title={t('travel.bestCaseTitle')}>
<InfoIcon className="w-3 h-3" />
</IconButton>
</div>
@ -120,12 +125,11 @@ export function TravelTimeCard({
{showInfo && <TravelTimeInfoPopup mode={mode} onClose={() => setShowInfo(false)} />}
{showBestInfo && (
<InfoPopup title="Best case travel time" onClose={() => setShowBestInfo(false)}>
<p className="text-sm text-warm-700 dark:text-warm-300 leading-relaxed">
Uses the fastest realistic journey time (if you time your departure well and catch good
connections). The default uses the <strong>median</strong>, representing a typical journey
regardless of when you leave.
</p>
<InfoPopup title={t('travel.bestCaseTitle')} onClose={() => setShowBestInfo(false)}>
<p
className="text-sm text-warm-700 dark:text-warm-300 leading-relaxed"
dangerouslySetInnerHTML={{ __html: t('travel.bestCaseDesc') }}
/>
</InfoPopup>
)}
@ -133,7 +137,7 @@ export function TravelTimeCard({
{slug && (
<div>
<span className="text-[10px] font-medium text-warm-500 dark:text-warm-400 uppercase tracking-wide">
Max time
{t('travel.maxTime')}
</span>
<Slider
min={sliderMin}
@ -145,9 +149,14 @@ export function TravelTimeCard({
onPointerUp={() => onDragEnd()}
/>
<div className="relative h-4 mt-1 mx-2.5 text-[10px] text-warm-500 dark:text-warm-400 leading-tight">
<span className="absolute left-0">{formatFilterValue(displayRange[0])} min</span>
<span className="absolute right-0">{formatFilterValue(displayRange[1])} min</span>
<span className="absolute left-0">{formatFilterValue(displayRange[0])} {t('common.min')}</span>
<span className="absolute right-0">{formatFilterValue(displayRange[1])} {t('common.min')}</span>
</div>
{filterImpact != null && filterImpact > 0 && (
<p className="text-[10px] text-warm-400 dark:text-warm-500 -mt-1 ml-2.5">
+{formatNumber(filterImpact)} without this filter
</p>
)}
</div>
)}
</div>

View file

@ -0,0 +1,60 @@
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { SUPPORTED_LANGUAGES, type LanguageCode } from '../../i18n';
export default function LanguageDropdown() {
const { i18n } = useTranslation();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const current = SUPPORTED_LANGUAGES.find((l) => l.code === i18n.language) ?? SUPPORTED_LANGUAGES[0];
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
const changeLanguage = (code: LanguageCode) => {
i18n.changeLanguage(code);
localStorage.setItem('language', code);
setOpen(false);
};
return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((v) => !v)}
className="flex items-center gap-1 px-2 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm"
aria-label="Language"
>
<span className="text-base leading-none">{current.flag}</span>
<svg className="w-3 h-3 text-warm-400" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3 5l3 3 3-3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{open && (
<div className="absolute right-0 top-9 w-40 bg-white dark:bg-warm-800 border border-warm-200 dark:border-warm-700 rounded-lg shadow-lg z-50 py-1">
{SUPPORTED_LANGUAGES.map((lang) => (
<button
key={lang.code}
onClick={() => changeLanguage(lang.code)}
className={`w-full flex items-center gap-2.5 px-3 py-1.5 text-sm ${
i18n.language === lang.code
? 'text-teal-600 dark:text-teal-400 font-medium bg-teal-50 dark:bg-teal-900/30'
: 'text-warm-700 dark:text-warm-300 hover:bg-warm-50 dark:hover:bg-warm-700'
}`}
>
<span className="text-base leading-none">{lang.flag}</span>
{lang.label}
</button>
))}
</div>
)}
</div>
);
}

View file

@ -16,7 +16,7 @@ export function Slider({ className, ...props }: SliderProps) {
{props.value?.map((_, i) => (
<SliderPrimitive.Thumb
key={i}
className="block h-6 w-6 rounded-full border-2 border-teal-600 dark:border-teal-500 bg-white dark:bg-navy-800 ring-offset-white dark:ring-offset-navy-950 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 before:absolute before:left-1/2 before:top-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:h-11 before:w-11 before:rounded-full before:content-['']"
className="block h-6 w-6 cursor-pointer rounded-full border-2 border-teal-600 dark:border-teal-500 bg-white dark:bg-navy-800 ring-offset-white dark:ring-offset-navy-950 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 before:absolute before:left-1/2 before:top-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:h-11 before:w-11 before:rounded-full before:content-['']"
/>
))}
</SliderPrimitive.Root>

View file

@ -17,8 +17,6 @@ export interface AiFiltersResult {
notes: string;
/** Human-readable summary of what was set */
summary: string;
/** The listing mode used (historical/buy/rent) */
listingType: string;
/** Number of properties matching the proposed filters (excludes travel time) */
matchCount: number;
}
@ -34,8 +32,7 @@ export interface AiFiltersContext {
interface UseAiFiltersResult {
fetchAiFilters: (
query: string,
context?: AiFiltersContext,
listingType?: string
context?: AiFiltersContext
) => Promise<AiFiltersResult | null>;
loading: boolean;
error: string | null;
@ -88,8 +85,7 @@ export function useAiFilters(): UseAiFiltersResult {
const fetchAiFilters = useCallback(
async (
query: string,
context?: AiFiltersContext,
listingType?: string
context?: AiFiltersContext
): Promise<AiFiltersResult | null> => {
abortRef.current?.abort();
const controller = new AbortController();
@ -104,7 +100,6 @@ export function useAiFilters(): UseAiFiltersResult {
try {
const url = apiUrl('ai-filters');
const bodyObj: Record<string, unknown> = { query };
if (listingType) bodyObj.listing_type = listingType;
if (context) {
bodyObj.context = {
filters: context.filters,
@ -155,7 +150,6 @@ export function useAiFilters(): UseAiFiltersResult {
travelTimeFilters,
notes: json.notes || '',
summary: summaryText,
listingType: json.listing_type || 'historical',
matchCount,
};
setNotes(result.notes || null);

View file

@ -126,8 +126,10 @@ export function useAuth() {
const result = await pb.collection('users').authRefresh();
setUser(recordToUser(result.record));
} catch (err) {
const msg = err instanceof Error ? err.message : 'Auth refresh failed';
setError(msg);
// Token is invalid/expired — clear auth state but don't set error,
// since this is a background refresh, not a user-initiated action
pb.authStore.clear();
setUser(null);
throw err;
} finally {
setLoading(false);

View file

@ -0,0 +1,93 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import type { FeatureMeta, FeatureFilters, Bounds } from '../types';
import { apiUrl, buildFilterString, logNonAbortError, authHeaders, isAbortError } from '../lib/api';
import type { TravelTimeEntry } from './useTravelTime';
const DEBOUNCE_MS = 400;
interface FilterCountsResponse {
total: number;
impacts: Record<string, number>;
}
/**
* Fetches per-filter marginal impact counts: for each active filter,
* how many more properties would be visible if that filter were removed.
*/
export function useFilterCounts(
filters: FeatureFilters,
features: FeatureMeta[],
bounds: Bounds | null,
travelTimeEntries: TravelTimeEntry[]
) {
const [impacts, setImpacts] = useState<Record<string, number>>({});
const [total, setTotal] = useState<number>(0);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const abortRef = useRef<AbortController | null>(null);
// Build the travel param string (same format as useMapData)
const travelParam = useMemo(() => {
const segments: string[] = [];
for (const entry of travelTimeEntries) {
if (!entry.slug) continue;
let seg = `${entry.mode}:${entry.slug}`;
if (entry.useBest) seg += ':best';
if (entry.timeRange) seg += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
segments.push(seg);
}
return segments.join('|');
}, [travelTimeEntries]);
useEffect(() => {
if (!bounds) return;
const filterCount = Object.keys(filters).length;
const hasTravelFilters = travelTimeEntries.some((e) => e.slug && e.timeRange);
if (filterCount === 0 && !hasTravelFilters) {
setImpacts({});
setTotal(0);
return;
}
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
if (abortRef.current) abortRef.current.abort();
abortRef.current = new AbortController();
try {
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
const filtersStr = buildFilterString(filters, features);
const params = new URLSearchParams({ bounds: boundsStr });
if (filtersStr) params.set('filters', filtersStr);
if (travelParam) params.set('travel', travelParam);
const res = await fetch(
apiUrl('filter-counts', params),
authHeaders({ signal: abortRef.current!.signal })
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json: FilterCountsResponse = await res.json();
setImpacts(json.impacts);
setTotal(json.total);
} catch (err) {
if (!isAbortError(err)) {
logNonAbortError('Failed to fetch filter counts', err);
}
}
}, DEBOUNCE_MS);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [filters, features, bounds, travelParam, travelTimeEntries]);
// Cancel in-flight on unmount
useEffect(() => {
return () => {
abortRef.current?.abort();
};
}, []);
return { impacts, total };
}

View file

@ -266,6 +266,14 @@ export function useHexagonSelection({
setSelectedPostcodeGeometry(null);
} else {
setAreaStats(stats);
// Re-fetch properties if the properties tab is active
if (rightPaneTab === 'properties') {
if (selectedHexagon.type === 'postcode') {
fetchPostcodeProperties(selectedHexagon.id, 0);
} else {
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
}
}
}
})
.catch((error) => {
@ -279,7 +287,7 @@ export function useHexagonSelection({
return () => {
cancelled = true;
};
}, [filterStr, selectedHexagon, fetchHexagonStats, fetchPostcodeStats]);
}, [filterStr, selectedHexagon, fetchHexagonStats, fetchPostcodeStats, rightPaneTab, fetchHexagonProperties, fetchPostcodeProperties]);
const handleLocationSearch = useCallback(
(postcode: string, geometry: PostcodeGeometry) => {

View file

@ -2,10 +2,12 @@ import { useState, useCallback, useRef, useEffect } from 'react';
import type { PlaceResult } from '../types';
import { authHeaders, logNonAbortError } from '../lib/api';
const POSTCODE_RE = /^[A-Z]{1,2}\d[A-Z\d]?\s*\d?[A-Z]{0,2}$/i;
/** Matches a full UK postcode with complete inward code (e.g. "E14 2DG", "SW1A1AA").
* Outcodes like "E14" or "SW1A" intentionally do NOT match they go through /api/places instead. */
const FULL_POSTCODE_RE = /^[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}$/i;
function looksLikePostcode(s: string) {
return POSTCODE_RE.test(s.trim());
return FULL_POSTCODE_RE.test(s.trim());
}
/** Normalize a UK postcode: uppercase, strip spaces, insert canonical space before inward code. */
@ -83,7 +85,7 @@ export function useLocationSearch(mode?: string) {
place_type: p.place_type,
lat: p.lat,
lon: p.lon,
city: p.city,
city: p.city === 'City of London' ? 'London' : p.city,
}));
setResults(placeResults);
setOpen(placeResults.length > 0);

View file

@ -7,43 +7,87 @@ interface PaneResizeHandlers {
}
export function usePaneResize(
initialWidth: number,
minWidth: number,
maxWidth: number,
side: 'left' | 'right'
): [number, PaneResizeHandlers] {
const [width, setWidth] = useState(initialWidth);
initialSize: number,
minSize: number,
maxSize: number,
side: 'left' | 'right' | 'top' | 'bottom'
): [number, PaneResizeHandlers, React.RefCallback<HTMLElement>] {
const [size, setSize] = useState(initialSize);
const draggingRef = useRef(false);
const liveSizeRef = useRef(initialSize);
const targetRef = useRef<HTMLElement | null>(null);
const containerOffsetRef = useRef(0);
const containerSizeRef = useRef(0);
const handlePointerDown = useCallback((e: React.PointerEvent) => {
e.preventDefault();
(e.target as HTMLElement).setPointerCapture(e.pointerId);
draggingRef.current = true;
const isVertical = side === 'top' || side === 'bottom';
const styleProp = isVertical ? 'height' : 'width';
const targetCallbackRef = useCallback((el: HTMLElement | null) => {
targetRef.current = el;
}, []);
const computeSize = useCallback(
(e: React.PointerEvent): number => {
if (isVertical) {
const total = containerSizeRef.current || window.innerHeight;
const resolvedMax = maxSize <= 1 ? total * maxSize : maxSize;
const pos = e.clientY - containerOffsetRef.current;
return side === 'top'
? Math.min(resolvedMax, Math.max(minSize, pos))
: Math.min(resolvedMax, Math.max(minSize, total - pos));
} else {
const resolvedMax = maxSize <= 1 ? window.innerWidth * maxSize : maxSize;
return side === 'left'
? Math.min(resolvedMax, Math.max(minSize, e.clientX))
: Math.min(resolvedMax, Math.max(minSize, window.innerWidth - e.clientX));
}
},
[side, isVertical, minSize, maxSize]
);
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
e.preventDefault();
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
draggingRef.current = true;
if (isVertical) {
const container = (e.currentTarget as HTMLElement).parentElement;
if (container) {
const rect = container.getBoundingClientRect();
containerOffsetRef.current = rect.top;
containerSizeRef.current = rect.height;
}
}
},
[isVertical]
);
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (!draggingRef.current) return;
const resolvedMax = maxWidth <= 1 ? window.innerWidth * maxWidth : maxWidth;
const newWidth =
side === 'left'
? Math.min(resolvedMax, Math.max(minWidth, e.clientX))
: Math.min(resolvedMax, Math.max(minWidth, window.innerWidth - e.clientX));
setWidth(newWidth);
const newSize = computeSize(e);
liveSizeRef.current = newSize;
if (targetRef.current) {
targetRef.current.style[styleProp] = `${newSize}px`;
} else {
setSize(newSize);
}
},
[side, minWidth, maxWidth]
[computeSize, styleProp]
);
const handlePointerUp = useCallback(() => {
draggingRef.current = false;
setSize(liveSizeRef.current);
}, []);
return [
width,
size,
{
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
},
targetCallbackRef,
];
}

View file

@ -13,275 +13,271 @@ import i18n from 'i18next';
const descriptions: Record<string, Record<string, string>> = {
fr: {
'Listing status': 'Indique si le bien provient de ventes historiques, est en vente ou en location',
'Property type': 'Type de bien : individuel, jumel\u00E9, mitoyen, appartement ou autre',
'Leasehold/Freehold': 'Indique si le bien est en bail ou en pleine propri\u00E9t\u00E9',
'Last known price': 'Dernier prix de vente enregistr\u00E9 au Land Registry',
'Estimated current price': 'Estimation du prix actuel ajust\u00E9 \u00E0 l\u2019inflation',
'Asking price': 'Prix demand\u00E9 pour les biens actuellement en vente',
'Price per sqm': 'Prix de vente divis\u00E9 par la surface totale',
'Est. price per sqm': 'Prix actuel estim\u00E9 divis\u00E9 par la surface totale',
'Asking price per sqm': 'Prix demand\u00E9 divis\u00E9 par la surface totale',
'Estimated monthly rent': 'Loyer mensuel priv\u00E9 m\u00E9dian pour le secteur',
'Asking rent (monthly)': 'Loyer mensuel affich\u00E9 pour les biens en location',
'Total floor area (sqm)': 'Surface int\u00E9rieure issue du diagnostic EPC',
'Number of bedrooms & living rooms': 'Nombre de pi\u00E8ces habitables selon le diagnostic EPC',
'Bedrooms': 'Nombre de chambres selon l\u2019annonce en ligne',
'Bathrooms': 'Nombre de salles de bain selon l\u2019annonce en ligne',
'Construction year': 'Ann\u00E9e de construction estim\u00E9e selon l\u2019EPC',
'Date of last transaction': 'Date de la derni\u00E8re vente enregistr\u00E9e au Land Registry',
'Listing date': 'Date de premi\u00E8re mise en ligne du bien',
'Former council house': 'Indique si le bien a \u00E9t\u00E9 r\u00E9pertori\u00E9 comme logement social',
'Current energy rating': 'Classement \u00E9nerg\u00E9tique EPC actuel (A = meilleur, G = pire)',
'Potential energy rating': 'Classement EPC potentiel si toutes les am\u00E9liorations recommand\u00E9es \u00E9taient r\u00E9alis\u00E9es',
'Interior height (m)': 'Hauteur moyenne d\u2019\u00E9tage selon le diagnostic EPC',
'Distance to nearest train or tube station (km)': 'Distance \u00E0 la gare ou station de m\u00E9tro la plus proche',
'Train or tube stations within 1km': 'Nombre de gares ou stations de m\u00E9tro \u00E0 moins d\u20191 km',
'Good+ primary schools within 2km': '\u00C9coles primaires not\u00E9es Bien ou Excellent par Ofsted dans un rayon de 2 km',
'Good+ secondary schools within 2km': 'Coll\u00E8ges/lyc\u00E9es not\u00E9s Bien ou Excellent par Ofsted dans un rayon de 2 km',
'Good+ primary schools within 5km': '\u00C9coles primaires not\u00E9es Bien ou Excellent par Ofsted dans un rayon de 5 km',
'Good+ secondary schools within 5km': 'Coll\u00E8ges/lyc\u00E9es not\u00E9s Bien ou Excellent par Ofsted dans un rayon de 5 km',
'Education, Skills and Training Score': 'Score de qualit\u00E9 \u00E9ducative du secteur (plus \u00E9lev\u00E9 = meilleur)',
'Income Score (rate)': 'Taux de pr\u00E9carit\u00E9 de revenu, invers\u00E9 (plus \u00E9lev\u00E9 = moins pr\u00E9caire)',
'Employment Score (rate)': 'Taux de pr\u00E9carit\u00E9 d\u2019emploi, invers\u00E9 (plus \u00E9lev\u00E9 = moins pr\u00E9caire)',
'Health Deprivation and Disability Score': 'Score de sant\u00E9 et handicap (plus \u00E9lev\u00E9 = meilleurs r\u00E9sultats)',
'Living Environment Score': 'Qualit\u00E9 de l\u2019environnement int\u00E9rieur et ext\u00E9rieur (plus \u00E9lev\u00E9 = meilleur)',
'Indoors Sub-domain Score': 'Qualit\u00E9 et \u00E9tat du logement (plus \u00E9lev\u00E9 = meilleur)',
'Outdoors Sub-domain Score': 'Qualit\u00E9 de l\u2019air et s\u00E9curit\u00E9 routi\u00E8re (plus \u00E9lev\u00E9 = meilleur)',
'Property type': 'Type de bien : individuel, jumelé, mitoyen, appartement ou autre',
'Leasehold/Freehold': 'Indique si le bien est en bail ou en pleine propriété',
'Last known price': 'Dernier prix de vente enregistré au Land Registry',
'Estimated current price': 'Estimation du prix actuel ajusté à linflation',
'Asking price': 'Prix demandé pour les biens actuellement en vente',
'Price per sqm': 'Prix de vente divisé par la surface totale',
'Est. price per sqm': 'Prix actuel estimé divisé par la surface totale',
'Asking price per sqm': 'Prix demandé divisé par la surface totale',
'Estimated monthly rent': 'Loyer mensuel privé médian pour le secteur',
'Asking rent (monthly)': 'Loyer mensuel affiché pour les biens en location',
'Total floor area (sqm)': 'Surface intérieure issue du diagnostic EPC',
'Number of bedrooms & living rooms': 'Nombre de pièces habitables selon le diagnostic EPC',
'Bedrooms': 'Nombre de chambres selon lannonce en ligne',
'Bathrooms': 'Nombre de salles de bain selon lannonce en ligne',
'Construction year': 'Année de construction estimée selon lEPC',
'Date of last transaction': 'Date de la dernière vente enregistrée au Land Registry',
'Listing date': 'Date de première mise en ligne du bien',
'Former council house': 'Indique si le bien a été répertorié comme logement social',
'Current energy rating': 'Classement énergétique EPC actuel (A = meilleur, G = pire)',
'Potential energy rating': 'Classement EPC potentiel si toutes les améliorations recommandées étaient réalisées',
'Interior height (m)': 'Hauteur moyenne détage selon le diagnostic EPC',
'Distance to nearest train or tube station (km)': 'Distance à la gare ou station de métro la plus proche',
'Good+ primary schools within 2km': 'Écoles primaires notées Bien ou Excellent par Ofsted dans un rayon de 2 km',
'Good+ secondary schools within 2km': 'Collèges/lycées notés Bien ou Excellent par Ofsted dans un rayon de 2 km',
'Good+ primary schools within 5km': 'Écoles primaires notées Bien ou Excellent par Ofsted dans un rayon de 5 km',
'Good+ secondary schools within 5km': 'Collèges/lycées notés Bien ou Excellent par Ofsted dans un rayon de 5 km',
'Education, Skills and Training Score': 'Score de qualité éducative du secteur (plus élevé = meilleur)',
'Income Score (rate)': 'Taux de précarité de revenu, inversé (plus élevé = moins précaire)',
'Employment Score (rate)': 'Taux de précarité demploi, inversé (plus élevé = moins précaire)',
'Health Deprivation and Disability Score': 'Score de santé et handicap (plus élevé = meilleurs résultats)',
'Living Environment Score': 'Qualité de lenvironnement intérieur et extérieur (plus élevé = meilleur)',
'Indoors Sub-domain Score': 'Qualité et état du logement (plus élevé = meilleur)',
'Outdoors Sub-domain Score': 'Qualité de lair et sécurité routière (plus élevé = meilleur)',
'Serious crime per 1k residents (avg/yr)': 'Taux de crimes graves pour 1 000 habitants par an',
'Minor crime per 1k residents (avg/yr)': 'Taux de d\u00E9lits mineurs pour 1 000 habitants par an',
'Serious crime (avg/yr)': 'Agr\u00E9gat des cat\u00E9gories de crimes graves par an',
'Minor crime (avg/yr)': 'Agr\u00E9gat des cat\u00E9gories de d\u00E9lits mineurs par an',
'Minor crime per 1k residents (avg/yr)': 'Taux de délits mineurs pour 1 000 habitants par an',
'Serious crime (avg/yr)': 'Agrégat des catégories de crimes graves par an',
'Minor crime (avg/yr)': 'Agrégat des catégories de délits mineurs par an',
'Violence and sexual offences (avg/yr)': 'Moyenne annuelle des violences et infractions sexuelles dans le secteur',
'Burglary (avg/yr)': 'Moyenne annuelle des cambriolages dans le secteur',
'Robbery (avg/yr)': 'Moyenne annuelle des vols avec violence dans le secteur',
'Vehicle crime (avg/yr)': 'Moyenne annuelle des crimes li\u00E9s aux v\u00E9hicules dans le secteur',
'Vehicle crime (avg/yr)': 'Moyenne annuelle des crimes liés aux véhicules dans le secteur',
'Anti-social behaviour (avg/yr)': 'Moyenne annuelle des comportements antisociaux dans le secteur',
'Criminal damage and arson (avg/yr)': 'Moyenne annuelle des d\u00E9gradations et incendies criminels dans le secteur',
'Criminal damage and arson (avg/yr)': 'Moyenne annuelle des dégradations et incendies criminels dans le secteur',
'Other theft (avg/yr)': 'Moyenne annuelle des autres vols dans le secteur',
'Theft from the person (avg/yr)': 'Moyenne annuelle des vols \u00E0 la personne dans le secteur',
'Shoplifting (avg/yr)': 'Moyenne annuelle des vols \u00E0 l\u2019\u00E9talage dans le secteur',
'Bicycle theft (avg/yr)': 'Moyenne annuelle des vols de v\u00E9los dans le secteur',
'Drugs (avg/yr)': 'Moyenne annuelle des infractions li\u00E9es aux stup\u00E9fiants dans le secteur',
'Possession of weapons (avg/yr)': 'Moyenne annuelle des infractions de possession d\u2019armes dans le secteur',
'Public order (avg/yr)': 'Moyenne annuelle des troubles \u00E0 l\u2019ordre public dans le secteur',
'Theft from the person (avg/yr)': 'Moyenne annuelle des vols à la personne dans le secteur',
'Shoplifting (avg/yr)': 'Moyenne annuelle des vols à létalage dans le secteur',
'Bicycle theft (avg/yr)': 'Moyenne annuelle des vols de vélos dans le secteur',
'Drugs (avg/yr)': 'Moyenne annuelle des infractions liées aux stupéfiants dans le secteur',
'Possession of weapons (avg/yr)': 'Moyenne annuelle des infractions de possession darmes dans le secteur',
'Public order (avg/yr)': 'Moyenne annuelle des troubles à lordre public dans le secteur',
'Other crime (avg/yr)': 'Moyenne annuelle des autres crimes dans le secteur',
'Median age': '\u00C2ge m\u00E9dian de la population locale',
'% White': 'Pourcentage de la population se d\u00E9clarant Blanche',
'% South Asian': 'Pourcentage de la population se d\u00E9clarant Sud-Asiatique',
'% Black': 'Pourcentage de la population se d\u00E9clarant Noire',
'% East Asian': 'Pourcentage de la population se d\u00E9clarant Est-Asiatique',
'% Mixed': 'Pourcentage de la population se d\u00E9clarant M\u00E9tisse ou de plusieurs groupes ethniques',
'% Other': 'Pourcentage de la population se d\u00E9clarant d\u2019un autre groupe ethnique',
'Median age': 'Âge médian de la population locale',
'% White': 'Pourcentage de la population se déclarant Blanche',
'% South Asian': 'Pourcentage de la population se déclarant Sud-Asiatique',
'% Black': 'Pourcentage de la population se déclarant Noire',
'% East Asian': 'Pourcentage de la population se déclarant Est-Asiatique',
'% Mixed': 'Pourcentage de la population se déclarant Métisse ou de plusieurs groupes ethniques',
'% Other': 'Pourcentage de la population se déclarant dun autre groupe ethnique',
'Distance to nearest park (km)': 'Distance au parc ou espace vert le plus proche',
'Number of parks within 2km': 'Nombre de parcs et espaces verts \u00E0 moins de 2 km',
'Number of restaurants within 2km': 'Nombre de restaurants et caf\u00E9s \u00E0 moins de 2 km',
'Number of grocery shops and supermarkets within 2km': 'Nombre d\u2019\u00E9piceries et supermarch\u00E9s \u00E0 moins de 2 km',
'Noise (dB)': 'Niveau de bruit routier au code postal en d\u00E9cibels (Lden)',
'Max available download speed (Mbps)': 'D\u00E9bit descendant maximal disponible au code postal',
'Number of parks within 2km': 'Nombre de parcs et espaces verts à moins de 2 km',
'Number of restaurants within 2km': 'Nombre de restaurants et cafés à moins de 2 km',
'Number of grocery shops and supermarkets within 2km': 'Nombre dépiceries et supermarchés à moins de 2 km',
'Noise (dB)': 'Niveau de bruit routier au code postal en décibels (Lden)',
'Max available download speed (Mbps)': 'Débit descendant maximal disponible au code postal',
},
de: {
'Listing status': 'Ob die Immobilie aus historischen Verk\u00E4ufen stammt, aktuell zum Verkauf oder zur Miete steht',
'Property type': 'Immobilientyp: freistehend, Doppelhaush\u00E4lfte, Reihenhaus, Wohnung oder sonstige',
'Listing status': 'Ob die Immobilie aus historischen Verkäufen stammt, aktuell zum Verkauf oder zur Miete steht',
'Property type': 'Immobilientyp: freistehend, Doppelhaushälfte, Reihenhaus, Wohnung oder sonstige',
'Leasehold/Freehold': 'Ob die Immobilie Erbbaurecht oder Volleigentum ist',
'Last known price': 'Letzter Verkaufspreis laut Land Registry',
'Estimated current price': 'Inflationsbereinigter Sch\u00E4tzwert der Immobilie',
'Asking price': 'Angebotspreis f\u00FCr aktuell zum Verkauf stehende Immobilien',
'Price per sqm': 'Verkaufspreis geteilt durch die Gesamtfl\u00E4che',
'Est. price per sqm': 'Gesch\u00E4tzter aktueller Preis geteilt durch die Gesamtfl\u00E4che',
'Asking price per sqm': 'Angebotspreis geteilt durch die Gesamtfl\u00E4che',
'Estimated current price': 'Inflationsbereinigter Schätzwert der Immobilie',
'Asking price': 'Angebotspreis für aktuell zum Verkauf stehende Immobilien',
'Price per sqm': 'Verkaufspreis geteilt durch die Gesamtfläche',
'Est. price per sqm': 'Geschätzter aktueller Preis geteilt durch die Gesamtfläche',
'Asking price per sqm': 'Angebotspreis geteilt durch die Gesamtfläche',
'Estimated monthly rent': 'Mittlere monatliche Privatmiete in der Gegend',
'Asking rent (monthly)': 'Angebotene Monatsmiete f\u00FCr Mietimmobilien',
'Total floor area (sqm)': 'Wohnfl\u00E4che laut EPC-Gutachten',
'Number of bedrooms & living rooms': 'Anzahl bewohnbarer R\u00E4ume laut EPC-Gutachten',
'Asking rent (monthly)': 'Angebotene Monatsmiete für Mietimmobilien',
'Total floor area (sqm)': 'Wohnfläche laut EPC-Gutachten',
'Number of bedrooms & living rooms': 'Anzahl bewohnbarer Räume laut EPC-Gutachten',
'Bedrooms': 'Anzahl Schlafzimmer laut Online-Inserat',
'Bathrooms': 'Anzahl Badezimmer laut Online-Inserat',
'Construction year': 'Gesch\u00E4tztes Baujahr laut EPC',
'Construction year': 'Geschätztes Baujahr laut EPC',
'Date of last transaction': 'Datum des letzten Verkaufs laut Land Registry',
'Listing date': 'Datum der Erstver\u00F6ffentlichung des Inserats',
'Listing date': 'Datum der Erstveröffentlichung des Inserats',
'Former council house': 'Ob die Immobilie jemals als Sozialbau erfasst wurde',
'Current energy rating': 'Aktuelle EPC-Energieeffizienzklasse (A = beste, G = schlechteste)',
'Potential energy rating': 'Potenzielle EPC-Klasse bei Umsetzung aller empfohlenen Ma\u00DFnahmen',
'Interior height (m)': 'Durchschnittliche Geschossh\u00F6he laut EPC-Gutachten',
'Distance to nearest train or tube station (km)': 'Entfernung zum n\u00E4chsten Bahn- oder U-Bahnhof',
'Train or tube stations within 1km': 'Anzahl Bahn- oder U-Bahnh\u00F6fe im Umkreis von 1 km',
'Potential energy rating': 'Potenzielle EPC-Klasse bei Umsetzung aller empfohlenen Maßnahmen',
'Interior height (m)': 'Durchschnittliche Geschosshöhe laut EPC-Gutachten',
'Distance to nearest train or tube station (km)': 'Entfernung zum nächsten Bahn- oder U-Bahnhof',
'Good+ primary schools within 2km': 'Von Ofsted mit Gut oder Hervorragend bewertete Grundschulen im Umkreis von 2 km',
'Good+ secondary schools within 2km': 'Von Ofsted mit Gut oder Hervorragend bewertete weiterf\u00FChrende Schulen im Umkreis von 2 km',
'Good+ secondary schools within 2km': 'Von Ofsted mit Gut oder Hervorragend bewertete weiterführende Schulen im Umkreis von 2 km',
'Good+ primary schools within 5km': 'Von Ofsted mit Gut oder Hervorragend bewertete Grundschulen im Umkreis von 5 km',
'Good+ secondary schools within 5km': 'Von Ofsted mit Gut oder Hervorragend bewertete weiterf\u00FChrende Schulen im Umkreis von 5 km',
'Education, Skills and Training Score': 'Bildungsqualit\u00E4tsscore der Gegend (h\u00F6her = besser)',
'Income Score (rate)': 'Einkommensbenachteiligungsrate, invertiert (h\u00F6her = weniger benachteiligt)',
'Employment Score (rate)': 'Besch\u00E4ftigungsbenachteiligungsrate, invertiert (h\u00F6her = weniger benachteiligt)',
'Health Deprivation and Disability Score': 'Gesundheits- und Behinderungsscore (h\u00F6her = bessere Ergebnisse)',
'Living Environment Score': 'Qualit\u00E4t der Innen- und Au\u00DFenumgebung (h\u00F6her = besser)',
'Indoors Sub-domain Score': 'Wohnqualit\u00E4t und -zustand (h\u00F6her = besser)',
'Outdoors Sub-domain Score': 'Luftqualit\u00E4t und Verkehrssicherheit (h\u00F6her = besser)',
'Good+ secondary schools within 5km': 'Von Ofsted mit Gut oder Hervorragend bewertete weiterführende Schulen im Umkreis von 5 km',
'Education, Skills and Training Score': 'Bildungsqualitätsscore der Gegend (höher = besser)',
'Income Score (rate)': 'Einkommensbenachteiligungsrate, invertiert (höher = weniger benachteiligt)',
'Employment Score (rate)': 'Beschäftigungsbenachteiligungsrate, invertiert (höher = weniger benachteiligt)',
'Health Deprivation and Disability Score': 'Gesundheits- und Behinderungsscore (höher = bessere Ergebnisse)',
'Living Environment Score': 'Qualität der Innen- und Außenumgebung (höher = besser)',
'Indoors Sub-domain Score': 'Wohnqualität und -zustand (höher = besser)',
'Outdoors Sub-domain Score': 'Luftqualität und Verkehrssicherheit (höher = besser)',
'Serious crime per 1k residents (avg/yr)': 'Rate schwerer Straftaten pro 1.000 Einwohner pro Jahr',
'Minor crime per 1k residents (avg/yr)': 'Rate leichter Straftaten pro 1.000 Einwohner pro Jahr',
'Serious crime (avg/yr)': 'Summe der schweren Straftaten-Kategorien pro Jahr',
'Minor crime (avg/yr)': 'Summe der leichten Straftaten-Kategorien pro Jahr',
'Violence and sexual offences (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Gewalt- und Sexualdelikte in der Gegend',
'Burglary (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Einbr\u00FCche in der Gegend',
'Robbery (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Raub\u00FCberf\u00E4lle in der Gegend',
'Vehicle crime (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Fahrzeugkriminalit\u00E4t in der Gegend',
'Anti-social behaviour (avg/yr)': 'J\u00E4hrlicher Durchschnitt des antisozialen Verhaltens in der Gegend',
'Criminal damage and arson (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Sachbesch\u00E4digungen und Brandstiftungen in der Gegend',
'Other theft (avg/yr)': 'J\u00E4hrlicher Durchschnitt des sonstigen Diebstahls in der Gegend',
'Theft from the person (avg/yr)': 'J\u00E4hrlicher Durchschnitt des Taschendiebstahls in der Gegend',
'Shoplifting (avg/yr)': 'J\u00E4hrlicher Durchschnitt des Ladendiebstahls in der Gegend',
'Bicycle theft (avg/yr)': 'J\u00E4hrlicher Durchschnitt des Fahrraddiebstahls in der Gegend',
'Drugs (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Drogendelikte in der Gegend',
'Possession of weapons (avg/yr)': 'J\u00E4hrlicher Durchschnitt der Waffenbesitzdelikte in der Gegend',
'Public order (avg/yr)': 'J\u00E4hrlicher Durchschnitt der St\u00F6rungen der \u00F6ffentlichen Ordnung in der Gegend',
'Other crime (avg/yr)': 'J\u00E4hrlicher Durchschnitt sonstiger Straftaten in der Gegend',
'Median age': 'Medianalter der lokalen Bev\u00F6lkerung',
'% White': 'Anteil der Bev\u00F6lkerung, der sich als Wei\u00DF identifiziert',
'% South Asian': 'Anteil der Bev\u00F6lkerung, der sich als S\u00FCdasiatisch identifiziert',
'% Black': 'Anteil der Bev\u00F6lkerung, der sich als Schwarz identifiziert',
'% East Asian': 'Anteil der Bev\u00F6lkerung, der sich als Ostasiatisch identifiziert',
'% Mixed': 'Anteil der Bev\u00F6lkerung, der sich als gemischt oder mehreren ethnischen Gruppen zugeh\u00F6rig identifiziert',
'% Other': 'Anteil der Bev\u00F6lkerung, der sich einer anderen ethnischen Gruppe zuordnet',
'Distance to nearest park (km)': 'Entfernung zum n\u00E4chsten Park oder Gr\u00FCnfl\u00E4che',
'Number of parks within 2km': 'Anzahl Parks und Gr\u00FCnfl\u00E4chen im Umkreis von 2 km',
'Number of restaurants within 2km': 'Anzahl Restaurants und Caf\u00E9s im Umkreis von 2 km',
'Number of grocery shops and supermarkets within 2km': 'Anzahl Lebensmittelgesch\u00E4fte und Superm\u00E4rkte im Umkreis von 2 km',
'Noise (dB)': 'Stra\u00DFenl\u00E4rmpegel an der Postleitzahl in Dezibel (Lden)',
'Max available download speed (Mbps)': 'Maximal verf\u00FCgbare Breitband-Downloadgeschwindigkeit an der Postleitzahl',
'Violence and sexual offences (avg/yr)': 'Jährlicher Durchschnitt der Gewalt- und Sexualdelikte in der Gegend',
'Burglary (avg/yr)': 'Jährlicher Durchschnitt der Einbrüche in der Gegend',
'Robbery (avg/yr)': 'Jährlicher Durchschnitt der Raubüberfälle in der Gegend',
'Vehicle crime (avg/yr)': 'Jährlicher Durchschnitt der Fahrzeugkriminalität in der Gegend',
'Anti-social behaviour (avg/yr)': 'Jährlicher Durchschnitt des antisozialen Verhaltens in der Gegend',
'Criminal damage and arson (avg/yr)': 'Jährlicher Durchschnitt der Sachbeschädigungen und Brandstiftungen in der Gegend',
'Other theft (avg/yr)': 'Jährlicher Durchschnitt des sonstigen Diebstahls in der Gegend',
'Theft from the person (avg/yr)': 'Jährlicher Durchschnitt des Taschendiebstahls in der Gegend',
'Shoplifting (avg/yr)': 'Jährlicher Durchschnitt des Ladendiebstahls in der Gegend',
'Bicycle theft (avg/yr)': 'Jährlicher Durchschnitt des Fahrraddiebstahls in der Gegend',
'Drugs (avg/yr)': 'Jährlicher Durchschnitt der Drogendelikte in der Gegend',
'Possession of weapons (avg/yr)': 'Jährlicher Durchschnitt der Waffenbesitzdelikte in der Gegend',
'Public order (avg/yr)': 'Jährlicher Durchschnitt der Störungen der öffentlichen Ordnung in der Gegend',
'Other crime (avg/yr)': 'Jährlicher Durchschnitt sonstiger Straftaten in der Gegend',
'Median age': 'Medianalter der lokalen Bevölkerung',
'% White': 'Anteil der Bevölkerung, der sich als Weiß identifiziert',
'% South Asian': 'Anteil der Bevölkerung, der sich als Südasiatisch identifiziert',
'% Black': 'Anteil der Bevölkerung, der sich als Schwarz identifiziert',
'% East Asian': 'Anteil der Bevölkerung, der sich als Ostasiatisch identifiziert',
'% Mixed': 'Anteil der Bevölkerung, der sich als gemischt oder mehreren ethnischen Gruppen zugehörig identifiziert',
'% Other': 'Anteil der Bevölkerung, der sich einer anderen ethnischen Gruppe zuordnet',
'Distance to nearest park (km)': 'Entfernung zum nächsten Park oder Grünfläche',
'Number of parks within 2km': 'Anzahl Parks und Grünflächen im Umkreis von 2 km',
'Number of restaurants within 2km': 'Anzahl Restaurants und Cafés im Umkreis von 2 km',
'Number of grocery shops and supermarkets within 2km': 'Anzahl Lebensmittelgeschäfte und Supermärkte im Umkreis von 2 km',
'Noise (dB)': 'Straßenlärmpegel an der Postleitzahl in Dezibel (Lden)',
'Max available download speed (Mbps)': 'Maximal verfügbare Breitband-Downloadgeschwindigkeit an der Postleitzahl',
},
zh: {
'Listing status': '\u8BE5\u623F\u4EA7\u662F\u5386\u53F2\u9500\u552E\u3001\u5F53\u524D\u5728\u552E\u8FD8\u662F\u51FA\u79DF',
'Property type': '\u623F\u4EA7\u7C7B\u578B\uFF1A\u72EC\u7ACB\u5F0F\u3001\u534A\u72EC\u7ACB\u5F0F\u3001\u8054\u6392\u3001\u516C\u5BD3\u6216\u5176\u4ED6',
'Leasehold/Freehold': '\u8BE5\u623F\u4EA7\u662F\u79DF\u8D41\u4EA7\u6743\u8FD8\u662F\u6C38\u4E45\u4EA7\u6743',
'Last known price': 'Land Registry\u8BB0\u5F55\u7684\u6700\u8FD1\u4E00\u6B21\u552E\u4EF7',
'Estimated current price': '\u7ECF\u901A\u80C0\u8C03\u6574\u540E\u7684\u5F53\u524D\u4F30\u8BA1\u4EF7\u503C',
'Asking price': '\u5F53\u524D\u5728\u552E\u623F\u4EA7\u7684\u6302\u724C\u4EF7',
'Price per sqm': '\u552E\u4EF7\u9664\u4EE5\u603B\u5EFA\u7B51\u9762\u79EF',
'Est. price per sqm': '\u4F30\u8BA1\u5F53\u524D\u4EF7\u683C\u9664\u4EE5\u603B\u5EFA\u7B51\u9762\u79EF',
'Asking price per sqm': '\u6302\u724C\u4EF7\u9664\u4EE5\u603B\u5EFA\u7B51\u9762\u79EF',
'Estimated monthly rent': '\u5F53\u5730\u79C1\u4EBA\u79DF\u8D41\u7684\u4E2D\u4F4D\u6708\u79DF',
'Asking rent (monthly)': '\u5F53\u524D\u51FA\u79DF\u623F\u4EA7\u7684\u6302\u724C\u6708\u79DF',
'Total floor area (sqm)': 'EPC\u8BC4\u4F30\u7684\u5BA4\u5185\u5EFA\u7B51\u9762\u79EF',
'Number of bedrooms & living rooms': 'EPC\u8BC4\u4F30\u7684\u5B9C\u5C45\u623F\u95F4\u6570',
'Bedrooms': '\u5728\u7EBF\u623F\u6E90\u4E2D\u7684\u5367\u5BA4\u6570\u91CF',
'Bathrooms': '\u5728\u7EBF\u623F\u6E90\u4E2D\u7684\u6D74\u5BA4\u6570\u91CF',
'Construction year': 'EPC\u8BC4\u4F30\u7684\u5EFA\u9020\u5E74\u4EFD',
'Date of last transaction': 'Land Registry\u8BB0\u5F55\u7684\u6700\u8FD1\u4E00\u6B21\u9500\u552E\u65E5\u671F',
'Listing date': '\u623F\u4EA7\u9996\u6B21\u5728\u7EBF\u4E0A\u5E02\u7684\u65E5\u671F',
'Former council house': '\u8BE5\u623F\u4EA7\u662F\u5426\u66FE\u88AB\u8BB0\u5F55\u4E3A\u516C\u5171\u4F4F\u623F',
'Current energy rating': '\u5F53\u524DEPC\u80FD\u6548\u8BC4\u7EA7\uFF08A = \u6700\u4F73\uFF0CG = \u6700\u5DEE\uFF09',
'Potential energy rating': '\u5B9E\u65BD\u6240\u6709\u5EFA\u8BAE\u6539\u8FDB\u540E\u7684\u6F5C\u5728EPC\u8BC4\u7EA7',
'Interior height (m)': 'EPC\u8BC4\u4F30\u7684\u5E73\u5747\u5C42\u9AD8',
'Distance to nearest train or tube station (km)': '\u5230\u6700\u8FD1\u706B\u8F66\u6216\u5730\u94C1\u7AD9\u7684\u8DDD\u79BB',
'Train or tube stations within 1km': '1\u516C\u91CC\u5185\u706B\u8F66\u6216\u5730\u94C1\u7AD9\u7684\u6570\u91CF',
'Good+ primary schools within 2km': 'Ofsted\u8BC4\u4E3A\u826F\u597D\u6216\u4F18\u79C0\u76842\u516C\u91CC\u5185\u5C0F\u5B66',
'Good+ secondary schools within 2km': 'Ofsted\u8BC4\u4E3A\u826F\u597D\u6216\u4F18\u79C0\u76842\u516C\u91CC\u5185\u4E2D\u5B66',
'Good+ primary schools within 5km': 'Ofsted\u8BC4\u4E3A\u826F\u597D\u6216\u4F18\u79C0\u76845\u516C\u91CC\u5185\u5C0F\u5B66',
'Good+ secondary schools within 5km': 'Ofsted\u8BC4\u4E3A\u826F\u597D\u6216\u4F18\u79C0\u76845\u516C\u91CC\u5185\u4E2D\u5B66',
'Education, Skills and Training Score': '\u5F53\u5730\u6559\u80B2\u8D28\u91CF\u5F97\u5206\uFF08\u8D8A\u9AD8\u8D8A\u597D\uFF09',
'Income Score (rate)': '\u6536\u5165\u8D2B\u56F0\u7387\uFF0C\u53CD\u5411\u6307\u6807\uFF08\u8D8A\u9AD8\u8D8A\u4E0D\u8D2B\u56F0\uFF09',
'Employment Score (rate)': '\u5C31\u4E1A\u8D2B\u56F0\u7387\uFF0C\u53CD\u5411\u6307\u6807\uFF08\u8D8A\u9AD8\u8D8A\u4E0D\u8D2B\u56F0\uFF09',
'Health Deprivation and Disability Score': '\u5065\u5EB7\u4E0E\u6B8B\u969C\u5F97\u5206\uFF08\u8D8A\u9AD8\u5065\u5EB7\u72B6\u51B5\u8D8A\u597D\uFF09',
'Living Environment Score': '\u5BA4\u5185\u5916\u73AF\u5883\u8D28\u91CF\uFF08\u8D8A\u9AD8\u8D8A\u597D\uFF09',
'Indoors Sub-domain Score': '\u4F4F\u623F\u8D28\u91CF\u548C\u72B6\u51B5\uFF08\u8D8A\u9AD8\u8D8A\u597D\uFF09',
'Outdoors Sub-domain Score': '\u7A7A\u6C14\u8D28\u91CF\u548C\u9053\u8DEF\u5B89\u5168\uFF08\u8D8A\u9AD8\u8D8A\u597D\uFF09',
'Serious crime per 1k residents (avg/yr)': '\u6BCF\u5343\u4EBA\u6BCF\u5E74\u4E25\u91CD\u72AF\u7F6A\u7387',
'Minor crime per 1k residents (avg/yr)': '\u6BCF\u5343\u4EBA\u6BCF\u5E74\u8F7B\u5FAE\u72AF\u7F6A\u7387',
'Serious crime (avg/yr)': '\u4E25\u91CD\u72AF\u7F6A\u7C7B\u522B\u5E74\u5EA6\u603B\u8BA1',
'Minor crime (avg/yr)': '\u8F7B\u5FAE\u72AF\u7F6A\u7C7B\u522B\u5E74\u5EA6\u603B\u8BA1',
'Violence and sexual offences (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u66B4\u529B\u548C\u6027\u72AF\u7F6A\u6570',
'Burglary (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u5165\u5BA4\u76D7\u7A83\u6570',
'Robbery (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u62A2\u52AB\u6570',
'Vehicle crime (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u8F66\u8F86\u72AF\u7F6A\u6570',
'Anti-social behaviour (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u53CD\u793E\u4F1A\u884C\u4E3A\u6570',
'Criminal damage and arson (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u5211\u4E8B\u6BC1\u574F\u548C\u7EB5\u706B\u6570',
'Other theft (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u5176\u4ED6\u76D7\u7A83\u6570',
'Theft from the person (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u4EBA\u8EAB\u76D7\u7A83\u6570',
'Shoplifting (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u5546\u5E97\u76D7\u7A83\u6570',
'Bicycle theft (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u81EA\u884C\u8F66\u76D7\u7A83\u6570',
'Drugs (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u6BD2\u54C1\u72AF\u7F6A\u6570',
'Possession of weapons (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u975E\u6CD5\u6301\u6709\u6B66\u5668\u6570',
'Public order (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u6270\u4E71\u516C\u5171\u79E9\u5E8F\u6570',
'Other crime (avg/yr)': '\u8BE5\u5730\u533A\u5E74\u5747\u5176\u4ED6\u72AF\u7F6A\u6570',
'Median age': '\u5F53\u5730\u4EBA\u53E3\u7684\u4E2D\u4F4D\u5E74\u9F84',
'% White': '\u767D\u4EBA\u4EBA\u53E3\u6BD4\u4F8B',
'% South Asian': '\u5357\u4E9A\u88D4\u4EBA\u53E3\u6BD4\u4F8B',
'% Black': '\u9ED1\u4EBA\u4EBA\u53E3\u6BD4\u4F8B',
'% East Asian': '\u4E1C\u4E9A\u88D4\u4EBA\u53E3\u6BD4\u4F8B',
'% Mixed': '\u6DF7\u8840\u6216\u591A\u65CF\u88D4\u4EBA\u53E3\u6BD4\u4F8B',
'% Other': '\u5176\u4ED6\u65CF\u88D4\u4EBA\u53E3\u6BD4\u4F8B',
'Distance to nearest park (km)': '\u5230\u6700\u8FD1\u516C\u56ED\u6216\u7EFF\u5730\u7684\u8DDD\u79BB',
'Number of parks within 2km': '2\u516C\u91CC\u5185\u516C\u56ED\u548C\u7EFF\u5730\u6570\u91CF',
'Number of restaurants within 2km': '2\u516C\u91CC\u5185\u9910\u5385\u548C\u5496\u5561\u9986\u6570\u91CF',
'Number of grocery shops and supermarkets within 2km': '2\u516C\u91CC\u5185\u98DF\u54C1\u5E97\u548C\u8D85\u5E02\u6570\u91CF',
'Noise (dB)': '\u8BE5\u90AE\u7F16\u7684\u9053\u8DEF\u566A\u97F3\u6C34\u5E73\uFF08\u5206\u8D1D\uFF0CLden\uFF09',
'Max available download speed (Mbps)': '\u8BE5\u90AE\u7F16\u53EF\u7528\u7684\u6700\u5927\u5BBD\u5E26\u4E0B\u8F7D\u901F\u5EA6',
'Listing status': '该房产是历史销售、当前在售还是出租',
'Property type': '房产类型:独立式、半独立式、联排、公寓或其他',
'Leasehold/Freehold': '该房产是租赁产权还是永久产权',
'Last known price': 'Land Registry记录的最近一次售价',
'Estimated current price': '经通胀调整后的当前估计价值',
'Asking price': '当前在售房产的挂牌价',
'Price per sqm': '售价除以总建筑面积',
'Est. price per sqm': '估计当前价格除以总建筑面积',
'Asking price per sqm': '挂牌价除以总建筑面积',
'Estimated monthly rent': '当地私人租赁的中位月租',
'Asking rent (monthly)': '当前出租房产的挂牌月租',
'Total floor area (sqm)': 'EPC评估的室内建筑面积',
'Number of bedrooms & living rooms': 'EPC评估的宜居房间数',
'Bedrooms': '在线房源中的卧室数量',
'Bathrooms': '在线房源中的浴室数量',
'Construction year': 'EPC评估的建造年份',
'Date of last transaction': 'Land Registry记录的最近一次销售日期',
'Listing date': '房产首次在线上市的日期',
'Former council house': '该房产是否曾被记录为公共住房',
'Current energy rating': '当前EPC能效评级A = 最佳G = 最差)',
'Potential energy rating': '实施所有建议改进后的潜在EPC评级',
'Interior height (m)': 'EPC评估的平均层高',
'Distance to nearest train or tube station (km)': '到最近火车或地铁站的距离',
'Good+ primary schools within 2km': 'Ofsted评为良好或优秀的2公里内小学',
'Good+ secondary schools within 2km': 'Ofsted评为良好或优秀的2公里内中学',
'Good+ primary schools within 5km': 'Ofsted评为良好或优秀的5公里内小学',
'Good+ secondary schools within 5km': 'Ofsted评为良好或优秀的5公里内中学',
'Education, Skills and Training Score': '当地教育质量得分(越高越好)',
'Income Score (rate)': '收入贫困率,反向指标(越高越不贫困)',
'Employment Score (rate)': '就业贫困率,反向指标(越高越不贫困)',
'Health Deprivation and Disability Score': '健康与残障得分(越高健康状况越好)',
'Living Environment Score': '室内外环境质量(越高越好)',
'Indoors Sub-domain Score': '住房质量和状况(越高越好)',
'Outdoors Sub-domain Score': '空气质量和道路安全(越高越好)',
'Serious crime per 1k residents (avg/yr)': '每千人每年严重犯罪率',
'Minor crime per 1k residents (avg/yr)': '每千人每年轻微犯罪率',
'Serious crime (avg/yr)': '严重犯罪类别年度总计',
'Minor crime (avg/yr)': '轻微犯罪类别年度总计',
'Violence and sexual offences (avg/yr)': '该地区年均暴力和性犯罪数',
'Burglary (avg/yr)': '该地区年均入室盗窃数',
'Robbery (avg/yr)': '该地区年均抢劫数',
'Vehicle crime (avg/yr)': '该地区年均车辆犯罪数',
'Anti-social behaviour (avg/yr)': '该地区年均反社会行为数',
'Criminal damage and arson (avg/yr)': '该地区年均刑事毁坏和纵火数',
'Other theft (avg/yr)': '该地区年均其他盗窃数',
'Theft from the person (avg/yr)': '该地区年均人身盗窃数',
'Shoplifting (avg/yr)': '该地区年均商店盗窃数',
'Bicycle theft (avg/yr)': '该地区年均自行车盗窃数',
'Drugs (avg/yr)': '该地区年均毒品犯罪数',
'Possession of weapons (avg/yr)': '该地区年均非法持有武器数',
'Public order (avg/yr)': '该地区年均扰乱公共秩序数',
'Other crime (avg/yr)': '该地区年均其他犯罪数',
'Median age': '当地人口的中位年龄',
'% White': '白人人口比例',
'% South Asian': '南亚裔人口比例',
'% Black': '黑人人口比例',
'% East Asian': '东亚裔人口比例',
'% Mixed': '混血或多族裔人口比例',
'% Other': '其他族裔人口比例',
'Distance to nearest park (km)': '到最近公园或绿地的距离',
'Number of parks within 2km': '2公里内公园和绿地数量',
'Number of restaurants within 2km': '2公里内餐厅和咖啡馆数量',
'Number of grocery shops and supermarkets within 2km': '2公里内食品店和超市数量',
'Noise (dB)': '该邮编的道路噪音水平分贝Lden',
'Max available download speed (Mbps)': '该邮编可用的最大宽带下载速度',
},
hu: {
'Listing status': 'Az ingatlan kor\u00E1bbi elad\u00E1sb\u00F3l sz\u00E1rmazik, jelenleg elad\u00F3 vagy kiad\u00F3',
'Property type': 'Ingatlant\u00EDpus: k\u00FCl\u00F6n\u00E1ll\u00F3, ikerh\u00E1z, sorh\u00E1z, lak\u00E1s vagy egy\u00E9b',
'Leasehold/Freehold': 'Az ingatlan b\u00E9rleti jog\u00FA vagy teljes tulajdon\u00FA',
'Last known price': 'A Land Registry-ben r\u00F6gz\u00EDtett utols\u00F3 elad\u00E1si \u00E1r',
'Estimated current price': 'Infl\u00E1ci\u00F3val korrig\u00E1lt becs\u00FClt jelenlegi \u00E9rt\u00E9k',
'Asking price': 'A jelenleg elad\u00E1sra k\u00EDn\u00E1lt ingatlanok ir\u00E1ny\u00E1ra',
'Price per sqm': 'Elad\u00E1si \u00E1r osztva az \u00F6sszes alapter\u00FClettel',
'Est. price per sqm': 'Becs\u00FClt jelenlegi \u00E1r osztva az \u00F6sszes alapter\u00FClettel',
'Asking price per sqm': 'Ir\u00E1ny\u00E1r osztva az \u00F6sszes alapter\u00FClettel',
'Estimated monthly rent': 'A k\u00F6rny\u00E9k medi\u00E1n havi mag\u00E1nb\u00E9rleti d\u00EDja',
'Asking rent (monthly)': 'A kiad\u00F3 ingatlanok hirdetett havi b\u00E9rleti d\u00EDja',
'Total floor area (sqm)': 'Az EPC felm\u00E9r\u00E9sb\u0151l sz\u00E1rmaz\u00F3 bels\u0151 alapter\u00FClet',
'Number of bedrooms & living rooms': 'Lak\u00F3szob\u00E1k sz\u00E1ma az EPC felm\u00E9r\u00E9s alapj\u00E1n',
'Bedrooms': 'H\u00E1l\u00F3szob\u00E1k sz\u00E1ma az online hirdet\u00E9s szerint',
'Bathrooms': 'F\u00FCrd\u0151szob\u00E1k sz\u00E1ma az online hirdet\u00E9s szerint',
'Construction year': 'Becs\u00FClt \u00E9p\u00EDt\u00E9si \u00E9v az EPC alapj\u00E1n',
'Date of last transaction': 'Az utols\u00F3 elad\u00E1s d\u00E1tuma a Land Registry szerint',
'Listing date': 'Az ingatlan els\u0151 online megjelen\u00E9s\u00E9nek d\u00E1tuma',
'Former council house': 'Az ingatlan szerepelt-e valaha \u00F6nkorm\u00E1nyzati lak\u00E1sk\u00E9nt',
'Current energy rating': 'Jelenlegi EPC energiabesorol\u00E1s (A = legjobb, G = legrosszabb)',
'Potential energy rating': 'Potenci\u00E1lis EPC besorol\u00E1s az \u00F6sszes javasolt fejleszt\u00E9s elv\u00E9gz\u00E9se ut\u00E1n',
'Interior height (m)': '\u00C1tlagos belmagass\u00E1g az EPC felm\u00E9r\u00E9s alapj\u00E1n',
'Distance to nearest train or tube station (km)': 'T\u00E1vols\u00E1g a legk\u00F6zelebbi vas\u00FAt- vagy metr\u00F3\u00E1llom\u00E1sig',
'Train or tube stations within 1km': 'Vas\u00FAt- vagy metr\u00F3\u00E1llom\u00E1sok sz\u00E1ma 1 km-en bel\u00FCl',
'Good+ primary schools within 2km': 'Ofsted \u00E1ltal J\u00F3 vagy Kiv\u00E1l\u00F3 min\u0151s\u00EDt\u00E9s\u0171 \u00E1ltal\u00E1nos iskol\u00E1k 2 km-en bel\u00FCl',
'Good+ secondary schools within 2km': 'Ofsted \u00E1ltal J\u00F3 vagy Kiv\u00E1l\u00F3 min\u0151s\u00EDt\u00E9s\u0171 k\u00F6z\u00E9piskol\u00E1k 2 km-en bel\u00FCl',
'Good+ primary schools within 5km': 'Ofsted \u00E1ltal J\u00F3 vagy Kiv\u00E1l\u00F3 min\u0151s\u00EDt\u00E9s\u0171 \u00E1ltal\u00E1nos iskol\u00E1k 5 km-en bel\u00FCl',
'Good+ secondary schools within 5km': 'Ofsted \u00E1ltal J\u00F3 vagy Kiv\u00E1l\u00F3 min\u0151s\u00EDt\u00E9s\u0171 k\u00F6z\u00E9piskol\u00E1k 5 km-en bel\u00FCl',
'Education, Skills and Training Score': 'A k\u00F6rny\u00E9k oktat\u00E1si min\u0151s\u00E9gi pontsz\u00E1ma (magasabb = jobb)',
'Income Score (rate)': 'J\u00F6vedelmi depriv\u00E1ci\u00F3s r\u00E1ta, invert\u00E1lva (magasabb = kev\u00E9sb\u00E9 h\u00E1tr\u00E1nyos)',
'Employment Score (rate)': 'Foglalkoztat\u00E1si depriv\u00E1ci\u00F3s r\u00E1ta, invert\u00E1lva (magasabb = kev\u00E9sb\u00E9 h\u00E1tr\u00E1nyos)',
'Health Deprivation and Disability Score': 'Eg\u00E9szs\u00E9g\u00FCgyi \u00E9s fogyat\u00E9koss\u00E1gi pontsz\u00E1m (magasabb = jobb eredm\u00E9nyek)',
'Living Environment Score': 'Bels\u0151 \u00E9s k\u00FCls\u0151 k\u00F6rnyezet min\u0151s\u00E9ge (magasabb = jobb)',
'Indoors Sub-domain Score': 'Lak\u00E1smin\u0151s\u00E9g \u00E9s \u00E1llapot (magasabb = jobb)',
'Outdoors Sub-domain Score': 'Leveg\u0151min\u0151s\u00E9g \u00E9s k\u00F6zleked\u00E9sbiztons\u00E1g (magasabb = jobb)',
'Serious crime per 1k residents (avg/yr)': 'S\u00FAlyos b\u0171ncselekm\u00E9nyek ar\u00E1nya 1000 lakosra \u00E9vente',
'Minor crime per 1k residents (avg/yr)': 'Kisebb b\u0171ncselekm\u00E9nyek ar\u00E1nya 1000 lakosra \u00E9vente',
'Serious crime (avg/yr)': 'S\u00FAlyos b\u0171ncselekm\u00E9nyi kateg\u00F3ri\u00E1k \u00E9ves \u00F6sszes\u00EDt\u00E9se',
'Minor crime (avg/yr)': 'Kisebb b\u0171ncselekm\u00E9nyi kateg\u00F3ri\u00E1k \u00E9ves \u00F6sszes\u00EDt\u00E9se',
'Violence and sexual offences (avg/yr)': 'Er\u0151szakos \u00E9s szexu\u00E1lis b\u0171ncselekm\u00E9nyek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Burglary (avg/yr)': 'Bet\u00F6r\u00E9sek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Robbery (avg/yr)': 'Rabl\u00E1sok \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Vehicle crime (avg/yr)': 'G\u00E9pj\u00E1rm\u0171vel kapcsolatos b\u0171ncselekm\u00E9nyek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Anti-social behaviour (avg/yr)': 'K\u00F6z\u00F6ss\u00E9gellenes magatart\u00E1s \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Criminal damage and arson (avg/yr)': 'Rong\u00E1l\u00E1s \u00E9s gy\u00FAjtogat\u00E1s \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Other theft (avg/yr)': 'Egy\u00E9b lop\u00E1sok \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Theft from the person (avg/yr)': 'Szem\u00E9lyek elleni lop\u00E1sok \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Shoplifting (avg/yr)': 'Bolti lop\u00E1sok \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Bicycle theft (avg/yr)': 'Ker\u00E9kp\u00E1rlop\u00E1sok \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Drugs (avg/yr)': 'K\u00E1b\u00EDt\u00F3szerrel kapcsolatos b\u0171ncselekm\u00E9nyek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Possession of weapons (avg/yr)': 'Fegyvertart\u00E1ssal kapcsolatos b\u0171ncselekm\u00E9nyek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Public order (avg/yr)': 'K\u00F6zrend elleni b\u0171ncselekm\u00E9nyek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Other crime (avg/yr)': 'Egy\u00E9b b\u0171ncselekm\u00E9nyek \u00E9ves \u00E1tlaga a k\u00F6rny\u00E9ken',
'Median age': 'A helyi lakoss\u00E1g medi\u00E1n \u00E9letkora',
'% White': 'A feh\u00E9rk\u00E9nt azonos\u00EDtott lakoss\u00E1g ar\u00E1nya',
'% South Asian': 'A d\u00E9l-\u00E1zsiaiként azonos\u00EDtott lakoss\u00E1g ar\u00E1nya',
'% Black': 'A feketek\u00E9nt azonos\u00EDtott lakoss\u00E1g ar\u00E1nya',
'% East Asian': 'A kelet-\u00E1zsiaiként azonos\u00EDtott lakoss\u00E1g ar\u00E1nya',
'% Mixed': 'A vegyes vagy t\u00F6bb etnikai csoporthoz tartoz\u00F3k\u00E9nt azonos\u00EDtott lakoss\u00E1g ar\u00E1nya',
'% Other': 'Az egy\u00E9b etnikai csoportba tartoz\u00F3k\u00E9nt azonos\u00EDtott lakoss\u00E1g ar\u00E1nya',
'Distance to nearest park (km)': 'T\u00E1vols\u00E1g a legk\u00F6zelebbi parkig vagy z\u00F6ldter\u00FCletig',
'Number of parks within 2km': 'Parkok \u00E9s z\u00F6ldter\u00FCletek sz\u00E1ma 2 km-en bel\u00FCl',
'Number of restaurants within 2km': '\u00C9ttermek \u00E9s k\u00E1v\u00E9z\u00F3k sz\u00E1ma 2 km-en bel\u00FCl',
'Number of grocery shops and supermarkets within 2km': '\u00C9lelmiszerboltok \u00E9s szupermarketek sz\u00E1ma 2 km-en bel\u00FCl',
'Noise (dB)': 'K\u00F6z\u00FAti zajszint az ir\u00E1ny\u00EDt\u00F3sz\u00E1mn\u00E1l decibelben (Lden)',
'Max available download speed (Mbps)': 'Az ir\u00E1ny\u00EDt\u00F3sz\u00E1mn\u00E1l el\u00E9rhet\u0151 maxim\u00E1lis sz\u00E9less\u00E1v\u00FA let\u00F6lt\u00E9si sebess\u00E9g',
'Listing status': 'Az ingatlan korábbi eladásból származik, jelenleg eladó vagy kiadó',
'Property type': 'Ingatlantípus: különálló, ikerház, sorház, lakás vagy egyéb',
'Leasehold/Freehold': 'Az ingatlan bérleti jogú vagy teljes tulajdonú',
'Last known price': 'A Land Registry-ben rögzített utolsó eladási ár',
'Estimated current price': 'Inflációval korrigált becsült jelenlegi érték',
'Asking price': 'A jelenleg eladásra kínált ingatlanok irányára',
'Price per sqm': 'Eladási ár osztva az összes alapterülettel',
'Est. price per sqm': 'Becsült jelenlegi ár osztva az összes alapterülettel',
'Asking price per sqm': 'Irányár osztva az összes alapterülettel',
'Estimated monthly rent': 'A környék medián havi magánbérleti díja',
'Asking rent (monthly)': 'A kiadó ingatlanok hirdetett havi bérleti díja',
'Total floor area (sqm)': 'Az EPC felmérésből származó belső alapterület',
'Number of bedrooms & living rooms': 'Lakószobák száma az EPC felmérés alapján',
'Bedrooms': 'Hálószobák száma az online hirdetés szerint',
'Bathrooms': 'Fürdőszobák száma az online hirdetés szerint',
'Construction year': 'Becsült építési év az EPC alapján',
'Date of last transaction': 'Az utolsó eladás dátuma a Land Registry szerint',
'Listing date': 'Az ingatlan első online megjelenésének dátuma',
'Former council house': 'Az ingatlan szerepelt-e valaha önkormányzati lakásként',
'Current energy rating': 'Jelenlegi EPC energiabesorolás (A = legjobb, G = legrosszabb)',
'Potential energy rating': 'Potenciális EPC besorolás az összes javasolt fejlesztés elvégzése után',
'Interior height (m)': 'Átlagos belmagasság az EPC felmérés alapján',
'Distance to nearest train or tube station (km)': 'Távolság a legközelebbi vasút- vagy metróállomásig',
'Good+ primary schools within 2km': 'Ofsted által Jó vagy Kiváló minősítésű általános iskolák 2 km-en belül',
'Good+ secondary schools within 2km': 'Ofsted által Jó vagy Kiváló minősítésű középiskolák 2 km-en belül',
'Good+ primary schools within 5km': 'Ofsted által Jó vagy Kiváló minősítésű általános iskolák 5 km-en belül',
'Good+ secondary schools within 5km': 'Ofsted által Jó vagy Kiváló minősítésű középiskolák 5 km-en belül',
'Education, Skills and Training Score': 'A környék oktatási minőségi pontszáma (magasabb = jobb)',
'Income Score (rate)': 'Jövedelmi deprivációs ráta, invertálva (magasabb = kevésbé hátrányos)',
'Employment Score (rate)': 'Foglalkoztatási deprivációs ráta, invertálva (magasabb = kevésbé hátrányos)',
'Health Deprivation and Disability Score': 'Egészségügyi és fogyatékossági pontszám (magasabb = jobb eredmények)',
'Living Environment Score': 'Belső és külső környezet minősége (magasabb = jobb)',
'Indoors Sub-domain Score': 'Lakásminőség és állapot (magasabb = jobb)',
'Outdoors Sub-domain Score': 'Levegőminőség és közlekedésbiztonság (magasabb = jobb)',
'Serious crime per 1k residents (avg/yr)': 'Súlyos bűncselekmények aránya 1000 lakosra évente',
'Minor crime per 1k residents (avg/yr)': 'Kisebb bűncselekmények aránya 1000 lakosra évente',
'Serious crime (avg/yr)': 'Súlyos bűncselekményi kategóriák éves összesítése',
'Minor crime (avg/yr)': 'Kisebb bűncselekményi kategóriák éves összesítése',
'Violence and sexual offences (avg/yr)': 'Erőszakos és szexuális bűncselekmények éves átlaga a környéken',
'Burglary (avg/yr)': 'Betörések éves átlaga a környéken',
'Robbery (avg/yr)': 'Rablások éves átlaga a környéken',
'Vehicle crime (avg/yr)': 'Gépjárművel kapcsolatos bűncselekmények éves átlaga a környéken',
'Anti-social behaviour (avg/yr)': 'Közösségellenes magatartás éves átlaga a környéken',
'Criminal damage and arson (avg/yr)': 'Rongálás és gyújtogatás éves átlaga a környéken',
'Other theft (avg/yr)': 'Egyéb lopások éves átlaga a környéken',
'Theft from the person (avg/yr)': 'Személyek elleni lopások éves átlaga a környéken',
'Shoplifting (avg/yr)': 'Bolti lopások éves átlaga a környéken',
'Bicycle theft (avg/yr)': 'Kerékpárlopások éves átlaga a környéken',
'Drugs (avg/yr)': 'Kábítószerrel kapcsolatos bűncselekmények éves átlaga a környéken',
'Possession of weapons (avg/yr)': 'Fegyvertartással kapcsolatos bűncselekmények éves átlaga a környéken',
'Public order (avg/yr)': 'Közrend elleni bűncselekmények éves átlaga a környéken',
'Other crime (avg/yr)': 'Egyéb bűncselekmények éves átlaga a környéken',
'Median age': 'A helyi lakosság medián életkora',
'% White': 'A fehérként azonosított lakosság aránya',
'% South Asian': 'A dél-ázsiaiként azonosított lakosság aránya',
'% Black': 'A feketeként azonosított lakosság aránya',
'% East Asian': 'A kelet-ázsiaiként azonosított lakosság aránya',
'% Mixed': 'A vegyes vagy több etnikai csoporthoz tartozóként azonosított lakosság aránya',
'% Other': 'Az egyéb etnikai csoportba tartozóként azonosított lakosság aránya',
'Distance to nearest park (km)': 'Távolság a legközelebbi parkig vagy zöldterületig',
'Number of parks within 2km': 'Parkok és zöldterületek száma 2 km-en belül',
'Number of restaurants within 2km': 'Éttermek és kávézók száma 2 km-en belül',
'Number of grocery shops and supermarkets within 2km': 'Élelmiszerboltok és szupermarketek száma 2 km-en belül',
'Noise (dB)': 'Közúti zajszint az irányítószámnál decibelben (Lden)',
'Max available download speed (Mbps)': 'Az irányítószámnál elérhető maximális szélessávú letöltési sebesség',
},
};

View file

@ -7,11 +7,11 @@ import hu from './locales/hu';
import zh from './locales/zh';
export const SUPPORTED_LANGUAGES = [
{ code: 'en', label: 'English', flag: '\uD83C\uDDEC\uD83C\uDDE7' },
{ code: 'fr', label: 'Fran\u00E7ais', flag: '\uD83C\uDDEB\uD83C\uDDF7' },
{ code: 'de', label: 'Deutsch', flag: '\uD83C\uDDE9\uD83C\uDDEA' },
{ code: 'hu', label: 'Magyar', flag: '\uD83C\uDDED\uD83C\uDDFA' },
{ code: 'zh', label: '\u4E2D\u6587', flag: '\uD83C\uDDE8\uD83C\uDDF3' },
{ code: 'en', label: 'English', flag: '\uD83C\uDDEC\uD83C\uDDE7' },
{ code: 'fr', label: 'Fran\u00E7ais', flag: '\uD83C\uDDEB\uD83C\uDDF7' },
{ code: 'de', label: 'Deutsch', flag: '\uD83C\uDDE9\uD83C\uDDEA' },
{ code: 'hu', label: 'Magyar', flag: '\uD83C\uDDED\uD83C\uDDFA' },
{ code: 'zh', label: '\u4E2D\u6587', flag: '\uD83C\uDDE8\uD83C\uDDF3' },
] as const;
export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code'];
@ -19,37 +19,37 @@ export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code'];
const supportedCodes: Set<string> = new Set(SUPPORTED_LANGUAGES.map((l) => l.code));
function detectLanguage(): string {
// 1. Explicit user choice (persisted from the language dropdown)
const stored = localStorage.getItem('language');
if (stored && supportedCodes.has(stored)) return stored;
// 1. Explicit user choice (persisted from the language dropdown)
const stored = localStorage.getItem('language');
if (stored && supportedCodes.has(stored)) return stored;
// 2. Browser preference (navigator.languages falls back to navigator.language)
for (const tag of navigator.languages ?? [navigator.language]) {
// Match full tag first (e.g. "zh-CN" → "zh"), then just the prefix
const lower = tag.toLowerCase();
if (supportedCodes.has(lower)) return lower;
const prefix = lower.split('-')[0];
if (supportedCodes.has(prefix)) return prefix;
}
// 2. Browser preference (navigator.languages falls back to navigator.language)
for (const tag of navigator.languages ?? [navigator.language]) {
// Match full tag first (e.g. "zh-CN" → "zh"), then just the prefix
const lower = tag.toLowerCase();
if (supportedCodes.has(lower)) return lower;
const prefix = lower.split('-')[0];
if (supportedCodes.has(prefix)) return prefix;
}
return 'en';
return 'en';
}
const initialLang = detectLanguage();
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
fr: { translation: fr },
de: { translation: de },
hu: { translation: hu },
zh: { translation: zh },
},
lng: initialLang,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React already escapes
},
resources: {
en: { translation: en },
fr: { translation: fr },
de: { translation: de },
hu: { translation: hu },
zh: { translation: zh },
},
lng: initialLang,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React already escapes
},
});
/**
@ -57,8 +57,8 @@ i18n.use(initReactI18next).init({
* Bypasses the strict type checking on t() for dynamic key construction.
*/
export function tDynamic(key: string): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (i18n.t as any)(key);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (i18n.t as any)(key);
}
export default i18n;

View file

@ -5,9 +5,9 @@ const de: Translations = {
common: {
save: 'Speichern',
cancel: 'Abbrechen',
close: 'Schlie\u00DFen',
delete: 'L\u00F6schen',
open: '\u00D6ffnen',
close: 'Schließen',
delete: 'Löschen',
open: 'Öffnen',
share: 'Teilen',
copy: 'Kopieren',
copied: 'Kopiert!',
@ -25,10 +25,10 @@ const de: Translations = {
area: 'Gebiet',
properties: 'Immobilien',
postcode: 'Postleitzahl',
noAreaSelected: 'Kein Gebiet ausgew\u00E4hlt',
noAreaSelected: 'Kein Gebiet ausgewählt',
noAreaSelectedDesc:
'Klicke auf ein farbiges Gebiet auf der Karte, um Kriminalit\u00E4t, Schulen, Preise und mehr zu sehen',
clickForDetails: 'F\u00FCr Details klicken',
'Klicke auf ein farbiges Gebiet auf der Karte, um Kriminalität, Schulen, Preise und mehr zu sehen',
clickForDetails: 'Für Details klicken',
property: 'Immobilie',
propertiesPlural: 'Immobilien',
},
@ -36,7 +36,7 @@ const de: Translations = {
// ── Header / Nav ───────────────────────────────────
header: {
appName: 'Perfect Postcode',
dashboard: '\u00DCbersicht',
dashboard: 'Übersicht',
learn: 'Infos',
pricing: 'Preise',
inviteFriends: 'Freunde einladen',
@ -47,8 +47,8 @@ const de: Translations = {
exportLabel: 'Exportieren',
exporting: 'Wird exportiert...',
exportToExcel: 'Als Excel exportieren',
openMenu: 'Men\u00FC \u00F6ffnen',
closeMenu: 'Men\u00FC schlie\u00DFen',
openMenu: 'Menü öffnen',
closeMenu: 'Menü schließen',
},
// ── User Menu ──────────────────────────────────────
@ -63,7 +63,7 @@ const de: Translations = {
// ── Mobile Menu ────────────────────────────────────
mobileMenu: {
menu: 'Men\u00FC',
menu: 'Menü',
home: 'Startseite',
},
@ -71,9 +71,9 @@ const de: Translations = {
auth: {
logIn: 'Anmelden',
createAccount: 'Konto erstellen',
resetPassword: 'Passwort zur\u00FCcksetzen',
resetPassword: 'Passwort zurücksetzen',
valueProp:
'Speichere Suchen, merke dir Immobilien und mach dort weiter, wo du aufgeh\u00F6rt hast.',
'Speichere Suchen, merke dir Immobilien und mach dort weiter, wo du aufgehört hast.',
continueWithGoogle: 'Weiter mit Google',
email: 'E-Mail',
emailPlaceholder: 'du@beispiel.de',
@ -81,24 +81,24 @@ const de: Translations = {
passwordPlaceholderRegister: 'Mind. 8 Zeichen',
passwordPlaceholderLogin: 'Dein Passwort',
forgotPassword: 'Passwort vergessen?',
resetSent: 'Pr\u00FCfe deine E-Mails f\u00FCr einen Link zum Zur\u00FCcksetzen.',
resetSent: 'Prüfe deine E-Mails für einen Link zum Zurücksetzen.',
pleaseWait: 'Bitte warten...',
sendResetLink: 'Link zum Zur\u00FCcksetzen senden',
backToLogin: 'Zur\u00FCck zur Anmeldung',
sendResetLink: 'Link zum Zurücksetzen senden',
backToLogin: 'Zurück zur Anmeldung',
},
// ── Upgrade Modal ──────────────────────────────────
upgrade: {
title: 'Ganz England entdecken',
description:
'Du erkundest gerade das Demogebiet. Erhalte lebenslangen Zugang zu jeder Postleitzahl, jedem Filter, jedem Viertel. Eine Zahlung, f\u00FCr immer.',
'Du erkundest gerade das Demogebiet. Erhalte lebenslangen Zugang zu jeder Postleitzahl, jedem Filter, jedem Viertel. Eine Zahlung, für immer.',
free: 'Kostenlos',
once: '/einmalig',
freeForEarly: 'Kostenlos f\u00FCr Fr\u00FChnutzer. Keine Kreditkarte erforderlich.',
oneTimePayment: 'Einmalzahlung. Lebenslanger Zugang. 30 Tage Geld-zur\u00FCck-Garantie.',
freeForEarly: 'Kostenlos für Frühnutzer. Keine Kreditkarte erforderlich.',
oneTimePayment: 'Einmalzahlung. Lebenslanger Zugang. 30 Tage Geld-zurück-Garantie.',
redirecting: 'Weiterleitung...',
claimFreeAccess: 'Kostenlosen Zugang sichern',
upgradeFor: 'Upgrade f\u00FCr {{price}}',
upgradeFor: 'Upgrade für {{price}}',
registerAndUpgrade: 'Registrieren & Upgraden',
alreadyHaveAccount: 'Bereits ein Konto? Anmelden',
continueWithDemo: 'Mit Demo fortfahren',
@ -128,83 +128,88 @@ const de: Translations = {
// ── Filters ────────────────────────────────────────
filters: {
activeFilters: 'Aktive Filter',
addFilter: 'Filter hinzuf\u00FCgen',
addFilter: 'Filter hinzufügen',
historical: 'Historisch',
buy: 'Kaufen',
rent: 'Mieten',
findingPerfectPostcode: 'Die perfekte Postleitzahl finden',
addFiltersHint:
'F\u00FCge unten Filter hinzu, um die Karte auf Gebiete einzugrenzen, die deinen Kriterien entsprechen',
'Füge unten Filter hinzu, um die Karte auf Gebiete einzugrenzen, die deinen Kriterien entsprechen',
upgradePrompt:
'Sieh Kriminalit\u00E4t, Schulen, L\u00E4rm, Breitband und 50+ weitere Filter f\u00FCr ganz England.',
'Sieh Kriminalität, Schulen, Lärm, Breitband und 50+ weitere Filter für ganz England.',
oneTimeLifetime: 'Einmalzahlung, lebenslanger Zugang.',
upgradeToFullMap: 'Zur Vollversion upgraden',
chooseFilters:
'W\u00E4hle die Filter, die dir wichtig sind. Die Karte aktualisiert sich sofort.',
'Wähle die Filter, die dir wichtig sind. Die Karte aktualisiert sich sofort.',
searchFeatures: 'Filter durchsuchen...',
noMatchingFeatures: 'Keine passenden Filter',
tryDifferentSearch: 'Versuche einen anderen Suchbegriff',
allFeaturesActive: 'Alle Filter sind aktiv',
removeFilterHint: 'Entferne einen Filter, um verf\u00FCgbare Merkmale zu sehen',
removeFilterHint: 'Entferne einen Filter, um verfügbare Merkmale zu sehen',
featureInfo: 'Filterinfo',
replayTutorial: 'Interaktives Tutorial erneut abspielen',
clearAll: 'Alle löschen',
clearAllTitle: 'Alle Filter löschen?',
clearAllSavePrompt: 'Möchtest du deine aktuellen Filter vor dem Löschen speichern?',
saveAndClear: 'Speichern & löschen',
clearWithoutSaving: 'Ohne Speichern löschen',
},
// ── Philosophy Popup ───────────────────────────────
philosophy: {
intro:
'Beginne mit deinen Muss-Kriterien, dann f\u00FCge Kann-Kriterien hinzu. Die Karte grenzt sich ein, wenn du Filter hinzuf\u00FCgst. Die verbleibenden Gebiete sind deine besten Treffer.',
'Beginne mit deinen Muss-Kriterien, dann füge Kann-Kriterien hinzu. Die Karte grenzt sich ein, wenn du Filter hinzufügst. Die verbleibenden Gebiete sind deine besten Treffer.',
step1Title: 'Budget und Grundlagen',
step1Desc: '(Preisrahmen, Wohnfl\u00E4che, Immobilientyp)',
step1Desc: '(Preisrahmen, Wohnfläche, Immobilientyp)',
step2Title: 'Pendelweg',
step2Desc: '(Fahrzeit zum Arbeitsplatz mit Auto, Fahrrad oder \u00D6PNV)',
step2Desc: '(Fahrzeit zum Arbeitsplatz mit Auto, Fahrrad oder ÖPNV)',
step3Title: 'Sicherheit',
step3Desc: '(Kriminalit\u00E4tsraten, L\u00E4rmpegel, Bodenstabilit\u00E4t)',
step3Desc: '(Kriminalitätsraten, Lärmpegel, Bodenstabilität)',
step4Title: 'Schulen',
step4Desc: '(nahe gelegene Schulen mit Ofsted-Bewertung Gut oder Hervorragend)',
step5Title: 'Lebensstil',
step5Desc: '(Restaurants, Parks, Breitbandgeschwindigkeit)',
step6Title: 'Energie',
step6Desc: '(EPC-Bewertungen, D\u00E4mmung, Heizkosten)',
tip: 'Tipp: Wenn nichts passt, lockere eine Bedingung nach der anderen, um zu sehen, welcher Kompromiss die meisten Optionen er\u00F6ffnet.',
step6Desc: '(EPC-Bewertungen, Dämmung, Heizkosten)',
tip: 'Tipp: Wenn nichts passt, lockere eine Bedingung nach der anderen, um zu sehen, welcher Kompromiss die meisten Optionen eröffnet.',
},
// ── Travel Time ────────────────────────────────────
travel: {
travelTime: 'Reisezeit ({{mode}})',
maxTime: 'Maximale Zeit',
selectDestination: 'Ziel ausw\u00E4hlen...',
selectDestination: 'Ziel auswählen...',
bestCase: 'Bestfall',
bestCaseTitle: 'Bestm\u00F6gliche Reisezeit',
bestCaseTitle: 'Bestmögliche Reisezeit',
bestCaseDesc:
'Verwendet die schnellste realistische Reisezeit (bei guter Abfahrtsplanung und guten Anschl\u00FCssen). Standard ist der <strong>Median</strong>, der eine typische Fahrt unabh\u00E4ngig vom Abfahrtszeitpunkt darstellt.',
'Verwendet die schnellste realistische Reisezeit (bei guter Abfahrtsplanung und guten Anschlüssen). Standard ist der <strong>Median</strong>, der eine typische Fahrt unabhängig vom Abfahrtszeitpunkt darstellt.',
previewOnMap: 'Auf Karte anzeigen',
stopPreviewing: 'Vorschau beenden',
removeTravelTime: 'Reisezeit entfernen',
addTravelTime: '{{mode}}-Reisezeit hinzuf\u00FCgen',
clearDestination: 'Ziel l\u00F6schen',
addTravelTime: '{{mode}}-Reisezeit hinzufügen',
clearDestination: 'Ziel löschen',
typeToFilter: 'Tippen zum Filtern...',
noDestinations: 'Keine Ziele gefunden',
modeCar: 'Auto',
modeBicycle: 'Fahrrad',
modeWalking: 'Zu Fu\u00DF',
modeTransit: '\u00D6PNV',
modeCarDesc: 'Fahrzeit \u00FCber die schnellste Stra\u00DFenroute',
modeWalking: 'Zu Fuß',
modeTransit: 'ÖPNV',
modeCarDesc: 'Fahrzeit über die schnellste Straßenroute',
modeBicycleDesc: 'Radfahrzeit auf fahrradfreundlichen Strecken',
modeWalkingDesc: 'Gehzeit \u00FCber Fu\u00DFwege und B\u00FCrgersteige',
modeWalkingDesc: 'Gehzeit über Fußwege und Bürgersteige',
modeTransitDesc: 'Reisezeit mit Bahn, U-Bahn und Bus',
},
// ── Travel Time Info Popup ─────────────────────────
travelInfo: {
transitDesc:
' mit \u00F6ffentlichen Verkehrsmitteln (Bus, Bahn, U-Bahn). Die Zeiten werden \u00FCber ein typisches Werktags-Morgenfenster berechnet.',
' mit öffentlichen Verkehrsmitteln (Bus, Bahn, U-Bahn). Die Zeiten werden über ein typisches Werktags-Morgenfenster berechnet.',
carDesc:
' mit dem Auto, basierend auf typischen Stra\u00DFengeschwindigkeiten und dem Stra\u00DFennetz.',
' mit dem Auto, basierend auf typischen Straßengeschwindigkeiten und dem Straßennetz.',
bicycleDesc: ' mit dem Fahrrad, auf fahrradfreundlichen Strecken.',
walkingDesc: ' zu Fu\u00DF, \u00FCber Fu\u00DFwege und B\u00FCrgersteige.',
walkingDesc: ' zu Fuß, über Fußwege und Bürgersteige.',
mainDesc:
'Zeigt, wie lange es dauert, das ausgew\u00E4hlte Ziel von jedem Gebiet aus zu erreichen',
'Zeigt, wie lange es dauert, das ausgewählte Ziel von jedem Gebiet aus zu erreichen',
sliderHint:
'Verwende den Schieberegler, um deine maximale Pendelzeit festzulegen.',
},
@ -214,21 +219,26 @@ const de: Translations = {
describeIdealArea: 'Beschreibe dein Wunschgebiet mit KI',
aiSearch: 'KI-Suche',
describeHint: 'beschreibe, wonach du suchst',
placeholder: 'z.\u00A0B. ruhige Gegend, unter \u00A3400k, nahe guten Schulen...',
placeholder: 'z. B. ruhige Gegend, unter £400k, nahe guten Schulen...',
example1: 'Sichere Gegend nahe guten Schulen',
example2: '30 Min. Pendelweg zu Kings Cross, unter \u00A3500k',
example2: '30 Min. Pendelweg zu Kings Cross, unter £500k',
example3: 'Ruhiges Dorf, 3 Schlafzimmer, schnelles Breitband',
analysing: 'Anfrage wird analysiert...',
searchingDestinations: 'Ziele werden gesucht...',
generatingFilters: 'Filter werden generiert...',
refiningResults: 'Ergebnisse werden verfeinert...',
weeklyLimitReached:
'Du hast das w\u00F6chentliche KI-Nutzungslimit erreicht. Es wird n\u00E4chste Woche automatisch zur\u00FCckgesetzt.',
'Du hast das wöchentliche KI-Nutzungslimit erreicht. Es wird nächste Woche automatisch zurückgesetzt.',
},
// ── Map Legend ─────────────────────────────────────
mapLegend: {
clearColourView: 'Farbansicht zur\u00FCcksetzen',
clearColourView: 'Farbansicht zurücksetzen',
historicalMatches: 'Historische Immobilientreffer',
propertiesForSale: 'Immobilien zum Verkauf',
propertiesForRent: 'Immobilien zur Miete',
numberOfProperties: 'Anzahl der Immobilien',
previewing: 'Vorschau von \u201c{{name}}\u201d',
},
// ── Properties Pane ────────────────────────────────
@ -236,12 +246,12 @@ const de: Translations = {
unknownAddress: 'Unbekannte Adresse',
unsaveProperty: 'Immobilie nicht mehr merken',
saveProperty: 'Immobilie merken',
lastSold: 'Letzter Verkauf: \u00A3{{price}}',
lastSold: 'Letzter Verkauf: £{{price}}',
estValue: 'Gesch. Wert:',
type: 'Typ:',
builtForm: 'Bauweise:',
tenure: 'Besitzart:',
floorArea: 'Wohnfl\u00E4che:',
floorArea: 'Wohnfläche:',
bedrooms: 'Schlafzimmer:',
bathrooms: 'Badezimmer:',
rooms: 'Zimmer:',
@ -253,34 +263,34 @@ const de: Translations = {
renovations: 'Renovierungen',
viewExternalListing: 'Externes Inserat ansehen',
perMonth: '/Monat',
perSqm: '/m\u00B2',
perSqm: '/m²',
searchPlaceholder: 'Nach Adresse oder Postleitzahl suchen...',
propertyData: 'Immobiliendaten',
propertyDataDesc:
'Preise stammen vom HM Land Registry (was K\u00E4ufer tats\u00E4chlich bezahlt haben). Wohnfl\u00E4che, Energiebewertungen, Baujahr und Besitzart stammen aus offiziellen EPC-Gutachten. Beide Quellen werden nach Adresse innerhalb jeder Postleitzahl abgeglichen.',
'Preise stammen vom HM Land Registry (was Käufer tatsächlich bezahlt haben). Wohnfläche, Energiebewertungen, Baujahr und Besitzart stammen aus offiziellen EPC-Gutachten. Beide Quellen werden nach Adresse innerhalb jeder Postleitzahl abgeglichen.',
},
// ── Area Pane ──────────────────────────────────────
areaPane: {
areaStatistics: 'Gebietsstatistiken',
statsFor: 'Statistiken f\u00FCr alle Immobilien in diesem {{type}}',
statsFor: 'Statistiken für alle Immobilien in diesem {{type}}',
matchingFilters: ', die allen aktiven Filtern entsprechen',
viewProperties: '{{count}} Immobilien ansehen',
priceHistory: 'Preisentwicklung',
journeysFrom: 'Verbindungen ab {{label}}',
to: 'Nach {{destination}}',
noJourneyData: 'Keine Verbindungsdaten verf\u00FCgbar',
noJourneyData: 'Keine Verbindungsdaten verfügbar',
viewOnGoogleMaps: 'Auf Google Maps ansehen',
walk: 'Zu Fu\u00DF',
walk: 'Zu Fuß',
cycle: 'Fahrrad',
},
// ── Histogram Legend ───────────────────────────────
histogramLegend: {
tealBars: 'T\u00FCrkise Balken',
tealBarsDesc: 'zeigen die Verteilung im ausgew\u00E4hlten Gebiet',
tealBars: 'Türkise Balken',
tealBarsDesc: 'zeigen die Verteilung im ausgewählten Gebiet',
greyBars: 'Graue Balken',
greyBarsDesc: 'zeigen die Gesamtverteilung \u00FCber alle Gebiete',
greyBarsDesc: 'zeigen die Gesamtverteilung über alle Gebiete',
dashedLine: 'Gestrichelte Linie',
dashedLineDesc: 'zeigt den landesweiten Durchschnitt',
},
@ -293,9 +303,9 @@ const de: Translations = {
// ── POI Pane ───────────────────────────────────────
poiPane: {
pois: 'POIs',
pointsOfInterest: 'Sehensw\u00FCrdigkeiten & Einrichtungen',
pointsOfInterest: 'Sehenswürdigkeiten & Einrichtungen',
poiDescription:
'Daten von OpenStreetMap. Umfasst Haltestellen, Gesch\u00E4fte, Restaurants, Gesundheitseinrichtungen, Freizeit und mehr. Regelm\u00E4\u00DFig aktualisiert mit vollst\u00E4ndiger Kategorieabdeckung.',
'Daten von OpenStreetMap. Umfasst Haltestellen, Geschäfte, Restaurants, Gesundheitseinrichtungen, Freizeit und mehr. Regelmäßig aktualisiert mit vollständiger Kategorieabdeckung.',
searchCategories: 'Kategorien durchsuchen...',
dataSourceInfo: 'Datenquelleninfo',
},
@ -319,7 +329,7 @@ const de: Translations = {
// ── Mobile Drawer ──────────────────────────────────
mobileDrawer: {
closeDrawer: 'Schublade schlie\u00DFen',
closeDrawer: 'Schublade schließen',
},
// ── Home Page ──────────────────────────────────────
@ -328,9 +338,9 @@ const de: Translations = {
heroTitle2: 'Wert',
heroTitle3: 'Minimale Kompromisse.',
heroSubtitle:
'Auf Immobiliensuche? Mach aus deiner gr\u00F6\u00DFten Investition deine kl\u00FCgste Entscheidung.',
'Auf Immobiliensuche? Mach aus deiner größten Investition deine klügste Entscheidung.',
heroDescription:
'So viele M\u00F6glichkeiten \u2014 die richtige Wahl kann \u00FCberw\u00E4ltigend sein. Unsere interaktive Karte macht es einfach: W\u00E4hle deine Muss-Kriterien und sieh sofort die passenden Gebiete.',
'So viele Möglichkeiten — die richtige Wahl kann überwältigend sein. Unsere interaktive Karte macht es einfach: Wähle deine Muss-Kriterien und sieh sofort die passenden Gebiete.',
exploreTheMap: 'Karte entdecken',
seeTheDifference: 'Den Unterschied sehen',
statProperties: 'Immobilien',
@ -339,201 +349,201 @@ const de: Translations = {
statPostcodeInEngland: 'Postleitzahl in England',
ourPhilosophy: 'Unsere Philosophie',
philosophyP1:
'Auf Rightmove w\u00E4hlt man zuerst ein Gebiet und hofft, dass es gut ist. Am Ende vergleicht man Kriminalit\u00E4tsstatistiken, Schulberichte und Breitband-Checker in einem Dutzend Tabs, eine Postleitzahl nach der anderen.',
'Auf Rightmove wählt man zuerst ein Gebiet und hofft, dass es gut ist. Am Ende vergleicht man Kriminalitätsstatistiken, Schulberichte und Breitband-Checker in einem Dutzend Tabs, eine Postleitzahl nach der anderen.',
philosophyP2:
'Wir drehen das um. Sag uns, was du brauchst (Budget, Pendelweg, Schulen, Sicherheit), und wir zeigen dir jedes Gebiet in England, das passt. Kein Raten. Keine verschwendeten Besichtigungen.',
howToUseIt: 'So funktioniert es',
howStep1Title: 'Lege deine Muss-Kriterien fest',
howStep1Desc: 'Budget, Pendelweg, Schulen \u2014 die Karte zeigt nur, was passt.',
howStep1Desc: 'Budget, Pendelweg, Schulen die Karte zeigt nur, was passt.',
howStep2Title: 'Entdecke Gebiete und versteckte Perlen',
howStep2Desc: 'Zoom rein, schau dir Details und Kann-Kriterien an.',
howStep3Title: 'Einzelne Postleitzahlen erkunden',
howStep3Desc:
'Sieh einzelne Immobilien, Verkaufspreise, Wohnfl\u00E4chen und vergleiche.',
'Sieh einzelne Immobilien, Verkaufspreise, Wohnflächen und vergleiche.',
howStep4Title: 'Engere Auswahl mit Zuversicht',
howStep4Desc:
'Jedes Gebiet auf deiner Liste erf\u00FCllt deine tats\u00E4chlichen Kriterien \u2014 nicht nur, was diese Woche inseriert war.',
'Jedes Gebiet auf deiner Liste erfüllt deine tatsächlichen Kriterien — nicht nur, was diese Woche inseriert war.',
othersVs: 'Andere vs',
listingPortals: 'Immobilienportale',
checkMyPostcode: '\u201EMeine Postleitzahl pr\u00FCfen\u201C',
checkMyPostcode: '„Meine Postleitzahl prüfen“',
areaGuides: 'Gebietsratgeber',
compSearchWithout: 'Suchen, ohne zuerst ein Gebiet auszuw\u00E4hlen',
compSearchWithoutSub: '(starte mit Bed\u00FCrfnissen, nicht mit einem Ort)',
compSearchWithout: 'Suchen, ohne zuerst ein Gebiet auszuwählen',
compSearchWithoutSub: '(starte mit Bedürfnissen, nicht mit einem Ort)',
compAreaData: 'Gebietsdaten',
compAreaDataSub: '(Kriminalit\u00E4t, Schulen, L\u00E4rm, Breitband)',
compAreaDataSub: '(Kriminalität, Schulen, Lärm, Breitband)',
compPropertyData: 'Immobilienspezifische Daten',
compPropertyDataSub: '(Preis, EPC, Wohnfl\u00E4che)',
compPropertyDataSub: '(Preis, EPC, Wohnfläche)',
compFilters: '56 kombinierbare Filter an einem Ort',
compFiltersSub: '(alle Einblicke, eine interaktive Karte)',
ctaTitle:
'Mach aus deiner gr\u00F6\u00DFten Investition deine kl\u00FCgste\u00A0Entscheidung.',
'Mach aus deiner größten Investition deine klügste Entscheidung.',
ctaDescription:
'Das verdient die richtigen Werkzeuge \u2014 \u00FCberlass es nicht dem Zufall.',
'Das verdient die richtigen Werkzeuge — überlass es nicht dem Zufall.',
},
// ── Pricing Page ───────────────────────────────────
pricingPage: {
title: 'Fr\u00FChzugangspreis',
title: 'Frühzugangspreis',
subtitle:
'Einmal zahlen, f\u00FCr immer nutzen. Je fr\u00FCher du dabei bist, desto weniger zahlst du.',
'Einmal zahlen, für immer nutzen. Je früher du dabei bist, desto weniger zahlst du.',
costContext:
'Ein Hauskauf kostet \u00A310.000+ an Grunderwerbsteuer, \u00A31.500 an Anwaltsgeb\u00FChren, \u00A3500 f\u00FCr ein Gutachten. W\u00E4hlst du das falsche Gebiet, steckst du mit einem langen Pendelweg, schlechten Schulen oder einer Stra\u00DFe fest, von der du nichts wusstest.',
lessThanSurvey: 'Weniger als ein Hausgutachten. Deutlich n\u00FCtzlicher.',
'Ein Hauskauf kostet £10.000+ an Grunderwerbsteuer, £1.500 an Anwaltsgebühren, £500 für ein Gutachten. Wählst du das falsche Gebiet, steckst du mit einem langen Pendelweg, schlechten Schulen oder einer Straße fest, von der du nichts wusstest.',
lessThanSurvey: 'Weniger als ein Hausgutachten. Deutlich nützlicher.',
currentTier: 'Aktuelle Stufe',
firstNUsers: 'Erste {{count}} Nutzer',
everyoneAfter: 'Alle danach',
nextNUsers: 'N\u00E4chste {{count}} Nutzer',
nextNUsers: 'Nächste {{count}} Nutzer',
lifetime: '/lebenslang',
spotsRemaining: '{{count}} Platz verbleibend',
spotsRemainingPlural: '{{count}} Pl\u00E4tze verbleibend',
spotsRemainingPlural: '{{count}} Plätze verbleibend',
filled: 'Vergeben',
openDashboard: '\u00DCbersicht \u00F6ffnen',
openDashboard: 'Übersicht öffnen',
getStarted: 'Jetzt starten',
getStartedPrice: 'Jetzt starten \u2014 {{price}}',
getStartedPrice: 'Jetzt starten {{price}}',
noCreditCard: 'Keine Kreditkarte erforderlich',
moneyBackGuarantee: '30 Tage Geld-zur\u00FCck-Garantie',
moneyBackGuarantee: '30 Tage Geld-zurück-Garantie',
soldOut: 'Ausverkauft',
upcoming: 'Demn\u00E4chst',
upcoming: 'Demnächst',
failedToLoad:
'Preise konnten nicht geladen werden. Bitte sp\u00E4ter erneut versuchen.',
feat1: '56 Datenebenen f\u00FCr ganz England',
'Preise konnten nicht geladen werden. Bitte später erneut versuchen.',
feat1: '56 Datenebenen für ganz England',
feat2: 'Jede Postleitzahl bewertet und filterbar',
feat3: 'Unbegrenztes Erkunden der Karte und Exporte',
feat4: 'Mehrere Jahrzehnte historischer Preisdaten',
feat5: 'Kriminalit\u00E4t, Schulen, Verkehr, Breitband und mehr',
feat6: 'Alle zuk\u00FCnftigen Datenaktualisierungen inklusive',
feat5: 'Kriminalität, Schulen, Verkehr, Breitband und mehr',
feat6: 'Alle zukünftigen Datenaktualisierungen inklusive',
},
// ── Learn Page ─────────────────────────────────────
learnPage: {
faq: 'H\u00E4ufige Fragen',
faq: 'Häufige Fragen',
dataSources: 'Datenquellen',
support: 'Support',
dataSourcesIntro: 'Diese Anwendung kombiniert {{count}} offene Datens\u00E4tze zu Immobilienpreisen, Energieeffizienz, Verkehr, Demografie, Kriminalit\u00E4t, Umwelt und mehr.',
faqIntro: 'Ob Sie kaufen, mieten oder einfach nur st\u00F6bern \u2013 so hilft Ihnen Perfect Postcode, das richtige Gebiet zu finden.',
dataSourcesIntro: 'Diese Anwendung kombiniert {{count}} offene Datensätze zu Immobilienpreisen, Energieeffizienz, Verkehr, Demografie, Kriminalität, Umwelt und mehr.',
faqIntro: 'Ob Sie kaufen, mieten oder einfach nur stöbern so hilft Ihnen Perfect Postcode, das richtige Gebiet zu finden.',
supportIntro: 'Haben Sie eine Frage? Schauen Sie in unsere FAQ oder kontaktieren Sie uns direkt.',
source: 'Quelle:',
optOut: 'Widerspruch gegen \u00F6ffentliche Offenlegung',
optOut: 'Widerspruch gegen öffentliche Offenlegung',
attribution: 'Quellenangaben',
attrLandRegistry: 'Enth\u00E4lt Daten des HM Land Registry \u00A9 Crown copyright and database right 2025.',
attrOgl: 'Enth\u00E4lt \u00F6ffentliche Informationen lizenziert unter der',
attrLandRegistry: 'Enthält Daten des HM Land Registry © Crown copyright and database right 2025.',
attrOgl: 'Enthält öffentliche Informationen lizenziert unter der',
attrOglLink: 'Open Government Licence v3.0',
attrOs: 'Enth\u00E4lt OS-Daten \u00A9 Crown copyright and database rights 2025.',
attrOs: 'Enthält OS-Daten © Crown copyright and database rights 2025.',
attrTfl: 'Betrieben mit TfL Open Data.',
attrOsm: 'Enth\u00E4lt Daten von',
attrOsmContrib: '\u00A9 OpenStreetMap contributors',
attrOsmLicense: 'verf\u00FCgbar unter der',
attrOsm: 'Enthält Daten von',
attrOsmContrib: '© OpenStreetMap contributors',
attrOsmLicense: 'verfügbar unter der',
attrOsmLicenseLink: 'Open Data Commons Open Database License (ODbL)',
// Data source names & descriptions
dsPricePaidName: 'Price Paid Data',
dsPricePaidOrigin: 'HM Land Registry',
dsPricePaidUse: 'Vollst\u00E4ndige historische Immobilien-Verkaufspreise f\u00FCr England.',
dsPricePaidUse: 'Vollständige historische Immobilien-Verkaufspreise für England.',
dsEpcName: 'Energy Performance Certificates (EPC)',
dsEpcOrigin: 'Ministry of Housing, Communities & Local Government',
dsEpcUse: 'Energieausweise f\u00FCr Wohngeb\u00E4ude mit Angaben zu Wohnfl\u00E4che, Zimmeranzahl, Baujahr, Energiebewertungen, Immobilientyp und Bauform. \u00DCber Adresse innerhalb jeder Postleitzahl mit Price-Paid-Daten verkn\u00FCpft. Eigent\u00FCmer k\u00F6nnen der \u00F6ffentlichen Offenlegung widersprechen.',
dsEpcUse: 'Energieausweise für Wohngebäude mit Angaben zu Wohnfläche, Zimmeranzahl, Baujahr, Energiebewertungen, Immobilientyp und Bauform. Über Adresse innerhalb jeder Postleitzahl mit Price-Paid-Daten verknüpft. Eigentümer können der öffentlichen Offenlegung widersprechen.',
dsNsplName: 'National Statistics Postcode Lookup (NSPL)',
dsNsplOrigin: 'ONS / ArcGIS',
dsNsplUse: 'Ordnet Postleitzahlen Koordinaten und statistischen Gebietscodes zu, um alle gebietsbezogenen Datens\u00E4tze mit einzelnen Immobilien zu verkn\u00FCpfen.',
dsNsplUse: 'Ordnet Postleitzahlen Koordinaten und statistischen Gebietscodes zu, um alle gebietsbezogenen Datensätze mit einzelnen Immobilien zu verknüpfen.',
dsIodName: 'English Indices of Deprivation 2025',
dsIodOrigin: 'Ministry of Housing, Communities & Local Government',
dsIodUse: 'Relative Benachteiligungswerte f\u00FCr Einkommen, Besch\u00E4ftigung, Bildung, Gesundheit, Kriminalit\u00E4t und Wohnumfeld f\u00FCr jedes Viertel in England.',
dsEthnicityName: 'Bev\u00F6lkerung nach Ethnie (Zensus 2021)',
dsIodUse: 'Relative Benachteiligungswerte für Einkommen, Beschäftigung, Bildung, Gesundheit, Kriminalität und Wohnumfeld für jedes Viertel in England.',
dsEthnicityName: 'Bevölkerung nach Ethnie (Zensus 2021)',
dsEthnicityOrigin: 'ONS',
dsEthnicityUse: 'Bev\u00F6lkerungsanteile nach ethnischer Gruppe (s\u00FCdasiatisch, ostasiatisch, schwarz, gemischt, wei\u00DF, andere) pro Bezirk.',
dsEthnicityUse: 'Bevölkerungsanteile nach ethnischer Gruppe (südasiatisch, ostasiatisch, schwarz, gemischt, weiß, andere) pro Bezirk.',
dsCrimeName: 'Street-level Crime Data',
dsCrimeOrigin: 'data.police.uk',
dsCrimeUse: 'Kriminalit\u00E4tsdaten auf Stra\u00DFenebene von 2023 bis 2025, aggregiert als Jahresdurchschnitte nach LSOA und Deliktsart (Gewalt, Einbruch, antisoziales Verhalten, Drogen, Fahrzeugkriminalit\u00E4t usw.).',
dsCrimeUse: 'Kriminalitätsdaten auf Straßenebene von 2023 bis 2025, aggregiert als Jahresdurchschnitte nach LSOA und Deliktsart (Gewalt, Einbruch, antisoziales Verhalten, Drogen, Fahrzeugkriminalität usw.).',
dsOsmName: 'OpenStreetMap POIs',
dsOsmOrigin: 'OpenStreetMap contributors / Geofabrik',
dsOsmUse: 'Sehensw\u00FCrdigkeiten und Einrichtungen wie Gesch\u00E4fte, Restaurants, Gesundheitseinrichtungen, Freizeit, Tourismus und mehr in ganz Gro\u00DFbritannien.',
dsOsmUse: 'Sehenswürdigkeiten und Einrichtungen wie Geschäfte, Restaurants, Gesundheitseinrichtungen, Freizeit, Tourismus und mehr in ganz Großbritannien.',
dsGreenspaceName: 'OS Open Greenspace',
dsGreenspaceOrigin: 'Ordnance Survey',
dsGreenspaceUse: 'Offizielle Gr\u00FCnfl\u00E4chengrenzen f\u00FCr Gro\u00DFbritannien, einschlie\u00DFlich \u00F6ffentlicher Parks, G\u00E4rten, Sportpl\u00E4tze und Spielpl\u00E4tze. Polygon-Schwerpunkte werden f\u00FCr die Parkn\u00E4hez\u00E4hlung und Entfernungsberechnung zum n\u00E4chsten Park verwendet.',
dsGreenspaceUse: 'Offizielle Grünflächengrenzen für Großbritannien, einschließlich öffentlicher Parks, Gärten, Sportplätze und Spielplätze. Polygon-Schwerpunkte werden für die Parknähezählung und Entfernungsberechnung zum nächsten Park verwendet.',
dsNaptanName: 'NaPTAN (Public Transport Stops)',
dsNaptanOrigin: 'Department for Transport',
dsNaptanUse: 'Standorte von Bahnh\u00F6fen und Haltestellen f\u00FCr Bahn, Bus, U-Bahn/Stra\u00DFenbahn, F\u00E4hre und Flugh\u00E4fen in ganz England.',
dsNaptanUse: 'Standorte von Bahnhöfen und Haltestellen für Bahn, Bus, U-Bahn/Straßenbahn, Fähre und Flughäfen in ganz England.',
dsNoiseName: 'Defra Noise Mapping',
dsNoiseOrigin: 'Defra / Environment Agency',
dsNoiseUse: 'Stra\u00DFenl\u00E4rmpegel (24-Stunden-gewichteter Durchschnitt) aus der strategischen L\u00E4rmkartierung 2022, hochaufl\u00F6send modelliert und an jeder Postleitzahl abgetastet.',
dsNoiseUse: 'Straßenlärmpegel (24-Stunden-gewichteter Durchschnitt) aus der strategischen Lärmkartierung 2022, hochauflösend modelliert und an jeder Postleitzahl abgetastet.',
dsOfstedName: 'Ofsted School Inspections',
dsOfstedOrigin: 'Ofsted',
dsOfstedUse: 'Neueste Inspektionsergebnisse f\u00FCr staatlich finanzierte Schulen (Stand April 2025). Pro Postleitzahl gemittelt f\u00FCr einen lokalen Schulqualit\u00E4tswert (1=Hervorragend bis 4=Unzureichend).',
dsOfstedUse: 'Neueste Inspektionsergebnisse für staatlich finanzierte Schulen (Stand April 2025). Pro Postleitzahl gemittelt für einen lokalen Schulqualitätswert (1=Hervorragend bis 4=Unzureichend).',
dsBroadbandName: 'Ofcom Broadband Performance',
dsBroadbandOrigin: 'Ofcom',
dsBroadbandUse: 'Festnetz-Breitbandabdeckung und maximale Download-Geschwindigkeiten nach Gebiet aus Ofcom Connected Nations 2025.',
dsCouncilTaxName: 'Council Tax Levels 2025-26',
dsCouncilTaxOrigin: 'Ministry of Housing, Communities & Local Government',
dsCouncilTaxUse: 'J\u00E4hrliche Council-Tax-S\u00E4tze f\u00FCr die Stufen A bis H f\u00FCr alle 296 Abrechnungsbeh\u00F6rden in England, f\u00FCr eine von zwei Erwachsenen bewohnte Immobilie. \u00DCber den Bezirkscode aus dem NSPL-Postleitzahlenverzeichnis mit Immobilien verkn\u00FCpft.',
dsCouncilTaxUse: 'Jährliche Council-Tax-Sätze für die Stufen A bis H für alle 296 Abrechnungsbehörden in England, für eine von zwei Erwachsenen bewohnte Immobilie. Über den Bezirkscode aus dem NSPL-Postleitzahlenverzeichnis mit Immobilien verknüpft.',
dsRentalName: 'Private Rental Market Statistics',
dsRentalOrigin: 'ONS / Valuation Office Agency',
dsRentalUse: 'Monatliche Medianmieten des privaten Mietmarkts nach Bezirk und Schlafzimmerkategorie (Okt. 2022 - Sept. 2023). \u00DCber Bezirkscode und gesch\u00E4tzte Schlafzimmeranzahl mit Immobilien verkn\u00FCpft.',
dsRentalUse: 'Monatliche Medianmieten des privaten Mietmarkts nach Bezirk und Schlafzimmerkategorie (Okt. 2022 - Sept. 2023). Über Bezirkscode und geschätzte Schlafzimmeranzahl mit Immobilien verknüpft.',
// FAQ section titles
faqFindingTitle: 'Ihr Gebiet finden',
faqCommuteTitle: 'Pendelweg und Reisezeit',
faqBudgetTitle: 'Budget und Preis-Leistung',
faqSafetyTitle: 'Sicherheit und Nachbarschaft',
faqFamiliesTitle: 'Familien und Schulen',
faqEnvironmentTitle: 'Umwelt und Lebensqualit\u00E4t',
faqEnvironmentTitle: 'Umwelt und Lebensqualität',
faqWhyTitle: 'Warum Perfect Postcode',
faqPricingTitle: 'Preise und Zugang',
faqTipsTitle: 'Tipps und Tricks',
// FAQ items — Finding Your Area
faqFinding1Q: 'Ich wei\u00DF nicht einmal, welche Gebiete ich mir ansehen soll. Kann mir das helfen?',
faqFinding1A: 'Genau daf\u00FCr ist es da. Legen Sie Ihre Filter fest (Budget, Pendelzeit, geringe Kriminalit\u00E4t, gute Schulen) und die Karte leuchtet auf, um Ihnen jedes Gebiet zu zeigen, das alle Kriterien erf\u00FCllt. Kein n\u00E4chtliches Googeln nach \u201Ebeste Wohngegenden bei Manchester\u201C mehr.',
faqFinding2Q: 'Ich ziehe irgendwohin, wo ich noch nie war. Wie fange ich \u00FCberhaupt an?',
faqFinding2A: 'Stellen Sie Ihre Filter f\u00FCr das ein, was Ihnen wichtig ist, und die Karte hebt sofort die passenden Gebiete hervor. Sie gehen von \u201EIch kenne keine einzige Stra\u00DFe\u201C zu einer Auswahlliste in wenigen Minuten.',
faqFinding3Q: 'Wie finde ich Gebiete, die alle meine Kriterien gleichzeitig erf\u00FCllen?',
faqFinding3A: 'Kombinieren Sie mehrere Filter (Kriminalit\u00E4t unter dem Durchschnitt, gute Schulen, Pendelweg unter 40 Minuten) und f\u00E4rben Sie die Karte nach Preis, um die Gebiete mit dem besten Preis-Leistungs-Verh\u00E4ltnis zu finden. Die Karte aktualisiert sich in Echtzeit, wenn Sie die Regler bewegen.',
faqFinding1Q: 'Ich weiß nicht einmal, welche Gebiete ich mir ansehen soll. Kann mir das helfen?',
faqFinding1A: 'Genau dafür ist es da. Legen Sie Ihre Filter fest (Budget, Pendelzeit, geringe Kriminalität, gute Schulen) und die Karte leuchtet auf, um Ihnen jedes Gebiet zu zeigen, das alle Kriterien erfüllt. Kein nächtliches Googeln nach „beste Wohngegenden bei Manchester“ mehr.',
faqFinding2Q: 'Ich ziehe irgendwohin, wo ich noch nie war. Wie fange ich überhaupt an?',
faqFinding2A: 'Stellen Sie Ihre Filter für das ein, was Ihnen wichtig ist, und die Karte hebt sofort die passenden Gebiete hervor. Sie gehen von „Ich kenne keine einzige Straße“ zu einer Auswahlliste in wenigen Minuten.',
faqFinding3Q: 'Wie finde ich Gebiete, die alle meine Kriterien gleichzeitig erfüllen?',
faqFinding3A: 'Kombinieren Sie mehrere Filter (Kriminalität unter dem Durchschnitt, gute Schulen, Pendelweg unter 40 Minuten) und färben Sie die Karte nach Preis, um die Gebiete mit dem besten Preis-Leistungs-Verhältnis zu finden. Die Karte aktualisiert sich in Echtzeit, wenn Sie die Regler bewegen.',
// FAQ items — Commute and Travel
faqCommute1Q: 'Kann ich sehen, wie lange mein Pendelweg aus verschiedenen Gebieten tats\u00E4chlich dauern w\u00FCrde?',
faqCommute1A: 'Legen Sie Ihren Arbeitsplatz als Ziel fest und wir f\u00E4rben jede Postleitzahl nach Fahrzeit \u2013 ob mit Auto, Fahrrad oder \u00F6ffentlichen Verkehrsmitteln. Filtern Sie nach Ihrer maximalen Pendelzeit und der Rest verschwindet.',
faqCommute1Q: 'Kann ich sehen, wie lange mein Pendelweg aus verschiedenen Gebieten tatsächlich dauern würde?',
faqCommute1A: 'Legen Sie Ihren Arbeitsplatz als Ziel fest und wir färben jede Postleitzahl nach Fahrzeit ob mit Auto, Fahrrad oder öffentlichen Verkehrsmitteln. Filtern Sie nach Ihrer maximalen Pendelzeit und der Rest verschwindet.',
faqCommute2Q: 'Wie ist das besser als Google Maps?',
faqCommute2A: 'Google Maps zeigt Ihnen eine Fahrt auf einmal. Wir f\u00E4rben jede Postleitzahl in England nach Pendelzeit in einem Blick, sodass Sie Hunderte von Gebieten nebeneinander vergleichen k\u00F6nnen, anstatt sie einzeln zu suchen.',
faqCommute2A: 'Google Maps zeigt Ihnen eine Fahrt auf einmal. Wir färben jede Postleitzahl in England nach Pendelzeit in einem Blick, sodass Sie Hunderte von Gebieten nebeneinander vergleichen können, anstatt sie einzeln zu suchen.',
// FAQ items — Budget and Value
faqBudget1Q: 'Wie finde ich Gebiete, in denen ich am meisten Wohnfl\u00E4che f\u00FCr mein Geld bekomme?',
faqBudget1A: 'Filtern Sie nach Preis pro m\u00B2 und Sie sehen sofort, welche Postleitzahlen am meisten Fl\u00E4che pro Pfund bieten. Kombinieren Sie es mit dem Energiebewertungsfilter, um Immobilien mit hohen Heizkosten zu vermeiden.',
faqBudget2Q: 'Wie stelle ich sicher, dass ein g\u00FCnstiges Gebiet nicht aus gutem Grund g\u00FCnstig ist?',
faqBudget2A: 'Legen Sie Benachteiligungswerte, Kriminalit\u00E4tsstatistiken, Schulbewertungen und Breitbandgeschwindigkeiten neben den Preis. Wenn eine Postleitzahl erschwinglich ist und bei allem, was z\u00E4hlt, gut abschneidet, haben Sie echten Wert gefunden \u2013 nicht nur einen niedrigen Preis mit Kompromissen, die Sie noch nicht bemerkt haben.',
faqBudget1Q: 'Wie finde ich Gebiete, in denen ich am meisten Wohnfläche für mein Geld bekomme?',
faqBudget1A: 'Filtern Sie nach Preis pro m² und Sie sehen sofort, welche Postleitzahlen am meisten Fläche pro Pfund bieten. Kombinieren Sie es mit dem Energiebewertungsfilter, um Immobilien mit hohen Heizkosten zu vermeiden.',
faqBudget2Q: 'Wie stelle ich sicher, dass ein günstiges Gebiet nicht aus gutem Grund günstig ist?',
faqBudget2A: 'Legen Sie Benachteiligungswerte, Kriminalitätsstatistiken, Schulbewertungen und Breitbandgeschwindigkeiten neben den Preis. Wenn eine Postleitzahl erschwinglich ist und bei allem, was zählt, gut abschneidet, haben Sie echten Wert gefunden nicht nur einen niedrigen Preis mit Kompromissen, die Sie noch nicht bemerkt haben.',
// FAQ items — Safety and Neighbourhood
faqSafety1Q: 'Wie kann ich pr\u00FCfen, ob ein Gebiet sicher ist, bevor ich dorthin ziehe?',
faqSafety1A: 'Wir \u00FCberlagern echte polizeilich erfasste Kriminalit\u00E4tsdaten, aufgeschl\u00FCsselt nach Art, \u00FCber jedes Viertel in England. Filtern Sie nach Gewaltkriminalit\u00E4t, Einbruch oder antisozialem Verhalten und sehen Sie sofort, welche Postleitzahlen die niedrigsten Zahlen haben.',
faqSafety2Q: 'Ich finde st\u00E4ndig Wohnungen, die online toll aussehen, aber dann stellt sich die Gegend als schwierig heraus.',
faqSafety2A: 'Genau daf\u00FCr gibt es dieses Tool. Kombinieren Sie Kriminalit\u00E4tsraten, L\u00E4rmpegel, Benachteiligungswerte, Pubs und Parks in der N\u00E4he sowie Breitbandgeschwindigkeiten auf einer Karte, damit Sie wissen, wie ein Viertel wirklich ist, bevor Sie eine Besichtigung buchen.',
faqSafety1Q: 'Wie kann ich prüfen, ob ein Gebiet sicher ist, bevor ich dorthin ziehe?',
faqSafety1A: 'Wir überlagern echte polizeilich erfasste Kriminalitätsdaten, aufgeschlüsselt nach Art, über jedes Viertel in England. Filtern Sie nach Gewaltkriminalität, Einbruch oder antisozialem Verhalten und sehen Sie sofort, welche Postleitzahlen die niedrigsten Zahlen haben.',
faqSafety2Q: 'Ich finde ständig Wohnungen, die online toll aussehen, aber dann stellt sich die Gegend als schwierig heraus.',
faqSafety2A: 'Genau dafür gibt es dieses Tool. Kombinieren Sie Kriminalitätsraten, Lärmpegel, Benachteiligungswerte, Pubs und Parks in der Nähe sowie Breitbandgeschwindigkeiten auf einer Karte, damit Sie wissen, wie ein Viertel wirklich ist, bevor Sie eine Besichtigung buchen.',
// FAQ items — Families and Schools
faqFamilies1Q: 'Kann ich Gebiete mit guten Schulen UND geringer Kriminalit\u00E4t in einer Suche finden?',
faqFamilies1A: 'Ja. Kombinieren Sie Filter f\u00FCr Ofsted-Bewertungen, Kriminalit\u00E4tsraten, Parks und alles andere, was f\u00FCr Ihre Familie wichtig ist, und die Karte hebt nur die Gebiete hervor, die alles erf\u00FCllen. Kein Abgleich \u00FCber f\u00FCnf verschiedene Websites mehr.',
faqFamilies2Q: 'Woher wei\u00DF ich, ob ein Viertel Parks und Spielpl\u00E4tze in der N\u00E4he hat?',
faqFamilies2A: 'Schalten Sie die POI-Ebene f\u00FCr Parks und Gr\u00FCnfl\u00E4chen ein, um sie direkt auf der Karte zu sehen. Sie k\u00F6nnen auch nach der Anzahl der fu\u00DFl\u00E4ufig erreichbaren Parks pro Postleitzahl filtern.',
faqFamilies1Q: 'Kann ich Gebiete mit guten Schulen UND geringer Kriminalität in einer Suche finden?',
faqFamilies1A: 'Ja. Kombinieren Sie Filter für Ofsted-Bewertungen, Kriminalitätsraten, Parks und alles andere, was für Ihre Familie wichtig ist, und die Karte hebt nur die Gebiete hervor, die alles erfüllen. Kein Abgleich über fünf verschiedene Websites mehr.',
faqFamilies2Q: 'Woher weiß ich, ob ein Viertel Parks und Spielplätze in der Nähe hat?',
faqFamilies2A: 'Schalten Sie die POI-Ebene für Parks und Grünflächen ein, um sie direkt auf der Karte zu sehen. Sie können auch nach der Anzahl der fußläufig erreichbaren Parks pro Postleitzahl filtern.',
// FAQ items — Environment and Quality of Life
faqEnv1Q: 'Kann ich energieeffiziente Wohnungen finden, die nicht an einer lauten Stra\u00DFe liegen?',
faqEnv1A: 'Filtern Sie nach EPC-Bewertung (A bis C), dann \u00FCberlagern Sie die Stra\u00DFenl\u00E4rmdaten, um alles \u00FCber Ihrem Schwellenwert auszuschlie\u00DFen. F\u00E4rben Sie nach einem der beiden Kriterien, um ruhige, effiziente Stra\u00DFen auf einen Blick zu erkennen.',
faqEnv1Q: 'Kann ich energieeffiziente Wohnungen finden, die nicht an einer lauten Straße liegen?',
faqEnv1A: 'Filtern Sie nach EPC-Bewertung (A bis C), dann überlagern Sie die Straßenlärmdaten, um alles über Ihrem Schwellenwert auszuschließen. Färben Sie nach einem der beiden Kriterien, um ruhige, effiziente Straßen auf einen Blick zu erkennen.',
faqEnv2Q: 'Zeigt es Hochwasser- oder Senkungsrisiken?',
faqEnv2A: 'Wir integrieren Bodenstabilit\u00E4tsdaten, damit Sie vor dem Kauf auf Senkungen, Schrumpf-Quell-Tone und andere geologische Risiken pr\u00FCfen k\u00F6nnen. Schlie\u00DFen Sie Risikogebiete fr\u00FChzeitig aus.',
faqEnv2A: 'Wir integrieren Bodenstabilitätsdaten, damit Sie vor dem Kauf auf Senkungen, Schrumpf-Quell-Tone und andere geologische Risiken prüfen können. Schließen Sie Risikogebiete frühzeitig aus.',
faqEnv3Q: 'Kann ich Gebiete mit schnellem Breitband finden, die wirklich ruhig sind?',
faqEnv3A: '\u00DCberlagern Sie den Breitbandfilter mit den Stra\u00DFenl\u00E4rmdaten, um Stra\u00DFen mit guter Anbindung und wenig Verkehrsl\u00E4rm zu finden. F\u00E4rben Sie nach einem der beiden Kriterien, um Gebiete auf einen Blick zu vergleichen.',
faqEnv3A: 'Überlagern Sie den Breitbandfilter mit den Straßenlärmdaten, um Straßen mit guter Anbindung und wenig Verkehrslärm zu finden. Färben Sie nach einem der beiden Kriterien, um Gebiete auf einen Blick zu vergleichen.',
// FAQ items — Why Perfect Postcode
faqWhy1Q: 'Ich benutze bereits Rightmove. Was bringt mir das zus\u00E4tzlich?',
faqWhy1A: 'Rightmove zeigt Ihnen H\u00E4user. Wir zeigen Ihnen Gebiete. Kriminalit\u00E4tsraten, Schulbewertungen, Breitbandgeschwindigkeiten, L\u00E4rmpegel, Benachteiligungswerte und mehr \u2013 alles filterbar auf einer Karte. Sie k\u00F6nnen ein Viertel beurteilen, bevor Sie sich die Angebote ansehen.',
faqWhy1Q: 'Ich benutze bereits Rightmove. Was bringt mir das zusätzlich?',
faqWhy1A: 'Rightmove zeigt Ihnen Häuser. Wir zeigen Ihnen Gebiete. Kriminalitätsraten, Schulbewertungen, Breitbandgeschwindigkeiten, Lärmpegel, Benachteiligungswerte und mehr alles filterbar auf einer Karte. Sie können ein Viertel beurteilen, bevor Sie sich die Angebote ansehen.',
faqWhy2Q: 'Kann ich das nicht alles kostenlos selbst recherchieren?',
faqWhy2A: 'Sie k\u00F6nnten Polizeidaten, Ofsted-Berichte, EPC-Register, Land-Registry-Eintr\u00E4ge und ONS-Statistiken eine Postleitzahl nach der anderen abgleichen. Oder Sie haben alles filterbar und farbkodiert auf einer Karte in Sekunden.',
faqWhy3Q: 'Woher stammen die Daten tats\u00E4chlich?',
faqWhy3A: 'Jeder Datensatz stammt aus offiziellen britischen Regierungsquellen: Land Registry, EPC-Register, ONS, Ofsted, Ofcom, data.police.uk und Defra. Wir scrapen keine Makler und erfinden nichts. Sie k\u00F6nnen jeden Eintrag anhand der Originalquelle \u00FCberpr\u00FCfen.',
faqWhy2A: 'Sie könnten Polizeidaten, Ofsted-Berichte, EPC-Register, Land-Registry-Einträge und ONS-Statistiken eine Postleitzahl nach der anderen abgleichen. Oder Sie haben alles filterbar und farbkodiert auf einer Karte in Sekunden.',
faqWhy3Q: 'Woher stammen die Daten tatsächlich?',
faqWhy3A: 'Jeder Datensatz stammt aus offiziellen britischen Regierungsquellen: Land Registry, EPC-Register, ONS, Ofsted, Ofcom, data.police.uk und Defra. Wir scrapen keine Makler und erfinden nichts. Sie können jeden Eintrag anhand der Originalquelle überprüfen.',
// FAQ items — Pricing and Access
faqPricing1Q: 'Lohnt es sich wirklich, f\u00FCr ein Immobilien-Suchtool zu bezahlen?',
faqPricing1A: 'Ein Hauskauf ist wahrscheinlich die gr\u00F6\u00DFte Anschaffung Ihres Lebens. Ein einziges Warnsignal zu erkennen (eine laute Stra\u00DFe, schlechtes Breitband, steigende Kriminalit\u00E4t) bevor Sie sich festlegen, k\u00F6nnte Ihnen Jahre des Bedauerns ersparen. Das kostet weniger als eine Tankf\u00FCllung.',
faqPricing1Q: 'Lohnt es sich wirklich, für ein Immobilien-Suchtool zu bezahlen?',
faqPricing1A: 'Ein Hauskauf ist wahrscheinlich die größte Anschaffung Ihres Lebens. Ein einziges Warnsignal zu erkennen (eine laute Straße, schlechtes Breitband, steigende Kriminalität) bevor Sie sich festlegen, könnte Ihnen Jahre des Bedauerns ersparen. Das kostet weniger als eine Tankfüllung.',
faqPricing2Q: 'Ist das ein Abonnement?',
faqPricing2A: 'Nein. Einmalzahlung, Ihres f\u00FCr immer. Nutzen Sie es intensiv w\u00E4hrend Ihrer Suche, kommen Sie zur\u00FCck, wenn Sie neugierig auf ein neues Gebiet sind, und es ist immer noch da, falls Sie erneut umziehen.',
faqPricing2A: 'Nein. Einmalzahlung, Ihres für immer. Nutzen Sie es intensiv während Ihrer Suche, kommen Sie zurück, wenn Sie neugierig auf ein neues Gebiet sind, und es ist immer noch da, falls Sie erneut umziehen.',
faqPricing3Q: 'Was kann ich mit der kostenlosen Version nutzen?',
faqPricing3A: 'Kostenlose Nutzer k\u00F6nnen alle Funktionen im Demogebiet erkunden (Innenstadt London, ungef\u00E4hr Zonen 1 bis 2). F\u00FCr den Zugang zu Daten f\u00FCr den Rest Englands ben\u00F6tigen Sie den lebenslangen Zugang.',
faqPricing4Q: 'Kann ich eine R\u00FCckerstattung erhalten?',
faqPricing4A: 'Selbstverst\u00E4ndlich. Wir bieten eine 30-Tage-Geld-zur\u00FCck-Garantie. Wenn Sie nicht zufrieden sind, schreiben Sie innerhalb von 30 Tagen an support@perfect-postcode.co.uk f\u00FCr eine vollst\u00E4ndige R\u00FCckerstattung.',
faqPricing3A: 'Kostenlose Nutzer können alle Funktionen im Demogebiet erkunden (Innenstadt London, ungefähr Zonen 1 bis 2). Für den Zugang zu Daten für den Rest Englands benötigen Sie den lebenslangen Zugang.',
faqPricing4Q: 'Kann ich eine Rückerstattung erhalten?',
faqPricing4A: 'Selbstverständlich. Wir bieten eine 30-Tage-Geld-zurück-Garantie. Wenn Sie nicht zufrieden sind, schreiben Sie innerhalb von 30 Tagen an support@perfect-postcode.co.uk für eine vollständige Rückerstattung.',
// FAQ items — Tips and Tricks
faqTips1Q: 'Wie nutze ich den KI-Filter, anstatt Filter einzeln hinzuzuf\u00FCgen?',
faqTips1A: 'Beschreiben Sie, was Sie suchen, z.\u00A0B. \u201Eruhige Gegend nahe guten Schulen mit schnellem Breitband unter \u00A3400k\u201C, und die KI richtet alle relevanten Filter auf einmal ein. Passen Sie danach manuell an.',
faqTips2Q: 'Kann ich eine Suche speichern und sp\u00E4ter darauf zur\u00FCckkommen?',
faqTips2A: 'Klicken Sie auf Speichern und alles wird erfasst: Ihre Filter, die Zoomstufe und die angezeigte Datenebene. Machen Sie genau dort weiter, wo Sie aufgeh\u00F6rt haben, oder teilen Sie den Link mit Ihrem Partner.',
faqTips1Q: 'Wie nutze ich den KI-Filter, anstatt Filter einzeln hinzuzufügen?',
faqTips1A: 'Beschreiben Sie, was Sie suchen, z. B. „ruhige Gegend nahe guten Schulen mit schnellem Breitband unter £400k“, und die KI richtet alle relevanten Filter auf einmal ein. Passen Sie danach manuell an.',
faqTips2Q: 'Kann ich eine Suche speichern und später darauf zurückkommen?',
faqTips2A: 'Klicken Sie auf Speichern und alles wird erfasst: Ihre Filter, die Zoomstufe und die angezeigte Datenebene. Machen Sie genau dort weiter, wo Sie aufgehört haben, oder teilen Sie den Link mit Ihrem Partner.',
faqTips3Q: 'Kann ich die angezeigten Daten exportieren?',
faqTips3A: 'Nutzen Sie den Export-Button, um die aktuell gefilterten Immobilien als Tabelle herunterzuladen. Der Export ber\u00FCcksichtigt alle aktiven Filter, sodass Sie genau die gew\u00FCnschten Daten erhalten.',
faqTips3A: 'Nutzen Sie den Export-Button, um die aktuell gefilterten Immobilien als Tabelle herunterzuladen. Der Export berücksichtigt alle aktiven Filter, sodass Sie genau die gewünschten Daten erhalten.',
},
// ── Account Page ───────────────────────────────────
@ -541,7 +551,7 @@ const de: Translations = {
emailLabel: 'E-Mail',
subscriptionLabel: 'Abonnement',
upgrade: 'Upgraden',
redirecting: 'Weiterleitung\u2026',
redirecting: 'Weiterleitung',
receiveNewsletter: 'Newsletter-E-Mails erhalten',
needHelp: 'Brauchst du Hilfe? Schreib uns an',
responseTime: 'Wir antworten in der Regel innerhalb von 24 Stunden.',
@ -552,20 +562,20 @@ const de: Translations = {
searches: 'Suchen',
noSavedSearches: 'Noch keine gespeicherten Suchen',
noSavedSearchesDesc:
'Speichere deine Filter und Kartenansicht, um genau dort weiterzumachen, wo du aufgeh\u00F6rt hast.',
'Speichere deine Filter und Kartenansicht, um genau dort weiterzumachen, wo du aufgehört hast.',
noSavedProperties: 'Noch keine gespeicherten Immobilien',
noSavedPropertiesDesc:
'Merke dir Immobilien w\u00E4hrend du erkundest und erstelle deine Auswahlliste, ohne den \u00DCberblick zu verlieren.',
openPostcode: 'Postleitzahl \u00F6ffnen',
'Merke dir Immobilien während du erkundest und erstelle deine Auswahlliste, ohne den Überblick zu verlieren.',
openPostcode: 'Postleitzahl öffnen',
viewListing: 'Inserat ansehen',
clickToRename: 'Klicken zum Umbenennen',
notesPlaceholder: 'Notiere deine Gedanken...',
deleteSearch: 'Suche l\u00F6schen',
deleteSearch: 'Suche löschen',
deleteSearchConfirm:
'M\u00F6chtest du diese gespeicherte Suche wirklich l\u00F6schen? Dies kann nicht r\u00FCckg\u00E4ngig gemacht werden.',
deleteProperty: 'Immobilie l\u00F6schen',
'Möchtest du diese gespeicherte Suche wirklich löschen? Dies kann nicht rückgängig gemacht werden.',
deleteProperty: 'Immobilie löschen',
deletePropertyConfirm:
'M\u00F6chtest du diese gespeicherte Immobilie wirklich l\u00F6schen? Dies kann nicht r\u00FCckg\u00E4ngig gemacht werden.',
'Möchtest du diese gespeicherte Immobilie wirklich löschen? Dies kann nicht rückgängig gemacht werden.',
bed: 'Schlafz.',
epc: 'EPC',
},
@ -573,7 +583,7 @@ const de: Translations = {
// ── Invites Page ───────────────────────────────────
invitesPage: {
inviteLinksLicensed:
'Einladungslinks sind f\u00FCr lizenzierte Nutzer verf\u00FCgbar.',
'Einladungslinks sind für lizenzierte Nutzer verfügbar.',
inviteAdminLabel: 'Freunde einladen (100% Rabatt)',
inviteReferralLabel: 'Freunde einladen (30% Rabatt)',
generateFreeInvite: 'Kostenlosen Einladungslink erstellen',
@ -586,7 +596,7 @@ const de: Translations = {
link: 'Link',
status: 'Status',
created: 'Erstellt',
redeemed: 'Eingel\u00F6st',
redeemed: 'Eingelöst',
pending: 'Ausstehend',
},
@ -604,21 +614,21 @@ const de: Translations = {
'Ein Freund hat 30% Rabatt auf lebenslangen Zugang mit dir geteilt.',
exploreEvery: 'Entdecke jedes Viertel in England',
propertyInfo:
'Immobilienpreise, Energiebewertungen, Kriminalit\u00E4tsstatistiken, Schulbewertungen und mehr',
invalidInvite: 'Ung\u00FCltige Einladung',
'Immobilienpreise, Energiebewertungen, Kriminalitätsstatistiken, Schulbewertungen und mehr',
invalidInvite: 'Ungültige Einladung',
inviteAlreadyUsed: 'Einladung bereits verwendet',
inviteAlreadyUsedDesc:
'Dieser Einladungslink wurde bereits eingel\u00F6st.',
invalidInviteLink: 'Ung\u00FCltiger Einladungslink',
'Dieser Einladungslink wurde bereits eingelöst.',
invalidInviteLink: 'Ungültiger Einladungslink',
invalidInviteLinkDesc:
'Dieser Einladungslink ist ung\u00FCltig oder abgelaufen.',
'Dieser Einladungslink ist ungültig oder abgelaufen.',
licenseActivated: 'Lizenz aktiviert!',
fullAccessGranted:
'Du hast jetzt vollen Zugang zu Perfect Postcode.',
activating: 'Wird aktiviert...',
activateLicense: 'Lizenz aktivieren',
claimDiscount: 'Rabatt einl\u00F6sen',
registerToClaim: 'Registrieren zum Einl\u00F6sen',
claimDiscount: 'Rabatt einlösen',
registerToClaim: 'Registrieren zum Einlösen',
youAlreadyHaveLicense: 'Du hast bereits eine Lizenz',
accountHasFullAccess: 'Dein Konto hat bereits vollen Zugang.',
failedToValidate: 'Einladungslink konnte nicht validiert werden',
@ -642,7 +652,7 @@ const de: Translations = {
poiCategories: '{{count}} POI-Kategorien',
travelDestination: '{{count}} Fahrziel',
travelDestinations: '{{count}} Fahrziele',
propertiesMatch: '{{count}} Immobilien stimmen \u00FCberein',
propertiesMatch: '{{count}} Immobilien stimmen überein',
setFilters: '{{count}} Filter setzen: {{list}}',
noFiltersSet: 'Keine Filter gesetzt',
toDestination: '{{mode}} nach {{label}} {{bounds}}',
@ -652,18 +662,18 @@ const de: Translations = {
// ── Tutorial ──────────────────────────────────────
tutorial: {
step1Title: 'Sagen Sie der Karte, was z\u00E4hlt',
step1Content: 'Legen Sie Ihr Budget, maximale Pendelzeit, Schulqualit\u00E4t und Kriminalit\u00E4tsschwelle fest. Was Ihnen wichtig ist. Nur qualifizierende Gebiete bleiben hervorgehoben. Nutzen Sie das Augensymbol, um nach beliebigem Merkmal einzuf\u00E4rben.',
step1Title: 'Sagen Sie der Karte, was zählt',
step1Content: 'Legen Sie Ihr Budget, maximale Pendelzeit, Schulqualität und Kriminalitätsschwelle fest. Was Ihnen wichtig ist. Nur qualifizierende Gebiete bleiben hervorgehoben. Nutzen Sie das Augensymbol, um nach beliebigem Merkmal einzufärben.',
step2Title: 'Oder einfach beschreiben',
step2Content: 'Tippen Sie auf Deutsch ein, was Sie suchen, z.\u00A0B. \u201Eruhige Gegend nahe guter Schulen unter \u00A3400k\u201C, und wir richten die Filter f\u00FCr Sie ein.',
step2Content: 'Tippen Sie auf Deutsch ein, was Sie suchen, z. B. „ruhige Gegend nahe guter Schulen unter £400k“, und wir richten die Filter für Sie ein.',
step3Title: 'Erkunden Sie, was es gibt',
step3Content: 'Schwenken und zoomen Sie durch England. Klicken Sie auf ein beliebiges farbiges Gebiet, um Kriminalit\u00E4t, Schulen, Preise, Breitband, L\u00E4rm und mehr zu sehen.',
step3Content: 'Schwenken und zoomen Sie durch England. Klicken Sie auf ein beliebiges farbiges Gebiet, um Kriminalität, Schulen, Preise, Breitband, Lärm und mehr zu sehen.',
step4Title: 'Direkt zu einem Ort springen',
step4Content: 'Suchen Sie nach einem Ort oder einer Postleitzahl, um sofort dorthin zu gelangen.',
step5Title: 'Ins Detail gehen',
step5Content: 'Sehen Sie Gebietsstatistiken, Histogramme und einzelne Immobiliendaten: Preise, Wohnfl\u00E4che, Energiebewertungen und mehr.',
step6Title: 'Was ist in der N\u00E4he?',
step6Content: 'Blenden Sie Schulen, Gesch\u00E4fte, Bahnh\u00F6fe, Parks und Restaurants auf der Karte ein, um zu sehen, was erreichbar ist.',
step5Content: 'Sehen Sie Gebietsstatistiken, Histogramme und einzelne Immobiliendaten: Preise, Wohnfläche, Energiebewertungen und mehr.',
step6Title: 'Was ist in der Nähe?',
step6Content: 'Blenden Sie Schulen, Geschäfte, Bahnhöfe, Parks und Restaurants auf der Karte ein, um zu sehen, was erreichbar ist.',
},
// ── Server-derived values ──────────────────────────
@ -675,7 +685,7 @@ const de: Translations = {
'Transport': 'Verkehr',
'Education': 'Bildung',
'Deprivation': 'Benachteiligung',
'Crime': 'Kriminalit\u00E4t',
'Crime': 'Kriminalität',
'Demographics': 'Demografie',
'Amenities': 'Infrastruktur',
@ -684,14 +694,14 @@ const de: Translations = {
'Property type': 'Immobilientyp',
'Leasehold/Freehold': 'Erbbaurecht/Volleigentum',
'Last known price': 'Letzter bekannter Preis',
'Estimated current price': 'Gesch\u00E4tzter aktueller Preis',
'Estimated current price': 'Geschätzter aktueller Preis',
'Asking price': 'Angebotspreis',
'Price per sqm': 'Preis pro m\u00B2',
'Est. price per sqm': 'Gesch. Preis pro m\u00B2',
'Asking price per sqm': 'Angebotspreis pro m\u00B2',
'Estimated monthly rent': 'Gesch\u00E4tzte Monatsmiete',
'Price per sqm': 'Preis pro m²',
'Est. price per sqm': 'Gesch. Preis pro m²',
'Asking price per sqm': 'Angebotspreis pro m²',
'Estimated monthly rent': 'Geschätzte Monatsmiete',
'Asking rent (monthly)': 'Angebotsmiete (monatlich)',
'Total floor area (sqm)': 'Gesamtwohnfl\u00E4che (m\u00B2)',
'Total floor area (sqm)': 'Gesamtwohnfläche (m²)',
'Number of bedrooms & living rooms': 'Anzahl Schlaf- & Wohnzimmer',
'Bedrooms': 'Schlafzimmer',
'Bathrooms': 'Badezimmer',
@ -701,26 +711,25 @@ const de: Translations = {
'Former council house': 'Ehemaliger Sozialbau',
'Current energy rating': 'Aktuelle Energiebewertung',
'Potential energy rating': 'Potenzielle Energiebewertung',
'Interior height (m)': 'Raumh\u00F6he (m)',
'Interior height (m)': 'Raumhöhe (m)',
// ─ Feature names (Transport) ─
'Distance to nearest train or tube station (km)': 'Entfernung zum n\u00E4chsten Bahn- oder U-Bahnhof (km)',
'Train or tube stations within 1km': 'Bahn- oder U-Bahnh\u00F6fe im Umkreis von 1 km',
'Distance to nearest train or tube station (km)': 'Entfernung zum nächsten Bahn- oder U-Bahnhof (km)',
// ─ Feature names (Education) ─
'Good+ primary schools within 2km': 'Gute+ Grundschulen im Umkreis von 2 km',
'Good+ secondary schools within 2km': 'Gute+ weiterf\u00FChrende Schulen im Umkreis von 2 km',
'Good+ secondary schools within 2km': 'Gute+ weiterführende Schulen im Umkreis von 2 km',
'Good+ primary schools within 5km': 'Gute+ Grundschulen im Umkreis von 5 km',
'Good+ secondary schools within 5km': 'Gute+ weiterf\u00FChrende Schulen im Umkreis von 5 km',
'Education, Skills and Training Score': 'Score f\u00FCr Bildung, Kompetenzen und Ausbildung',
'Good+ secondary schools within 5km': 'Gute+ weiterführende Schulen im Umkreis von 5 km',
'Education, Skills and Training Score': 'Score für Bildung, Kompetenzen und Ausbildung',
// ─ Feature names (Deprivation) ─
'Income Score (rate)': 'Einkommensscore (Rate)',
'Employment Score (rate)': 'Besch\u00E4ftigungsscore (Rate)',
'Health Deprivation and Disability Score': 'Score f\u00FCr Gesundheit und Behinderung',
'Employment Score (rate)': 'Beschäftigungsscore (Rate)',
'Health Deprivation and Disability Score': 'Score für Gesundheit und Behinderung',
'Living Environment Score': 'Score der Wohnumgebung',
'Indoors Sub-domain Score': 'Score der Wohnqualit\u00E4t (innen)',
'Outdoors Sub-domain Score': 'Score der Umgebungsqualit\u00E4t (au\u00DFen)',
'Indoors Sub-domain Score': 'Score der Wohnqualität (innen)',
'Outdoors Sub-domain Score': 'Score der Umgebungsqualität (außen)',
// ─ Feature names (Crime) ─
'Serious crime per 1k residents (avg/yr)': 'Schwere Straftaten pro 1k Einwohner (Durchschn./Jahr)',
@ -728,36 +737,36 @@ const de: Translations = {
'Serious crime (avg/yr)': 'Schwere Straftaten (Durchschn./Jahr)',
'Minor crime (avg/yr)': 'Leichte Straftaten (Durchschn./Jahr)',
'Violence and sexual offences (avg/yr)': 'Gewalt- und Sexualdelikte (Durchschn./Jahr)',
'Burglary (avg/yr)': 'Einbr\u00FCche (Durchschn./Jahr)',
'Robbery (avg/yr)': 'Raub\u00FCberf\u00E4lle (Durchschn./Jahr)',
'Vehicle crime (avg/yr)': 'Fahrzeugkriminalit\u00E4t (Durchschn./Jahr)',
'Burglary (avg/yr)': 'Einbrüche (Durchschn./Jahr)',
'Robbery (avg/yr)': 'Raubüberfälle (Durchschn./Jahr)',
'Vehicle crime (avg/yr)': 'Fahrzeugkriminalität (Durchschn./Jahr)',
'Anti-social behaviour (avg/yr)': 'Antisoziales Verhalten (Durchschn./Jahr)',
'Criminal damage and arson (avg/yr)': 'Sachbesch\u00E4digung und Brandstiftung (Durchschn./Jahr)',
'Criminal damage and arson (avg/yr)': 'Sachbeschädigung und Brandstiftung (Durchschn./Jahr)',
'Other theft (avg/yr)': 'Sonstiger Diebstahl (Durchschn./Jahr)',
'Theft from the person (avg/yr)': 'Taschendiebstahl (Durchschn./Jahr)',
'Shoplifting (avg/yr)': 'Ladendiebstahl (Durchschn./Jahr)',
'Bicycle theft (avg/yr)': 'Fahrraddiebstahl (Durchschn./Jahr)',
'Drugs (avg/yr)': 'Drogendelikte (Durchschn./Jahr)',
'Possession of weapons (avg/yr)': 'Waffenbesitz (Durchschn./Jahr)',
'Public order (avg/yr)': 'St\u00F6rung der \u00F6ffentlichen Ordnung (Durchschn./Jahr)',
'Public order (avg/yr)': 'Störung der öffentlichen Ordnung (Durchschn./Jahr)',
'Other crime (avg/yr)': 'Sonstige Straftaten (Durchschn./Jahr)',
// ─ Feature names (Demographics) ─
'Median age': 'Medianalter',
'% White': '% Wei\u00DF',
'% South Asian': '% S\u00FCdasiatisch',
'% White': '% Weiß',
'% South Asian': '% Südasiatisch',
'% Black': '% Schwarz',
'% East Asian': '% Ostasiatisch',
'% Mixed': '% Gemischt',
'% Other': '% Sonstige',
// ─ Feature names (Amenities) ─
'Distance to nearest park (km)': 'Entfernung zum n\u00E4chsten Park (km)',
'Distance to nearest park (km)': 'Entfernung zum nächsten Park (km)',
'Number of parks within 2km': 'Anzahl Parks im Umkreis von 2 km',
'Number of restaurants within 2km': 'Anzahl Restaurants im Umkreis von 2 km',
'Number of grocery shops and supermarkets within 2km': 'Anzahl Lebensmittelgesch\u00E4fte und Superm\u00E4rkte im Umkreis von 2 km',
'Noise (dB)': 'L\u00E4rm (dB)',
'Max available download speed (Mbps)': 'Max. verf\u00FCgbare Downloadgeschwindigkeit (Mbps)',
'Number of grocery shops and supermarkets within 2km': 'Anzahl Lebensmittelgeschäfte und Supermärkte im Umkreis von 2 km',
'Noise (dB)': 'Lärm (dB)',
'Max available download speed (Mbps)': 'Max. verfügbare Downloadgeschwindigkeit (Mbps)',
// ─ Enum values ─
@ -765,7 +774,7 @@ const de: Translations = {
'For sale': 'Zum Verkauf',
'For rent': 'Zur Miete',
'Detached': 'Freistehend',
'Semi-Detached': 'Doppelhaush\u00E4lfte',
'Semi-Detached': 'Doppelhaushälfte',
'Terraced': 'Reihenhaus',
'Flats/Maisonettes': 'Wohnungen/Maisonetten',
'Other': 'Sonstige',
@ -780,25 +789,25 @@ const de: Translations = {
'Ethnic composition': 'Ethnische Zusammensetzung',
// ─ POI group names ─
'Public Transport': '\u00D6ffentlicher Nahverkehr',
'Public Transport': 'Öffentlicher Nahverkehr',
'Leisure': 'Freizeit',
'Health': 'Gesundheit',
'Emergency Services': 'Rettungsdienste',
'Groceries': 'Lebensmittel',
'Local Businesses': 'Lokale Gesch\u00E4fte',
'Local Businesses': 'Lokale Geschäfte',
'Culture': 'Kultur',
'Services': 'Dienstleistungen',
'Shops': 'Gesch\u00E4fte',
'Shops': 'Geschäfte',
// ─ POI categories ─
'Airport': 'Flughafen',
'Ferry': 'F\u00E4hre',
'Ferry': 'Fähre',
'Rail station': 'Bahnhof',
'Bus stop': 'Bushaltestelle',
'Bus station': 'Busbahnhof',
'Taxi rank': 'Taxistand',
'Metro or Tram stop': 'U-Bahn- oder Stra\u00DFenbahnhaltestelle',
'Caf\u00E9': 'Caf\u00E9',
'Metro or Tram stop': 'U-Bahn- oder Straßenbahnhaltestelle',
'Café': 'Café',
'Restaurant': 'Restaurant',
'Pub': 'Pub',
'Bar': 'Bar',
@ -812,12 +821,12 @@ const de: Translations = {
'Sports Centre': 'Sportzentrum',
'Entertainment': 'Unterhaltung',
'Supermarket': 'Supermarkt',
'Convenience Store': 'Sp\u00E4tkauf',
'Bakery': 'B\u00E4ckerei',
'Butcher & Fishmonger': 'Metzgerei & Fischh\u00E4ndler',
'Greengrocer': 'Gem\u00FCseh\u00E4ndler',
'Off-Licence': 'Getr\u00E4nkeladen',
'Deli & Specialty': 'Feinkost & Spezialit\u00E4ten',
'Convenience Store': 'Spätkauf',
'Bakery': 'Bäckerei',
'Butcher & Fishmonger': 'Metzgerei & Fischhändler',
'Greengrocer': 'Gemüsehändler',
'Off-Licence': 'Getränkeladen',
'Deli & Specialty': 'Feinkost & Spezialitäten',
'Fashion & Clothing': 'Mode & Bekleidung',
'Electronics': 'Elektronik',
'Charity Shop': 'Secondhand-Laden',
@ -826,18 +835,18 @@ const de: Translations = {
'Bookshop': 'Buchhandlung',
'Pet Shop': 'Tierhandlung',
'Sports & Outdoor': 'Sport & Outdoor',
'Newsagent': 'Zeitungsh\u00E4ndler',
'Newsagent': 'Zeitungshändler',
'Department Store': 'Kaufhaus',
'Gift & Hobby': 'Geschenke & Hobby',
'Specialist Shop': 'Fachgesch\u00E4ft',
'Specialist Shop': 'Fachgeschäft',
'Hairdresser & Beauty': 'Friseur & Kosmetik',
'Gym & Fitness': 'Fitnessstudio',
'Dry Cleaner & Laundry': 'Reinigung & W\u00E4scherei',
'Dry Cleaner & Laundry': 'Reinigung & Wäscherei',
'Car Services': 'Autoservice',
'Post Office': 'Postamt',
'Vet & Pet Care': 'Tierarzt & Tierpflege',
'Bank': 'Bank',
'Travel Agent': 'Reiseb\u00FCro',
'Travel Agent': 'Reisebüro',
'Police': 'Polizei',
'Fire Station': 'Feuerwache',
'Ambulance Station': 'Rettungswache',
@ -849,18 +858,18 @@ const de: Translations = {
'Physiotherapy': 'Physiotherapie',
'Counselling & Therapy': 'Beratung & Therapie',
'Care Home': 'Pflegeheim',
'Medical & Mobility': 'Medizintechnik & Mobilit\u00E4t',
'Medical & Mobility': 'Medizintechnik & Mobilität',
'Museum': 'Museum',
'Gallery': 'Galerie',
'Library': 'Bibliothek',
'Place of Worship': 'Gebetsst\u00E4tte',
'Place of Worship': 'Gebetsstätte',
'Arts Centre': 'Kunstzentrum',
'Zoo': 'Zoo',
'Tourist Attraction': 'Touristenattraktion',
'School': 'Schule',
'Hotel': 'Hotel',
'Local Business': 'Lokales Gesch\u00E4ft',
'Offices': 'B\u00FCros',
'Local Business': 'Lokales Geschäft',
'Offices': 'Büros',
'EV Charging': 'E-Ladestation',
'Fuel Station': 'Tankstelle',
'Community Centre': 'Gemeindezentrum',
@ -868,7 +877,7 @@ const de: Translations = {
// ─ Suffixes (used in formatters) ─
'/mo': '/Monat',
'/yr': '/Jahr',
' sqm': ' m\u00B2',
' sqm': ' m²',
' km': ' km',
' m': ' m',
' dB': ' dB',

View file

@ -139,6 +139,11 @@ const en = {
removeFilterHint: 'Remove a filter to see available features',
featureInfo: 'Feature info',
replayTutorial: 'Replay interactive tutorial',
clearAll: 'Clear all',
clearAllTitle: 'Clear all filters?',
clearAllSavePrompt: 'Would you like to save your current filters before clearing?',
saveAndClear: 'Save & Clear',
clearWithoutSaving: 'Clear without saving',
},
// ── Philosophy Popup ───────────────────────────────
@ -199,9 +204,9 @@ const en = {
describeIdealArea: 'Describe your ideal area with AI',
aiSearch: 'AI Search',
describeHint: "describe what you're looking for",
placeholder: 'e.g. quiet area, under \u00A3400k, near good schools...',
placeholder: 'e.g. quiet area, under £400k, near good schools...',
example1: 'Safe area near good schools',
example2: '30 min commute to Kings Cross, under \u00A3500k',
example2: '30 min commute to Kings Cross, under £500k',
example3: 'Quiet village, 3 bed, fast broadband',
analysing: 'Analysing your query...',
searchingDestinations: 'Searching for destinations...',
@ -213,6 +218,11 @@ const en = {
// ── Map Legend ─────────────────────────────────────
mapLegend: {
clearColourView: 'Clear colour view',
historicalMatches: 'Historical property matches',
propertiesForSale: 'Properties for sale',
propertiesForRent: 'Properties for rent',
numberOfProperties: 'Number of properties',
previewing: 'Previewing \u201c{{name}}\u201d',
},
// ── Properties Pane ────────────────────────────────
@ -220,7 +230,7 @@ const en = {
unknownAddress: 'Unknown Address',
unsaveProperty: 'Unsave property',
saveProperty: 'Save property',
lastSold: 'Last sold: \u00A3{{price}}',
lastSold: 'Last sold: £{{price}}',
estValue: 'Est. value:',
type: 'Type:',
builtForm: 'Built form:',
@ -237,7 +247,7 @@ const en = {
renovations: 'Renovations',
viewExternalListing: 'View external listing',
perMonth: '/mo',
perSqm: '/m\u00B2',
perSqm: '/m²',
searchPlaceholder: 'Search by address or postcode...',
propertyData: 'Property Data',
propertyDataDesc: 'Prices come from HM Land Registry (what buyers actually paid). Floor area, energy ratings, construction year, and tenure come from official EPC surveys. Both sources are matched by address within each postcode.',
@ -322,16 +332,16 @@ const en = {
philosophyP2: 'We flip that. Tell us what you need (budget, commute, schools, safety) and we show you every area in England that qualifies. No guesswork. No wasted viewings.',
howToUseIt: 'How to use it',
howStep1Title: 'Set your must-haves',
howStep1Desc: 'Budget, commute, schools \u2014 the map shows only what qualifies.',
howStep1Desc: 'Budget, commute, schools the map shows only what qualifies.',
howStep2Title: 'Explore areas and discover hidden gems',
howStep2Desc: 'Zoom in, dig into details and nice to haves.',
howStep3Title: 'Drill into postcodes',
howStep3Desc: 'See individual properties, sale prices, floor area, and compare.',
howStep4Title: 'Shortlist with confidence',
howStep4Desc: 'Every area on your list meets your actual criteria \u2014 not just what was listed that week.',
howStep4Desc: 'Every area on your list meets your actual criteria not just what was listed that week.',
othersVs: 'Others vs',
listingPortals: 'Listing portals',
checkMyPostcode: '\u201CCheck my postcode\u201D',
checkMyPostcode: '“Check my postcode”',
areaGuides: 'Area guides',
compSearchWithout: 'Search without choosing an area first',
compSearchWithoutSub: '(start with needs, not a location)',
@ -341,7 +351,7 @@ const en = {
compPropertyDataSub: '(price, EPC, floor area)',
compFilters: '56 combinable filters in one place',
compFiltersSub: '(all insights, one interactive map)',
ctaTitle: 'Make your biggest investment your smartest\u00A0move.',
ctaTitle: 'Make your biggest investment your smartest move.',
ctaDescription: "This deserves proper tools behind it, don't leave it to luck.",
},
@ -349,7 +359,7 @@ const en = {
pricingPage: {
title: 'Early access pricing',
subtitle: 'Pay once, access forever. The earlier you join, the less you pay.',
costContext: "Buying a home costs \u00A310k+ in stamp duty, \u00A31,500 in solicitor fees, \u00A3500 for a survey. Get the wrong area and you're stuck with a long commute, bad schools, or a road you didn't know about.",
costContext: "Buying a home costs £10k+ in stamp duty, £1,500 in solicitor fees, £500 for a survey. Get the wrong area and you're stuck with a long commute, bad schools, or a road you didn't know about.",
lessThanSurvey: 'Less than a home survey. Far more useful.',
currentTier: 'Current tier',
firstNUsers: 'First {{count}} users',
@ -386,13 +396,13 @@ const en = {
source: 'Source:',
optOut: 'Opt out of public disclosure',
attribution: 'Attribution',
attrLandRegistry: 'Contains HM Land Registry data \u00A9 Crown copyright and database right 2025.',
attrLandRegistry: 'Contains HM Land Registry data © Crown copyright and database right 2025.',
attrOgl: 'Contains public sector information licensed under the',
attrOglLink: 'Open Government Licence v3.0',
attrOs: 'Contains OS data \u00A9 Crown copyright and database rights 2025.',
attrOs: 'Contains OS data © Crown copyright and database rights 2025.',
attrTfl: 'Powered by TfL Open Data.',
attrOsm: 'Contains data from',
attrOsmContrib: '\u00A9 OpenStreetMap contributors',
attrOsmContrib: '© OpenStreetMap contributors',
attrOsmLicense: 'available under the',
attrOsmLicenseLink: 'Open Data Commons Open Database License (ODbL)',
// Data source names & descriptions
@ -497,12 +507,12 @@ const en = {
faqPricing3Q: 'What can I access on the free tier?',
faqPricing3A: 'Free users can explore all features within the demo area (inner London, roughly zones 1 to 2). To access data for the rest of England, you need lifetime access.',
faqPricing4Q: 'Can I get a refund?',
faqPricing4A: 'Absolutely. We offer a 30-day money-back guarantee. If you\u2019re not satisfied, email support@perfect-postcode.co.uk within 30 days for a full refund.',
faqPricing4A: 'Absolutely. We offer a 30-day money-back guarantee. If youre not satisfied, email support@perfect-postcode.co.uk within 30 days for a full refund.',
// FAQ items — Tips and Tricks
faqTips1Q: 'How do I use the AI filter instead of adding filters one by one?',
faqTips1A: 'Type what you want in plain English, something like "quiet area near good schools with fast broadband under \u00A3400k", and it\'ll set up all the relevant filters in one go. Tweak any of them manually afterwards.',
faqTips1A: 'Type what you want in plain English, something like "quiet area near good schools with fast broadband under £400k", and it\'ll set up all the relevant filters in one go. Tweak any of them manually afterwards.',
faqTips2Q: 'Can I save a search and come back to it later?',
faqTips2A: 'Hit the save button and everything is captured: your filters, zoom level, and which data layer you\u2019re colouring by. Pick up exactly where you left off or share the link with your partner.',
faqTips2A: 'Hit the save button and everything is captured: your filters, zoom level, and which data layer youre colouring by. Pick up exactly where you left off or share the link with your partner.',
faqTips3Q: "Can I export the data I'm looking at?",
faqTips3A: 'Use the export button to download the currently filtered properties as a spreadsheet. The export respects all your active filters, so you get exactly the data you want.',
},
@ -512,7 +522,7 @@ const en = {
emailLabel: 'Email',
subscriptionLabel: 'Subscription',
upgrade: 'Upgrade',
redirecting: 'Redirecting\u2026',
redirecting: 'Redirecting',
receiveNewsletter: 'Receive newsletter emails',
needHelp: 'Need help? Email us at',
responseTime: 'We typically respond within 24 hours.',
@ -613,15 +623,15 @@ const en = {
step1Title: 'Tell the map what matters',
step1Content: '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.',
step2Title: 'Or just describe it',
step2Content: 'Type what you want in plain English, like "quiet area near good schools under \u00A3400k", and we\u2019ll set up the filters for you.',
step3Title: 'Explore what\u2019s out there',
step2Content: 'Type what you want in plain English, like "quiet area near good schools under £400k", and well set up the filters for you.',
step3Title: 'Explore whats out there',
step3Content: 'Pan and zoom across England. Click any coloured area to see crime, schools, prices, broadband, noise, and more about that neighbourhood.',
step4Title: 'Jump to a location',
step4Content: 'Search for any place or postcode to fly straight there.',
step5Title: 'Dig into the details',
step5Content: 'See area statistics, histograms, and individual property records: prices, floor area, energy ratings, and more.',
step6Title: 'What\u2019s nearby?',
step6Content: 'Toggle schools, shops, stations, parks, and restaurants on the map to see what\u2019s within reach.',
step6Title: 'Whats nearby?',
step6Content: 'Toggle schools, shops, stations, parks, and restaurants on the map to see whats within reach.',
},
// ── Server-derived values ──────────────────────────
@ -663,7 +673,6 @@ const en = {
// ─ Feature names (Transport) ─
'Distance to nearest train or tube station (km)': 'Distance to nearest train or tube station (km)',
'Train or tube stations within 1km': 'Train or tube stations within 1km',
// ─ Feature names (Education) ─
'Good+ primary schools within 2km': 'Good+ primary schools within 2km',
@ -756,7 +765,7 @@ const en = {
'Bus station': 'Bus station',
'Taxi rank': 'Taxi rank',
'Metro or Tram stop': 'Metro or Tram stop',
'Caf\u00E9': 'Caf\u00E9',
'Café': 'Café',
'Restaurant': 'Restaurant',
'Pub': 'Pub',
'Bar': 'Bar',

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -141,6 +141,11 @@ const zh: Translations = {
removeFilterHint: '移除一个筛选条件以查看可用的数据指标',
featureInfo: '数据指标信息',
replayTutorial: '重新播放交互教程',
clearAll: '全部清除',
clearAllTitle: '清除所有筛选条件?',
clearAllSavePrompt: '是否要在清除前保存当前的筛选条件?',
saveAndClear: '保存并清除',
clearWithoutSaving: '不保存直接清除',
},
// ── Philosophy Popup ───────────────────────────────
@ -201,9 +206,9 @@ const zh: Translations = {
describeIdealArea: '用 AI 描述您的理想区域',
aiSearch: 'AI 搜索',
describeHint: '描述您要找的区域',
placeholder: '例如:安静的区域,低于 \u00A340万靠近好学校...',
placeholder: '例如:安静的区域,低于 £40万靠近好学校...',
example1: '安全的区域,靠近好学校',
example2: '到国王十字站30分钟通勤低于 \u00A350万',
example2: '到国王十字站30分钟通勤低于 £50万',
example3: '安静的村庄3间卧室快速宽带',
analysing: '正在分析您的需求...',
searchingDestinations: '正在搜索目的地...',
@ -215,6 +220,11 @@ const zh: Translations = {
// ── Map Legend ─────────────────────────────────────
mapLegend: {
clearColourView: '清除颜色视图',
historicalMatches: '历史房产匹配',
propertiesForSale: '待售房产',
propertiesForRent: '待租房产',
numberOfProperties: '房产数量',
previewing: '预览\u201c{{name}}\u201d',
},
// ── Properties Pane ────────────────────────────────
@ -222,7 +232,7 @@ const zh: Translations = {
unknownAddress: '地址未知',
unsaveProperty: '取消收藏',
saveProperty: '收藏房产',
lastSold: '上次成交价:\u00A3{{price}}',
lastSold: '上次成交价:£{{price}}',
estValue: '估计价值:',
type: '类型:',
builtForm: '建筑形式:',
@ -239,7 +249,7 @@ const zh: Translations = {
renovations: '翻新记录',
viewExternalListing: '查看外部房源',
perMonth: '/月',
perSqm: '/m\u00B2',
perSqm: '/m²',
searchPlaceholder: '按地址或邮编搜索...',
propertyData: '房产数据',
propertyDataDesc: '价格来自英国土地注册局(买家实际支付的金额)。建筑面积、能源评级、建造年份和产权来自官方能源性能证书调查。两个数据源通过每个邮编内的地址进行匹配。',
@ -343,7 +353,7 @@ const zh: Translations = {
compPropertyDataSub: '(价格、能源性能证书、建筑面积)',
compFilters: '56 项可组合筛选条件,尽在一处',
compFiltersSub: '(所有信息,一张交互式地图)',
ctaTitle: '让您最大的投资成为最明智的\u00A0决定。',
ctaTitle: '让您最大的投资成为最明智的 决定。',
ctaDescription: '这值得用专业的工具来做,别全靠运气。',
},
@ -351,7 +361,7 @@ const zh: Translations = {
pricingPage: {
title: '早期访问价格',
subtitle: '一次付款,永久访问。越早加入,价格越优惠。',
costContext: '买房需要支付超过 \u00A310,000 的印花税、\u00A31,500 的律师费、\u00A3500 的房屋评估费。选错区域,您可能要忍受漫长的通勤、差劲的学校,或一条您事先不知道的嘈杂马路。',
costContext: '买房需要支付超过 £10,000 的印花税、£1,500 的律师费、£500 的房屋评估费。选错区域,您可能要忍受漫长的通勤、差劲的学校,或一条您事先不知道的嘈杂马路。',
lessThanSurvey: '不到一次房屋评估的费用,却有用得多。',
currentTier: '当前档位',
firstNUsers: '前 {{count}} 名用户',
@ -514,7 +524,7 @@ const zh: Translations = {
emailLabel: '邮箱',
subscriptionLabel: '订阅',
upgrade: '升级',
redirecting: '跳转中\u2026',
redirecting: '跳转中',
receiveNewsletter: '接收新闻邮件',
needHelp: '需要帮助?请发邮件至',
responseTime: '我们通常在 24 小时内回复。',
@ -592,38 +602,38 @@ const zh: Translations = {
// ── Format / Time ──────────────────────────────────
format: {
justNow: '\u521A\u521A',
minutesAgo: '{{count}}\u5206\u949F\u524D',
hoursAgo: '{{count}}\u5C0F\u65F6\u524D',
daysAgo: '{{count}}\u5929\u524D',
nFilters: '{{count}} \u4E2A\u7B5B\u9009',
noFilters: '\u65E0\u7B5B\u9009',
poiCategory: '{{count}} \u4E2A POI \u7C7B\u522B',
poiCategories: '{{count}} \u4E2A POI \u7C7B\u522B',
travelDestination: '{{count}} \u4E2A\u51FA\u884C\u76EE\u7684\u5730',
travelDestinations: '{{count}} \u4E2A\u51FA\u884C\u76EE\u7684\u5730',
propertiesMatch: '{{count}} \u5957\u623F\u4EA7\u7B26\u5408',
setFilters: '\u8BBE\u7F6E {{count}} \u4E2A\u7B5B\u9009\uFF1A{{list}}',
noFiltersSet: '\u672A\u8BBE\u7F6E\u7B5B\u9009',
toDestination: '{{mode}}\u5230 {{label}} {{bounds}}',
lessThanMin: '< {{max}} \u5206\u949F',
moreThanMin: '> {{min}} \u5206\u949F',
justNow: '刚刚',
minutesAgo: '{{count}}分钟前',
hoursAgo: '{{count}}小时前',
daysAgo: '{{count}}天前',
nFilters: '{{count}} 个筛选',
noFilters: '无筛选',
poiCategory: '{{count}} 个 POI 类别',
poiCategories: '{{count}} 个 POI 类别',
travelDestination: '{{count}} 个出行目的地',
travelDestinations: '{{count}} 个出行目的地',
propertiesMatch: '{{count}} 套房产符合',
setFilters: '设置 {{count}} 个筛选:{{list}}',
noFiltersSet: '未设置筛选',
toDestination: '{{mode}} {{label}} {{bounds}}',
lessThanMin: '< {{max}} 分钟',
moreThanMin: '> {{min}} 分钟',
},
// ── Tutorial ──────────────────────────────────────
tutorial: {
step1Title: '\u544A\u8BC9\u5730\u56FE\u4EC0\u4E48\u91CD\u8981',
step1Content: '\u8BBE\u7F6E\u9884\u7B97\u3001\u901A\u52E4\u4E0A\u9650\u3001\u5B66\u6821\u8D28\u91CF\u3001\u72AF\u7F6A\u95E8\u69DB\u3002\u60A8\u5173\u5FC3\u7684\u4E00\u5207\u3002\u53EA\u6709\u7B26\u5408\u6761\u4EF6\u7684\u533A\u57DF\u4F1A\u4FDD\u6301\u9AD8\u4EAE\u3002\u4F7F\u7528\u773C\u775B\u56FE\u6807\u6309\u4EFB\u610F\u7279\u5F81\u7740\u8272\u3002',
step2Title: '\u6216\u8005\u76F4\u63A5\u63CF\u8FF0',
step2Content: '\u7528\u4E2D\u6587\u8F93\u5165\u60A8\u7684\u9700\u6C42\uFF0C\u4F8B\u5982\u201C\u5B89\u9759\u7684\u5730\u533A\uFF0C\u9760\u8FD1\u597D\u5B66\u6821\uFF0C\u00A3400k \u4EE5\u4E0B\u201D\uFF0C\u6211\u4EEC\u4F1A\u4E3A\u60A8\u8BBE\u7F6E\u7B5B\u9009\u3002',
step3Title: '\u63A2\u7D22\u73B0\u6709\u4F4F\u5B85',
step3Content: '\u5728\u82F1\u683C\u5170\u5404\u5730\u5E73\u79FB\u548C\u7F29\u653E\u3002\u70B9\u51FB\u4EFB\u4F55\u5F69\u8272\u533A\u57DF\u67E5\u770B\u72AF\u7F6A\u3001\u5B66\u6821\u3001\u4EF7\u683C\u3001\u5BBD\u5E26\u3001\u566A\u97F3\u7B49\u4FE1\u606F\u3002',
step4Title: '\u8DF3\u8F6C\u5230\u67D0\u4E2A\u4F4D\u7F6E',
step4Content: '\u641C\u7D22\u4EFB\u4F55\u5730\u70B9\u6216\u90AE\u7F16\uFF0C\u5373\u53EF\u76F4\u63A5\u8DF3\u8F6C\u3002',
step5Title: '\u6DF1\u5165\u4E86\u89E3\u7EC6\u8282',
step5Content: '\u67E5\u770B\u533A\u57DF\u7EDF\u8BA1\u3001\u76F4\u65B9\u56FE\u548C\u5355\u4E2A\u623F\u4EA7\u8BB0\u5F55\uFF1A\u4EF7\u683C\u3001\u5EFA\u7B51\u9762\u79EF\u3001\u80FD\u6548\u8BC4\u7EA7\u7B49\u3002',
step6Title: '\u9644\u8FD1\u6709\u4EC0\u4E48\uFF1F',
step6Content: '\u5728\u5730\u56FE\u4E0A\u5F00\u542F\u5B66\u6821\u3001\u5546\u5E97\u3001\u8F66\u7AD9\u3001\u516C\u56ED\u548C\u9910\u5385\u56FE\u5C42\uFF0C\u67E5\u770B\u5468\u8FB9\u8BBE\u65BD\u3002',
step1Title: '告诉地图什么重要',
step1Content: '设置预算、通勤上限、学校质量、犯罪门槛。您关心的一切。只有符合条件的区域会保持高亮。使用眼睛图标按任意特征着色。',
step2Title: '或者直接描述',
step2Content: '用中文输入您的需求例如“安静的地区靠近好学校£400k 以下”,我们会为您设置筛选。',
step3Title: '探索现有住宅',
step3Content: '在英格兰各地平移和缩放。点击任何彩色区域查看犯罪、学校、价格、宽带、噪音等信息。',
step4Title: '跳转到某个位置',
step4Content: '搜索任何地点或邮编,即可直接跳转。',
step5Title: '深入了解细节',
step5Content: '查看区域统计、直方图和单个房产记录:价格、建筑面积、能效评级等。',
step6Title: '附近有什么?',
step6Content: '在地图上开启学校、商店、车站、公园和餐厅图层,查看周边设施。',
},
// ── Server-derived values ──────────────────────────
@ -631,209 +641,208 @@ const zh: Translations = {
// The English keys MUST match exactly what the API returns.
server: {
// ─ Feature group names ─
'Properties': '\u623F\u4EA7',
'Transport': '\u4EA4\u901A',
'Education': '\u6559\u80B2',
'Deprivation': '\u8D2B\u56F0\u6307\u6570',
'Crime': '\u72AF\u7F6A',
'Demographics': '\u4EBA\u53E3\u7EDF\u8BA1',
'Amenities': '\u914D\u5957\u8BBE\u65BD',
'Properties': '房产',
'Transport': '交通',
'Education': '教育',
'Deprivation': '贫困指数',
'Crime': '犯罪',
'Demographics': '人口统计',
'Amenities': '配套设施',
// ─ Feature names (Properties) ─
'Listing status': '\u623F\u6E90\u72B6\u6001',
'Property type': '\u623F\u4EA7\u7C7B\u578B',
'Leasehold/Freehold': '\u79DF\u8D41\u4EA7\u6743/\u6C38\u4E45\u4EA7\u6743',
'Last known price': '\u4E0A\u6B21\u6210\u4EA4\u4EF7',
'Estimated current price': '\u4F30\u8BA1\u5F53\u524D\u4EF7\u683C',
'Asking price': '\u6302\u724C\u4EF7',
'Price per sqm': '\u6BCF\u5E73\u65B9\u7C73\u4EF7\u683C',
'Est. price per sqm': '\u4F30\u8BA1\u6BCF\u5E73\u65B9\u7C73\u4EF7\u683C',
'Asking price per sqm': '\u6302\u724C\u4EF7\u6BCF\u5E73\u65B9\u7C73',
'Estimated monthly rent': '\u4F30\u8BA1\u6708\u79DF',
'Asking rent (monthly)': '\u6708\u79DF',
'Total floor area (sqm)': '\u603B\u5EFA\u7B51\u9762\u79EF\uFF08\u5E73\u65B9\u7C73\uFF09',
'Number of bedrooms & living rooms': '\u5367\u5BA4\u548C\u5BA2\u5385\u6570\u91CF',
'Bedrooms': '\u5367\u5BA4',
'Bathrooms': '\u6D74\u5BA4',
'Construction year': '\u5EFA\u9020\u5E74\u4EFD',
'Date of last transaction': '\u4E0A\u6B21\u4EA4\u6613\u65E5\u671F',
'Listing date': '\u4E0A\u5E02\u65E5\u671F',
'Former council house': '\u539F\u516C\u5171\u4F4F\u623F',
'Current energy rating': '\u5F53\u524D\u80FD\u6E90\u8BC4\u7EA7',
'Potential energy rating': '\u6F5C\u5728\u80FD\u6E90\u8BC4\u7EA7',
'Interior height (m)': '\u5BA4\u5185\u5C42\u9AD8\uFF08\u7C73\uFF09',
'Listing status': '房源状态',
'Property type': '房产类型',
'Leasehold/Freehold': '租赁产权/永久产权',
'Last known price': '上次成交价',
'Estimated current price': '估计当前价格',
'Asking price': '挂牌价',
'Price per sqm': '每平方米价格',
'Est. price per sqm': '估计每平方米价格',
'Asking price per sqm': '挂牌价每平方米',
'Estimated monthly rent': '估计月租',
'Asking rent (monthly)': '月租',
'Total floor area (sqm)': '总建筑面积(平方米)',
'Number of bedrooms & living rooms': '卧室和客厅数量',
'Bedrooms': '卧室',
'Bathrooms': '浴室',
'Construction year': '建造年份',
'Date of last transaction': '上次交易日期',
'Listing date': '上市日期',
'Former council house': '原公共住房',
'Current energy rating': '当前能源评级',
'Potential energy rating': '潜在能源评级',
'Interior height (m)': '室内层高(米)',
// ─ Feature names (Transport) ─
'Distance to nearest train or tube station (km)': '\u5230\u6700\u8FD1\u706B\u8F66\u6216\u5730\u94C1\u7AD9\u7684\u8DDD\u79BB\uFF08\u516C\u91CC\uFF09',
'Train or tube stations within 1km': '1\u516C\u91CC\u5185\u706B\u8F66\u6216\u5730\u94C1\u7AD9\u6570\u91CF',
'Distance to nearest train or tube station (km)': '到最近火车或地铁站的距离(公里)',
// ─ Feature names (Education) ─
'Good+ primary schools within 2km': '2\u516C\u91CC\u5185\u826F\u597D+\u5C0F\u5B66\u6570\u91CF',
'Good+ secondary schools within 2km': '2\u516C\u91CC\u5185\u826F\u597D+\u4E2D\u5B66\u6570\u91CF',
'Good+ primary schools within 5km': '5\u516C\u91CC\u5185\u826F\u597D+\u5C0F\u5B66\u6570\u91CF',
'Good+ secondary schools within 5km': '5\u516C\u91CC\u5185\u826F\u597D+\u4E2D\u5B66\u6570\u91CF',
'Education, Skills and Training Score': '\u6559\u80B2\u3001\u6280\u80FD\u548C\u57F9\u8BAD\u5F97\u5206',
'Good+ primary schools within 2km': '2公里内良好+小学数量',
'Good+ secondary schools within 2km': '2公里内良好+中学数量',
'Good+ primary schools within 5km': '5公里内良好+小学数量',
'Good+ secondary schools within 5km': '5公里内良好+中学数量',
'Education, Skills and Training Score': '教育、技能和培训得分',
// ─ Feature names (Deprivation) ─
'Income Score (rate)': '\u6536\u5165\u5F97\u5206\uFF08\u6BD4\u7387\uFF09',
'Employment Score (rate)': '\u5C31\u4E1A\u5F97\u5206\uFF08\u6BD4\u7387\uFF09',
'Health Deprivation and Disability Score': '\u5065\u5EB7\u4E0E\u6B8B\u969C\u5F97\u5206',
'Living Environment Score': '\u5C45\u4F4F\u73AF\u5883\u5F97\u5206',
'Indoors Sub-domain Score': '\u5BA4\u5185\u5B50\u9886\u57DF\u5F97\u5206',
'Outdoors Sub-domain Score': '\u5BA4\u5916\u5B50\u9886\u57DF\u5F97\u5206',
'Income Score (rate)': '收入得分(比率)',
'Employment Score (rate)': '就业得分(比率)',
'Health Deprivation and Disability Score': '健康与残障得分',
'Living Environment Score': '居住环境得分',
'Indoors Sub-domain Score': '室内子领域得分',
'Outdoors Sub-domain Score': '室外子领域得分',
// ─ Feature names (Crime) ─
'Serious crime per 1k residents (avg/yr)': '\u6BCF\u5343\u4EBA\u4E25\u91CD\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Minor crime per 1k residents (avg/yr)': '\u6BCF\u5343\u4EBA\u8F7B\u5FAE\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Serious crime (avg/yr)': '\u4E25\u91CD\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Minor crime (avg/yr)': '\u8F7B\u5FAE\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Violence and sexual offences (avg/yr)': '\u66B4\u529B\u548C\u6027\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Burglary (avg/yr)': '\u5165\u5BA4\u76D7\u7A83\uFF08\u5E74\u5747\uFF09',
'Robbery (avg/yr)': '\u62A2\u52AB\uFF08\u5E74\u5747\uFF09',
'Vehicle crime (avg/yr)': '\u8F66\u8F86\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Anti-social behaviour (avg/yr)': '\u53CD\u793E\u4F1A\u884C\u4E3A\uFF08\u5E74\u5747\uFF09',
'Criminal damage and arson (avg/yr)': '\u5211\u4E8B\u6BC1\u574F\u548C\u7EB5\u706B\uFF08\u5E74\u5747\uFF09',
'Other theft (avg/yr)': '\u5176\u4ED6\u76D7\u7A83\uFF08\u5E74\u5747\uFF09',
'Theft from the person (avg/yr)': '\u4EBA\u8EAB\u76D7\u7A83\uFF08\u5E74\u5747\uFF09',
'Shoplifting (avg/yr)': '\u5546\u5E97\u76D7\u7A83\uFF08\u5E74\u5747\uFF09',
'Bicycle theft (avg/yr)': '\u81EA\u884C\u8F66\u76D7\u7A83\uFF08\u5E74\u5747\uFF09',
'Drugs (avg/yr)': '\u6BD2\u54C1\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Possession of weapons (avg/yr)': '\u975E\u6CD5\u6301\u6709\u6B66\u5668\uFF08\u5E74\u5747\uFF09',
'Public order (avg/yr)': '\u6270\u4E71\u516C\u5171\u79E9\u5E8F\uFF08\u5E74\u5747\uFF09',
'Other crime (avg/yr)': '\u5176\u4ED6\u72AF\u7F6A\uFF08\u5E74\u5747\uFF09',
'Serious crime per 1k residents (avg/yr)': '每千人严重犯罪(年均)',
'Minor crime per 1k residents (avg/yr)': '每千人轻微犯罪(年均)',
'Serious crime (avg/yr)': '严重犯罪(年均)',
'Minor crime (avg/yr)': '轻微犯罪(年均)',
'Violence and sexual offences (avg/yr)': '暴力和性犯罪(年均)',
'Burglary (avg/yr)': '入室盗窃(年均)',
'Robbery (avg/yr)': '抢劫(年均)',
'Vehicle crime (avg/yr)': '车辆犯罪(年均)',
'Anti-social behaviour (avg/yr)': '反社会行为(年均)',
'Criminal damage and arson (avg/yr)': '刑事毁坏和纵火(年均)',
'Other theft (avg/yr)': '其他盗窃(年均)',
'Theft from the person (avg/yr)': '人身盗窃(年均)',
'Shoplifting (avg/yr)': '商店盗窃(年均)',
'Bicycle theft (avg/yr)': '自行车盗窃(年均)',
'Drugs (avg/yr)': '毒品犯罪(年均)',
'Possession of weapons (avg/yr)': '非法持有武器(年均)',
'Public order (avg/yr)': '扰乱公共秩序(年均)',
'Other crime (avg/yr)': '其他犯罪(年均)',
// ─ Feature names (Demographics) ─
'Median age': '\u4E2D\u4F4D\u5E74\u9F84',
'% White': '% \u767D\u4EBA',
'% South Asian': '% \u5357\u4E9A\u88D4',
'% Black': '% \u9ED1\u4EBA',
'% East Asian': '% \u4E1C\u4E9A\u88D4',
'% Mixed': '% \u6DF7\u8840',
'% Other': '% \u5176\u4ED6',
'Median age': '中位年龄',
'% White': '% 白人',
'% South Asian': '% 南亚裔',
'% Black': '% 黑人',
'% East Asian': '% 东亚裔',
'% Mixed': '% 混血',
'% Other': '% 其他',
// ─ Feature names (Amenities) ─
'Distance to nearest park (km)': '\u5230\u6700\u8FD1\u516C\u56ED\u7684\u8DDD\u79BB\uFF08\u516C\u91CC\uFF09',
'Number of parks within 2km': '2\u516C\u91CC\u5185\u516C\u56ED\u6570\u91CF',
'Number of restaurants within 2km': '2\u516C\u91CC\u5185\u9910\u5385\u6570\u91CF',
'Number of grocery shops and supermarkets within 2km': '2\u516C\u91CC\u5185\u98DF\u54C1\u5E97\u548C\u8D85\u5E02\u6570\u91CF',
'Noise (dB)': '\u566A\u97F3\uFF08\u5206\u8D1D\uFF09',
'Max available download speed (Mbps)': '\u6700\u5927\u53EF\u7528\u4E0B\u8F7D\u901F\u5EA6\uFF08Mbps\uFF09',
'Distance to nearest park (km)': '到最近公园的距离(公里)',
'Number of parks within 2km': '2公里内公园数量',
'Number of restaurants within 2km': '2公里内餐厅数量',
'Number of grocery shops and supermarkets within 2km': '2公里内食品店和超市数量',
'Noise (dB)': '噪音(分贝)',
'Max available download speed (Mbps)': '最大可用下载速度Mbps',
// ─ Enum values ─
'Historical sale': '\u5386\u53F2\u4EA4\u6613',
'For sale': '\u5728\u552E',
'For rent': '\u51FA\u79DF',
'Detached': '\u72EC\u7ACB\u5F0F\u4F4F\u5B85',
'Semi-Detached': '\u534A\u72EC\u7ACB\u5F0F\u4F4F\u5B85',
'Terraced': '\u8054\u6392\u4F4F\u5B85',
'Flats/Maisonettes': '\u516C\u5BD3/\u590D\u5F0F\u516C\u5BD3',
'Other': '\u5176\u4ED6',
'Freehold': '\u6C38\u4E45\u4EA7\u6743',
'Leasehold': '\u79DF\u8D41\u4EA7\u6743',
'Yes': '\u662F',
'No': '\u5426',
'Historical sale': '历史交易',
'For sale': '在售',
'For rent': '出租',
'Detached': '独立式住宅',
'Semi-Detached': '半独立式住宅',
'Terraced': '联排住宅',
'Flats/Maisonettes': '公寓/复式公寓',
'Other': '其他',
'Freehold': '永久产权',
'Leasehold': '租赁产权',
'Yes': '',
'No': '',
// ─ Stacked chart labels ─
'Serious crime': '\u4E25\u91CD\u72AF\u7F6A',
'Minor crime': '\u8F7B\u5FAE\u72AF\u7F6A',
'Ethnic composition': '\u65CF\u88D4\u7EC4\u6210',
'Serious crime': '严重犯罪',
'Minor crime': '轻微犯罪',
'Ethnic composition': '族裔组成',
// ─ POI group names ─
'Public Transport': '\u516C\u5171\u4EA4\u901A',
'Leisure': '\u4F11\u95F2',
'Health': '\u5065\u5EB7',
'Emergency Services': '\u7D27\u6025\u670D\u52A1',
'Groceries': '\u98DF\u54C1\u6742\u8D27',
'Local Businesses': '\u672C\u5730\u5546\u4E1A',
'Culture': '\u6587\u5316',
'Services': '\u670D\u52A1',
'Shops': '\u5546\u5E97',
'Public Transport': '公共交通',
'Leisure': '休闲',
'Health': '健康',
'Emergency Services': '紧急服务',
'Groceries': '食品杂货',
'Local Businesses': '本地商业',
'Culture': '文化',
'Services': '服务',
'Shops': '商店',
// ─ POI categories ─
'Airport': '\u673A\u573A',
'Ferry': '\u6E21\u8F6E',
'Rail station': '\u706B\u8F66\u7AD9',
'Bus stop': '\u516C\u4EA4\u7AD9',
'Bus station': '\u516C\u4EA4\u67A2\u7EBD',
'Taxi rank': '\u51FA\u79DF\u8F66\u7AD9',
'Metro or Tram stop': '\u5730\u94C1\u6216\u6709\u8F68\u7535\u8F66\u7AD9',
'Caf\u00E9': '\u5496\u5561\u9986',
'Restaurant': '\u9910\u5385',
'Pub': '\u9152\u5427',
'Bar': '\u9152\u5427',
'Fast Food': '\u5FEB\u9910',
'Nightclub': '\u591C\u5E97',
'Cinema': '\u7535\u5F71\u9662',
'Theatre': '\u5267\u9662',
'Live Music & Events': '\u73B0\u573A\u97F3\u4E50\u4E0E\u6D3B\u52A8',
'Park': '\u516C\u56ED',
'Playground': '\u6E38\u4E50\u573A',
'Sports Centre': '\u4F53\u80B2\u4E2D\u5FC3',
'Entertainment': '\u5A31\u4E50',
'Supermarket': '\u8D85\u5E02',
'Convenience Store': '\u4FBF\u5229\u5E97',
'Bakery': '\u9762\u5305\u623A',
'Butcher & Fishmonger': '\u8089\u94FA\u4E0E\u9C7C\u94FA',
'Greengrocer': '\u679C\u852C\u5E97',
'Off-Licence': '\u9152\u7C7B\u5546\u5E97',
'Deli & Specialty': '\u719F\u98DF\u4E0E\u7279\u4EA7\u5E97',
'Fashion & Clothing': '\u65F6\u88C5\u670D\u9970',
'Electronics': '\u7535\u5B50\u4EA7\u54C1',
'Charity Shop': '\u6148\u5584\u5546\u5E97',
'DIY & Hardware': '\u5EFA\u6750\u4E94\u91D1',
'Home & Garden': '\u5BB6\u5C45\u4E0E\u56ED\u827A',
'Bookshop': '\u4E66\u5E97',
'Pet Shop': '\u5BA0\u7269\u5E97',
'Sports & Outdoor': '\u4F53\u80B2\u4E0E\u6237\u5916',
'Newsagent': '\u62A5\u520A\u4EAD',
'Department Store': '\u767E\u8D27\u5546\u5E97',
'Gift & Hobby': '\u793C\u54C1\u4E0E\u7231\u597D',
'Specialist Shop': '\u4E13\u4E1A\u5546\u5E97',
'Hairdresser & Beauty': '\u7F8E\u53D1\u4E0E\u7F8E\u5BB9',
'Gym & Fitness': '\u5065\u8EAB\u623F',
'Dry Cleaner & Laundry': '\u5E72\u6D17\u4E0E\u6D17\u8863',
'Car Services': '\u6C7D\u8F66\u670D\u52A1',
'Post Office': '\u90AE\u5C40',
'Vet & Pet Care': '\u5BA0\u7269\u533B\u9662\u4E0E\u62A4\u7406',
'Bank': '\u94F6\u884C',
'Travel Agent': '\u65C5\u884C\u793E',
'Police': '\u8B66\u5BDF',
'Fire Station': '\u6D88\u9632\u7AD9',
'Ambulance Station': '\u6025\u6551\u7AD9',
'GP Surgery': '\u5168\u79D1\u8BCA\u6240',
'Dentist': '\u7259\u79D1',
'Pharmacy': '\u836F\u623F',
'Hospital & Clinic': '\u533B\u9662\u4E0E\u8BCA\u6240',
'Optician': '\u773C\u955C\u5E97',
'Physiotherapy': '\u7406\u7597',
'Counselling & Therapy': '\u5FC3\u7406\u54A8\u8BE2\u4E0E\u6CBB\u7597',
'Care Home': '\u517B\u8001\u9662',
'Medical & Mobility': '\u533B\u7597\u5668\u68B0\u4E0E\u8F85\u52A9\u8BBE\u5907',
'Museum': '\u535A\u7269\u9986',
'Gallery': '\u7F8E\u672F\u9986',
'Library': '\u56FE\u4E66\u9986',
'Place of Worship': '\u5B97\u6559\u573A\u6240',
'Arts Centre': '\u827A\u672F\u4E2D\u5FC3',
'Zoo': '\u52A8\u7269\u56ED',
'Tourist Attraction': '\u65C5\u6E38\u666F\u70B9',
'School': '\u5B66\u6821',
'Hotel': '\u9152\u5E97',
'Local Business': '\u672C\u5730\u5546\u4E1A',
'Offices': '\u5199\u5B57\u697C',
'EV Charging': '\u7535\u52A8\u8F66\u5145\u7535\u7AD9',
'Fuel Station': '\u52A0\u6CB9\u7AD9',
'Community Centre': '\u793E\u533A\u4E2D\u5FC3',
'Airport': '机场',
'Ferry': '渡轮',
'Rail station': '火车站',
'Bus stop': '公交站',
'Bus station': '公交枢纽',
'Taxi rank': '出租车站',
'Metro or Tram stop': '地铁或有轨电车站',
'Café': '咖啡馆',
'Restaurant': '餐厅',
'Pub': '酒吧',
'Bar': '酒吧',
'Fast Food': '快餐',
'Nightclub': '夜店',
'Cinema': '电影院',
'Theatre': '剧院',
'Live Music & Events': '现场音乐与活动',
'Park': '公园',
'Playground': '游乐场',
'Sports Centre': '体育中心',
'Entertainment': '娱乐',
'Supermarket': '超市',
'Convenience Store': '便利店',
'Bakery': '面包戺',
'Butcher & Fishmonger': '肉铺与鱼铺',
'Greengrocer': '果蔬店',
'Off-Licence': '酒类商店',
'Deli & Specialty': '熟食与特产店',
'Fashion & Clothing': '时装服饰',
'Electronics': '电子产品',
'Charity Shop': '慈善商店',
'DIY & Hardware': '建材五金',
'Home & Garden': '家居与园艺',
'Bookshop': '书店',
'Pet Shop': '宠物店',
'Sports & Outdoor': '体育与户外',
'Newsagent': '报刊亭',
'Department Store': '百货商店',
'Gift & Hobby': '礼品与爱好',
'Specialist Shop': '专业商店',
'Hairdresser & Beauty': '美发与美容',
'Gym & Fitness': '健身房',
'Dry Cleaner & Laundry': '干洗与洗衣',
'Car Services': '汽车服务',
'Post Office': '邮局',
'Vet & Pet Care': '宠物医院与护理',
'Bank': '银行',
'Travel Agent': '旅行社',
'Police': '警察',
'Fire Station': '消防站',
'Ambulance Station': '急救站',
'GP Surgery': '全科诊所',
'Dentist': '牙科',
'Pharmacy': '药房',
'Hospital & Clinic': '医院与诊所',
'Optician': '眼镜店',
'Physiotherapy': '理疗',
'Counselling & Therapy': '心理咨询与治疗',
'Care Home': '养老院',
'Medical & Mobility': '医疗器械与辅助设备',
'Museum': '博物馆',
'Gallery': '美术馆',
'Library': '图书馆',
'Place of Worship': '宗教场所',
'Arts Centre': '艺术中心',
'Zoo': '动物园',
'Tourist Attraction': '旅游景点',
'School': '学校',
'Hotel': '酒店',
'Local Business': '本地商业',
'Offices': '写字楼',
'EV Charging': '电动车充电站',
'Fuel Station': '加油站',
'Community Centre': '社区中心',
// ─ Suffixes (used in formatters) ─
'/mo': '/\u6708',
'/yr': '/\u5E74',
' sqm': ' \u5E73\u65B9\u7C73',
' km': ' \u516C\u91CC',
' m': ' \u7C73',
' dB': ' \u5206\u8D1D',
' years': ' \u5E74',
' rooms': ' \u95F4',
'/mo': '/',
'/yr': '/',
' sqm': ' 平方米',
' km': ' 公里',
' m': ' ',
' dB': ' 分贝',
' years': ' ',
' rooms': ' ',
},
};

View file

@ -141,15 +141,6 @@ const FEATURE_ICON_PATHS: Record<string, ReactNode> = {
),
// ── Transport ────────────────────────────────
'Train or tube stations within 1km': (
<>
<rect x="4" y="3" width="16" height="14" rx="2" />
<path d="M4 11h16" />
<circle cx="8" cy="15" r="1" fill="currentColor" />
<circle cx="16" cy="15" r="1" fill="currentColor" />
<path d="M8 21l-2-4h12l-2 4" />
</>
),
'Distance to nearest train or tube station (km)': (
<>
<path d="M12 2v8" />