export type Locale = 'en' | 'fi' | 'sv'; const baseMessages = { en: { brand: 'lomavuokraus.fi', navProfile: 'Profile', navMyListings: 'My listings', navNewListing: 'New listing', navAdmin: 'Admin', navApprovals: 'Approvals', navUsers: 'Users', navMonitoring: 'Monitoring', navSettings: 'Settings', 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', footerCookieNotice: 'We use only essential cookies for login and security. By using this site you consent; if you do not accept, please do not use the site.', 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.', monitorTitle: 'Admin: monitoring', monitorLead: 'Live status for Hetzner nodes, Kubernetes pods, and PostgreSQL.', monitorRefresh: 'Refresh now', monitorLastUpdated: 'Last updated', monitorNoData: 'No data yet.', monitorHetznerTitle: 'Hetzner nodes', monitorHetznerMissingToken: 'Set HCLOUD_TOKEN to show Hetzner nodes.', monitorHetznerEmpty: 'No Hetzner servers returned.', monitorK8sTitle: 'Kubernetes', monitorNodesTitle: 'Nodes', monitorPodsTitle: 'Pods', monitorNoPods: 'No pods in lomavuokraus namespaces.', monitorRestarts: 'Restarts', monitorAge: 'Age', monitorDbTitle: 'PostgreSQL', monitorServerTime: 'Server time', monitorDbSize: 'Database size', monitorDbRecovery: 'Recovery mode', monitorConnections: 'Connections', monitorHealthy: 'Healthy', monitorAttention: 'Needs attention', monitorLoadFailed: 'Failed to load monitoring data.', monitorCreated: 'Created', monitorLastReady: 'Last Ready transition', adminSettingsTitle: 'Admin: settings', adminSettingsLead: 'Site-wide feature toggles and policies.', 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', billingSettingsTitle: 'Billing assistant', billingSettingsLead: 'Opt into automated billing emails via the n8n agent. Set defaults and per-listing overrides.', billingEnableLabel: 'Enable billing assistant for my listings', billingAccountNameLabel: 'Billing account owner name', billingAccountPlaceholder: 'Use profile default', billingIbanLabel: 'IBAN for payouts', billingIbanPlaceholder: 'Use profile default', billingIncludeVat: 'Include a VAT line on invoices', billingListingsTitle: 'Per-listing billing', billingListingsLead: 'Override billing details per listing; blank fields inherit your profile defaults.', billingNoListings: 'No listings available yet.', billingVatChoice: 'VAT line preference', billingVatInherit: 'Use profile choice', billingVatYes: 'Include VAT line', billingVatNo: 'Do not include VAT line', billingDisabledHint: 'Enable the billing assistant to manage invoice details and VAT preferences.', billingLoadFailed: 'Failed to load billing settings.', billingSaveFailed: 'Could not save billing settings.', billingSaved: 'Billing settings saved.', settingsSaved: 'Settings saved.', 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: 'Price (starting from)', listingAmenities: 'Amenities', listingNoAmenities: 'No amenities listed yet.', listingContact: 'Contact', contactLoginToView: 'Log in to view contact details for this listing.', 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.', settingContactVisibilityTitle: 'Listing contact visibility', settingContactVisibilityHelp: 'Hide host contact details from anonymous visitors so only logged-in users can see them.', settingRequireLoginForContact: 'Require login to see contact details', 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: 'Price starting from (weeknight, € / night)', priceWeekendLabel: 'Price starting from (weekend, € / night)', priceHintHelp: 'These prices are indicative only (starting from), not a binding offer.', priceStartingFromShort: 'Starting from {price}€ / night', 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}', makeCover: 'Make cover', existingImageLabel: 'Existing image', imageRemoveLastError: 'Add another image before removing the last one.', imageRemoveFailed: 'Failed to remove image.', 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', amenityEvNearby: 'EV charging nearby', amenityEvOnSite: 'EV charging on-site', amenityWheelchairAccessible: 'Wheelchair accessible', amenitySkiPass: 'Ski pass included', evChargingLabel: 'EV charging', evChargingYes: 'EV charging available', evChargingNo: 'No EV charging', evChargingAny: 'Any', evChargingExplain: 'Is there EV charging available at the property?', 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', navAdmin: 'Ylläpito', navApprovals: 'Tarkastettavat', navUsers: 'Käyttäjät', navMonitoring: 'Valvonta', navSettings: 'Asetukset', 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.', settingContactVisibilityTitle: 'Ilmoitusten yhteystiedot', settingContactVisibilityHelp: 'Piilota isännän yhteystiedot kirjautumattomilta, näytä ne vain kirjautuneille käyttäjille.', settingRequireLoginForContact: 'Vaadi kirjautuminen yhteystietoihin', 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', footerCookieNotice: 'Käytämme vain välttämättömiä evästeitä kirjautumiseen ja turvallisuuteen. Käyttämällä sivustoa hyväksyt evästeet; jos et hyväksy, älä käytä sivustoa.', 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ä.', monitorTitle: 'Ylläpito: valvonta', monitorLead: 'Hetzner-solmujen, Kubernetes-podien ja PostgreSQL:n tilanne yhdessä näkymässä.', monitorRefresh: 'Päivitä nyt', monitorLastUpdated: 'Päivitetty', monitorNoData: 'Ei dataa vielä.', monitorHetznerTitle: 'Hetzner-solmut', monitorHetznerMissingToken: 'Aseta HCLOUD_TOKEN, jotta Hetzner-solmut saadaan näkyviin.', monitorHetznerEmpty: 'Hetzner ei palauttanut palvelimia.', monitorK8sTitle: 'Kubernetes', monitorNodesTitle: 'Solmut', monitorPodsTitle: 'Podit', monitorNoPods: 'Ei podeja lomavuokraus-namespacessa.', monitorRestarts: 'Restartit', monitorAge: 'Ikä', monitorDbTitle: 'PostgreSQL', monitorServerTime: 'Palvelimen aika', monitorDbSize: 'Tietokannan koko', monitorDbRecovery: 'Recovery-tila', monitorConnections: 'Yhteydet', monitorHealthy: 'Kunnossa', monitorAttention: 'Huomio', monitorLoadFailed: 'Valvontadatan haku epäonnistui.', monitorCreated: 'Luotu', monitorLastReady: 'Viimeisin Ready-muutos', adminSettingsTitle: 'Ylläpito: asetukset', adminSettingsLead: 'Sivuston laajuiset ominaisuusasetukset ja käytännöt.', 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', billingSettingsTitle: 'Laskutusavustin', billingSettingsLead: 'Ota n8n-laskutus käyttöön halutessasi ja määritä oletus- ja kohdekohtaiset tiedot.', billingEnableLabel: 'Ota laskutusavustin käyttöön kohteilleni', billingAccountNameLabel: 'Tilinomistajan nimi', billingAccountPlaceholder: 'Käytä profiilin oletusta', billingIbanLabel: 'IBAN-maksuissa', billingIbanPlaceholder: 'Käytä profiilin oletusta', billingIncludeVat: 'Lisää laskulle ALV-rivi', billingListingsTitle: 'Kohdekohtaiset asetukset', billingListingsLead: 'Ylikirjoita tiedot kohteittain; tyhjä kenttä käyttää profiilin arvoa.', billingNoListings: 'Ei kohteita vielä.', billingVatChoice: 'ALV-rivin valinta', billingVatInherit: 'Käytä profiilin asetusta', billingVatYes: 'Lisää ALV-rivi', billingVatNo: 'Älä lisää ALV-riviä', billingDisabledHint: 'Ota laskutusavustin käyttöön lisätäksesi laskutustiedot ja ALV-valinnat.', billingLoadFailed: 'Laskutusasetusten lataus epäonnistui.', billingSaveFailed: 'Laskutusasetusten tallennus epäonnistui.', billingSaved: 'Laskutusasetukset tallennettu.', settingsSaved: 'Asetukset tallennettu.', 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 (alkaen)', listingAmenities: 'Varustelu', listingNoAmenities: 'Varustelua ei ole listattu.', listingContact: 'Yhteystiedot', contactLoginToView: 'Kirjaudu sisään nähdäksesi tämän ilmoituksen 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: 'Hinta alkaen (arki, € / yö)', priceWeekendLabel: 'Hinta alkaen (viikonloppu, € / yö)', priceHintHelp: 'Hinnat ovat suuntaa-antavia (alkaen), eivät sitovia.', priceStartingFromShort: 'Alkaen {price}€ / yö', 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}', makeCover: 'Aseta kansikuvaksi', existingImageLabel: 'Nykyinen kuva', imageRemoveLastError: 'Lisää toinen kuva ennen viimeisen poistamista.', imageRemoveFailed: 'Kuvan poistaminen epäonnistui.', 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ä', amenityEvNearby: 'Sähköauton lataus lähellä', amenityEvOnSite: 'Sähköauton lataus kohteessa', amenityWheelchairAccessible: 'Esteetön / pyörätuolilla', amenitySkiPass: 'Hissilippu sisältyy', evChargingLabel: 'Sähköauton lataus', evChargingYes: 'Latausmahdollisuus', evChargingNo: 'Ei latausta', evChargingAny: 'Kaikki', evChargingExplain: 'Onko kohteessa 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.', 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 = { ...baseMessages.en, navProfile: 'Profil', navMyListings: 'Mina annonser', navNewListing: 'Ny annons', navAdmin: 'Admin', navApprovals: 'Godkännanden', navUsers: 'Användare', navMonitoring: 'Övervakning', navSettings: 'Inställningar', navLogout: 'Logga ut', navLogin: 'Logga in', navSignup: 'Registrera dig', navBrowse: 'Bläddra bland annonser', navLanguage: 'Språk', monitorTitle: 'Admin: övervakning', monitorLead: 'Livesstatus för Hetzner-noder, Kubernetes-pods och PostgreSQL.', monitorRefresh: 'Uppdatera nu', monitorLastUpdated: 'Senast uppdaterad', monitorNoData: 'Ingen data ännu.', monitorHetznerTitle: 'Hetzner-noder', monitorHetznerMissingToken: 'Sätt HCLOUD_TOKEN för att visa Hetzner-noder.', monitorHetznerEmpty: 'Inga Hetzner-servrar hittades.', monitorK8sTitle: 'Kubernetes', monitorNodesTitle: 'Noder', monitorPodsTitle: 'Pods', monitorNoPods: 'Inga pods i lomavuokraus-namespaces.', monitorRestarts: 'Omstarter', monitorAge: 'Ålder', monitorDbTitle: 'PostgreSQL', monitorServerTime: 'Servertid', monitorDbSize: 'Databasstorlek', monitorDbRecovery: 'Recovery-läge', monitorConnections: 'Anslutningar', monitorHealthy: 'OK', monitorAttention: 'Behöver åtgärd', monitorLoadFailed: 'Kunde inte hämta övervakningsdata.', monitorCreated: 'Skapad', monitorLastReady: 'Senaste Ready-ändring', adminSettingsTitle: 'Admin: inställningar', adminSettingsLead: 'Webbplatsövergripande funktioner och policyer.', 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.', settingContactVisibilityTitle: 'Synlighet för kontaktuppgifter', settingContactVisibilityHelp: 'Dölj värdens kontaktuppgifter för anonyma besökare så att bara inloggade ser dem.', settingRequireLoginForContact: 'Kräv inloggning för att se kontaktuppgifter', billingSettingsTitle: 'Faktureringsassistent', billingSettingsLead: 'Aktivera n8n-fakturor om du vill och ange standard- samt objektspecifika uppgifter.', billingEnableLabel: 'Aktivera faktureringsassistent för mina annonser', billingAccountNameLabel: 'Kontoinnehavarens namn', billingAccountPlaceholder: 'Använd profilens standard', billingIbanLabel: 'IBAN för utbetalningar', billingIbanPlaceholder: 'Använd profilens standard', billingIncludeVat: 'Ta med momsrad på fakturor', billingListingsTitle: 'Objektvisa inställningar', billingListingsLead: 'Åsidosätt uppgifter per annons; tomma fält använder profilens värden.', billingNoListings: 'Inga annonser ännu.', billingVatChoice: 'Momsrad', billingVatInherit: 'Använd profilinställning', billingVatYes: 'Lägg till momsrad', billingVatNo: 'Lägg inte till momsrad', billingDisabledHint: 'Aktivera assistenten för att hantera fakturauppgifter och momsval.', billingLoadFailed: 'Kunde inte ladda faktureringsinställningar.', billingSaveFailed: 'Kunde inte spara faktureringsinställningar.', billingSaved: 'Faktureringsinställningar sparade.', settingsSaved: 'Inställningar sparade.', 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: 'Pris från (vardag, € / natt)', priceWeekendLabel: 'Pris från (helg, € / natt)', priceHintHelp: 'Priserna är endast vägledande (från), inte ett bindande erbjudande.', priceStartingFromShort: 'Från {price}€ / natt', priceWeekdayShort: '{price}€ vardag', priceWeekendShort: '{price}€ helg', makeCover: 'Gör till omslag', existingImageLabel: 'Befintlig bild', imageRemoveLastError: 'Lägg till en annan bild innan du tar bort den sista.', imageRemoveFailed: 'Det gick inte att ta bort bilden.', priceNotSet: 'Ej angivet', listingPrices: 'Pris (från)', listingContact: 'Kontakt', contactLoginToView: 'Logga in för att se kontaktuppgifter för denna annons.', capacityUnknown: 'Kapacitet ej angiven', amenityEvAvailable: 'EV-laddning i närheten', amenityEvNearby: 'EV-laddning i närheten', amenityEvOnSite: 'EV-laddning på plats', amenityWheelchairAccessible: 'Rullstolsanpassat', amenitySkiPass: 'Liftkort ingår', amenityMicrowave: 'Mikrovågsugn', amenityFreeParking: 'Gratis parkering', evChargingLabel: 'EV-laddning', evChargingYes: 'EV-laddning finns', evChargingNo: 'Ingen EV-laddning', evChargingAny: 'Alla', evChargingExplain: 'Finns det EV-laddning på plats?', footerCookieNotice: 'Vi använder endast nödvändiga cookies för inloggning och säkerhet. Genom att använda sajten godkänner du cookies; om du inte gör det, använd inte webbplatsen.', }; 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) { const table = messages[locale] ?? messages.en; const template = String(table[key] ?? messages.en[key]); if (!vars) return template; return Object.entries(vars).reduce((acc, [k, v]) => acc.replace(`{${k}}`, String(v)), template); }