lomavuokraus/lib/i18n.ts
2025-12-06 23:34:28 +02:00

679 lines
32 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',
listingPrices: 'Pricing',
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',
localeSectionTitle: 'Listing content',
localeReady: 'Ready',
localePartial: 'In progress',
localeMissing: 'Missing',
aiHelperTitle: 'AI translation helper',
aiHelperLead: 'Let AI fill the other languages for you.',
aiOptionalHint: 'Optional: AI helper can fill other languages.',
aiAutoExplain: 'Enter the texts in any one language and click the button; AI will fill the remaining locales.',
aiAutoTranslate: 'Auto-translate missing languages',
aiAutoTranslating: 'Translating…',
aiAutoSuccess: 'Translations updated with AI.',
aiAutoError: 'AI translation failed. Please try again or use manual mode below.',
aiHelperNote: 'Uses OpenAI to translate missing texts.',
aiManualLead: 'If auto-translate fails, copy this prompt to your AI assistant and paste the JSON reply below.',
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.',
translationMissing: 'Add at least one language with a title and description.',
saveDraft: 'Save draft',
missingFields: 'Missing: {fields}',
loginToCreate: 'Please log in first to create a listing.',
slugLabel: 'Slug',
slugHelp: 'Short, easy-to-type link name; use lowercase and hyphens (e.g. lake-cabin).',
slugPreview: 'Your listing link will be: {url}',
slugChecking: 'Checking availability…',
slugAvailable: 'Slug is available',
slugTaken: 'Slug already in use',
slugCheckError: 'Could not check slug right now',
localeInput: 'Locale',
titleLabel: 'Title',
descriptionLabel: 'Description',
teaserLabel: 'Teaser',
teaserHelp: 'Short intro shown on cards',
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',
priceWeekdayLabel: 'Weeknight price (€ / night)',
priceWeekendLabel: 'Weekend price (€ / night)',
priceHintHelp: 'Set separate weeknight and weekend prices in euros (optional, not a binding offer).',
priceWeekdayShort: '{price}€ weekday',
priceWeekendShort: '{price}€ weekend',
priceNotSet: 'Not provided',
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',
amenityMicrowave: 'Microwave',
amenityFreeParking: 'Free parking',
amenityEvAvailable: 'EV charging nearby',
amenitySkiPass: 'Ski pass included',
evChargingLabel: 'EV charging nearby',
evChargingYes: 'Charging nearby',
evChargingNo: 'No charging nearby',
evChargingAny: 'Any',
evChargingExplain: 'Is there EV charging available on-site or nearby?',
capacityGuests: '{count} guests',
capacityBedrooms: '{count} bedrooms',
capacityBeds: '{count} beds',
capacityBathrooms: '{count} bathrooms',
capacityUnknown: 'Capacity not set',
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.',
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',
localeSectionTitle: 'Ilmoituksen sisältö',
localeReady: 'Valmis',
localePartial: 'Kesken',
localeMissing: 'Puuttuu',
aiHelperTitle: 'AI-käännösapu',
aiHelperLead: 'Anna tekoälyn täydentää muut kielet puolestasi.',
aiOptionalHint: 'Valinnainen: AI-avustaja voi täyttää muut kielet.',
aiAutoExplain: 'Täytä tekstit yhdellä kielellä ja paina nappia; AI täyttää loput kielet.',
aiAutoTranslate: 'Käännä puuttuvat kielet',
aiAutoTranslating: 'Käännetään…',
aiAutoSuccess: 'Käännökset päivitetty AI:lla.',
aiAutoError: 'Käännös epäonnistui. Yritä uudelleen tai käytä manuaalitilaa alla.',
aiHelperNote: 'Käyttää OpenAI:ta puuttuvien tekstien kääntämiseen.',
aiManualLead: 'Jos automaattinen käännös ei toimi, kopioi prompti tekoälylle ja liitä JSON-vastaus alle.',
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.',
translationMissing: 'Täytä vähintään yhden kielen otsikko ja kuvaus.',
saveDraft: 'Tallenna luonnos',
missingFields: 'Puuttuu: {fields}',
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',
listingPrices: 'Hinta',
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: 'Keksi lyhyt ja helppo linkki; käytä pieniä kirjaimia ja väliviivoja (esim. saimaa-mokki).',
slugPreview: 'Ilmoituksen linkki on: {url}',
slugChecking: 'Tarkistetaan saatavuutta…',
slugAvailable: 'Slug on vapaa',
slugTaken: 'Slug on jo käytössä',
slugCheckError: 'Slugia ei voitu tarkistaa nyt',
localeInput: 'Kieli',
titleLabel: 'Otsikko',
descriptionLabel: 'Kuvaus',
teaserLabel: 'Tiivistelmä',
teaserHelp: 'Lyhyt ingressi kortteihin',
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',
priceWeekdayLabel: 'Arkiyön hinta (€ / yö)',
priceWeekendLabel: 'Viikonlopun hinta (€ / yö)',
priceHintHelp: 'Aseta erilliset hinnat arki- ja viikonloppuyöille euroissa (valinnainen, ei sitova).',
priceWeekdayShort: '{price}€ arki',
priceWeekendShort: '{price}€ viikonloppu',
priceNotSet: 'Ei ilmoitettu',
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',
amenityMicrowave: 'Mikroaaltouuni',
amenityFreeParking: 'Maksuton pysäköinti',
amenityEvAvailable: 'Sähköauton lataus lähellä',
amenitySkiPass: 'Hissilippu sisältyy',
evChargingLabel: 'Sähköauton lataus lähellä',
evChargingYes: 'Latausta lähellä',
evChargingNo: 'Ei latausta lähellä',
evChargingAny: 'Kaikki',
evChargingExplain: 'Onko kohteessa tai lähistöllä sähköauton latausmahdollisuus?',
capacityGuests: '{count} vierasta',
capacityBedrooms: '{count} makuuhuonetta',
capacityBeds: '{count} vuodetta',
capacityBathrooms: '{count} kylpyhuonetta',
capacityUnknown: 'Kapasiteettia ei ole asetettu',
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: 'Hitta på en kort och enkel länk; använd små bokstäver och bindestreck (t.ex. sjo-stuga).',
slugPreview: 'Länk till annonsen: {url}',
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',
localeSectionTitle: 'Annonsinnehåll',
localeReady: 'Klar',
localePartial: 'Pågår',
localeMissing: 'Saknas',
aiHelperTitle: 'AI-översättningshjälp',
aiHelperLead: 'Låt AI fylla i de andra språken åt dig.',
aiOptionalHint: 'Valfritt: AI-hjälpen kan fylla i andra språk.',
aiAutoExplain: 'Fyll texterna på ett språk och klicka på knappen så fyller AI i resten.',
aiAutoTranslate: 'Översätt saknade språk',
aiAutoTranslating: 'Översätter…',
aiAutoSuccess: 'Översättningar uppdaterades med AI.',
aiAutoError: 'AI-översättning misslyckades. Försök igen eller använd manuellt läge nedan.',
aiHelperNote: 'Använder OpenAI för att översätta saknade texter.',
aiManualLead: 'Om autokorrespondens misslyckas, kopiera prompten till din AI och klistra in JSON-svaret nedan.',
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. Se till att det är giltig JSON med locales-nyckel.',
aiApplySuccess: 'Översättningar uppdaterades från AI-svaret.',
translationMissing: 'Lägg till minst ett språk med titel och beskrivning.',
saveDraft: 'Spara utkast',
missingFields: 'Saknas: {fields}',
slugChecking: 'Kontrollerar tillgänglighet…',
slugAvailable: 'Sluggen är ledig',
slugTaken: 'Sluggen används redan',
slugCheckError: 'Kunde inte kontrollera sluggen nu',
teaserHelp: 'Kort ingress som syns i korten',
priceWeekdayLabel: 'Vardagspris (€ / natt)',
priceWeekendLabel: 'Helgpris (€ / natt)',
priceHintHelp: 'Ange separata priser för vardag och helg i euro per natt (frivilligt).',
priceWeekdayShort: '{price}€ vardag',
priceWeekendShort: '{price}€ helg',
priceNotSet: 'Ej angivet',
listingPrices: 'Priser',
capacityUnknown: 'Kapacitet ej angiven',
amenityEvAvailable: 'EV-laddning i närheten',
amenitySkiPass: 'Liftkort ingår',
amenityMicrowave: 'Mikrovågsugn',
amenityFreeParking: 'Gratis parkering',
evChargingLabel: 'EV-laddning i närheten',
evChargingYes: 'Laddning i närheten',
evChargingNo: 'Ingen laddning i närheten',
evChargingAny: 'Alla',
evChargingExplain: 'Finns det EV-laddning på plats eller i närheten?',
};
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);
}