lomavuokraus/lib/i18n.ts
2025-11-27 23:09:14 +02:00

524 lines
24 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export type Locale = 'en' | 'fi';
const allMessages = {
en: {
brand: 'lomavuokraus.fi',
navProfile: 'Profile',
navMyListings: 'My listings',
navNewListing: 'New listing',
navApprovals: 'Approvals',
navUsers: 'Users',
navLogout: 'Logout',
navLogin: 'Login',
navSignup: 'Sign up',
navBrowse: 'Browse listings',
navLanguage: 'Language',
approvalsBadge: '{count}',
heroEyebrow: 'lomavuokraus.fi',
heroTitle: 'Find your next Finnish getaway',
heroBody: 'A fast, modern marketplace for holiday rentals. Built with the Next.js App Router and ready for Kubernetes from day one.',
ctaViewSample: 'View a sample listing',
ctaHealth: 'Check health endpoint',
ctaBrowse: 'Browse listings',
highlightQualityTitle: 'Quality stays',
highlightQualityBody: 'Curated cabins and villas with clear availability and simple booking.',
highlightLocalTitle: 'Local-first',
highlightLocalBody: 'Built for Finnish seasons with fast content delivery in the Nordics.',
highlightApiTitle: 'API-friendly',
highlightApiBody: 'Structured data so you can surface listings wherever you need them.',
runtimeConfigTitle: 'Runtime configuration',
runtimeConfigLead: 'Build-time and runtime values used by the service.',
runtimeAppEnv: 'APP_ENV',
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
aboutTitle: 'About lomavuokraus.fi',
aboutLead: 'A focused marketplace for Finnish holiday rentals with fast browsing, clear moderation, and a simple host experience.',
pricingTitle: 'Pricing',
pricingLead: 'Straightforward pricing for hosts with no surprises.',
pricingMonthly: 'Monthly',
pricingAnnual: 'Annual',
pricingPerMonth: 'per month',
pricingPerYear: 'per year (save 20%)',
pricingMonthlyBody: 'Start with a monthly plan at 10€ per listing.',
pricingAnnualBody: 'Pay annually for 100€ per listing and keep your costs predictable.',
pricingPerListing: 'Pricing is per active listing. Cancel anytime.',
pricingNotesTitle: 'Notes',
pricingNotesBody: 'We keep pricing simple while we build out hosting tools, messaging, and integrations. All current features are included.',
footerAbout: 'About',
footerPricing: 'Pricing',
footerPrivacy: 'Privacy & cookies',
loginTitle: 'Login',
emailLabel: 'Email',
passwordLabel: 'Password',
loginButton: 'Login',
loggingIn: 'Logging in…',
loginSuccess: 'Login successful.',
registerTitle: 'Create an account',
registerLead: 'Verify email is required, and an admin will approve your account.',
nameOptional: 'Name (optional)',
passwordHint: 'Password (min 8 chars)',
registerButton: 'Register',
registering: 'Submitting…',
registerSuccess: 'Registration successful. Check your email for a verification link.',
forgotTitle: 'Forgot your password?',
forgotLead: 'Enter your email and we will send a reset link.',
forgotSubmit: 'Send reset link',
forgotSuccess: 'If that email exists, a reset link has been sent.',
forgotError: 'Could not send reset link right now.',
resetTitle: 'Reset password',
resetLead: 'Set a new password for your account.',
resetSubmit: 'Set new password',
resetSuccess: 'Password updated. You can log in now.',
resetError: 'Failed to reset password. The link may be invalid or expired.',
resetMissingToken: 'Reset token missing. Please use the link from your email.',
forgotCta: 'Forgot password?',
pendingAdminTitle: 'Admin: pending items',
pendingUsersTitle: 'Pending users',
pendingListingsTitle: 'Pending listings',
noPendingUsers: 'No pending users.',
noPendingListings: 'No pending listings.',
statusLabel: 'status',
slugsLabel: 'Slugs',
verifiedLabel: 'verified',
approve: 'Approve',
approveAdmin: 'Approve + make admin',
reject: 'Reject',
remove: 'Remove',
publish: 'Publish',
approvalsMessage: 'Listing updated',
userUpdated: 'User updated',
adminRequired: 'Admin access required',
adminUsersTitle: 'Admin: users',
adminUsersLead: 'Manage user roles and approvals.',
tableEmail: 'Email',
tableRole: 'Role',
tableStatus: 'Status',
tableVerified: 'Verified',
tableApproved: 'Approved',
myListingsTitle: 'My listings',
createNewListing: 'Create new listing',
noListings: 'No listings yet.',
createOne: 'Create one',
loading: 'Loading…',
view: 'View',
removing: 'Removing…',
removed: 'Listing removed',
removeConfirm: 'Remove this listing? It will be hidden from others.',
myProfileTitle: 'My profile',
notLoggedIn: 'Not logged in',
profileEmail: 'Email',
profileName: 'Name',
profilePhone: 'Phone',
profileRole: 'Role',
profileStatus: 'Status',
profileEmailVerified: 'Email verified',
profileApproved: 'Approved',
profileUpdated: 'Profile updated',
emailLocked: 'Email cannot be changed',
save: 'Save',
saving: 'Saving…',
yes: 'yes',
no: 'no',
verifyTitle: 'Email verification',
verifyOk: 'Your email is verified. An admin will approve your account shortly.',
verifyFail: 'Verification failed or token invalid.',
listingLocation: 'Location',
listingAddress: 'Address',
listingCapacity: 'Capacity',
listingAmenities: 'Amenities',
listingNoAmenities: 'No amenities listed yet.',
listingContact: 'Contact',
listingMoreInfo: 'More info',
availabilityTitle: 'Availability calendar',
availabilityLegendBooked: 'Booked',
availabilityMissing: 'Calendar not connected yet.',
localeLabel: 'Locale',
homeCrumb: 'Home',
createListingTitle: 'Create listing',
loginToCreate: 'Please log in first to create a listing.',
slugLabel: 'Slug',
localeInput: 'Locale',
titleLabel: 'Title',
descriptionLabel: 'Description',
teaserLabel: 'Teaser',
countryLabel: 'Country',
regionLabel: 'Region',
cityLabel: 'City',
contactNameLabel: 'Contact name',
contactEmailLabel: 'Contact email',
streetAddressLabel: 'Street address',
addressNoteLabel: 'Address note (optional)',
addressNotePlaceholder: 'Directions, parking, or arrival details',
latitudeLabel: 'Latitude (optional)',
longitudeLabel: 'Longitude (optional)',
maxGuestsLabel: 'Max guests',
bedroomsLabel: 'Bedrooms',
bedsLabel: 'Beds',
bathroomsLabel: 'Bathrooms',
priceHintLabel: 'Price ballpark (€ / night)',
priceHintHelp: 'Rough nightly price in euros (not a binding offer).',
calendarUrlsLabel: 'Availability calendars (iCal URLs, one per line)',
calendarUrlsHelp: 'Paste iCal links from other platforms. We will merge them to show availability.',
imagesLabel: 'Images',
imagesHelp: 'Upload up to {count} images (max {sizeMb}MB each).',
imagesTooMany: 'Too many images (max {count}).',
imagesTooLarge: 'Image is too large (max {sizeMb}MB).',
imagesReadFailed: 'Could not read selected images.',
imagesRequired: 'Please upload at least one image.',
coverImageLabel: 'Cover image order',
coverImageHelp: '1-based index of the uploaded images (defaults to 1)',
coverChoice: 'Cover position {index}',
submitListing: 'Create listing',
submittingListing: 'Submitting…',
createListingSuccess: 'Listing created with id {id} (status: {status})',
approvalsCountLabel: 'Approvals',
approvalsPending: '{count} pending',
amenitySauna: 'Sauna',
amenityFireplace: 'Fireplace',
amenityWifi: 'Wi-Fi',
amenityPets: 'Pets allowed',
amenityLake: 'By the lake',
amenityAirConditioning: 'Air conditioning',
amenityKitchen: 'Kitchen',
amenityDishwasher: 'Dishwasher',
amenityWashingMachine: 'Washing machine',
amenityBarbecue: 'Barbecue grill',
amenityEvFree: 'EV charging (free)',
amenityEvPaid: 'EV charging (paid)',
evChargingLabel: 'EV charging',
evChargingNone: 'Not available',
evChargingFree: 'Free for guests',
evChargingPaid: 'Paid on-site',
capacityGuests: '{count} guests',
capacityBedrooms: '{count} bedrooms',
capacityBeds: '{count} beds',
capacityBathrooms: '{count} bathrooms',
browseListingsTitle: 'Browse listings',
browseListingsLead: 'Search public listings, filter by location, and explore them on the map.',
sampleBadge: 'Sample',
privacyTitle: 'Privacy & cookies',
privacyUpdated: 'Updated: {date}',
privacyCollectTitle: 'What data we collect',
privacyCollectAccounts: 'Account data: email, password hash, name, phone (optional), role/status.',
privacyCollectListings: 'Listing data: location, contact details, amenities, photos, translations.',
privacyCollectLogs: 'Operational logs: minimal request metadata for diagnostics.',
privacyUseTitle: 'How we use your data',
privacyUseAuth: 'To authenticate users and manage sessions.',
privacyUseListings: 'To publish and moderate listings.',
privacyUseMail: 'To send transactional email (verification, password reset).',
privacyUseLegal: 'To comply with legal requests or protect the service.',
privacyStoreTitle: 'Storage & retention',
privacyStoreDb: 'Data is stored in our Postgres database hosted in the EU.',
privacyStoreBackups: 'Backups are retained for disaster recovery; removed accounts/listings may persist in backups for a limited time.',
privacyCookiesTitle: 'Cookies',
privacyCookiesSession: 'We use essential cookies only: a session cookie for login/authentication.',
privacyCookiesNoTracking: 'No analytics, marketing, or tracking cookies are used.',
privacySharingTitle: 'Sharing',
privacySharingAds: 'We do not sell or share personal data with advertisers.',
privacySharingOps: 'Data may be shared with service providers strictly for running the service (email delivery, hosting).',
privacyRightsTitle: 'Your rights',
privacyRightsAccess: 'Request access, correction, or deletion of your data.',
privacyRightsConsent: 'Withdraw consent for non-essential processing (we currently process only essentials).',
privacyRightsContact: 'Contact support for any privacy questions.',
searchLabel: 'Search',
searchPlaceholder: 'Search by name, description, or city',
cityFilter: 'City',
regionFilter: 'Region',
searchButton: 'Search',
searchAmenities: 'Amenities',
searchAvailability: 'Availability',
startDate: 'Start date',
endDate: 'End date',
availabilityOnlyWithCalendar: 'Only listings with a connected calendar are shown when filtering by dates.',
availableForDates: 'Available for selected dates',
notAvailableForDates: 'Unavailable for selected dates',
calendarConnected: 'Calendar connected',
clearFilters: 'Clear filters',
addressSearchLabel: 'Find listings near an address',
addressSearchPlaceholder: 'Street, city, or place',
locateAddress: 'Locate on map',
addressRadiusLabel: 'Within {km} km',
listingsFound: '{count} listings',
openListing: 'Open listing',
mapNoResults: 'No listings match your filters.',
evChargingAny: 'Any',
addressNotFound: 'Address not found',
addressLookupFailed: 'Failed to locate address',
loadingMap: 'Loading map…',
latestListingsTitle: 'Latest listings',
latestListingsLead: 'Fresh rentals as they are published. Tap to browse.',
},
fi: {
brand: 'lomavuokraus.fi',
navProfile: 'Profiili',
navMyListings: 'Omat kohteet',
navNewListing: 'Luo kohde',
navApprovals: 'Tarkastettavat',
navUsers: 'Käyttäjät',
navLogout: 'Kirjaudu ulos',
navLogin: 'Kirjaudu',
navSignup: 'Rekisteröidy',
navBrowse: 'Selaa kohteita',
navLanguage: 'Kieli',
approvalsBadge: '{count}',
heroEyebrow: 'lomavuokraus.fi',
heroTitle: 'Löydä seuraava mökkilomasi',
heroBody: 'Selaa suomalaisten mökkien, huoneistojen ja villojen ilmoituksia suoraan omistajilta. Jokainen ilmoitus tarkistetaan ennen julkaisua, ja otat vuokranantajaan yhteyttä suoraan — yksinkertaista ja läpinäkyvää.',
ctaViewSample: 'Katso esimerkkikohde',
ctaHealth: 'Tarkista health-päätepiste',
ctaBrowse: 'Selaa kohteita',
highlightQualityTitle: 'Laadukkaat kohteet',
highlightQualityBody: 'Kuratoidut mökit ja huvilat, selkeät saatavuudet ja helppo varaus.',
highlightLocalTitle: 'Suomi edellä',
highlightLocalBody: 'Tehty Suomen olosuhteisiin, nopea sisällönjakelu Pohjoismaissa.',
highlightApiTitle: 'API-ystävällinen',
highlightApiBody: 'Strukturoitu data, jotta löydät kohteet kaikissa kanavissa.',
runtimeConfigTitle: 'Ajoaikainen konfiguraatio',
runtimeConfigLead: 'Ajo- ja rakennusaikaiset arvot, joita palvelu käyttää.',
runtimeAppEnv: 'APP_ENV',
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
aboutTitle: 'Tietoja lomavuokraus.fi:stä',
aboutLead: 'Keskittynyt suomalainen loma-asuntopalvelu: nopea selaus, selkeä moderointi ja mutkaton kokemus vuokraajalle.',
pricingTitle: 'Hinnasto',
pricingLead: 'Selkeä hinnoittelu vuokraajille ilman yllätyksiä.',
pricingMonthly: 'Kuukausi',
pricingAnnual: 'Vuosimaksu',
pricingPerMonth: 'kuukaudessa',
pricingPerYear: 'vuodessa (20 % säästö)',
pricingMonthlyBody: 'Aloita kuukausihinnalla 10 € per kohde.',
pricingAnnualBody: 'Maksa vuodessa 100 € per kohde ja pidä kulut ennustettavina.',
pricingPerListing: 'Hinta on per aktiivinen kohde. Voit perua milloin vain.',
pricingNotesTitle: 'Huomiot',
pricingNotesBody: 'Pidämme hinnoittelun yksinkertaisena samalla kun rakennamme uusia isäntätyökaluja, viestejä ja integraatioita. Kaikki nykyiset ominaisuudet sisältyvät.',
footerAbout: 'Tietoa',
footerPricing: 'Hinnasto',
footerPrivacy: 'Tietosuoja ja evästeet',
loginTitle: 'Kirjaudu sisään',
emailLabel: 'Sähköposti',
passwordLabel: 'Salasana',
loginButton: 'Kirjaudu',
loggingIn: 'Kirjaudutaan…',
loginSuccess: 'Kirjautuminen onnistui.',
registerTitle: 'Luo tili',
registerLead: 'Sähköpostin varmistus vaaditaan, ja ylläpitäjä hyväksyy tilin.',
nameOptional: 'Nimi (valinnainen)',
passwordHint: 'Salasana (väh. 8 merkkiä)',
registerButton: 'Rekisteröidy',
registering: 'Lähetetään…',
registerSuccess: 'Rekisteröinti onnistui. Tarkista sähköpostisi vahvistuslinkin vuoksi.',
forgotTitle: 'Unohditko salasanasi?',
forgotLead: 'Syötä sähköpostisi niin lähetämme palautuslinkin.',
forgotSubmit: 'Lähetä palautuslinkki',
forgotSuccess: 'Jos sähköposti löytyy, palautuslinkki on lähetetty.',
forgotError: 'Linkin lähetys epäonnistui.',
resetTitle: 'Vaihda salasana',
resetLead: 'Aseta uusi salasana tilillesi.',
resetSubmit: 'Aseta uusi salasana',
resetSuccess: 'Salasana vaihdettu. Voit nyt kirjautua sisään.',
resetError: 'Salasanan vaihto epäonnistui. Linkki voi olla vanhentunut.',
resetMissingToken: 'Palautustunniste puuttuu. Käytä sähköpostista saatua linkkiä.',
forgotCta: 'Unohdit salasanan?',
pendingAdminTitle: 'Ylläpito: tarkastettavat',
pendingUsersTitle: 'Odottavat käyttäjät',
pendingListingsTitle: 'Odottavat kohteet',
noPendingUsers: 'Ei odottavia käyttäjiä.',
noPendingListings: 'Ei odottavia kohteita.',
statusLabel: 'tila',
slugsLabel: 'Osoitepolut',
verifiedLabel: 'vahvistettu',
approve: 'Hyväksy',
approveAdmin: 'Hyväksy + admin',
reject: 'Hylkää',
remove: 'Poista käytöstä',
publish: 'Julkaise',
approvalsMessage: 'Kohde päivitetty',
userUpdated: 'Käyttäjä päivitetty',
adminRequired: 'Ylläpitäjän oikeudet vaaditaan',
adminUsersTitle: 'Ylläpito: käyttäjät',
adminUsersLead: 'Hallinnoi rooleja ja hyväksyntöjä.',
tableEmail: 'Sähköposti',
tableRole: 'Rooli',
tableStatus: 'Tila',
tableVerified: 'Vahvistettu',
tableApproved: 'Hyväksytty',
myListingsTitle: 'Omat kohteet',
createNewListing: 'Luo uusi kohde',
noListings: 'Ei kohteita vielä.',
createOne: 'Luo kohde',
loading: 'Ladataan…',
view: 'Näytä',
removing: 'Poistetaan…',
removed: 'Kohde poistettu näkyvistä',
removeConfirm: 'Poista kohde? Se piilotetaan muilta.',
myProfileTitle: 'Oma profiili',
notLoggedIn: 'Et ole kirjautunut',
profileEmail: 'Sähköposti',
profileName: 'Nimi',
profilePhone: 'Puhelin',
profileRole: 'Rooli',
profileStatus: 'Tila',
profileEmailVerified: 'Sähköposti vahvistettu',
profileApproved: 'Hyväksytty',
profileUpdated: 'Profiili päivitetty',
emailLocked: 'Sähköpostia ei voi vaihtaa',
save: 'Tallenna',
saving: 'Tallennetaan…',
yes: 'kyllä',
no: 'ei',
verifyTitle: 'Sähköpostin vahvistus',
verifyOk: 'Sähköposti vahvistettu. Ylläpitäjä hyväksyy tilin pian.',
verifyFail: 'Vahvistus epäonnistui tai token on virheellinen.',
listingLocation: 'Sijainti',
listingAddress: 'Osoite',
listingCapacity: 'Tilat',
listingAmenities: 'Varustelu',
listingNoAmenities: 'Varustelua ei ole listattu.',
listingContact: 'Yhteystiedot',
listingMoreInfo: 'Lisätietoja',
availabilityTitle: 'Saatavuuskalenteri',
availabilityLegendBooked: 'Varattu',
availabilityMissing: 'Kalenteria ei ole vielä yhdistetty.',
localeLabel: 'Kieli',
homeCrumb: 'Etusivu',
createListingTitle: 'Luo kohde',
loginToCreate: 'Kirjaudu ensin luodaksesi kohteen.',
slugLabel: 'Osoitepolku',
localeInput: 'Kieli',
titleLabel: 'Otsikko',
descriptionLabel: 'Kuvaus',
teaserLabel: 'Tiivistelmä',
countryLabel: 'Maa',
regionLabel: 'Maakunta/alue',
cityLabel: 'Kunta/kaupunki',
contactNameLabel: 'Yhteyshenkilö',
contactEmailLabel: 'Yhteyssähköposti',
streetAddressLabel: 'Katuosoite',
addressNoteLabel: 'Saapumisohje (valinnainen)',
addressNotePlaceholder: 'Reittiohje, pysäköinti tai muut saapumisohjeet',
latitudeLabel: 'Leveysaste (valinnainen)',
longitudeLabel: 'Pituusaste (valinnainen)',
maxGuestsLabel: 'Vieraita enintään',
bedroomsLabel: 'Makuuhuoneita',
bedsLabel: 'Vuoteita',
bathroomsLabel: 'Kylpyhuoneita',
priceHintLabel: 'Hinta-arvio (€ / yö)',
priceHintHelp: 'Suuntaa-antava hinta euroina per yö (ei sitova).',
calendarUrlsLabel: 'Saatavuuskalenterit (iCal-osoitteet, yksi per rivi)',
calendarUrlsHelp: 'Liitä iCal-linkit muilta alustoilta. Yhdistämme ne saatavuuden näyttämiseen.',
imagesLabel: 'Kuvat',
imagesHelp: 'Lataa enintään {count} kuvaa (max {sizeMb} Mt / kuva).',
imagesTooMany: 'Liikaa kuvia (enintään {count}).',
imagesTooLarge: 'Kuva on liian suuri (max {sizeMb} Mt).',
imagesReadFailed: 'Kuvien luku epäonnistui.',
imagesRequired: 'Lataa vähintään yksi kuva.',
coverImageLabel: 'Kansikuvan järjestys',
coverImageHelp: 'Monettako ladatuista kuvista käytetään kansikuvana (oletus 1)',
coverChoice: 'Kansipaikka {index}',
submitListing: 'Luo kohde',
submittingListing: 'Lähetetään…',
createListingSuccess: 'Kohde luotu id:llä {id} (tila: {status})',
approvalsCountLabel: 'Tarkastettavat',
approvalsPending: '{count} odottaa',
amenitySauna: 'Sauna',
amenityFireplace: 'Takka',
amenityWifi: 'Wi-Fi',
amenityPets: 'Lemmikit sallittu',
amenityLake: 'Järven rannalla',
amenityAirConditioning: 'Ilmastointi',
amenityKitchen: 'Keittiö',
amenityDishwasher: 'Astianpesukone',
amenityWashingMachine: 'Pyykinpesukone',
amenityBarbecue: 'Grilli',
amenityEvFree: 'Sähköauton lataus (ilmainen)',
amenityEvPaid: 'Sähköauton lataus (maksullinen)',
evChargingLabel: 'Sähköauton lataus',
evChargingNone: 'Ei saatavilla',
evChargingFree: 'Ilmainen asiakkaille',
evChargingPaid: 'Maksullinen',
capacityGuests: '{count} vierasta',
capacityBedrooms: '{count} makuuhuonetta',
capacityBeds: '{count} vuodetta',
capacityBathrooms: '{count} kylpyhuonetta',
browseListingsTitle: 'Selaa kohteita',
browseListingsLead: 'Hae julkaistuja kohteita, rajaa sijainnilla ja tutki kartalla.',
sampleBadge: 'Mallikohde',
privacyTitle: 'Tietosuoja ja evästeet',
privacyUpdated: 'Päivitetty: {date}',
privacyCollectTitle: 'Mitä tietoja keräämme',
privacyCollectAccounts: 'Käyttäjätiedot: sähköposti, salasanatiiviste, nimi, puhelin (valinnainen), rooli/tila.',
privacyCollectListings: 'Kohdetiedot: sijainti, yhteystiedot, varustelu, kuvat, kieliversiot.',
privacyCollectLogs: 'Lokit: rajattu pyyntömeta diagnostiikkaan.',
privacyUseTitle: 'Miten käytämme tietoja',
privacyUseAuth: 'Kirjautumiseen ja sessioiden hallintaan.',
privacyUseListings: 'Kohteiden julkaisuun ja moderointiin.',
privacyUseMail: 'Transaktio­sähköposteihin (vahvistus, salasanan palautus).',
privacyUseLegal: 'Lakivelvoitteiden täyttämiseen ja palvelun suojaamiseen.',
privacyStoreTitle: 'Säilytys',
privacyStoreDb: 'Tiedot tallennetaan EU:ssa olevaan Postgres-tietokantaan.',
privacyStoreBackups: 'Varmuuskopioita säilytetään palautusta varten; poistetut tiedot voivat esiintyä varmuuskopioissa rajatun ajan.',
privacyCookiesTitle: 'Evästeet',
privacyCookiesSession: 'Käytämme vain välttämättömiä evästeitä: kirjautumissessio.',
privacyCookiesNoTracking: 'Ei analytiikka-, mainos- tai seurantateknologioita.',
privacySharingTitle: 'Tietojen jakaminen',
privacySharingAds: 'Emme myy tai jaa henkilötietoja mainostajille.',
privacySharingOps: 'Palveluntarjoajille vain palvelun pyörittämiseen (sähköposti, hosting).',
privacyRightsTitle: 'Oikeutesi',
privacyRightsAccess: 'Voit pyytää tietojen nähtävyyttä, oikaisua tai poistoa.',
privacyRightsConsent: 'Voit perua suostumuksen ei-välttämättömästä käsittelystä (tällä hetkellä vain välttämättömät).',
privacyRightsContact: 'Ota yhteyttä, jos sinulla on kysyttävää tietosuojasta.',
searchLabel: 'Haku',
searchPlaceholder: 'Hae nimellä, kuvauksella tai paikkakunnalla',
cityFilter: 'Kaupunki/kunta',
regionFilter: 'Maakunta/alue',
searchButton: 'Hae',
searchAmenities: 'Varustelu',
searchAvailability: 'Saatavuus',
startDate: 'Alkupäivä',
endDate: 'Loppupäivä',
availabilityOnlyWithCalendar: 'Päiväsuodatus näyttää vain kohteet, joissa on kalenteri yhdistettynä.',
availableForDates: 'Vapaa valituille päiville',
notAvailableForDates: 'Ei vapaana valituille päiville',
calendarConnected: 'Kalenteri yhdistetty',
clearFilters: 'Tyhjennä suodattimet',
addressSearchLabel: 'Etsi kohteita osoitteen läheltä',
addressSearchPlaceholder: 'Katu, kaupunki tai paikka',
locateAddress: 'Paikanna kartalle',
addressRadiusLabel: '{km} km säteellä',
listingsFound: '{count} kohdetta',
openListing: 'Avaa kohde',
mapNoResults: 'Suodattimilla ei löytynyt kohteita.',
evChargingAny: 'Kaikki',
addressNotFound: 'Osoitetta ei löydy',
addressLookupFailed: 'Paikannus epäonnistui',
loadingMap: 'Ladataan karttaa…',
latestListingsTitle: 'Viimeisimmät kohteet',
latestListingsLead: 'Tuoreimmat kohteet heti julkaisun jälkeen.',
},
} as const;
export const messages = allMessages;
export type MessageKey = keyof typeof allMessages.en;
function normalizeLocale(input?: string | null): Locale | null {
if (!input) return null;
const lower = input.toLowerCase();
if (lower.startsWith('fi')) return 'fi';
if (lower.startsWith('en')) return 'en';
return null;
}
export function resolveLocale(opts: { cookieLocale?: string | null; acceptLanguage?: string | null }): Locale {
const fromCookie = normalizeLocale(opts.cookieLocale);
if (fromCookie) return fromCookie;
const fromHeader = normalizeLocale(opts.acceptLanguage);
if (fromHeader) return fromHeader;
return 'en';
}
export function t(locale: Locale, key: MessageKey, vars?: Record<string, string | number>) {
const table = messages[locale] ?? messages.en;
const template = String(table[key] ?? messages.en[key]);
if (!vars) return template;
return Object.entries(vars).reduce<string>((acc, [k, v]) => acc.replace(`{${k}}`, String(v)), template);
}