fix(i18n): prepare for Lokalise

This commit is contained in:
Michael Zetterberg
2025-01-03 14:54:46 +01:00
parent cbc17e2c5b
commit d2ce9c0d7c
120 changed files with 1703 additions and 1042 deletions

View File

@@ -25,7 +25,7 @@ export default async function IntroSection({
const { streetAddress, city } = address
const { distanceToCentre } = location
const formattedDistanceText = intl.formatMessage(
{ id: "Distance in km to city centre" },
{ id: "{number} km to city centre" },
{ number: getSingleDecimal(distanceToCentre / 1000) }
)
const lang = getLang()
@@ -37,7 +37,7 @@ export default async function IntroSection({
)
const formattedTripAdvisorText = hasTripAdvisorData
? intl.formatMessage(
{ id: "Tripadvisor reviews" },
{ id: "{rating} ({count} reviews on Tripadvisor)" },
{ rating: tripAdvisor.rating, count: tripAdvisor.numberOfReviews }
)
: ""

View File

@@ -83,6 +83,9 @@ export default function Sidebar({
}, 200)
}
const viewAsMapMsg = intl.formatMessage({ id: "View as map" })
const viewAsListMsg = intl.formatMessage({ id: "View as list" })
return (
<>
<aside
@@ -98,17 +101,13 @@ export default function Sidebar({
onClick={toggleFullScreenSidebar}
>
<Body textTransform="bold" color="textMediumContrast" asChild>
<span>
{intl.formatMessage({
id: isFullScreenSidebar ? "View as map" : "View as list",
})}
</span>
<span>{isFullScreenSidebar ? viewAsMapMsg : viewAsListMsg}</span>
</Body>
</Button>
<div className={styles.sidebarContent}>
<Title as="h4" level="h2" textTransform="regular">
{intl.formatMessage(
{ id: "Things nearby HOTEL_NAME" },
{ id: "Things nearby {hotelName}" },
{ hotelName }
)}
</Title>
@@ -138,7 +137,14 @@ export default function Sidebar({
}
>
<span>{poi.name}</span>
<span>{poi.distance} km</span>
<span>
{intl.formatMessage(
{ id: "{distanceInKm} km" },
{
distanceInKm: poi.distance,
}
)}
</span>
</button>
</li>
))}

View File

@@ -105,7 +105,7 @@ export default function DynamicMap({
className={styles.dynamicMap}
style={{ "--hotel-map-height": mapHeight } as React.CSSProperties}
aria-label={intl.formatMessage(
{ id: "Things nearby HOTEL_NAME" },
{ id: "Things nearby {hotelName}" },
{ hotelName }
)}
>

View File

@@ -52,7 +52,12 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
size={20}
/>
<Body color="black">{poi.name}</Body>
<Caption>{poi.distance} km</Caption>
<Caption>
{intl.formatMessage(
{ id: "{distanceInKm} km" },
{ distanceInKm: poi.distance }
)}
</Caption>
</li>
))}
</ul>

View File

@@ -33,7 +33,10 @@ export default async function StaticMap({
width={380}
height={mapHeight}
zoomLevel={zoomLevel}
altText={intl.formatMessage({ id: "Map of HOTEL_NAME" }, { hotelName })}
altText={intl.formatMessage(
{ id: "Map of {hotelName}" },
{ hotelName }
)}
mapId={mapId}
/>
<ScandicMarker

View File

@@ -46,8 +46,8 @@ export default function PreviewImages({
<Lightbox
images={images}
dialogTitle={intl.formatMessage(
{ id: "Image gallery" },
{ name: hotelName }
{ id: "{title} - Image gallery" },
{ title: hotelName }
)}
isOpen={lightboxIsOpen}
onClose={() => setLightboxIsOpen(false)}

View File

@@ -29,7 +29,10 @@ export function RoomCard({ room }: RoomCardProps) {
<div className={styles.imageContainer}>
<ImageGallery
images={images}
title={intl.formatMessage({ id: "Image gallery" }, { name })}
title={intl.formatMessage(
{ id: "{title} - Image gallery" },
{ title: name }
)}
height={200}
/>
</div>
@@ -45,7 +48,9 @@ export function RoomCard({ room }: RoomCardProps) {
</Subtitle>
<Body color="grey">
{intl.formatMessage(
{ id: "hotelPages.rooms.roomCard.persons" },
{
id: "{size} ({max, plural, one {{range} person} other {{range} persons}})",
},
{
size,
max: totalOccupancy.max,

View File

@@ -54,8 +54,12 @@ export function Rooms({ rooms }: RoomsProps) {
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={allRoomsVisible}
textShowMore="Show more rooms"
textShowLess="Show less rooms"
textShowMore={intl.formatMessage({
id: "Show more rooms",
})}
textShowLess={intl.formatMessage({
id: "Show less rooms",
})}
/>
) : null}
</SectionContainer>

View File

@@ -49,7 +49,7 @@ export default async function ContactInformation({
color="peach80"
textDecoration="underline"
>
Google Maps
{intl.formatMessage({ id: "Google Maps" })}
</Link>
</div>
<div className={styles.contact}>

View File

@@ -18,8 +18,18 @@ export default async function CheckInAmenity({
trackingId="amenities:check-in"
>
<Body textTransform="bold">{intl.formatMessage({ id: "Times" })}</Body>
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check in from" })}: ${checkInTime}`}</Body>
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check out at latest" })}: ${checkOutTime}`}</Body>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Check in from: {checkInTime}" },
{ checkInTime }
)}
</Body>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Check out at latest: {checkOutTime}" },
{ checkOutTime }
)}
</Body>
</AccordionItem>
)
}

View File

@@ -13,22 +13,34 @@ export default async function ParkingList({
address,
}: ParkingListProps) {
const intl = await getIntl()
const canMakeReservationYesMsg = intl.formatMessage({
id: "Parking can be reserved in advance: Yes",
})
const canMakeReservationNoMsg = intl.formatMessage({
id: "Parking can be reserved in advance: No",
})
return (
<Body color="uiTextHighContrast" asChild>
<ul className={styles.listStyling}>
{numberOfChargingSpaces ? (
<li>
{intl.formatMessage(
{ id: "Number of charging points for electric cars" },
{ id: "Number of charging points for electric cars: {number}" },
{ number: numberOfChargingSpaces }
)}
</li>
) : null}
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
<li>
{canMakeReservation
? canMakeReservationYesMsg
: canMakeReservationNoMsg}
</li>
{numberOfParkingSpots ? (
<li>
{intl.formatMessage(
{ id: "Number of parking spots" },
{ id: "Number of parking spots: {number}" },
{ number: numberOfParkingSpots }
)}
</li>
@@ -36,13 +48,15 @@ export default async function ParkingList({
{distanceToHotel ? (
<li>
{intl.formatMessage(
{ id: "Distance to hotel" },
{ distance: distanceToHotel }
{ id: "Distance to hotel: {distanceInM} m" },
{ distanceInM: distanceToHotel }
)}
</li>
) : null}
{address ? (
<li>{`${intl.formatMessage({ id: "Address" })}: ${address}`}</li>
<li>
{intl.formatMessage({ id: "Address: {address}" }, { address })}
</li>
) : null}
</ul>
</Body>

View File

@@ -47,7 +47,7 @@ export default async function ParkingPrices({
? freeParking
? intl.formatMessage({ id: "Free parking" })
: formatPrice(intl, parking.amount, currency)
: "N/A"}
: intl.formatMessage({ id: "N/A" })}
</Body>
</div>
{parking.startTime &&
@@ -58,7 +58,13 @@ export default async function ParkingPrices({
{intl.formatMessage({ id: "From" })}
</Body>
<Body color="uiTextHighContrast">
{parking.startTime}-{parking.endTime}
{intl.formatMessage(
{ id: "{parkingStartTime}-{parkingEndTime}" },
{
parkingStartTime: parking.startTime,
parkingEndTime: parking.endTime,
}
)}
</Body>
</div>
)}

View File

@@ -29,14 +29,14 @@ export async function getRoomText(roomSizes: number[]) {
let roomText
if (largestRoom === smallestRoom) {
roomText = intl.formatMessage(
{ id: "{number} square meters" },
{ number: largestRoom }
{ id: "{value} square meters" },
{ value: largestRoom }
)
} else if (smallestRoom != null && largestRoom) {
{
roomText = intl.formatMessage(
{
id: "{number} to {number} square meters",
id: "{smallest} to {largest} square meters",
},
{ largest: largestRoom, smallest: smallestRoom }
)
@@ -53,14 +53,14 @@ export async function getSeatingText(roomSeating: number[]) {
let seatingText
if (biggestSeating === smallestSeating) {
seatingText = intl.formatMessage(
{ id: "{number} persons" },
{ id: "{value} persons" },
{ number: biggestSeating }
)
} else if (smallestSeating != null && biggestSeating) {
{
seatingText = intl.formatMessage(
{
id: "{number} to {number} persons",
id: "{lowest} to {highest} persons",
},
{ highest: biggestSeating, lowest: smallestSeating }
)

View File

@@ -5,8 +5,6 @@ import { getIntl } from "@/i18n"
import styles from "./openingHours.module.css"
import type { OpeningHoursProps } from "@/types/components/hotelPage/sidepeek/openingHours"
import { DaysEnum } from "@/types/components/hotelPage/sidepeek/restaurantBar"
import type { RestaurantOpeningHoursDay } from "@/types/hotel"
export default async function OpeningHours({
openingHours,
@@ -16,45 +14,83 @@ export default async function OpeningHours({
}: OpeningHoursProps) {
const intl = await getIntl()
const closed = intl.formatMessage({ id: "Closed" })
const alwaysOpen = intl.formatMessage({ id: "Always open" })
const closedMsg = intl.formatMessage({ id: "Closed" })
const alwaysOpenMsg = intl.formatMessage({ id: "Always open" })
const days: (keyof typeof openingHours)[] = [
DaysEnum.Monday,
DaysEnum.Tuesday,
DaysEnum.Wednesday,
DaysEnum.Thursday,
DaysEnum.Friday,
DaysEnum.Saturday,
DaysEnum.Sunday,
]
// In order
const weekdayDefinitions = [
{
key: "monday",
label: intl.formatMessage({ id: "monday" }),
},
{
key: "tuesday",
label: intl.formatMessage({ id: "tuesday" }),
},
{
key: "wednesday",
label: intl.formatMessage({ id: "wednesday" }),
},
{
key: "thursday",
label: intl.formatMessage({ id: "thursday" }),
},
{
key: "friday",
label: intl.formatMessage({ id: "friday" }),
},
{
key: "saturday",
label: intl.formatMessage({ id: "saturday" }),
},
{
key: "sunday",
label: intl.formatMessage({ id: "sunday" }),
},
] as const
const groupedOpeningHours: { [key: string]: string[] } = {}
const groupedOpeningHours: string[] = []
days.forEach((day) => {
const today = openingHours[day] as RestaurantOpeningHoursDay
let key: string
let rangeWeekdays: string[] = []
let rangeValue = ""
for (let i = 0, n = weekdayDefinitions.length; i < n; ++i) {
const weekdayDefinition = weekdayDefinitions[i]
const weekday = openingHours[weekdayDefinition.key]
const label = weekdayDefinition.label
if (weekday) {
let newValue = null
if (today.isClosed) {
key = closed
} else if (today.alwaysOpen) {
key = alwaysOpen
} else {
key = `${today.openingTime}-${today.closingTime}`
if (weekday.alwaysOpen) {
newValue = alwaysOpenMsg
} else if (weekday.isClosed) {
newValue = closedMsg
} else if (weekday.openingTime && weekday.closingTime) {
newValue = `${weekday.openingTime}-${weekday.closingTime}`
}
if (newValue !== null) {
if (rangeValue === newValue) {
if (rangeWeekdays.length > 1) {
rangeWeekdays.splice(-1, 1, label) // Replace last element
} else {
rangeWeekdays.push(label)
}
} else {
if (rangeValue) {
groupedOpeningHours.push(
`${rangeWeekdays.join("-")}: ${rangeValue}`
)
}
rangeValue = newValue
rangeWeekdays = [label]
}
}
if (rangeValue && i === n - 1) {
// Flush everything at the end
groupedOpeningHours.push(`${rangeWeekdays.join("-")}: ${rangeValue}`)
}
}
if (!groupedOpeningHours[key]) {
groupedOpeningHours[key] = []
}
const formattedDay = day.charAt(0).toUpperCase() + day.slice(1)
groupedOpeningHours[key].push(intl.formatMessage({ id: formattedDay }))
})
function formatDayInterval(days: string[]) {
if (days.length === 1) {
return days[0]
}
return `${days[0]}-${days[days.length - 1]}`
}
return (
@@ -62,10 +98,10 @@ export default async function OpeningHours({
<Body textTransform="bold" asChild>
<h5>{heading ?? openingHours.name}</h5>
</Body>
{Object.entries(groupedOpeningHours).map(([time, days]) => {
{groupedOpeningHours.map((groupedOpeningHour) => {
return (
<Body color="uiTextHighContrast" key={time}>
{`${formatDayInterval(days)}: ${time}`}
<Body color="uiTextHighContrast" key={groupedOpeningHour}>
{groupedOpeningHour}
</Body>
)
})}

View File

@@ -98,7 +98,7 @@ export default async function RestaurantBarItem({
) : null}
{ctaUrl ? (
<ButtonLink fullWidth theme="base" intent="secondary" href={ctaUrl}>
{`${intl.formatMessage({ id: "Discover" })} ${name}`}
{intl.formatMessage({ id: "Discover {name}" }, { name })}
</ButtonLink>
) : null}
</div>

View File

@@ -28,13 +28,27 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
<div className={styles.innerContent}>
<Body color="baseTextMediumContrast">
{roomSize.min === roomSize.max
? roomSize.min
: `${roomSize.min} - ${roomSize.max}`}
m².{" "}
{intl.formatMessage(
{ id: "booking.accommodatesUpTo" },
{ range: totalOccupancy.range, max: totalOccupancy.max }
)}
? intl.formatMessage(
{
id: "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}",
},
{
roomSize: roomSize.min,
max: totalOccupancy.max,
range: totalOccupancy.range,
}
)
: intl.formatMessage(
{
id: "{roomSizeMin} - {roomSizeMax} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}",
},
{
roomSizeMin: roomSize.min,
roomSizeMax: roomSize.max,
max: totalOccupancy.max,
range: totalOccupancy.range,
}
)}
</Body>
<div className={styles.imageContainer}>
<ImageGallery images={images} title={room.name} height={280} />
@@ -44,9 +58,7 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
<div className={styles.innerContent}>
<Subtitle type="two" color="uiTextHighContrast" asChild>
<h3>
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
</h3>
<h3>{intl.formatMessage({ id: "This room is equipped with" })}</h3>
</Subtitle>
<ul className={styles.facilityList}>
{room.roomFacilities
@@ -77,10 +89,10 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
<div className={styles.innerContent}>
<Subtitle type="two" color="uiTextHighContrast" asChild>
<h3>{intl.formatMessage({ id: "booking.bedOptions" })}</h3>
<h3>{intl.formatMessage({ id: "Bed options" })}</h3>
</Subtitle>
<Body color="grey">
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
{intl.formatMessage({ id: "Based on availability" })}
</Body>
<ul className={styles.bedOptions}>
{room.roomTypes.map((roomType) => {
@@ -107,7 +119,7 @@ export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
<div className={styles.buttonContainer}>
<Button fullWidth theme="base" intent="primary" asChild>
<Link href={ctaUrl}>
{intl.formatMessage({ id: "booking.selectRoom" })}
{intl.formatMessage({ id: "Select room" })}
</Link>
</Button>
</div>

View File

@@ -27,7 +27,7 @@ export default async function Facility({ data }: FacilityProps) {
)}
<div className={styles.information}>
<Subtitle color="burgundy" asChild type="one">
<Title level="h3">{intl.formatMessage({ id: `${data.type}` })}</Title>
<Title level="h3">{intl.formatMessage({ id: data.type })}</Title>
</Subtitle>
<div>
<Subtitle type="two" color="uiTextHighContrast">
@@ -36,13 +36,25 @@ export default async function Facility({ data }: FacilityProps) {
<div className={styles.openingHours}>
<Body color="uiTextHighContrast">
{ordinaryOpeningTimes.alwaysOpen
? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}`
: `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`}
? intl.formatMessage({ id: "Mon-Fri Always open" })
: intl.formatMessage(
{ id: "Mon-Fri {openingTime}-${closingTime}" },
{
openingTime: ordinaryOpeningTimes.openingTime,
closingTime: ordinaryOpeningTimes.closingTime,
}
)}
</Body>
<Body color="uiTextHighContrast">
{weekendOpeningTimes.alwaysOpen
? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}`
: `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`}
? intl.formatMessage({ id: "Sat-Sun Always open" })
: intl.formatMessage(
{ id: "Sat-Sun {openingTime}-${closingTime}" },
{
openingTime: weekendOpeningTimes.openingTime,
closingTime: weekendOpeningTimes.closingTime,
}
)}
</Body>
</div>
</div>

View File

@@ -39,7 +39,10 @@ export default function TabNavigation({
hash: HotelHashValues.overview,
text: intl.formatMessage({ id: "Overview" }),
},
{ hash: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) },
{
hash: HotelHashValues.rooms,
text: intl.formatMessage({ id: "Rooms" }),
},
{
hash: HotelHashValues.restaurant,
text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }),