392 lines
16 KiB
TypeScript
392 lines
16 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.',
|
|
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 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.',
|
|
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 (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);
|
|
}
|