Language and logout bug
Some checks failed
CI / Check (push) Failing after 6m56s
Build and publish Docker image / build-and-push (push) Successful in 7m26s

This commit is contained in:
Andras Schmelczer 2026-06-27 10:45:48 +01:00
parent f047e50989
commit f1601257c7
12 changed files with 287 additions and 122 deletions

View file

@ -24,8 +24,8 @@ const en = {
copy: 'Copy',
copied: 'Copied!',
copiedToClipboard: 'Copied to clipboard',
loading: 'Loading...',
loadMore: 'Load More',
loading: 'Loading',
loadMore: 'Load more',
remaining: '{{count}} remaining',
search: 'Search',
all: 'All',
@ -52,7 +52,7 @@ const en = {
places: 'places',
noData: 'No data',
allLow: 'All low',
connectingToServer: 'Connecting to server...',
connectingToServer: 'Connecting to server',
closePane: 'Close pane',
yes: 'Yes',
no: 'No',
@ -66,15 +66,15 @@ const en = {
dashboard: 'Dashboard',
learn: 'Learn',
pricing: 'Pricing',
inviteFriends: 'Invite Friends',
inviteFriends: 'Invite friends',
saved: 'Saved',
logIn: 'Log in',
createAccount: 'Create account',
sharing: 'Sharing...',
sharing: 'Sharing',
exportLabel: 'Export',
exporting: 'Exporting...',
exporting: 'Exporting',
exportToExcel: 'Export to Excel',
exportReady: 'Export ready. Your download should start.',
exportReady: 'Export ready. Your download will start automatically.',
exportFailed: 'Export failed.',
exportTimedOut: 'Export timed out. Try again.',
exportUnavailable: 'The map is still loading. Try again in a moment.',
@ -89,7 +89,8 @@ const en = {
modeFilters: 'Postcodes matching filters',
modeFiltersHint: 'Export every postcode visible on the map that matches your current filters.',
modeList: 'List of postcodes',
modeListHint: 'Add your own postcodes one by one — spacing and casing are fixed for you.',
modeListHint:
'Add your own postcodes one by one — we tidy up the spacing and capitalisation for you.',
listLabel: 'Postcodes',
listPlaceholder: 'e.g. SW1A 1AA',
addRow: 'Add postcode',
@ -600,7 +601,7 @@ const en = {
resetPassword: 'Reset password',
valueProp: 'Save searches, bookmark properties, and build a shortlist of areas that fit.',
dashboardActionsValueProp:
'Create a free account to save, share and export your searches — free, no card needed.',
'Create a free account to save, share and export your searches — no card needed.',
continueWithGoogle: 'Continue with Google',
email: 'Email',
emailPlaceholder: 'you@example.com',
@ -609,7 +610,7 @@ const en = {
passwordPlaceholderLogin: 'Your password',
forgotPassword: 'Forgot password?',
resetSent: 'Check your email for a reset link.',
pleaseWait: 'Please wait...',
pleaseWait: 'Please wait',
sendResetLink: 'Send reset link',
backToLogin: 'Back to login',
registerConsent:
@ -623,13 +624,13 @@ const en = {
// ── Upgrade Modal ──────────────────────────────────
upgrade: {
// Registered (logged-in, non-paying) users — the lifetime upsell.
title: 'Unlock unlimited filters',
title: 'Stack unlimited filters',
description:
"You've used all 5 filters on your free account. Get lifetime access to stack unlimited filters across every postcode and neighbourhood in England. One payment, forever.",
'Youve used all 5 filters on your free account. Get lifetime access to stack unlimited filters across every postcode and neighbourhood in England. One payment, forever.',
// Anonymous users — create a free account first.
titleRegister: 'Create a free account',
descriptionRegister:
'Free accounts unlock up to 5 filters plus save, share and export — no card needed. Want unlimited filters across all of England? Go lifetime.',
'Free accounts include up to 5 filters plus save, share and export — no card needed. Want unlimited filters across all of England? Go lifetime.',
descriptionSharedRegister:
'This is a shared search. Create a free account to build your own with up to 5 filters and save, share & export — or go lifetime for unlimited.',
registerFree: 'Create free account',
@ -638,10 +639,10 @@ const en = {
free: 'Free',
freeForEarly: 'Free for early adopters. No credit card required.',
oneTimePayment: 'One-time payment. Lifetime access.',
redirecting: 'Redirecting...',
redirecting: 'Redirecting',
claimFreeAccess: 'Claim free access',
upgradeFor: 'Upgrade for {{price}}',
registerAndUpgrade: 'Register & Upgrade',
registerAndUpgrade: 'Create account & upgrade',
alreadyHaveAccount: 'Already have an account? Log in',
continueFree: 'Keep exploring for now',
checkoutFailed: 'Checkout failed',
@ -649,13 +650,13 @@ const en = {
// ── Save Search Modal ──────────────────────────────
saveSearch: {
title: 'Save Search',
title: 'Save search',
saved: 'Search saved',
savedSuccess: 'Your search has been saved successfully.',
savedSuccess: 'Your search has been saved.',
viewSavedSearches: 'View saved searches',
name: 'Name',
namePlaceholder: 'My search',
saving: 'Saving...',
saving: 'Saving',
},
// ── License Success ────────────────────────────────
@ -676,7 +677,7 @@ const en = {
// ── Filters ────────────────────────────────────────
filters: {
activeFilters: 'Active Filters',
activeFilters: 'Active filters',
addFilter: 'Add filters',
findingPerfectPostcode: 'Finding the Perfect Postcode',
addFiltersHint: 'Add filters below to narrow the map to areas that match your criteria',
@ -688,7 +689,7 @@ const en = {
registerSubPrompt: 'Free — no card needed. Unlimited filters with lifetime access.',
registerCta: 'Create free account',
chooseFilters: 'Click Add to filter. The small buttons show data details or colour the map.',
searchFeatures: 'Search features...',
searchFeatures: 'Search features',
noMatchingFeatures: 'No matching features',
tryDifferentSearch: 'Try a different search term',
allFeaturesActive: 'All features are active',
@ -697,7 +698,7 @@ const en = {
aboutData: 'About this data',
aboutDataShort: 'About',
colourMap: 'Colour map',
colourMapShort: 'Colour map',
colourMapShort: 'Colour',
clearColourMap: 'Clear map colour',
addFilterAction: 'Add',
addFilterLabel: 'Add filter',
@ -708,8 +709,8 @@ const en = {
clearAllSavePrompt: 'Would you like to save your current filters before clearing?',
clearAllUpdatePrompt:
'Update <strong>{{name}}</strong> with your current filters before clearing?',
saveAndClear: 'Save & Clear',
updateAndClear: 'Update & Clear',
saveAndClear: 'Save & clear',
updateAndClear: 'Update & clear',
clearWithoutSaving: 'Clear without saving',
clearWithoutUpdating: 'Clear without updating',
filtersOut: 'filters out {{value}}',
@ -755,9 +756,9 @@ const en = {
// ── Travel Time ────────────────────────────────────
travel: {
travelTime: 'Travel Time ({{mode}})',
travelTime: 'Travel time ({{mode}})',
maxTime: 'Max time',
selectDestination: 'Select destination...',
selectDestination: 'Select destination',
bestCase: 'Best case',
bestCaseTitle: 'Best case travel time',
bestCaseDesc:
@ -765,21 +766,21 @@ const en = {
noChange: 'No change',
noChangeTitle: 'No-change journeys only',
noChangeDesc:
'Restricts to journeys with <strong>no transfers</strong> —walk, board one transit service, then walk to the destination. Useful when you want a single straight-through commute.',
'Restricts to journeys with <strong>no transfers</strong> — walk, board one transit service, then walk to the destination. Useful when you want a single straight-through commute.',
oneChange: 'One change',
oneChangeTitle: 'One-change journeys',
oneChangeDesc:
'Allows journeys with <strong>at most one transfer</strong> —so a single change between two transit services is fine, but no more. A middle ground between no-change and unrestricted journeys.',
'Allows journeys with <strong>at most one transfer</strong> — so a single change between two transit services is fine, but no more. A middle ground between no-change and unrestricted journeys.',
noBuses: 'No buses',
noBusesTitle: 'Excluding buses',
noBusesDesc:
'Drops bus services from the allowed transit modes —<strong>rail, tube, tram and ferry only</strong>. Helpful for filtering to journeys that avoid traffic delays.',
'Drops bus services from the allowed transit modes — <strong>rail, tube, tram and ferry only</strong>. Helpful for filtering to journeys that avoid traffic delays.',
previewOnMap: 'Preview on map',
stopPreviewing: 'Stop previewing',
removeTravelTime: 'Remove travel time',
addTravelTime: 'Add {{mode}} travel time',
clearDestination: 'Clear destination',
typeToFilter: 'Type to filter...',
typeToFilter: 'Type to filter',
noDestinations: 'No destinations found',
modeCar: 'Car',
modeBicycle: 'Bicycle',
@ -811,10 +812,10 @@ const en = {
example1: 'Same schools, cheaper postcode',
example2: 'Best value within 30 minutes of work',
example3: 'Underpriced areas near good primaries',
analysing: 'Analysing your query...',
searchingDestinations: 'Searching for destinations...',
generatingFilters: 'Generating filters...',
refiningResults: 'Refining results...',
analysing: 'Analysing your query',
searchingDestinations: 'Searching for destinations',
generatingFilters: 'Generating filters',
refiningResults: 'Refining results',
weeklyLimitReached:
'Youve reached the weekly AI usage limit. Itll reset automatically next week.',
generateFailed: 'Failed to generate filters',
@ -858,7 +859,7 @@ const en = {
// ── Properties Pane ────────────────────────────────
propertyCard: {
unknownAddress: 'Unknown Address',
unknownAddress: 'Unknown address',
estValue: 'Est. value:',
type: 'Type:',
builtForm: 'Built form:',
@ -881,7 +882,7 @@ const en = {
tenureOwnerOccupied: 'Owner-occupied',
tenureRentedPrivate: 'Privately rented',
tenureRentedSocial: 'Social housing',
searchPlaceholder: 'Search by address or postcode...',
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.',
@ -901,20 +902,20 @@ const en = {
noFiltersAffectStats: 'No active filters; stats cover all properties in this area.',
filteredStatsEmpty: 'Filtered stats are empty',
showAllStatsHint:
'{{count}} properties exist before filters. Switch to all properties to inspect this area.',
'{{count}} properties here without filters. Switch to all properties to inspect this area.',
showAllStatsFallback:
'Switch to all properties to inspect this area without the active filters.',
showAllStats: 'Show all properties',
closestStations: 'Closest stations',
noNearbyStations: 'No train or tube stations within 2km',
closestBlockingFilters: 'Closest changes to include this area',
closestBlockingFilters: 'Smallest filter changes to include this area',
lowerMinTo: 'Lower minimum to {{value}}',
raiseMaxTo: 'Raise maximum to {{value}}',
allowCategory: 'Allow {{value}}',
missingFilterValue: 'No value for this filter; remove it',
noFilterDataShort: 'No data',
travelTo: 'Travel to {{destination}}',
viewProperties: 'View {{count}} Properties',
viewProperties: 'View {{count}} properties',
viewPropertiesShort: 'View properties',
priceHistory: 'Price History',
journeysFrom: 'Journey times for {{label}}',
@ -950,7 +951,7 @@ const en = {
// ── Street View ────────────────────────────────────
streetView: {
title: 'Street View',
openLarge: 'Open Street View larger',
openLarge: 'Expand Street View',
expandedTitle: 'Expanded Street View',
},
@ -960,7 +961,7 @@ const en = {
pointsOfInterest: 'Points of Interest',
poiDescription:
'Sourced from OpenStreetMap, NaPTAN, and GEOLYTIX Grocery Retail Points. Covers transport stops, shops, chain supermarkets, restaurants, healthcare, leisure, and more.',
searchCategories: 'Search categories...',
searchCategories: 'Search categories',
dataSourceInfo: 'Data source info',
},
@ -974,7 +975,7 @@ const en = {
// ── Location Search ────────────────────────────────
locationSearch: {
placeholder: 'Search places or postcodes...',
placeholder: 'Search places or postcodes',
noResults: 'No matching places or postcodes',
postcodeNotFound: 'Postcode not found',
lookupFailed: 'Lookup failed',
@ -1019,7 +1020,7 @@ const en = {
showcaseNearby: '{{value}} nearby',
showcasePoliticalVoteShare: 'Political vote share',
showcaseGe2024: '2024 GE',
showcaseLotsMore: '...and lots more',
showcaseLotsMore: 'and lots more',
showcaseMinutes: '{{count}} min',
showcaseSendShortlist: 'Send the shortlist',
showcaseDownloadXlsx: 'Download .xlsx',
@ -1055,18 +1056,18 @@ const en = {
showcaseStep3Stat4Label: 'Broadband',
showcaseStep3Stat4Value: '1 Gbps available',
showcaseStep3Stat5Label: 'Primary schools',
showcaseStep3Stat5Value: '3 outstanding within 1 mile',
showcaseStep3Stat5Value: '3 Outstanding within 1 mile',
showcaseStep4Tab: 'Scout',
showcaseStep4Title: 'Scout it out yourself',
showcaseStep4Body:
'Take three grounded starting points into the real world. Walk the streets, test the commute, and compare viewings with context.',
'Take your top three postcodes into the real world. Walk the streets, test the commute, and compare viewings with context.',
showcaseStep4FileName: 'areas-to-scout.xlsx',
showcaseStep4ExportLabel: 'Export to Excel',
showcaseStep4ColPostcode: 'Postcode',
showcaseStep4ColScore: 'Fit',
showcaseStep4ColCommute: 'Commute',
showcaseStep4ColPrice: 'Median sold',
showcaseStep4Conclusion: 'You can start your journey from here.',
showcaseStep4Conclusion: 'Start scouting from here.',
statProperties: 'historical sales',
statFilters: 'combinable filters',
statEvery: 'Every',
@ -1077,7 +1078,7 @@ const en = {
priceStripSpots: '{{count}} spot left at this price.',
priceStripSpotsPlural: '{{count}} spots left at this price.',
priceStripCta: 'See pricing',
ourPhilosophy: 'Reputation is priced in. Value isnt yet',
ourPhilosophy: 'Reputation is priced in. Value isnt yet.',
philosophyP1:
'A postcodes price is partly its fundamentals and partly its reputation. People pay up for names they know, while the postcode one street over — same schools, same commute — sits quietly cheaper because nobody has bid it up. Spotting that gap means cross-checking sold prices, Ofsted, crime, noise and floor areas tab by tab.',
philosophyP2:
@ -1147,7 +1148,7 @@ const en = {
filled: 'Filled',
openDashboard: 'Open dashboard',
getStarted: 'Get started',
getStartedPrice: 'Get started - {{price}}',
getStartedPrice: 'Get started {{price}}',
noCreditCard: 'No credit card required',
moneyBack: '14-day money-back guarantee.',
@ -1155,7 +1156,7 @@ const en = {
upcoming: 'Upcoming',
failedToLoad: 'Failed to load pricing. Please try again later.',
feat1: '40+ filters and 200+ data fields',
feat2: 'Every postcode searchable from your needs',
feat2: 'Every postcode searched against your needs',
feat3: 'Unlimited map exploration, saved searches and exports',
feat4: '13M historical transactions and price context',
feat5: 'Commute, schools, crime, noise, broadband and more',
@ -1169,9 +1170,9 @@ const en = {
articles: 'Articles',
support: 'Support',
dataSourcesIntro:
'This application combines {{count}} open datasets covering property prices, energy performance, transport, demographics, crime, environment, and more.',
'Perfect Postcode combines {{count}} open datasets covering property prices, energy performance, transport, demographics, crime, environment, and more.',
faqIntro:
'Whether youre narrowing a first-time buyer search, checking an unfamiliar postcode, or building a viewing shortlist, heres how Perfect Postcode helps you work out where to look.',
'Narrowing a first-time buyer search, checking an unfamiliar postcode, or building a viewing shortlist? Heres how Perfect Postcode helps you work out where to look.',
articlesIntro:
'Browse the public guides for property search, commute, schools, postcode checks, regional comparisons, data coverage, methodology, and privacy.',
supportIntro: 'Have a question? Check our FAQ or reach out to us directly.',
@ -1181,7 +1182,7 @@ const en = {
'Short clips from our social channels — each one shows a single search in action, from quiet streets to school catchments and commute times.',
video01Title: 'Find the cheaper twin',
video01Desc:
'Type your whole house brief in one plain sentence and watch every matching postcode in England sort by value, surfacing the cheaper twin nobody bid up.',
'Type your whole house brief in one plain sentence and watch every matching postcode in England sort by value, surfacing the cheaper twin everyone overlooked.',
video02Title: 'The 20-minute commute map',
video02Desc:
'Colour London by commute to the centre, prune to a 20-minute ride, and watch identical journeys split into the names everyone knows and quieter postcodes nobody bid up.',
@ -1190,7 +1191,7 @@ const en = {
'Tap any postcode and a panel opens with its sold prices, school catchments, crime and Street View — so you can tell whether you are paying for value or just a reputation.',
video04Title: 'The overlooked quiet street',
video04Desc:
'Famous postcodes price in their reputation, but a listing photo stays silent on noise. Filter by decibels to find the genuinely quiet street one over that nobody bid up.',
'Famous postcodes price in their reputation, but a listing photo stays silent on noise. Filter by decibels to find the genuinely quiet street one over that never paid the premium.',
video05Title: 'The catchment without the premium',
video05Desc:
'A Leeds family search for a good primary, low crime and a budget under £350k surfaces the quietly cheaper streets that share the very same catchment.',
@ -1290,24 +1291,24 @@ const en = {
dsRentalName: 'Private Rental Market Statistics',
dsRentalOrigin: 'ONS / Valuation Office Agency',
dsRentalUse:
'Median monthly private rental prices by local authority and bedroom category (Oct 2022 - Sep 2023). Joined to properties via local authority district code and estimated bedroom count.',
'Median monthly private rental prices by local authority and bedroom category (Oct 2022 Sep 2023). Joined to properties via local authority district code and estimated bedroom count.',
dsElectionName: '2024 General Election Results',
dsElectionOrigin: 'UK Parliament',
dsElectionUse:
'Candidate-level results for the July 2024 UK General Election. Aggregated to constituency level: voter turnout (%) and party vote shares (%). Joined to properties via the parliamentary constituency code (pcon) from the NSPL postcode lookup.',
// FAQ section titles
faqFindingTitle: 'Where To Look',
faqFindingTitle: 'Where to Look',
faqCommuteTitle: 'Travel Times',
faqBudgetTitle: 'Estimated Prices',
faqSafetyTitle: 'Safety and Neighbourhood',
faqFamiliesTitle: 'Families and Schools',
faqEnvironmentTitle: 'Environment and Quality of Life',
faqDueDiligenceTitle: 'What To Check',
faqDueDiligenceTitle: 'What to Check',
faqPrivacyTitle: 'Privacy',
faqWhyTitle: 'Why Perfect Postcode',
faqPricingTitle: 'Access',
faqTipsTitle: 'Map Tips',
faqBehindDataTitle: 'Behind The Data',
faqBehindDataTitle: 'Behind the Data',
// FAQ items — Finding Your Area
faqFinding1Q: 'Where should I look once the obvious areas are too expensive?',
faqFinding1A:
@ -1338,7 +1339,7 @@ const en = {
// FAQ items — Safety and Neighbourhood
faqSafety1Q: 'What type of crime is common around this postcode?',
faqSafety1A:
'Crime is broken down by type, including violence, burglary, robbery, vehicle crime, antisocial behaviour, shoplifting, drugs, and public order. You can focus on the risks that matter to you instead of relying on one vague safety score.',
'Crime is broken down by type, including violence, burglary, robbery, vehicle crime, anti-social behaviour, shoplifting, drugs, and public order. You can focus on the risks that matter to you instead of relying on one vague safety score.',
faqSafety2Q: 'What should I check before viewing an unfamiliar street?',
faqSafety2A:
'Check crime, road noise, broadband, parks, food shops, schools, and commute before you book. Listing photos are useful, but they shouldnt be the first time you learn what the street is like.',
@ -1419,15 +1420,15 @@ const en = {
faqBehindData3A:
'Police-recorded street-level crime is published at LSOA level — small neighbourhood areas of about 1,500 residents. Every postcode inside the same LSOA inherits the same yearly totals, so a quiet residential street and a high street one block over can show identical figures if they fall on the same side of the boundary. Per-capita rates can also look unusually high in postcodes covering hospitals, university campuses or industrial estates, because those areas record incidents normally but have very few residents on paper to divide the count across.',
faqBehindData4Q:
'What does a "school catchments" count mean — can my child attend those schools?',
'What does a “school catchments” count mean — can my child attend those schools?',
faqBehindData4A:
'Each count is the number of Good or Outstanding state schools whose modelled historical catchment area covers the postcode. We simulate how Englands distance-based admissions allocate places: children (Census 2021) apply to nearby schools, trading distance against Ofsted rating, and an oversubscribed school admits its closest applicants until full — its catchment radius is the distance of the last child admitted, the same "last distance offered" councils publish, and the model is calibrated against hundreds of those published figures. It estimates where pupils plausibly get a place; it is not an official admission area. Faith and selective admissions, sibling priority and yearly boundary changes are not modelled, so always confirm catchments and admission rules with the school or local authority before relying on them for a decision.',
faqBehindData5Q: 'Why does a postcode show "Gigabit" when not every home has fibre?',
'Each count is the number of Good or Outstanding state schools whose modelled historical catchment area covers the postcode. We simulate how Englands distance-based admissions allocate places: children (Census 2021) apply to nearby schools, trading distance against Ofsted rating, and an oversubscribed school admits its closest applicants until full — its catchment radius is the distance of the last child admitted, the same “last distance offered” councils publish, and the model is calibrated against hundreds of those published figures. It estimates where pupils plausibly get a place; it is not an official admission area. Faith and selective admissions, sibling priority and yearly boundary changes are not modelled, so always confirm catchments and admission rules with the school or local authority before relying on them for a decision.',
faqBehindData5Q: 'Why does a postcode show “Gigabit” when not every home has fibre?',
faqBehindData5A:
'Broadband coverage from Ofcom Connected Nations is reported per postcode as the percentage of premises that can get each speed tier. We display the highest tier with any availability, so a postcode where even one home can reach Gigabit reads "Gigabit available". It is the right answer for "is full-fibre on this street at all?", but does not guarantee every flat in a block can be ordered today. Always verify with the providers for your specific address before signing.',
'Broadband coverage from Ofcom Connected Nations is reported per postcode as the percentage of premises that can get each speed tier. We display the highest tier with any availability, so a postcode where even one home can reach Gigabit reads “Gigabit available”. It is the right answer for “is full-fibre on this street at all?”, but does not guarantee every flat in a block can be ordered today. Always verify with the providers for your specific address before signing.',
faqBehindData6Q: 'Why dont the public-transport times change for evenings or weekends?',
faqBehindData6A:
'Transit times are computed once per destination for a Tuesday morning departure window (07:3008:30) using full GTFS timetables. The "normal" figure is the median journey in that window, and "best case" is the 5th percentile. Off-peak, late-night and weekend services are not modelled, so a postcode that only has a peak-only bus can still look transit-good on the map. Treat the numbers as a weekday commute proxy rather than an all-day estimate.',
'Transit times are computed once per destination for a Tuesday morning departure window (07:3008:30) using full GTFS timetables. The “normal” figure is the median journey in that window, and “best case” is the 5th percentile. Off-peak, late-night and weekend services are not modelled, so a postcode that only has a peak-only bus can still look transit-good on the map. Treat the numbers as a weekday commute proxy rather than an all-day estimate.',
},
// ── Account Page ───────────────────────────────────
@ -1443,7 +1444,7 @@ const en = {
noShareLinksYet: 'No shared links yet',
copyShareLink: 'Copy shared link',
clicksLabel: 'clicks',
fetchShareLinksError: 'Failed to fetch share links',
fetchShareLinksError: 'Failed to fetch shared links',
updateNewsletterError: 'Failed to update newsletter',
},
@ -1454,11 +1455,11 @@ const en = {
noSavedSearchesDesc:
'Save your filters and map view so you can pick up exactly where you left off.',
clickToRename: 'Click to rename',
notesPlaceholder: 'Jot down your thoughts...',
notesPlaceholder: 'Jot down your thoughts',
deleteSearch: 'Delete search',
deleteSearchConfirm: 'Are you sure you want to delete this saved search? This cant be undone.',
isBeingUpdated: '<strong>{{name}}</strong> is being updated',
updating: 'Updating...',
updating: 'Updating',
loadFailed: 'Failed to load searches',
saveFailed: 'Failed to save search',
deleteFailed: 'Failed to delete search',
@ -1502,13 +1503,13 @@ const en = {
inviteAlreadyUsedDesc: 'This invite link has already been redeemed.',
invalidInviteLink: 'Invalid invite link',
invalidInviteLinkDesc: 'This invite link is invalid or has expired.',
licenseActivated: 'License activated!',
licenseActivated: 'Licence activated!',
fullAccessGranted: 'You now have full access to Perfect Postcode.',
activating: 'Activating...',
activateLicense: 'Activate license',
activating: 'Activating',
activateLicense: 'Activate licence',
claimDiscount: 'Claim discount',
registerToClaim: 'Register to claim',
youAlreadyHaveLicense: 'You already have a license',
youAlreadyHaveLicense: 'You already have a licence',
accountHasFullAccess: 'Your account already has full access.',
failedToValidate: 'Failed to validate invite link',
redeemFailed: 'Failed to redeem invite',
@ -1547,22 +1548,22 @@ const en = {
crimeHotspots: {
label: 'Crime hotspots',
detail:
'Client-side heatmap of street-level crimes published by police.uk over the most recent months. Police.uk coordinates are anonymised snap-to-grid points, not exact offence locations, so the heatmap should be read as an approximation of relative density rather than a precise map of incidents.',
'A heatmap of street-level crimes published by police.uk over the most recent months. Police.uk coordinates are anonymised snap-to-grid points, not exact offence locations, so the heatmap should be read as an approximation of relative density rather than a precise map of incidents.',
},
treesOutsideWoodlands: {
label: 'Trees & woodland',
detail:
'Forest Research Trees Outside Woodland (TOW) v1 canopy polygons — lone trees and groups of trees — unioned with National Forest Inventory (NFI) woodland blocks (≥0.5 ha) that TOW deliberately excludes. Together they cover both street trees and actual woods. Polygon opacity scales with canopy area.',
'Forest Research Trees Outside Woodland (TOW) v1 canopy polygons — lone trees and groups of trees — combined with National Forest Inventory (NFI) woodland blocks (≥0.5 ha) that TOW deliberately excludes. Together they cover both street trees and actual woods. Polygon opacity scales with canopy area.',
},
propertyBorders: {
label: 'Property borders',
detail:
'HM Land Registry INSPIRE Index Polygons — the position and indicative extent of freehold registered property in England & Wales, drawn as outlines at street level. These are "general boundaries" for guidance only, not the precise legal boundary of a property, and they exclude leasehold-only interests and unregistered land (roughly 8590% of freehold land is covered). This information is subject to Crown copyright and database rights 2026 and is reproduced with the permission of HM Land Registry. The polygons (including the associated geometry, namely x, y co-ordinates) are subject to Crown copyright and database rights 2026 Ordnance Survey AC0000851063. Licensed under the Open Government Licence v3.0.',
'HM Land Registry INSPIRE Index Polygons — the position and indicative extent of freehold registered property in England & Wales, drawn as outlines at street level. These are “general boundaries” for guidance only, not the precise legal boundary of a property, and they exclude leasehold-only interests and unregistered land (roughly 8590% of freehold land is covered). This information is subject to Crown copyright and database rights 2026 and is reproduced with the permission of HM Land Registry. The polygons (including the associated geometry, namely x, y co-ordinates) are subject to Crown copyright and database rights 2026 Ordnance Survey AC0000851063. Licensed under the Open Government Licence v3.0.',
},
newDevelopments: {
label: 'New developments',
detail:
'A forward-looking pipeline of new housing. Blue markers mark sites on the statutory MHCLG Brownfield Land registers — each carrying an estimated net-dwelling capacity and planning-permission status — together with Homes England Land Hub disposal sites. These show where new homes are planned, often years before they appear in EPC or sale records. Dwelling figures are capacity estimates, not commitments, and a site on a register is an opportunity rather than a guarantee of construction. Licensed under the Open Government Licence v3.0.',
'A pipeline of new housing. Blue markers show sites on the statutory MHCLG Brownfield Land registers — each carrying an estimated net-dwelling capacity and planning-permission status — together with Homes England Land Hub disposal sites. These show where new homes are planned, often years before they appear in EPC or sale records. Dwelling figures are capacity estimates, not commitments, and a site on a register is an opportunity rather than a guarantee of construction. Licensed under the Open Government Licence v3.0.',
},
},
@ -1599,7 +1600,7 @@ const en = {
gender: 'Gender',
pupilsLabel: 'Pupils',
pupils: 'pupils',
freeMeal: 'Free meal',
freeMeal: 'Free meals',
ofsted: 'Ofsted',
sixthForm: 'Sixth form',
religion: 'Religion',
@ -1639,7 +1640,7 @@ const en = {
'Set your budget, commute limit, school quality, crime threshold, noise tolerance, broadband needs, or whatever matters to you. Only matching areas stay lit. Use Colour to shade the map by any feature.',
step2Title: 'Or just describe it',
step2Content:
'Type what you want in plain English, like "quiet area near good schools under £400k", and well set up the filters for you.',
'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 why it matches: crime, schools, prices, broadband, noise, and more.',
@ -1653,7 +1654,7 @@ const en = {
'Toggle schools, shops, stations, parks, and restaurants on the map to see whats within reach.',
step7Title: 'Layer on more context',
step7Content:
'Toggle overlays like crime hotspots, noise, tree cover, property boundaries, and new-build sites on the map, and switch the basemap to suit.',
'Toggle overlays like crime hotspots, noise, tree cover, property boundaries, and new-build sites on the map, and switch the base map to suit.',
},
// ── Server-derived values ──────────────────────────