678 lines
32 KiB
TypeScript
678 lines
32 KiB
TypeScript
export type Locale = 'en' | 'fi' | 'sv';
|
||
|
||
const baseMessages = {
|
||
en: {
|
||
brand: 'lomavuokraus.fi',
|
||
navProfile: 'Profile',
|
||
navMyListings: 'My listings',
|
||
navNewListing: 'New listing',
|
||
navApprovals: 'Approvals',
|
||
navUsers: 'Users',
|
||
navLogout: 'Logout',
|
||
navLogin: 'Login',
|
||
navSignup: 'Sign up',
|
||
navBrowse: 'Browse listings',
|
||
navLanguage: 'Language',
|
||
approvalsBadge: '{count}',
|
||
heroEyebrow: 'lomavuokraus.fi',
|
||
heroTitle: 'Find your next Finnish getaway',
|
||
heroBody: 'Discover Finnish cabins, apartments and villas posted directly by their owners. Listings are verified before publishing and you contact hosts directly — simple and transparent.',
|
||
ctaViewSample: 'View a sample listing',
|
||
ctaHealth: 'Check health endpoint',
|
||
ctaBrowse: 'Browse listings',
|
||
highlightQualityTitle: 'Quality stays',
|
||
highlightQualityBody: 'Curated cabins and villas with clear availability and simple booking.',
|
||
highlightLocalTitle: 'Local-first',
|
||
highlightLocalBody: 'Built for Finnish seasons with fast content delivery in the Nordics.',
|
||
highlightApiTitle: 'API-friendly',
|
||
highlightApiBody: 'Structured data so you can surface listings wherever you need them.',
|
||
runtimeConfigTitle: 'Runtime configuration',
|
||
runtimeConfigLead: 'Build-time and runtime values used by the service.',
|
||
runtimeAppEnv: 'APP_ENV',
|
||
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
||
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
|
||
aboutTitle: 'About lomavuokraus.fi',
|
||
aboutLead: 'A focused marketplace for Finnish holiday rentals with fast browsing, clear moderation, and a simple host experience.',
|
||
pricingTitle: 'Pricing',
|
||
pricingLead: 'Straightforward pricing for hosts with no surprises.',
|
||
pricingMonthly: 'Monthly',
|
||
pricingAnnual: 'Annual',
|
||
pricingPerMonth: 'per month',
|
||
pricingPerYear: 'per year (save 20%)',
|
||
pricingMonthlyBody: 'Start with a monthly plan at 10€ per listing.',
|
||
pricingAnnualBody: 'Pay annually for 100€ per listing and keep your costs predictable.',
|
||
pricingPerListing: 'Pricing is per active listing. Cancel anytime.',
|
||
pricingNotesTitle: 'Notes',
|
||
pricingNotesBody: 'We keep pricing simple while we build out hosting tools, messaging, and integrations. All current features are included.',
|
||
footerAbout: 'About',
|
||
footerPricing: 'Pricing',
|
||
footerPrivacy: 'Privacy & cookies',
|
||
loginTitle: 'Login',
|
||
emailLabel: 'Email',
|
||
passwordLabel: 'Password',
|
||
loginButton: 'Login',
|
||
loggingIn: 'Logging in…',
|
||
loginSuccess: 'Login successful.',
|
||
registerTitle: 'Create an account',
|
||
registerLead: 'Verify email is required, and an admin will approve your account.',
|
||
nameOptional: 'Name (optional)',
|
||
passwordHint: 'Password (min 8 chars)',
|
||
registerButton: 'Register',
|
||
registering: 'Submitting…',
|
||
registerSuccess: 'Registration successful. Check your email for a verification link.',
|
||
forgotTitle: 'Forgot your password?',
|
||
forgotLead: 'Enter your email and we will send a reset link.',
|
||
forgotSubmit: 'Send reset link',
|
||
forgotSuccess: 'If that email exists, a reset link has been sent.',
|
||
forgotError: 'Could not send reset link right now.',
|
||
resetTitle: 'Reset password',
|
||
resetLead: 'Set a new password for your account.',
|
||
resetSubmit: 'Set new password',
|
||
resetSuccess: 'Password updated. You can log in now.',
|
||
resetError: 'Failed to reset password. The link may be invalid or expired.',
|
||
resetMissingToken: 'Reset token missing. Please use the link from your email.',
|
||
forgotCta: 'Forgot password?',
|
||
pendingAdminTitle: 'Admin: pending items',
|
||
pendingUsersTitle: 'Pending users',
|
||
pendingListingsTitle: 'Pending listings',
|
||
noPendingUsers: 'No pending users.',
|
||
noPendingListings: 'No pending listings.',
|
||
statusLabel: 'status',
|
||
slugsLabel: 'Slugs',
|
||
verifiedLabel: 'verified',
|
||
approve: 'Approve',
|
||
approveAdmin: 'Approve + make admin',
|
||
reject: 'Reject',
|
||
remove: 'Remove',
|
||
publish: 'Publish',
|
||
approvalsMessage: 'Listing updated',
|
||
userUpdated: 'User updated',
|
||
adminRequired: 'Admin access required',
|
||
adminUsersTitle: 'Admin: users',
|
||
adminUsersLead: 'Manage user roles and approvals.',
|
||
tableEmail: 'Email',
|
||
tableRole: 'Role',
|
||
tableStatus: 'Status',
|
||
tableVerified: 'Verified',
|
||
tableApproved: 'Approved',
|
||
myListingsTitle: 'My listings',
|
||
createNewListing: 'Create new listing',
|
||
noListings: 'No listings yet.',
|
||
createOne: 'Create one',
|
||
loading: 'Loading…',
|
||
view: 'View',
|
||
removing: 'Removing…',
|
||
removed: 'Listing removed',
|
||
removeConfirm: 'Remove this listing? It will be hidden from others.',
|
||
myProfileTitle: 'My profile',
|
||
notLoggedIn: 'Not logged in',
|
||
profileEmail: 'Email',
|
||
profileName: 'Name',
|
||
profilePhone: 'Phone',
|
||
profileRole: 'Role',
|
||
profileStatus: 'Status',
|
||
profileEmailVerified: 'Email verified',
|
||
profileApproved: 'Approved',
|
||
profileUpdated: 'Profile updated',
|
||
emailLocked: 'Email cannot be changed',
|
||
save: 'Save',
|
||
saving: 'Saving…',
|
||
yes: 'yes',
|
||
no: 'no',
|
||
verifyTitle: 'Email verification',
|
||
verifyOk: 'Your email is verified. An admin will approve your account shortly.',
|
||
verifyFail: 'Verification failed or token invalid.',
|
||
listingLocation: 'Location',
|
||
listingAddress: 'Address',
|
||
listingCapacity: 'Capacity',
|
||
listingPrices: 'Pricing',
|
||
listingAmenities: 'Amenities',
|
||
listingNoAmenities: 'No amenities listed yet.',
|
||
listingContact: 'Contact',
|
||
listingMoreInfo: 'More info',
|
||
availabilityTitle: 'Availability calendar',
|
||
availabilityLegendBooked: 'Booked',
|
||
availabilityMissing: 'Calendar not connected yet.',
|
||
localeLabel: 'Locale',
|
||
homeCrumb: 'Home',
|
||
createListingTitle: 'Create listing',
|
||
languageTabsLabel: 'Listing languages',
|
||
languageTabsHint: 'Add translations for each supported language',
|
||
localeSectionTitle: 'Listing content',
|
||
localeReady: 'Ready',
|
||
localePartial: 'In progress',
|
||
localeMissing: 'Missing',
|
||
aiHelperTitle: 'AI translation helper',
|
||
aiHelperLead: 'Let AI fill the other languages for you.',
|
||
aiOptionalHint: 'Optional: AI helper can fill other languages.',
|
||
aiAutoExplain: 'Enter the texts in any one language and click the button; AI will fill the remaining locales.',
|
||
aiAutoTranslate: 'Auto-translate missing languages',
|
||
aiAutoTranslating: 'Translating…',
|
||
aiAutoSuccess: 'Translations updated with AI.',
|
||
aiAutoError: 'AI translation failed. Please try again or use manual mode below.',
|
||
aiHelperNote: 'Uses OpenAI to translate missing texts.',
|
||
aiManualLead: 'If auto-translate fails, copy this prompt to your AI assistant and paste the JSON reply below.',
|
||
aiPromptLabel: 'Prompt to send to AI',
|
||
aiCopyPrompt: 'Copy prompt',
|
||
aiPromptCopied: 'Copied',
|
||
aiCopyError: 'Copy failed',
|
||
aiResponseLabel: 'Paste AI response (JSON)',
|
||
aiApply: 'Apply AI response',
|
||
aiApplyError: 'Could not read AI response. Please ensure it is valid JSON with a locales object.',
|
||
aiApplySuccess: 'Translations updated from AI response.',
|
||
translationMissing: 'Add at least one language with a title and description.',
|
||
saveDraft: 'Save draft',
|
||
missingFields: 'Missing: {fields}',
|
||
loginToCreate: 'Please log in first to create a listing.',
|
||
slugLabel: 'Slug',
|
||
slugHelp: 'Short, easy-to-type link name; use lowercase and hyphens (e.g. lake-cabin).',
|
||
slugPreview: 'Your listing link will be: {url}',
|
||
slugChecking: 'Checking availability…',
|
||
slugAvailable: 'Slug is available',
|
||
slugTaken: 'Slug already in use',
|
||
slugCheckError: 'Could not check slug right now',
|
||
localeInput: 'Locale',
|
||
titleLabel: 'Title',
|
||
descriptionLabel: 'Description',
|
||
teaserLabel: 'Teaser',
|
||
teaserHelp: 'Short intro shown on cards',
|
||
countryLabel: 'Country',
|
||
regionLabel: 'Region',
|
||
cityLabel: 'City',
|
||
contactNameLabel: 'Contact name',
|
||
contactEmailLabel: 'Contact email',
|
||
streetAddressLabel: 'Street address',
|
||
addressNoteLabel: 'Address note (optional)',
|
||
addressNotePlaceholder: 'Directions, parking, or arrival details',
|
||
latitudeLabel: 'Latitude (optional)',
|
||
longitudeLabel: 'Longitude (optional)',
|
||
maxGuestsLabel: 'Max guests',
|
||
bedroomsLabel: 'Bedrooms',
|
||
bedsLabel: 'Beds',
|
||
bathroomsLabel: 'Bathrooms',
|
||
priceWeekdayLabel: 'Weeknight price (€ / night)',
|
||
priceWeekendLabel: 'Weekend price (€ / night)',
|
||
priceHintHelp: 'Set separate weeknight and weekend prices in euros (optional, not a binding offer).',
|
||
priceWeekdayShort: '{price}€ weekday',
|
||
priceWeekendShort: '{price}€ weekend',
|
||
priceNotSet: 'Not provided',
|
||
calendarUrlsLabel: 'Availability calendars (iCal URLs, one per line)',
|
||
calendarUrlsHelp: 'Paste iCal links from other platforms. We will merge them to show availability.',
|
||
imagesLabel: 'Images',
|
||
imagesHelp: 'Upload up to {count} images (max {sizeMb}MB each).',
|
||
imagesTooMany: 'Too many images (max {count}).',
|
||
imagesTooLarge: 'Image is too large (max {sizeMb}MB).',
|
||
imagesReadFailed: 'Could not read selected images.',
|
||
imagesRequired: 'Please upload at least one image.',
|
||
coverImageLabel: 'Cover image order',
|
||
coverImageHelp: '1-based index of the uploaded images (defaults to 1)',
|
||
coverChoice: 'Cover position {index}',
|
||
submitListing: 'Create listing',
|
||
submittingListing: 'Submitting…',
|
||
createListingSuccess: 'Listing created with id {id} (status: {status})',
|
||
approvalsCountLabel: 'Approvals',
|
||
approvalsPending: '{count} pending',
|
||
amenitySauna: 'Sauna',
|
||
amenityFireplace: 'Fireplace',
|
||
amenityWifi: 'Wi-Fi',
|
||
amenityPets: 'Pets allowed',
|
||
amenityLake: 'By the lake',
|
||
amenityAirConditioning: 'Air conditioning',
|
||
amenityKitchen: 'Kitchen',
|
||
amenityDishwasher: 'Dishwasher',
|
||
amenityWashingMachine: 'Washing machine',
|
||
amenityBarbecue: 'Barbecue grill',
|
||
amenityMicrowave: 'Microwave',
|
||
amenityFreeParking: 'Free parking',
|
||
amenityEvAvailable: 'EV charging nearby',
|
||
amenitySkiPass: 'Ski pass included',
|
||
evChargingLabel: 'EV charging nearby',
|
||
evChargingYes: 'Charging nearby',
|
||
evChargingNo: 'No charging nearby',
|
||
evChargingExplain: 'Is there EV charging available on-site or nearby?',
|
||
capacityGuests: '{count} guests',
|
||
capacityBedrooms: '{count} bedrooms',
|
||
capacityBeds: '{count} beds',
|
||
capacityBathrooms: '{count} bathrooms',
|
||
capacityUnknown: 'Capacity not set',
|
||
browseListingsTitle: 'Browse listings',
|
||
browseListingsLead: 'Search public listings, filter by location, and explore them on the map.',
|
||
sampleBadge: 'Sample',
|
||
privacyTitle: 'Privacy & cookies',
|
||
privacyUpdated: 'Updated: {date}',
|
||
privacyCollectTitle: 'What data we collect',
|
||
privacyCollectAccounts: 'Account data: email, password hash, name, phone (optional), role/status.',
|
||
privacyCollectListings: 'Listing data: location, contact details, amenities, photos, translations.',
|
||
privacyCollectLogs: 'Operational logs: minimal request metadata for diagnostics.',
|
||
privacyUseTitle: 'How we use your data',
|
||
privacyUseAuth: 'To authenticate users and manage sessions.',
|
||
privacyUseListings: 'To publish and moderate listings.',
|
||
privacyUseMail: 'To send transactional email (verification, password reset).',
|
||
privacyUseLegal: 'To comply with legal requests or protect the service.',
|
||
privacyStoreTitle: 'Storage & retention',
|
||
privacyStoreDb: 'Data is stored in our Postgres database hosted in the EU.',
|
||
privacyStoreBackups: 'Backups are retained for disaster recovery; removed accounts/listings may persist in backups for a limited time.',
|
||
privacyCookiesTitle: 'Cookies',
|
||
privacyCookiesSession: 'We use essential cookies only: a session cookie for login/authentication.',
|
||
privacyCookiesNoTracking: 'No analytics, marketing, or tracking cookies are used.',
|
||
privacySharingTitle: 'Sharing',
|
||
privacySharingAds: 'We do not sell or share personal data with advertisers.',
|
||
privacySharingOps: 'Data may be shared with service providers strictly for running the service (email delivery, hosting).',
|
||
privacyRightsTitle: 'Your rights',
|
||
privacyRightsAccess: 'Request access, correction, or deletion of your data.',
|
||
privacyRightsConsent: 'Withdraw consent for non-essential processing (we currently process only essentials).',
|
||
privacyRightsContact: 'Contact support for any privacy questions.',
|
||
searchLabel: 'Search',
|
||
searchPlaceholder: 'Search by name, description, or city',
|
||
cityFilter: 'City',
|
||
regionFilter: 'Region',
|
||
searchButton: 'Search',
|
||
searchAmenities: 'Amenities',
|
||
searchAvailability: 'Availability',
|
||
startDate: 'Start date',
|
||
endDate: 'End date',
|
||
availabilityOnlyWithCalendar: 'Only listings with a connected calendar are shown when filtering by dates.',
|
||
availableForDates: 'Available for selected dates',
|
||
notAvailableForDates: 'Unavailable for selected dates',
|
||
calendarConnected: 'Calendar connected',
|
||
clearFilters: 'Clear filters',
|
||
addressSearchLabel: 'Find listings near an address',
|
||
addressSearchPlaceholder: 'Street, city, or place',
|
||
locateAddress: 'Locate on map',
|
||
addressRadiusLabel: 'Within {km} km',
|
||
listingsFound: '{count} listings',
|
||
openListing: 'Open listing',
|
||
mapNoResults: 'No listings match your filters.',
|
||
addressNotFound: 'Address not found',
|
||
addressLookupFailed: 'Failed to locate address',
|
||
loadingMap: 'Loading map…',
|
||
latestListingsTitle: 'Latest listings',
|
||
latestListingsLead: 'Fresh rentals as they are published. Tap to browse.',
|
||
},
|
||
fi: {
|
||
brand: 'lomavuokraus.fi',
|
||
navProfile: 'Profiili',
|
||
navMyListings: 'Omat kohteet',
|
||
navNewListing: 'Luo kohde',
|
||
navApprovals: 'Tarkastettavat',
|
||
navUsers: 'Käyttäjät',
|
||
navLogout: 'Kirjaudu ulos',
|
||
navLogin: 'Kirjaudu',
|
||
navSignup: 'Rekisteröidy',
|
||
navBrowse: 'Selaa kohteita',
|
||
navLanguage: 'Kieli',
|
||
approvalsBadge: '{count}',
|
||
heroEyebrow: 'lomavuokraus.fi',
|
||
heroTitle: 'Löydä seuraava mökkilomasi',
|
||
heroBody: 'Selaa suomalaisten mökkien, huoneistojen ja villojen ilmoituksia suoraan omistajilta. Jokainen ilmoitus tarkistetaan ennen julkaisua, ja otat vuokranantajaan yhteyttä suoraan — yksinkertaista ja läpinäkyvää.',
|
||
languageTabsLabel: 'Ilmoituksen kielet',
|
||
languageTabsHint: 'Lisää käännökset kaikille tuetuille kielille',
|
||
localeSectionTitle: 'Ilmoituksen sisältö',
|
||
localeReady: 'Valmis',
|
||
localePartial: 'Kesken',
|
||
localeMissing: 'Puuttuu',
|
||
aiHelperTitle: 'AI-käännösapu',
|
||
aiHelperLead: 'Anna tekoälyn täydentää muut kielet puolestasi.',
|
||
aiOptionalHint: 'Valinnainen: AI-avustaja voi täyttää muut kielet.',
|
||
aiAutoExplain: 'Täytä tekstit yhdellä kielellä ja paina nappia; AI täyttää loput kielet.',
|
||
aiAutoTranslate: 'Käännä puuttuvat kielet',
|
||
aiAutoTranslating: 'Käännetään…',
|
||
aiAutoSuccess: 'Käännökset päivitetty AI:lla.',
|
||
aiAutoError: 'Käännös epäonnistui. Yritä uudelleen tai käytä manuaalitilaa alla.',
|
||
aiHelperNote: 'Käyttää OpenAI:ta puuttuvien tekstien kääntämiseen.',
|
||
aiManualLead: 'Jos automaattinen käännös ei toimi, kopioi prompti tekoälylle ja liitä JSON-vastaus alle.',
|
||
aiPromptLabel: 'Prompti tekoälylle',
|
||
aiCopyPrompt: 'Kopioi prompti',
|
||
aiPromptCopied: 'Kopioitu',
|
||
aiCopyError: 'Kopiointi epäonnistui',
|
||
aiResponseLabel: 'Liitä tekoälyn vastaus (JSON)',
|
||
aiApply: 'Käytä AI-vastausta',
|
||
aiApplyError: 'Vastausta ei voitu lukea. Varmista, että se on kelvollista JSONia ja sisältää locales-avaimen.',
|
||
aiApplySuccess: 'Käännökset päivitetty AI-vastauksesta.',
|
||
translationMissing: 'Täytä vähintään yhden kielen otsikko ja kuvaus.',
|
||
saveDraft: 'Tallenna luonnos',
|
||
missingFields: 'Puuttuu: {fields}',
|
||
ctaViewSample: 'Katso esimerkkikohde',
|
||
ctaHealth: 'Tarkista health-päätepiste',
|
||
ctaBrowse: 'Selaa kohteita',
|
||
highlightQualityTitle: 'Laadukkaat kohteet',
|
||
highlightQualityBody: 'Kuratoidut mökit ja huvilat, selkeät saatavuudet ja helppo varaus.',
|
||
highlightLocalTitle: 'Suomi edellä',
|
||
highlightLocalBody: 'Tehty Suomen olosuhteisiin, nopea sisällönjakelu Pohjoismaissa.',
|
||
highlightApiTitle: 'API-ystävällinen',
|
||
highlightApiBody: 'Strukturoitu data, jotta löydät kohteet kaikissa kanavissa.',
|
||
runtimeConfigTitle: 'Ajoaikainen konfiguraatio',
|
||
runtimeConfigLead: 'Ajo- ja rakennusaikaiset arvot, joita palvelu käyttää.',
|
||
runtimeAppEnv: 'APP_ENV',
|
||
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
||
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
|
||
aboutTitle: 'Tietoja lomavuokraus.fi:stä',
|
||
aboutLead: 'Keskittynyt suomalainen loma-asuntopalvelu: nopea selaus, selkeä moderointi ja mutkaton kokemus vuokraajalle.',
|
||
pricingTitle: 'Hinnasto',
|
||
pricingLead: 'Selkeä hinnoittelu vuokraajille ilman yllätyksiä.',
|
||
pricingMonthly: 'Kuukausi',
|
||
pricingAnnual: 'Vuosimaksu',
|
||
pricingPerMonth: 'kuukaudessa',
|
||
pricingPerYear: 'vuodessa (20 % säästö)',
|
||
pricingMonthlyBody: 'Aloita kuukausihinnalla 10 € per kohde.',
|
||
pricingAnnualBody: 'Maksa vuodessa 100 € per kohde ja pidä kulut ennustettavina.',
|
||
pricingPerListing: 'Hinta on per aktiivinen kohde. Voit perua milloin vain.',
|
||
pricingNotesTitle: 'Huomiot',
|
||
pricingNotesBody: 'Pidämme hinnoittelun yksinkertaisena samalla kun rakennamme uusia isäntätyökaluja, viestejä ja integraatioita. Kaikki nykyiset ominaisuudet sisältyvät.',
|
||
footerAbout: 'Tietoa',
|
||
footerPricing: 'Hinnasto',
|
||
footerPrivacy: 'Tietosuoja ja evästeet',
|
||
loginTitle: 'Kirjaudu sisään',
|
||
emailLabel: 'Sähköposti',
|
||
passwordLabel: 'Salasana',
|
||
loginButton: 'Kirjaudu',
|
||
loggingIn: 'Kirjaudutaan…',
|
||
loginSuccess: 'Kirjautuminen onnistui.',
|
||
registerTitle: 'Luo tili',
|
||
registerLead: 'Sähköpostin varmistus vaaditaan, ja ylläpitäjä hyväksyy tilin.',
|
||
nameOptional: 'Nimi (valinnainen)',
|
||
passwordHint: 'Salasana (väh. 8 merkkiä)',
|
||
registerButton: 'Rekisteröidy',
|
||
registering: 'Lähetetään…',
|
||
registerSuccess: 'Rekisteröinti onnistui. Tarkista sähköpostisi vahvistuslinkin vuoksi.',
|
||
forgotTitle: 'Unohditko salasanasi?',
|
||
forgotLead: 'Syötä sähköpostisi niin lähetämme palautuslinkin.',
|
||
forgotSubmit: 'Lähetä palautuslinkki',
|
||
forgotSuccess: 'Jos sähköposti löytyy, palautuslinkki on lähetetty.',
|
||
forgotError: 'Linkin lähetys epäonnistui.',
|
||
resetTitle: 'Vaihda salasana',
|
||
resetLead: 'Aseta uusi salasana tilillesi.',
|
||
resetSubmit: 'Aseta uusi salasana',
|
||
resetSuccess: 'Salasana vaihdettu. Voit nyt kirjautua sisään.',
|
||
resetError: 'Salasanan vaihto epäonnistui. Linkki voi olla vanhentunut.',
|
||
resetMissingToken: 'Palautustunniste puuttuu. Käytä sähköpostista saatua linkkiä.',
|
||
forgotCta: 'Unohdit salasanan?',
|
||
pendingAdminTitle: 'Ylläpito: tarkastettavat',
|
||
pendingUsersTitle: 'Odottavat käyttäjät',
|
||
pendingListingsTitle: 'Odottavat kohteet',
|
||
noPendingUsers: 'Ei odottavia käyttäjiä.',
|
||
noPendingListings: 'Ei odottavia kohteita.',
|
||
statusLabel: 'tila',
|
||
slugsLabel: 'Osoitepolut',
|
||
verifiedLabel: 'vahvistettu',
|
||
approve: 'Hyväksy',
|
||
approveAdmin: 'Hyväksy + admin',
|
||
reject: 'Hylkää',
|
||
remove: 'Poista käytöstä',
|
||
publish: 'Julkaise',
|
||
approvalsMessage: 'Kohde päivitetty',
|
||
userUpdated: 'Käyttäjä päivitetty',
|
||
adminRequired: 'Ylläpitäjän oikeudet vaaditaan',
|
||
adminUsersTitle: 'Ylläpito: käyttäjät',
|
||
adminUsersLead: 'Hallinnoi rooleja ja hyväksyntöjä.',
|
||
tableEmail: 'Sähköposti',
|
||
tableRole: 'Rooli',
|
||
tableStatus: 'Tila',
|
||
tableVerified: 'Vahvistettu',
|
||
tableApproved: 'Hyväksytty',
|
||
myListingsTitle: 'Omat kohteet',
|
||
createNewListing: 'Luo uusi kohde',
|
||
noListings: 'Ei kohteita vielä.',
|
||
createOne: 'Luo kohde',
|
||
loading: 'Ladataan…',
|
||
view: 'Näytä',
|
||
removing: 'Poistetaan…',
|
||
removed: 'Kohde poistettu näkyvistä',
|
||
removeConfirm: 'Poista kohde? Se piilotetaan muilta.',
|
||
myProfileTitle: 'Oma profiili',
|
||
notLoggedIn: 'Et ole kirjautunut',
|
||
profileEmail: 'Sähköposti',
|
||
profileName: 'Nimi',
|
||
profilePhone: 'Puhelin',
|
||
profileRole: 'Rooli',
|
||
profileStatus: 'Tila',
|
||
profileEmailVerified: 'Sähköposti vahvistettu',
|
||
profileApproved: 'Hyväksytty',
|
||
profileUpdated: 'Profiili päivitetty',
|
||
emailLocked: 'Sähköpostia ei voi vaihtaa',
|
||
save: 'Tallenna',
|
||
saving: 'Tallennetaan…',
|
||
yes: 'kyllä',
|
||
no: 'ei',
|
||
verifyTitle: 'Sähköpostin vahvistus',
|
||
verifyOk: 'Sähköposti vahvistettu. Ylläpitäjä hyväksyy tilin pian.',
|
||
verifyFail: 'Vahvistus epäonnistui tai token on virheellinen.',
|
||
listingLocation: 'Sijainti',
|
||
listingAddress: 'Osoite',
|
||
listingCapacity: 'Tilat',
|
||
listingPrices: 'Hinta',
|
||
listingAmenities: 'Varustelu',
|
||
listingNoAmenities: 'Varustelua ei ole listattu.',
|
||
listingContact: 'Yhteystiedot',
|
||
listingMoreInfo: 'Lisätietoja',
|
||
availabilityTitle: 'Saatavuuskalenteri',
|
||
availabilityLegendBooked: 'Varattu',
|
||
availabilityMissing: 'Kalenteria ei ole vielä yhdistetty.',
|
||
localeLabel: 'Kieli',
|
||
homeCrumb: 'Etusivu',
|
||
createListingTitle: 'Luo kohde',
|
||
loginToCreate: 'Kirjaudu ensin luodaksesi kohteen.',
|
||
slugLabel: 'Osoitepolku',
|
||
slugHelp: 'Keksi lyhyt ja helppo linkki; käytä pieniä kirjaimia ja väliviivoja (esim. saimaa-mokki).',
|
||
slugPreview: 'Ilmoituksen linkki on: {url}',
|
||
slugChecking: 'Tarkistetaan saatavuutta…',
|
||
slugAvailable: 'Slug on vapaa',
|
||
slugTaken: 'Slug on jo käytössä',
|
||
slugCheckError: 'Slugia ei voitu tarkistaa nyt',
|
||
localeInput: 'Kieli',
|
||
titleLabel: 'Otsikko',
|
||
descriptionLabel: 'Kuvaus',
|
||
teaserLabel: 'Tiivistelmä',
|
||
teaserHelp: 'Lyhyt ingressi kortteihin',
|
||
countryLabel: 'Maa',
|
||
regionLabel: 'Maakunta/alue',
|
||
cityLabel: 'Kunta/kaupunki',
|
||
contactNameLabel: 'Yhteyshenkilö',
|
||
contactEmailLabel: 'Yhteyssähköposti',
|
||
streetAddressLabel: 'Katuosoite',
|
||
addressNoteLabel: 'Saapumisohje (valinnainen)',
|
||
addressNotePlaceholder: 'Reittiohje, pysäköinti tai muut saapumisohjeet',
|
||
latitudeLabel: 'Leveysaste (valinnainen)',
|
||
longitudeLabel: 'Pituusaste (valinnainen)',
|
||
maxGuestsLabel: 'Vieraita enintään',
|
||
bedroomsLabel: 'Makuuhuoneita',
|
||
bedsLabel: 'Vuoteita',
|
||
bathroomsLabel: 'Kylpyhuoneita',
|
||
priceWeekdayLabel: 'Arkiyön hinta (€ / yö)',
|
||
priceWeekendLabel: 'Viikonlopun hinta (€ / yö)',
|
||
priceHintHelp: 'Aseta erilliset hinnat arki- ja viikonloppuyöille euroissa (valinnainen, ei sitova).',
|
||
priceWeekdayShort: '{price}€ arki',
|
||
priceWeekendShort: '{price}€ viikonloppu',
|
||
priceNotSet: 'Ei ilmoitettu',
|
||
calendarUrlsLabel: 'Saatavuuskalenterit (iCal-osoitteet, yksi per rivi)',
|
||
calendarUrlsHelp: 'Liitä iCal-linkit muilta alustoilta. Yhdistämme ne saatavuuden näyttämiseen.',
|
||
imagesLabel: 'Kuvat',
|
||
imagesHelp: 'Lataa enintään {count} kuvaa (max {sizeMb} Mt / kuva).',
|
||
imagesTooMany: 'Liikaa kuvia (enintään {count}).',
|
||
imagesTooLarge: 'Kuva on liian suuri (max {sizeMb} Mt).',
|
||
imagesReadFailed: 'Kuvien luku epäonnistui.',
|
||
imagesRequired: 'Lataa vähintään yksi kuva.',
|
||
coverImageLabel: 'Kansikuvan järjestys',
|
||
coverImageHelp: 'Monettako ladatuista kuvista käytetään kansikuvana (oletus 1)',
|
||
coverChoice: 'Kansipaikka {index}',
|
||
submitListing: 'Luo kohde',
|
||
submittingListing: 'Lähetetään…',
|
||
createListingSuccess: 'Kohde luotu id:llä {id} (tila: {status})',
|
||
approvalsCountLabel: 'Tarkastettavat',
|
||
approvalsPending: '{count} odottaa',
|
||
amenitySauna: 'Sauna',
|
||
amenityFireplace: 'Takka',
|
||
amenityWifi: 'Wi-Fi',
|
||
amenityPets: 'Lemmikit sallittu',
|
||
amenityLake: 'Järven rannalla',
|
||
amenityAirConditioning: 'Ilmastointi',
|
||
amenityKitchen: 'Keittiö',
|
||
amenityDishwasher: 'Astianpesukone',
|
||
amenityWashingMachine: 'Pyykinpesukone',
|
||
amenityBarbecue: 'Grilli',
|
||
amenityMicrowave: 'Mikroaaltouuni',
|
||
amenityFreeParking: 'Maksuton pysäköinti',
|
||
amenityEvAvailable: 'Sähköauton lataus lähellä',
|
||
amenitySkiPass: 'Hissilippu sisältyy',
|
||
evChargingLabel: 'Sähköauton lataus lähellä',
|
||
evChargingYes: 'Latausta lähellä',
|
||
evChargingNo: 'Ei latausta lähellä',
|
||
evChargingAny: 'Kaikki',
|
||
evChargingExplain: 'Onko kohteessa tai lähistöllä sähköauton latausmahdollisuus?',
|
||
capacityGuests: '{count} vierasta',
|
||
capacityBedrooms: '{count} makuuhuonetta',
|
||
capacityBeds: '{count} vuodetta',
|
||
capacityBathrooms: '{count} kylpyhuonetta',
|
||
capacityUnknown: 'Kapasiteettia ei ole asetettu',
|
||
browseListingsTitle: 'Selaa kohteita',
|
||
browseListingsLead: 'Hae julkaistuja kohteita, rajaa sijainnilla ja tutki kartalla.',
|
||
sampleBadge: 'Mallikohde',
|
||
privacyTitle: 'Tietosuoja ja evästeet',
|
||
privacyUpdated: 'Päivitetty: {date}',
|
||
privacyCollectTitle: 'Mitä tietoja keräämme',
|
||
privacyCollectAccounts: 'Käyttäjätiedot: sähköposti, salasanatiiviste, nimi, puhelin (valinnainen), rooli/tila.',
|
||
privacyCollectListings: 'Kohdetiedot: sijainti, yhteystiedot, varustelu, kuvat, kieliversiot.',
|
||
privacyCollectLogs: 'Lokit: rajattu pyyntömeta diagnostiikkaan.',
|
||
privacyUseTitle: 'Miten käytämme tietoja',
|
||
privacyUseAuth: 'Kirjautumiseen ja sessioiden hallintaan.',
|
||
privacyUseListings: 'Kohteiden julkaisuun ja moderointiin.',
|
||
privacyUseMail: 'Transaktiosähköposteihin (vahvistus, salasanan palautus).',
|
||
privacyUseLegal: 'Lakivelvoitteiden täyttämiseen ja palvelun suojaamiseen.',
|
||
privacyStoreTitle: 'Säilytys',
|
||
privacyStoreDb: 'Tiedot tallennetaan EU:ssa olevaan Postgres-tietokantaan.',
|
||
privacyStoreBackups: 'Varmuuskopioita säilytetään palautusta varten; poistetut tiedot voivat esiintyä varmuuskopioissa rajatun ajan.',
|
||
privacyCookiesTitle: 'Evästeet',
|
||
privacyCookiesSession: 'Käytämme vain välttämättömiä evästeitä: kirjautumissessio.',
|
||
privacyCookiesNoTracking: 'Ei analytiikka-, mainos- tai seurantateknologioita.',
|
||
privacySharingTitle: 'Tietojen jakaminen',
|
||
privacySharingAds: 'Emme myy tai jaa henkilötietoja mainostajille.',
|
||
privacySharingOps: 'Palveluntarjoajille vain palvelun pyörittämiseen (sähköposti, hosting).',
|
||
privacyRightsTitle: 'Oikeutesi',
|
||
privacyRightsAccess: 'Voit pyytää tietojen nähtävyyttä, oikaisua tai poistoa.',
|
||
privacyRightsConsent: 'Voit perua suostumuksen ei-välttämättömästä käsittelystä (tällä hetkellä vain välttämättömät).',
|
||
privacyRightsContact: 'Ota yhteyttä, jos sinulla on kysyttävää tietosuojasta.',
|
||
searchLabel: 'Haku',
|
||
searchPlaceholder: 'Hae nimellä, kuvauksella tai paikkakunnalla',
|
||
cityFilter: 'Kaupunki/kunta',
|
||
regionFilter: 'Maakunta/alue',
|
||
searchButton: 'Hae',
|
||
searchAmenities: 'Varustelu',
|
||
searchAvailability: 'Saatavuus',
|
||
startDate: 'Alkupäivä',
|
||
endDate: 'Loppupäivä',
|
||
availabilityOnlyWithCalendar: 'Päiväsuodatus näyttää vain kohteet, joissa on kalenteri yhdistettynä.',
|
||
availableForDates: 'Vapaa valituille päiville',
|
||
notAvailableForDates: 'Ei vapaana valituille päiville',
|
||
calendarConnected: 'Kalenteri yhdistetty',
|
||
clearFilters: 'Tyhjennä suodattimet',
|
||
addressSearchLabel: 'Etsi kohteita osoitteen läheltä',
|
||
addressSearchPlaceholder: 'Katu, kaupunki tai paikka',
|
||
locateAddress: 'Paikanna kartalle',
|
||
addressRadiusLabel: '{km} km säteellä',
|
||
listingsFound: '{count} kohdetta',
|
||
openListing: 'Avaa kohde',
|
||
mapNoResults: 'Suodattimilla ei löytynyt kohteita.',
|
||
evChargingAny: 'Kaikki',
|
||
addressNotFound: 'Osoitetta ei löydy',
|
||
addressLookupFailed: 'Paikannus epäonnistui',
|
||
loadingMap: 'Ladataan karttaa…',
|
||
latestListingsTitle: 'Viimeisimmät kohteet',
|
||
latestListingsLead: 'Tuoreimmat kohteet heti julkaisun jälkeen.',
|
||
},
|
||
} as const;
|
||
|
||
const svMessages: Record<keyof typeof baseMessages.en, string> = {
|
||
...baseMessages.en,
|
||
navProfile: 'Profil',
|
||
navMyListings: 'Mina annonser',
|
||
navNewListing: 'Ny annons',
|
||
navApprovals: 'Godkännanden',
|
||
navUsers: 'Användare',
|
||
navLogout: 'Logga ut',
|
||
navLogin: 'Logga in',
|
||
navSignup: 'Registrera dig',
|
||
navBrowse: 'Bläddra bland annonser',
|
||
navLanguage: 'Språk',
|
||
slugHelp: 'Hitta på en kort och enkel länk; använd små bokstäver och bindestreck (t.ex. sjo-stuga).',
|
||
slugPreview: 'Länk till annonsen: {url}',
|
||
heroTitle: 'Hitta ditt nästa finska getaway',
|
||
heroBody: 'Upptäck stugor, lägenheter och villor direkt från ägarna. Annonser verifieras innan publicering och du kontaktar värdarna direkt — enkelt och transparent.',
|
||
ctaBrowse: 'Bläddra bland annonser',
|
||
createListingTitle: 'Skapa annons',
|
||
languageTabsLabel: 'Annonsens språk',
|
||
languageTabsHint: 'Lägg till översättningar för varje språk',
|
||
localeSectionTitle: 'Annonsinnehåll',
|
||
localeReady: 'Klar',
|
||
localePartial: 'Pågår',
|
||
localeMissing: 'Saknas',
|
||
aiHelperTitle: 'AI-översättningshjälp',
|
||
aiHelperLead: 'Låt AI fylla i de andra språken åt dig.',
|
||
aiOptionalHint: 'Valfritt: AI-hjälpen kan fylla i andra språk.',
|
||
aiAutoExplain: 'Fyll texterna på ett språk och klicka på knappen så fyller AI i resten.',
|
||
aiAutoTranslate: 'Översätt saknade språk',
|
||
aiAutoTranslating: 'Översätter…',
|
||
aiAutoSuccess: 'Översättningar uppdaterades med AI.',
|
||
aiAutoError: 'AI-översättning misslyckades. Försök igen eller använd manuellt läge nedan.',
|
||
aiHelperNote: 'Använder OpenAI för att översätta saknade texter.',
|
||
aiManualLead: 'Om autokorrespondens misslyckas, kopiera prompten till din AI och klistra in JSON-svaret nedan.',
|
||
aiPromptLabel: 'Prompt till AI',
|
||
aiCopyPrompt: 'Kopiera prompt',
|
||
aiPromptCopied: 'Kopierad',
|
||
aiCopyError: 'Kopiering misslyckades',
|
||
aiResponseLabel: 'Klistra in AI-svar (JSON)',
|
||
aiApply: 'Använd AI-svar',
|
||
aiApplyError: 'Kunde inte läsa AI-svaret. Se till att det är giltig JSON med locales-nyckel.',
|
||
aiApplySuccess: 'Översättningar uppdaterades från AI-svaret.',
|
||
translationMissing: 'Lägg till minst ett språk med titel och beskrivning.',
|
||
saveDraft: 'Spara utkast',
|
||
missingFields: 'Saknas: {fields}',
|
||
slugChecking: 'Kontrollerar tillgänglighet…',
|
||
slugAvailable: 'Sluggen är ledig',
|
||
slugTaken: 'Sluggen används redan',
|
||
slugCheckError: 'Kunde inte kontrollera sluggen nu',
|
||
teaserHelp: 'Kort ingress som syns i korten',
|
||
priceWeekdayLabel: 'Vardagspris (€ / natt)',
|
||
priceWeekendLabel: 'Helgpris (€ / natt)',
|
||
priceHintHelp: 'Ange separata priser för vardag och helg i euro per natt (frivilligt).',
|
||
priceWeekdayShort: '{price}€ vardag',
|
||
priceWeekendShort: '{price}€ helg',
|
||
priceNotSet: 'Ej angivet',
|
||
listingPrices: 'Priser',
|
||
capacityUnknown: 'Kapacitet ej angiven',
|
||
amenityEvAvailable: 'EV-laddning i närheten',
|
||
amenitySkiPass: 'Liftkort ingår',
|
||
amenityMicrowave: 'Mikrovågsugn',
|
||
amenityFreeParking: 'Gratis parkering',
|
||
evChargingLabel: 'EV-laddning i närheten',
|
||
evChargingYes: 'Laddning i närheten',
|
||
evChargingNo: 'Ingen laddning i närheten',
|
||
evChargingAny: 'Alla',
|
||
evChargingExplain: 'Finns det EV-laddning på plats eller i närheten?',
|
||
};
|
||
|
||
export const messages = { ...baseMessages, sv: svMessages } as const;
|
||
export type MessageKey = keyof typeof baseMessages.en;
|
||
|
||
function normalizeLocale(input?: string | null): Locale | null {
|
||
if (!input) return null;
|
||
const lower = input.toLowerCase();
|
||
if (lower.startsWith('fi')) return 'fi';
|
||
if (lower.startsWith('en')) return 'en';
|
||
if (lower.startsWith('sv')) return 'sv';
|
||
return null;
|
||
}
|
||
|
||
export function resolveLocale(opts: { cookieLocale?: string | null; acceptLanguage?: string | null }): Locale {
|
||
const fromCookie = normalizeLocale(opts.cookieLocale);
|
||
if (fromCookie) return fromCookie;
|
||
const fromHeader = normalizeLocale(opts.acceptLanguage);
|
||
if (fromHeader) return fromHeader;
|
||
return 'en';
|
||
}
|
||
|
||
export function t(locale: Locale, key: MessageKey, vars?: Record<string, string | number>) {
|
||
const table = messages[locale] ?? messages.en;
|
||
const template = String(table[key] ?? messages.en[key]);
|
||
if (!vars) return template;
|
||
return Object.entries(vars).reduce<string>((acc, [k, v]) => acc.replace(`{${k}}`, String(v)), template);
|
||
}
|