lomavuokraus/lib/i18n.ts
2025-12-21 21:58:08 +02:00

871 lines
42 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',
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…',
edit: 'Edit',
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…',
edit: 'Muokkaa',
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<keyof typeof baseMessages.en, string> = {
...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',
edit: 'Redigera',
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<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);
}