lomavuokraus/lib/i18n.ts
2025-11-24 17:15:20 +02:00

364 lines
14 KiB
TypeScript

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.',
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',
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',
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 hint (cents)',
imagesLabel: 'Images (one URL per line, max 10)',
coverImageLabel: 'Cover image line number',
coverImageHelp: 'Which image line should be shown as the cover in listings (defaults to 1)',
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.',
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.',
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',
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',
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 (senttiä)',
imagesLabel: 'Kuvat (yksi URL per rivi, max 10)',
coverImageLabel: 'Kansikuvan rivinumero',
coverImageHelp: 'Mikä kuvista näytetään kansikuvana listauksissa (oletus 1)',
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.',
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<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);
}