export type Locale = 'en' | 'fi'; const allMessages = { en: { brand: 'lomavuokraus.fi', navProfile: 'Profile', navMyListings: 'My listings', navNewListing: 'New listing', navApprovals: 'Approvals', navUsers: 'Users', navLogout: 'Logout', navLogin: 'Login', navSignup: 'Sign up', navBrowse: 'Browse listings', navLanguage: 'Language', approvalsBadge: '{count}', heroEyebrow: 'lomavuokraus.fi', heroTitle: 'Find your next Finnish getaway', heroBody: 'A fast, modern marketplace for holiday rentals. Built with the Next.js App Router and ready for Kubernetes from day one.', ctaViewSample: 'View a sample listing', ctaHealth: 'Check health endpoint', ctaBrowse: 'Browse listings', highlightQualityTitle: 'Quality stays', highlightQualityBody: 'Curated cabins and villas with clear availability and simple booking.', highlightLocalTitle: 'Local-first', highlightLocalBody: 'Built for Finnish seasons with fast content delivery in the Nordics.', highlightApiTitle: 'API-friendly', highlightApiBody: 'Structured data so you can surface listings wherever you need them.', runtimeConfigTitle: 'Runtime configuration', runtimeAppEnv: 'APP_ENV', runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL', runtimeApiBase: 'NEXT_PUBLIC_API_BASE', 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', localeLabel: 'Locale', homeCrumb: 'Home', createListingTitle: 'Create listing', loginToCreate: 'Please log in first to create a listing.', slugLabel: 'Slug', localeInput: 'Locale', titleLabel: 'Title', descriptionLabel: 'Description', teaserLabel: 'Teaser', countryLabel: 'Country', regionLabel: 'Region', cityLabel: 'City', contactNameLabel: 'Contact name', contactEmailLabel: 'Contact email', streetAddressLabel: 'Street address', addressNoteLabel: 'Address note (optional)', addressNotePlaceholder: 'Directions, parking, or arrival details', latitudeLabel: 'Latitude (optional)', longitudeLabel: 'Longitude (optional)', maxGuestsLabel: 'Max guests', bedroomsLabel: 'Bedrooms', bedsLabel: 'Beds', bathroomsLabel: 'Bathrooms', priceHintLabel: 'Price ballpark (€ / night)', priceHintHelp: 'Rough nightly price in euros (not a binding offer).', 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', 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', 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: 'Nopea, moderni vuokramökkipalvelu. Rakennettu Next.js App Routerilla ja valmiina Kubernetes-ympäristöön.', 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', runtimeAppEnv: 'APP_ENV', runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL', runtimeApiBase: 'NEXT_PUBLIC_API_BASE', 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', localeLabel: 'Kieli', homeCrumb: 'Etusivu', createListingTitle: 'Luo kohde', loginToCreate: 'Kirjaudu ensin luodaksesi kohteen.', slugLabel: 'Osoitepolku', localeInput: 'Kieli', titleLabel: 'Otsikko', descriptionLabel: 'Kuvaus', teaserLabel: 'Tiivistelmä', countryLabel: 'Maa', regionLabel: 'Maakunta/alue', cityLabel: 'Kunta/kaupunki', contactNameLabel: 'Yhteyshenkilö', contactEmailLabel: 'Yhteyssähköposti', streetAddressLabel: 'Katuosoite', addressNoteLabel: 'Saapumisohje (valinnainen)', addressNotePlaceholder: 'Reittiohje, pysäköinti tai muut saapumisohjeet', latitudeLabel: 'Leveysaste (valinnainen)', longitudeLabel: 'Pituusaste (valinnainen)', maxGuestsLabel: 'Vieraita enintään', bedroomsLabel: 'Makuuhuoneita', bedsLabel: 'Vuoteita', bathroomsLabel: 'Kylpyhuoneita', priceHintLabel: 'Hinta-arvio (€ / yö)', priceHintHelp: 'Suuntaa-antava hinta euroina per yö (ei sitova).', 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', 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', clearFilters: 'Tyhjennä suodattimet', addressSearchLabel: 'Etsi kohteita osoitteen läheltä', addressSearchPlaceholder: 'Katu, kaupunki tai paikka', locateAddress: 'Paikanna kartalle', addressRadiusLabel: '{km} km säteellä', listingsFound: '{count} kohdetta', openListing: 'Avaa kohde', mapNoResults: 'Suodattimilla ei löytynyt kohteita.', evChargingAny: 'Kaikki', addressNotFound: 'Osoitetta ei löydy', addressLookupFailed: 'Paikannus epäonnistui', loadingMap: 'Ladataan karttaa…', latestListingsTitle: 'Viimeisimmät kohteet', latestListingsLead: 'Tuoreimmat kohteet heti julkaisun jälkeen.', }, } as const; export const messages = allMessages; export type MessageKey = keyof typeof allMessages.en; function normalizeLocale(input?: string | null): Locale | null { if (!input) return null; const lower = input.toLowerCase(); if (lower.startsWith('fi')) return 'fi'; if (lower.startsWith('en')) return 'en'; return null; } export function resolveLocale(opts: { cookieLocale?: string | null; acceptLanguage?: string | null }): Locale { const fromCookie = normalizeLocale(opts.cookieLocale); if (fromCookie) return fromCookie; const fromHeader = normalizeLocale(opts.acceptLanguage); if (fromHeader) return fromHeader; return 'en'; } export function t(locale: Locale, key: MessageKey, vars?: Record) { 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); }