lomavuokraus/lib/i18n.ts
2025-11-29 19:42:21 +02:00

597 lines
28 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' | 'sv';
const baseMessages = {
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: 'Discover Finnish cabins, apartments and villas posted directly by their owners. Listings are verified before publishing and you contact hosts directly — simple and transparent.',
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',
languageTabsLabel: 'Listing languages',
languageTabsHint: 'Add translations for each supported language',
localeReady: 'Ready',
localePartial: 'In progress',
localeMissing: 'Missing',
aiHelperTitle: 'AI translation helper',
aiHelperLead: 'Copy the prompt to your AI assistant, let it translate missing locales, and paste the JSON reply back.',
aiPromptLabel: 'Prompt to send to AI',
aiCopyPrompt: 'Copy prompt',
aiPromptCopied: 'Copied',
aiCopyError: 'Copy failed',
aiResponseLabel: 'Paste AI response (JSON)',
aiApply: 'Apply AI response',
aiApplyError: 'Could not read AI response. Please ensure it is valid JSON with a locales object.',
aiApplySuccess: 'Translations updated from AI response.',
aiHelperNote: 'The AI should return only JSON with the same keys.',
translationMissing: 'Add at least one language with a title and description.',
loginToCreate: 'Please log in first to create a listing.',
slugLabel: 'Slug',
slugHelp: 'Unique link name, use lowercase letters and hyphens only (e.g. lake-cabin).',
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ää.',
languageTabsLabel: 'Ilmoituksen kielet',
languageTabsHint: 'Lisää käännökset kaikille tuetuille kielille',
localeReady: 'Valmis',
localePartial: 'Kesken',
localeMissing: 'Puuttuu',
aiHelperTitle: 'AI-käännösapu',
aiHelperLead: 'Kopioi prompti tekoälylle, käännä puuttuvat kielet ja liitä JSON-vastaus takaisin.',
aiPromptLabel: 'Prompti tekoälylle',
aiCopyPrompt: 'Kopioi prompti',
aiPromptCopied: 'Kopioitu',
aiCopyError: 'Kopiointi epäonnistui',
aiResponseLabel: 'Liitä tekoälyn vastaus (JSON)',
aiApply: 'Käytä AI-vastausta',
aiApplyError: 'Vastausta ei voitu lukea. Varmista, että se on kelvollista JSONia ja sisältää locales-avaimen.',
aiApplySuccess: 'Käännökset päivitetty AI-vastauksesta.',
aiHelperNote: 'Tekoälyn tulisi palauttaa vain samaa avainrakennetta noudattava JSON.',
translationMissing: 'Täytä vähintään yhden kielen otsikko ja kuvaus.',
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',
slugHelp: 'Yksilöllinen linkki, käytä pieniä kirjaimia ja väliviivoja (esim. saimaa-mokki).',
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;
const svMessages: Record<keyof typeof baseMessages.en, string> = {
...baseMessages.en,
navProfile: 'Profil',
navMyListings: 'Mina annonser',
navNewListing: 'Ny annons',
navApprovals: 'Godkännanden',
navUsers: 'Användare',
navLogout: 'Logga ut',
navLogin: 'Logga in',
navSignup: 'Registrera dig',
navBrowse: 'Bläddra bland annonser',
navLanguage: 'Språk',
slugHelp: 'Unik länk, använd små bokstäver och bindestreck (t.ex. sjo-stuga).',
heroTitle: 'Hitta ditt nästa finska getaway',
heroBody: 'Upptäck stugor, lägenheter och villor direkt från ägarna. Annonser verifieras innan publicering och du kontaktar värdarna direkt — enkelt och transparent.',
ctaBrowse: 'Bläddra bland annonser',
createListingTitle: 'Skapa annons',
languageTabsLabel: 'Annonsens språk',
languageTabsHint: 'Lägg till översättningar för varje språk',
localeReady: 'Klar',
localePartial: 'Pågår',
localeMissing: 'Saknas',
aiHelperTitle: 'AI-översättningshjälp',
aiHelperLead: 'Kopiera prompten till din AI-assistent, låt den översätta saknade språk och klistra in JSON-svaret här.',
aiPromptLabel: 'Prompt till AI',
aiCopyPrompt: 'Kopiera prompt',
aiPromptCopied: 'Kopierad',
aiCopyError: 'Kopiering misslyckades',
aiResponseLabel: 'Klistra in AI-svar (JSON)',
aiApply: 'Använd AI-svar',
aiApplyError: 'Kunde inte läsa AI-svaret. Kontrollera att det är giltig JSON med locales-nyckeln.',
aiApplySuccess: 'Översättningar uppdaterade från AI-svaret.',
aiHelperNote: 'AI:n ska bara returnera JSON med samma nycklar.',
translationMissing: 'Lägg till minst ett språk med titel och beskrivning.',
};
export const messages = { ...baseMessages, sv: svMessages } as const;
export type MessageKey = keyof typeof baseMessages.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';
if (lower.startsWith('sv')) return 'sv';
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);
}