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

@@ -5,11 +5,13 @@ import { overview } from "@/constants/routes/myPages"
import { getProfile } from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
export default async function ProtectedLayout({
children,
}: React.PropsWithChildren) {
const intl = await getIntl()
const session = await auth()
/**
* Fallback to make sure every route nested in the
@@ -54,7 +56,7 @@ export default async function ProtectedLayout({
console.error(`[layout:protected] unhandled user loading error`)
break
}
return <p>Something went wrong!</p>
return <p>{intl.formatMessage({ id: "Something went wrong!" })}</p>
}
if (!user) {

View File

@@ -1,12 +1,16 @@
"use client"
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
import { useIntl } from "react-intl"
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
const intl = useIntl()
useEffect(() => {
if (!error) return
@@ -17,7 +21,12 @@ export default function Error({
return (
<p>
<strong>
Breadcrumbs failed for this page ({error.digest}@{Date.now()})
{intl.formatMessage(
{ id: "Breadcrumbs failed for this page ({errorId})" },
{
errorId: `${error.digest}@${Date.now()}`,
}
)}
</strong>
</p>
)

View File

@@ -2,12 +2,15 @@
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
import { useIntl } from "react-intl"
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
const intl = useIntl()
useEffect(() => {
if (!error) return
@@ -18,7 +21,12 @@ export default function Error({
return (
<p>
<strong>
Error loading this page ({error.digest}@{Date.now()})
{intl.formatMessage(
{ id: "Error loading this page ({errorId})" },
{
errorId: `${error.digest}@${Date.now()}`,
}
)}
</strong>
</p>
)

View File

@@ -30,12 +30,43 @@ export default async function MembershipCardSlot({
membershipCards.map((card, idx) => (
<div className={styles.card} key={idx}>
<Subtitle className={styles.subTitle}>
Name: {card.membershipType}
{intl.formatMessage(
{ id: "Name: {cardMembershipType}" },
{
cardMembershipType: card.membershipType,
}
)}
</Subtitle>
<span> Current Points: {card.currentPoints} </span>
<span> Member Since: {card.memberSince}</span>
<span> Number: {card.membershipNumber}</span>
<span>Expiration Date: {card.expirationDate.split("T")[0]}</span>
<span>
{intl.formatMessage(
{ id: "Current Points {points, number}" },
{ points: card.currentPoints }
)}
</span>
<span>
{intl.formatMessage(
{ id: "Member Since: {value}" },
{
value: card.memberSince,
}
)}
</span>
<span>
{intl.formatMessage(
{ id: "Number: {membershipNumber}" },
{
membershipNumber: card.membershipNumber,
}
)}
</span>
<span>
{intl.formatMessage(
{ id: "Expiration Date: {expirationDate}" },
{
expirationDate: card.expirationDate.split("T")[0],
}
)}
</span>
</div>
))}
<Link href="#" variant="icon">

View File

@@ -2,10 +2,13 @@
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import type { ErrorPage } from "@/types/next/error"
export default function ProfileError({ error }: ErrorPage) {
const intl = useIntl()
useEffect(() => {
if (!error) return
@@ -13,5 +16,5 @@ export default function ProfileError({ error }: ErrorPage) {
Sentry.captureException(error)
}, [error])
return <h1>Error happened, Profile</h1>
return <h1>{intl.formatMessage({ id: "Error happened, Profile" })}</h1>
}

View File

@@ -30,6 +30,24 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
return null
}
const addressParts = []
if (user.address.streetAddress) {
addressParts.push(user.address.streetAddress)
}
if (user.address.city) {
addressParts.push(user.address.city)
}
if (user.address.country) {
addressParts.push(user.address.country)
}
const addressOutput =
addressParts.length > 0
? addressParts.join(", ")
: intl.formatMessage({ id: "N/A" })
const defaultLanguage = languages[params.lang]
const language = languageSelect.find((l) => l.value === user.language)
return (
@@ -90,18 +108,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
<Body color="burgundy" textTransform="bold">
{intl.formatMessage({ id: "Address" })}
</Body>
<Body color="burgundy">
{user.address.streetAddress
? `${user.address.streetAddress}, `
: ""}
{user.address.city ? `${user.address.city}, ` : ""}
{user.address.country ? `${user.address.country}` : ""}
{!user.address.streetAddress &&
!user.address.city &&
!user.address.country
? "N/A"
: null}
</Body>
<Body color="burgundy">{addressOutput}</Body>
</div>
<div className={styles.item}>
<LockIcon color="burgundy" />

View File

@@ -1,19 +1,13 @@
"use client" // Error components must be Client Components
"use client"
import * as Sentry from "@sentry/nextjs"
import {
useParams,
usePathname,
useRouter,
useSearchParams,
} from "next/navigation"
import { useParams, useRouter, useSearchParams } from "next/navigation"
import { startTransition, useEffect, useRef } from "react"
import { useIntl } from "react-intl"
import { login } from "@/constants/routes/handleAuth"
import { SESSION_EXPIRED } from "@/server/errors/trpc"
import { findLang } from "@/utils/languages"
import styles from "./error.module.css"
import type { LangParams } from "@/types/params"
@@ -63,13 +57,10 @@ export default function Error({
currentSearchParamsRef.current = currentSearchParams
}, [searchParams, reset, router])
const pathname = usePathname()
const lang = findLang(pathname)
return (
<section className={styles.layout}>
<div className={styles.content}>
{lang}: {intl.formatMessage({ id: "Something went wrong!" })}
{intl.formatMessage({ id: "Something went wrong!" })}
</div>
</section>
)

View File

@@ -1,17 +1,26 @@
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { LangParams, LayoutArgs, StatusParams } from "@/types/params"
export default function MiddlewareError({
export default async function MiddlewareError({
params,
}: LayoutArgs<LangParams & StatusParams>) {
setLang(params.lang)
const intl = await getIntl()
return (
<div className={styles.layout}>
Middleware Error {params.lang}: {params.status}
{intl.formatMessage(
{ id: "Middleware error {lang}: {status}" },
{
lang: params.lang,
status: params.status,
}
)}
</div>
)
}

View File

@@ -5,6 +5,7 @@ import { getProfile } from "@/lib/trpc/memoizedRequests"
import AccountPage from "@/components/Webviews/AccountPage"
import LoyaltyPage from "@/components/Webviews/LoyaltyPage"
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
import type {
@@ -18,11 +19,12 @@ export default async function ContentTypePage({
params,
}: PageArgs<LangParams & ContentTypeWebviewParams & UIDParams, {}>) {
setLang(params.lang)
const intl = await getIntl()
const user = await getProfile()
if (!user) {
console.log(`[webview:page] unable to load user`)
return <p>Error: No user could be loaded</p>
return <p>{intl.formatMessage({ id: "Error: No user could be loaded" })}</p>
}
if ("error" in user) {
@@ -36,9 +38,13 @@ export default async function ContentTypePage({
console.log(`[webview:page] user error, redirecting to: ${redirectURL}`)
redirect(redirectURL)
case "notfound":
return <p>Error: user not found</p>
return <p>{intl.formatMessage({ id: "Error: user not found" })}</p>
case "unknown":
return <p>Unknown error occurred loading user</p>
return (
<p>
{intl.formatMessage({ id: "Unknown error occurred loading user" })}
</p>
)
default:
const u: never = user
console.log(`[webview:page] unhandled user loading error`)

View File

@@ -2,6 +2,7 @@
import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import styles from "./global-error.module.css"
@@ -12,6 +13,8 @@ export default function GlobalError({
}) {
console.log({ global_error: error })
const intl = useIntl()
useEffect(() => {
Sentry.captureException(error)
}, [error])
@@ -20,7 +23,7 @@ export default function GlobalError({
<html>
<body>
<div className={styles.layout}>
<h1>Something went really wrong!</h1>
<h1>{intl.formatMessage({ id: "Something went really wrong!" })}</h1>
</div>
</body>
</html>

View File

@@ -1,5 +1,7 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import JsonToHtml from "@/components/JsonToHtml"
import SectionContainer from "@/components/Section/Container"
@@ -14,6 +16,8 @@ import type { AccordionProps } from "@/types/components/blocks/Accordion"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
export default function AccordionSection({ accordion, title }: AccordionProps) {
const intl = useIntl()
const showToggleButton = accordion.length > 5
const [allAccordionsVisible, setAllAccordionsVisible] =
useState(!showToggleButton)
@@ -44,8 +48,12 @@ export default function AccordionSection({ accordion, title }: AccordionProps) {
<ShowMoreButton
loadMoreData={toggleAccordions}
showLess={allAccordionsVisible}
textShowMore="See all FAQ"
textShowLess="See less FAQ"
textShowMore={intl.formatMessage({
id: "See all FAQ",
})}
textShowLess={intl.formatMessage({
id: "See less FAQ",
})}
/>
) : null}
</SectionContainer>

View File

@@ -6,7 +6,6 @@ import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import SectionWrapper from "../SectionWrapper"
@@ -35,9 +34,25 @@ export default async function LoyaltyLevels({
}
async function LevelCard({ level }: LevelCardProps) {
const lang = getLang()
const intl = await getIntl()
const pointsString = `${level.required_points.toLocaleString(lang)} ${intl.formatMessage({ id: "points" })} `
let pointsMsg: React.ReactNode = intl.formatMessage(
{ id: "{pointsAmount, number} points" },
{ pointsAmount: level.required_points }
)
if (level.required_nights) {
pointsMsg = intl.formatMessage<React.ReactNode>(
{
id: "{pointsAmount, number} points <highlight>or {nightsAmount, number} nights</highlight>",
},
{
pointsAmount: level.required_points,
nightsAmount: level.required_nights,
highlight: (str) => <span className={styles.redText}>{str}</span>,
}
)
}
return (
<article className={styles.card}>
@@ -47,18 +62,15 @@ async function LevelCard({ level }: LevelCardProps) {
color="primaryLightOnSurfaceAccent"
tilted="large"
>
{intl.formatMessage({ id: "Level" })} {level.user_facing_tag}
{intl.formatMessage(
{ id: "Level {level}" },
{ level: level.user_facing_tag }
)}
</BiroScript>
<MembershipLevelIcon level={level.level_id} color="red" />
</header>
<Title textAlign="center" level="h5">
{pointsString}
{level.required_nights ? (
<span className={styles.redText}>
{intl.formatMessage({ id: "or" })} {level.required_nights}{" "}
{intl.formatMessage({ id: "nights" })}
</span>
) : null}
{pointsMsg}
</Title>
<div className={styles.textContainer}>
{level.rewards.map((reward) => (

View File

@@ -19,14 +19,15 @@ export default async function MembershipNumber({
return (
<div className={classNames}>
<Caption color="pale">
{intl.formatMessage({ id: "Membership ID" })}
{": "}
{intl.formatMessage({ id: "Membership ID: " })}
</Caption>
<span className={styles.icon}>
<Caption className={styles.icon} color="pale" asChild>
<code>{membership?.membershipNumber ?? "N/A"}</code>
<code>
{membership?.membershipNumber ?? intl.formatMessage({ id: "N/A" })}
</code>
</Caption>
{membership && (
{membership?.membershipNumber && (
<CopyButton membershipNumber={membership.membershipNumber} />
)}
</span>

View File

@@ -24,15 +24,18 @@ export default async function Friend({
}
const isHighestLevel = isHighestMembership(membership.membershipLevel)
const lvlMessageHighest = intl.formatMessage({ id: "Highest level" })
const lvlMessageLevel = intl.formatMessage(
{ id: "Level {level}" },
{ level: membershipLevels[membership.membershipLevel] }
)
return (
<section className={styles.friend}>
<header className={styles.header}>
<Body color="white" textTransform="bold" textAlign="center">
{intl.formatMessage(
isHighestLevel
? { id: "Highest floor" }
: { id: `Level ${membershipLevels[membership.membershipLevel]}` }
)}
{isHighestLevel ? lvlMessageHighest : lvlMessageLevel}
</Body>
<MembershipLevelIcon
level={MembershipLevelEnum[membership.membershipLevel]}

View File

@@ -24,7 +24,7 @@ export default async function ExpiringPoints({ user }: UserProps) {
<section>
<Body color="white" textTransform="bold" textAlign="center">
{intl.formatMessage(
{ id: "spendable points expiring by" },
{ id: "{points} spendable points expiring by {date}" },
{
points: intl.formatNumber(membership.pointsToExpire),
date: d.format(dateFormat),

View File

@@ -1,7 +1,5 @@
import { useIntl } from "react-intl"
import useLang from "@/hooks/useLang"
import styles from "./levelSummary.module.css"
import type { LevelSummaryProps } from "@/types/components/overviewTable"
@@ -10,17 +8,29 @@ export default function LevelSummary({
level,
showDescription = true,
}: LevelSummaryProps) {
const lang = useLang()
const intl = useIntl()
let pointsMsg: React.ReactNode = intl.formatMessage(
{ id: "{pointsAmount, number} points" },
{ pointsAmount: level.required_points }
)
if (level.required_nights) {
pointsMsg = intl.formatMessage<React.ReactNode>(
{
id: "{pointsAmount, number} points or {nightsAmount, number} nights",
},
{
pointsAmount: level.required_points,
nightsAmount: level.required_nights,
highlight: (str) => <span className={styles.redText}>{str}</span>,
}
)
}
return (
<div className={styles.levelSummary}>
<span className={styles.levelRequirements}>
{level.required_points.toLocaleString(lang)}
{"p "}
{level.required_nights
? `${intl.formatMessage({ id: "or" })} ${level.required_nights} ${intl.formatMessage({ id: "nights" })}`
: ""}
</span>
<span className={styles.levelRequirements}>{pointsMsg}</span>
{showDescription && (
<p className={styles.levelSummaryText}>{level.description}</p>
)}

View File

@@ -22,12 +22,19 @@ export default function Row({ transaction }: RowProps) {
const pathName = usePathname()
const isWebview = webviews.includes(pathName)
const nightString = `${transaction.nights} ${transaction.nights === 1 ? intl.formatMessage({ id: "night" }) : intl.formatMessage({ id: "nights" })}`
const nightsMsg = intl.formatMessage(
{
id: "{nightsAmount, plural, one {# night} other {# nights}}",
},
{
nightsAmount: transaction.nights,
}
)
let description =
transaction.hotelName && transaction.city
? `${transaction.hotelName}, ${transaction.city} ${nightString}`
: `${nightString}`
? `${transaction.hotelName}, ${transaction.city} ${nightsMsg}`
: `${nightsMsg}`
switch (transaction.type) {
case Transactions.rewardType.stay:

View File

@@ -11,16 +11,16 @@ import styles from "./clientTable.module.css"
import type { ClientTableProps } from "@/types/components/myPages/myPage/earnAndBurn"
const tableHeadings = [
"Points",
"Description",
"Booking number",
"Arrival date",
]
export default function ClientTable({ transactions }: ClientTableProps) {
const intl = useIntl()
const tableHeadings = [
intl.formatMessage({ id: "Points" }),
intl.formatMessage({ id: "Description" }),
intl.formatMessage({ id: "Booking number" }),
intl.formatMessage({ id: "Arrival date" }),
]
return (
<div className={styles.container}>
<Table>
@@ -28,9 +28,7 @@ export default function ClientTable({ transactions }: ClientTableProps) {
<Table.TR>
{tableHeadings.map((heading) => (
<Table.TH key={heading}>
<Body textTransform="bold">
{intl.formatMessage({ id: heading })}
</Body>
<Body textTransform="bold">{heading}</Body>
</Table.TH>
))}
</Table.TR>

View File

@@ -10,8 +10,6 @@ import useLang from "@/hooks/useLang"
import AwardPoints from "../../EarnAndBurn/AwardPoints"
const tableHeadings = ["Points", "Expiration Date"]
export default function ExpiringPointsTable({
points,
expirationDate,
@@ -23,15 +21,18 @@ export default function ExpiringPointsTable({
const lang = useLang()
const expiration = dt(expirationDate).locale(lang).format("DD MMM YYYY")
const tableHeadings = [
intl.formatMessage({ id: "Points" }),
intl.formatMessage({ id: "Expiration Date" }),
]
return (
<Table>
<Table.THead>
<Table.TR>
{tableHeadings.map((heading) => (
<Table.TH key={heading}>
<Body textTransform="bold">
{intl.formatMessage({ id: heading })}
</Body>
<Body textTransform="bold">{heading}</Body>
</Table.TH>
))}
</Table.TR>

View File

@@ -52,7 +52,7 @@ export default async function NextLevelRewardsBlock({
<div className={styles.textContainer}>
<Body color="peach50" textAlign="center">
{intl.formatMessage(
{ id: "As our" },
{ id: "As our {level}" },
{ level: nextLevelRewards.level?.name }
)}
</Body>

View File

@@ -14,7 +14,10 @@ export default function MembershipNumberBadge({
return (
<div className={styles.rewardBadge}>
<Caption textAlign="center" color="uiTextHighContrast">
{intl.formatMessage({ id: "Membership ID" })}: {membershipNumber}
{intl.formatMessage(
{ id: "Membership ID: {id}" },
{ id: membershipNumber }
)}
</Caption>
</div>
)

View File

@@ -1,6 +1,7 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
@@ -18,6 +19,7 @@ import type { StayCardProps } from "@/types/components/myPages/stays/stayCard"
export default function StayCard({ stay }: StayCardProps) {
const lang = useLang()
const intl = useIntl()
// TODO: Temporary loading. Remove when current web is deleted.
const [loading, setLoading] = useState(false)
@@ -55,7 +57,7 @@ export default function StayCard({ stay }: StayCardProps) {
<Caption asChild>
<time dateTime={arrivalDateTime}>{arrivalDate}</time>
</Caption>
{" - "}
{intl.formatMessage({ id: " - " })}
<Caption asChild>
<time dateTime={departDateTime}>{departDate}</time>
</Caption>

View File

@@ -51,7 +51,7 @@ export default async function HotelListingItem({
</div>
<Caption color="uiTextPlaceholder">
{intl.formatMessage(
{ id: "Distance in km to city centre" },
{ id: "{number} km to city centre" },
{
number: getSingleDecimal(
hotel.location.distanceToCentre / 1000

View File

@@ -1,4 +1,5 @@
"use client"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -34,13 +35,12 @@ export default function MobileToggleButton({
? JSON.parse(decodeURIComponent(location))
: null
const nights = dt(d.toDate).diff(dt(d.fromDate), "days")
const selectedFromDate = dt(d.fromDate).locale(lang).format("D MMM")
const selectedToDate = dt(d.toDate).locale(lang).format("D MMM")
const locationAndDateIsSet = parsedLocation && d
const totalNights = dt(d.toDate).diff(dt(d.fromDate), "days")
const totalRooms = rooms.length
const totalAdults = rooms.reduce((acc, room) => {
if (room.adults) {
@@ -55,6 +55,32 @@ export default function MobileToggleButton({
return acc
}, 0)
const totalNightsMsg = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights }
)
const totalAdultsMsg = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults }
)
const totalChildrenMsg = intl.formatMessage(
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
{ totalChildren }
)
const totalRoomsMsg = intl.formatMessage(
{ id: "{totalRooms, plural, one {# room} other {# rooms}}" },
{ totalRooms }
)
const totalDetails = [totalAdultsMsg]
if (totalChildren > 0) {
totalDetails.push(totalChildrenMsg)
}
totalDetails.push(totalRoomsMsg)
return (
<div
className={locationAndDateIsSet ? styles.complete : styles.partial}
@@ -76,13 +102,16 @@ export default function MobileToggleButton({
<Divider color="baseSurfaceSubtleNormal" variant="vertical" />
<div>
<Caption type="bold" color="red">
{intl.formatMessage(
{ id: "booking.nights" },
{ totalNights: nights }
)}
{totalNightsMsg}
</Caption>
<Body>
{selectedFromDate} - {selectedToDate}
{intl.formatMessage(
{ id: "{selectedFromDate} - {selectedToDate}" },
{
selectedFromDate,
selectedToDate,
}
)}
</Body>
</div>
<div className={styles.icon}>
@@ -96,17 +125,17 @@ export default function MobileToggleButton({
<div>
<Caption color="red">{parsedLocation?.name}</Caption>
<Caption>
{`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage(
{ id: "booking.nights" },
{ totalNights: nights }
)}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${
totalChildren > 0
? intl.formatMessage(
{ id: "booking.children" },
{ totalChildren }
) + ", "
: ""
}${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`}
{intl.formatMessage(
{
id: "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}",
},
{
selectedFromDate,
selectedToDate,
totalNights: totalNightsMsg,
details: totalDetails.join(", "),
}
)}
</Caption>
</div>
<div className={styles.icon}>
@@ -132,7 +161,10 @@ export function MobileToggleButtonSkeleton() {
<Divider color="baseSurfaceSubtleNormal" variant="vertical" />
<div>
<Caption type="bold" color="red">
{intl.formatMessage({ id: "booking.nights" }, { totalNights: 0 })}
{intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: 0 }
)}
</Caption>
<SkeletonShimmer height="24px" />
</div>

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 }),

View File

@@ -1,7 +1,9 @@
"use client"
import { da, de, fi, nb, sv } from "date-fns/locale"
import { useCallback, useEffect, useRef, useState } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { Lang } from "@/constants/languages"
import { dt } from "@/lib/dt"
@@ -26,6 +28,8 @@ const locales = {
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
const lang = useLang()
const intl = useIntl()
const [isOpen, setIsOpen] = useState(false)
const selectedDate = useWatch({ name })
const { register, setValue } = useFormContext()
@@ -131,7 +135,13 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
>
<Body className={styles.body} asChild color="uiTextHighContrast">
<span>
{selectedFromDate} - {selectedToDate}
{intl.formatMessage(
{ id: "{selectedFromDate} - {selectedToDate}" },
{
selectedFromDate,
selectedToDate,
}
)}
</span>
</Body>
</button>

View File

@@ -40,8 +40,10 @@ export default async function FooterDetails() {
<div className={styles.bottomContainer}>
<div className={styles.copyrightContainer}>
<Footnote type="label" textTransform="uppercase">
© {currentYear}{" "}
{intl.formatMessage({ id: "Copyright all rights reserved" })}
{intl.formatMessage(
{ id: "© {currentYear} Scandic AB All rights reserved" },
{ currentYear }
)}
</Footnote>
</div>
<div className={styles.navigationContainer}>
@@ -99,8 +101,10 @@ export async function FooterDetailsSkeleton() {
<div className={styles.bottomContainer}>
<div className={styles.copyrightContainer}>
<Footnote type="label" textTransform="uppercase">
© {currentYear}{" "}
{intl.formatMessage({ id: "Copyright all rights reserved" })}
{intl.formatMessage(
{ id: "© {currentYear} Scandic AB All rights reserved" },
{ currentYear }
)}
</Footnote>
</div>
<div className={styles.navigationContainer}>

View File

@@ -19,10 +19,10 @@ export default function Voucher() {
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
const reward = intl.formatMessage({ id: "Book reward night" })
const disabledBookingOptionsHeader = intl.formatMessage({
id: "Disabled booking options header",
id: "We're sorry",
})
const disabledBookingOptionsText = intl.formatMessage({
id: "Disabled booking options text",
id: "Codes, cheques and reward nights aren't available on the new website yet.",
})
return (

View File

@@ -41,7 +41,9 @@ export default function FormContent({
<Caption color="red" type="bold">
{nights > 0
? intl.formatMessage(
{ id: "booking.nights" },
{
id: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: nights }
)
: intl.formatMessage({
@@ -102,7 +104,10 @@ export function BookingWidgetFormContentSkeleton() {
</div>
<div className={styles.when}>
<Caption color="red" type="bold">
{intl.formatMessage({ id: "booking.nights" }, { totalNights: 0 })}
{intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: 0 }
)}
</Caption>
<SkeletonShimmer width={"100%"} />
</div>

View File

@@ -164,9 +164,11 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
{/* TODO: Update copy once ready */}
<Body>
{intl.formatMessage<React.ReactNode>(
{ id: "signupPage.terms" },
{
termsAndConditions: (str) => (
id: "By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.",
},
{
termsAndConditionsLink: (str) => (
<Link
variant="underscored"
color="peach80"

View File

@@ -33,13 +33,13 @@ export default function GuestsRoomsPickerDialog({
const { getFieldState, trigger, setValue } =
useFormContext<BookingWidgetSchema>()
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
const addRoomLabel = intl.formatMessage({ id: "Add room" })
const doneLabel = intl.formatMessage({ id: "Done" })
const disabledBookingOptionsHeader = intl.formatMessage({
id: "Disabled booking options header",
id: "We're sorry",
})
const disabledBookingOptionsText = intl.formatMessage({
id: "Disabled adding room",
id: "Adding room is not available on the new website yet.",
})
const handleClose = useCallback(async () => {

View File

@@ -137,6 +137,38 @@ function Trigger({
}) {
const intl = useIntl()
const parts = []
parts.push(
intl.formatMessage(
{ id: "{totalRooms, plural, one {# room} other {# rooms}}" },
{ totalRooms: rooms.length }
)
)
parts.push(
intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) }
)
)
if (rooms.some((room) => room.child.length > 0)) {
parts.push(
intl.formatMessage(
{
id: "{totalChildren, plural, one {# child} other {# children}}",
},
{
totalChildren: rooms.reduce(
(acc, room) => acc + room.child.length,
0
),
}
)
)
}
return (
<Button
className={`${className} ${styles.btn}`}
@@ -144,29 +176,7 @@ function Trigger({
onPress={triggerFn}
>
<Body color="uiTextHighContrast">
<span>
{intl.formatMessage(
{ id: "booking.rooms" },
{ totalRooms: rooms.length }
)}
{", "}
{intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) }
)}
{rooms.some((room) => room.child.length > 0)
? ", " +
intl.formatMessage(
{ id: "booking.children" },
{
totalChildren: rooms.reduce(
(acc, room) => acc + room.child.length,
0
),
}
)
: null}
</span>
<span>{parts.join(", ")}</span>
</Body>
</Button>
)

View File

@@ -60,14 +60,19 @@ export default function MobileMenu({
}
}, [isHamburgerMenuOpen])
const closeMsg = intl.formatMessage({
id: "Close menu",
})
const openMsg = intl.formatMessage({
id: "Open menu",
})
return (
<>
<button
type="button"
className={`${styles.hamburger} ${isHamburgerExtended ? styles.isExpanded : ""}`}
aria-label={intl.formatMessage({
id: isHamburgerExtended ? "Close menu" : "Open menu",
})}
aria-label={isHamburgerExtended ? closeMsg : openMsg}
onClick={() => toggleDropdown(DropdownTypeEnum.HamburgerMenu)}
>
<span className={styles.bar} />

View File

@@ -50,7 +50,10 @@ export default function MyPagesMenu({
<Avatar initials={getInitials(user.firstName, user.lastName)} />
<Body textTransform="bold" color="baseTextHighContrast" asChild>
<span>
{intl.formatMessage({ id: "Hi" })} {user.firstName}!
{intl.formatMessage(
{ id: "Hi {firstName}!" },
{ firstName: user.firstName }
)}
</span>
</Body>
<ChevronDownSmallIcon

View File

@@ -40,13 +40,19 @@ export default function MyPagesMenuContent({
<nav className={styles.myPagesMenuContent} ref={myPagesMenuContentRef}>
<div className={introClassName}>
<Subtitle type="two" className={styles.userName}>
{intl.formatMessage({ id: "Hi" })} {user.firstName}!
{intl.formatMessage(
{ id: "Hi {firstName}!" },
{ firstName: user.firstName }
)}
</Subtitle>
{membershipLevel && membershipPoints ? (
<Caption className={styles.friendTypeWrapper}>
<span className={styles.friendType}>{membershipLevel.name}</span>
<span>
{membershipPoints} {intl.formatMessage({ id: "points" })}
{intl.formatMessage(
{ id: "{pointsAmount, number} points" },
{ pointsAmount: membershipPoints }
)}
</span>
</Caption>
) : null}

View File

@@ -36,10 +36,10 @@ export default function Confirmation({
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "booking.confirmation.membershipInfo.heading",
id: "Failed to verify membership",
})}
text={intl.formatMessage({
id: "booking.confirmation.membershipInfo.text",
id: "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
})}
/>
)}

View File

@@ -24,7 +24,9 @@ export default function Header({
const intl = useIntl()
const text = intl.formatMessage<React.ReactNode>(
{ id: "booking.confirmation.text" },
{
id: "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
},
{
emailLink: (str) => (
<Link color="burgundy" href="#" textDecoration="underline">
@@ -57,7 +59,7 @@ export default function Header({
<header className={styles.header}>
<hgroup className={styles.hgroup}>
<Title as="h2" color="red" textTransform="uppercase" type="h2">
{intl.formatMessage({ id: "booking.confirmation.title" })}
{intl.formatMessage({ id: "Booking confirmation" })}
</Title>
<Title as="h2" color="burgundy" textTransform="uppercase" type="h1">
{hotel.name}

View File

@@ -1,4 +1,5 @@
"use client"
import { useIntl } from "react-intl"
import Link from "@/components/TempDesignSystem/Link"
@@ -23,8 +24,14 @@ export default function HotelDetails({
<div className={styles.hotel}>
<Body color="uiTextHighContrast">{hotel.name}</Body>
<Body color="uiTextHighContrast">
{hotel.address.streetAddress}, {hotel.address.zipCode}{" "}
{hotel.address.city}
{intl.formatMessage(
{ id: "{streetAddress}, {zipCode} {city}" },
{
streetAddress: hotel.address.streetAddress,
zipCode: hotel.address.zipCode,
city: hotel.address.city,
}
)}
</Body>
<Body asChild color="uiTextHighContrast">
<Link
@@ -64,7 +71,7 @@ export default function HotelDetails({
<div className={styles.toast}>
<Toast variant="info">
<ul className={styles.list}>
<li>N/A</li>
<li>{intl.formatMessage({ id: "N/A" })}</li>
</ul>
</Toast>
</div>

View File

@@ -26,8 +26,16 @@ export default function PaymentDetails({
</Subtitle>
<div className={styles.payment}>
<Body color="uiTextHighContrast">
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}{" "}
{intl.formatMessage({ id: "has been paid" })}
{intl.formatMessage(
{ id: "{amount} has been paid" },
{
amount: formatPrice(
intl,
booking.totalPrice,
booking.currencyCode
),
}
)}
</Body>
<Body color="uiTextHighContrast">
{dt(booking.createDateTime)

View File

@@ -1,4 +1,5 @@
"use client"
import { notFound } from "next/navigation"
import { useIntl } from "react-intl"
@@ -47,7 +48,7 @@ export default function Receipt({
{booking.rateDefinition.isMemberRate ? (
<div className={styles.memberPrice}>
<Body color="uiTextPlaceholder">
<s>N/A</s>
<s>{intl.formatMessage({ id: "N/A" })}</s>
</Body>
<Body color="red">
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
@@ -60,7 +61,7 @@ export default function Receipt({
)}
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.adults" },
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{
totalAdults: booking.adults,
}
@@ -156,7 +157,12 @@ export default function Receipt({
<ChevronRightSmallIcon />
</Button>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })} N/A EUR
{intl.formatMessage(
{ id: "Approx. {value}" },
{
value: "N/A EUR",
}
)}
</Caption>
</div>
</div>

View File

@@ -1,4 +1,5 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
@@ -23,6 +24,7 @@ export default function Room({ booking, img, roomName }: RoomProps) {
const intl = useIntl()
const lang = useLang()
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
const fromDate = dt(booking.checkInDate).locale(lang)
const toDate = dt(booking.checkOutDate).locale(lang)
return (
@@ -33,7 +35,12 @@ export default function Room({ booking, img, roomName }: RoomProps) {
{intl.formatMessage({ id: "Room" })} 1
</Subtitle> */}
<Subtitle color="uiTextHighContrast" type="two">
{`${intl.formatMessage({ id: "Reservation number" })} ${booking.confirmationNumber}`}
{intl.formatMessage(
{ id: "Reservation number {value}" },
{
value: booking.confirmationNumber,
}
)}
</Subtitle>
</div>
<div className={styles.benefits}>
@@ -81,7 +88,13 @@ export default function Room({ booking, img, roomName }: RoomProps) {
{intl.formatMessage({ id: "Check-in" })}
</Body>
<Body color="uiTextHighContrast">
{`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
{intl.formatMessage(
{ id: "{checkInDate} from {checkInTime}" },
{
checkInDate: fromDate.format("ddd, D MMM"),
checkInTime: fromDate.format("HH:mm"),
}
)}
</Body>
</li>
<li className={styles.listItem}>
@@ -89,14 +102,22 @@ export default function Room({ booking, img, roomName }: RoomProps) {
{intl.formatMessage({ id: "Check-out" })}
</Body>
<Body color="uiTextHighContrast">
{`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
{intl.formatMessage(
{ id: "{checkOutDate} from {checkOutTime}" },
{
checkOutDate: toDate.format("ddd, D MMM"),
checkOutTime: toDate.format("HH:mm"),
}
)}
</Body>
</li>
<li className={styles.listItem}>
<Body color="uiTextPlaceholder">
{intl.formatMessage({ id: "Breakfast" })}
</Body>
<Body color="uiTextHighContrast">N/A</Body>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "N/A" })}
</Body>
</li>
<li className={styles.listItem}>
<Body color="uiTextPlaceholder">
@@ -110,19 +131,24 @@ export default function Room({ booking, img, roomName }: RoomProps) {
<Body color="uiTextPlaceholder">
{intl.formatMessage({ id: "Rebooking" })}
</Body>
<Body color="uiTextHighContrast">N/A</Body>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "N/A" })}
</Body>
</li>
</ul>
<div className={styles.guest}>
<Body color="uiTextPlaceholder">
{intl.formatMessage({ id: "Main guest" })}
</Body>
<Body color="uiTextHighContrast">
{`${booking.guest.firstName} ${booking.guest.lastName}`}
</Body>
<Body color="uiTextHighContrast">{guestName}</Body>
{booking.guest.membershipNumber ? (
<Body color="uiTextHighContrast">
{`${intl.formatMessage({ id: "Friend no." })} ${booking.guest.membershipNumber}`}
{intl.formatMessage(
{ id: "Friend no. {value}" },
{
value: booking.guest.membershipNumber,
}
)}
</Body>
) : null}
{booking.guest.phoneNumber ? (

View File

@@ -16,6 +16,9 @@ export default function Contact({ hotel }: ContactProps) {
const lang = useLang()
const intl = useIntl()
const addressStr = `${hotel.address.streetAddress}, `
const cityStr = hotel.address.city
return (
<section className={styles.wrapper}>
<address className={styles.address}>
@@ -24,8 +27,11 @@ export default function Contact({ hotel }: ContactProps) {
<Body textTransform="bold">
{intl.formatMessage({ id: "Address" })}
</Body>
<Body>{`${hotel.address.streetAddress}, `}</Body>
<Body>{hotel.address.city}</Body>
<Body>
{addressStr}
<br />
{cityStr}
</Body>
</li>
<li>
<Body textTransform="bold">
@@ -34,7 +40,9 @@ export default function Contact({ hotel }: ContactProps) {
<Link
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
>
<span className={styles.link}>Google Maps</span>
<span className={styles.link}>
{intl.formatMessage({ id: "Google Maps" })}
</span>
</Link>
</li>
<li>

View File

@@ -24,12 +24,10 @@ export default function BedTypeInfo({ hasMultipleBedTypes }: BedTypeInfoProps) {
id: "Extra bed will be provided additionally",
})
const combinedStr = `${availabilityText}. ${extraBedText}`
if (hasMultipleBedTypes && hasChildWithExtraBed) {
return (
<Body>
{availabilityText}. {extraBedText}
</Body>
)
return <Body>{combinedStr}</Body>
}
if (hasMultipleBedTypes) {

View File

@@ -90,7 +90,9 @@ export default function Breakfast({ packages }: BreakfastProps) {
subtitle={
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
? intl.formatMessage<React.ReactNode>(
{ id: "breakfast.price.free" },
{
id: "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult",
},
{
amount: formatPrice(
intl,
@@ -103,7 +105,7 @@ export default function Breakfast({ packages }: BreakfastProps) {
}
)
: intl.formatMessage(
{ id: "breakfast.price" },
{ id: "{amount}/night per adult" },
{
amount: formatPrice(
intl,

View File

@@ -89,10 +89,10 @@ export default function JoinScandicFriendsCard({
<Footnote color="uiTextPlaceholder">
{intl.formatMessage<React.ReactNode>(
{
id: "signup.terms",
id: "By signing up you accept the Scandic Friends <termsAndConditionsLink>Terms and Conditions</termsAndConditionsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic's customer service",
},
{
termsLink: (str) => (
termsAndConditionsLink: (str) => (
<Link
variant="default"
textDecoration="underline"

View File

@@ -57,7 +57,7 @@ export default function MemberPriceModal({
)}
</div>
<Button intent="primary" theme="base" onClick={() => setIsOpen(false)}>
OK
{intl.formatMessage({ id: "OK" })}
</Button>
</div>
</Modal>

View File

@@ -12,7 +12,6 @@ import useLang from "@/hooks/useLang"
import styles from "./signup.module.css"
export default function Signup({ name }: { name: string }) {
const lang = useLang()
const intl = useIntl()
const [isJoinChecked, setIsJoinChecked] = useState(false)
@@ -35,7 +34,9 @@ export default function Signup({ name }: { name: string }) {
<div className={styles.dateField}>
<header>
<Caption type="bold">
{intl.formatMessage({ id: "Birth date" })} *
<span className={styles.required}>
{intl.formatMessage({ id: "Birth date" })}
</span>
</Caption>
</header>
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />

View File

@@ -13,3 +13,7 @@
display: grid;
gap: var(--Spacing-x1);
}
.required:after {
content: " *";
}

View File

@@ -15,6 +15,9 @@ export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
const hotel = hotelData.data.attributes
const image = hotel.hotelContent?.images
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
return (
<header className={styles.header}>
<Image
@@ -30,14 +33,12 @@ export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
<Title as="h1" level="h1" color="white" className={styles.title}>
{hotel.name}
</Title>
<address className={styles.address}>
<Caption color="white">
{hotel.address.streetAddress}, {hotel.address.city}
</Caption>
<div className={styles.address}>
<Caption color="white">{addressStr}</Caption>
<Caption color="white"></Caption>
<Caption color="white">
{intl.formatMessage(
{ id: "Distance in km to city centre" },
{ id: "{number} km to city centre" },
{
number: getSingleDecimal(
hotel.location.distanceToCentre / 1000
@@ -45,7 +46,7 @@ export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
}
)}
</Caption>
</address>
</div>
</div>
<ToggleSidePeek hotelId={hotel.operaId} />
</div>

View File

@@ -160,7 +160,7 @@ export default function PaymentClient({
(errorMessage: string) => {
toast.error(
intl.formatMessage({
id: "payment.error.failed",
id: "We had an issue processing your booking. Please try again. No charges have been made.",
})
)
const currentPaymentMethod = methods.getValues("paymentMethod")
@@ -312,10 +312,6 @@ export default function PaymentClient({
return <LoadingSpinner />
}
const guaranteeing = intl.formatMessage({ id: "guaranteeing" })
const paying = intl.formatMessage({ id: "paying" })
const paymentVerb = mustBeGuaranteed ? guaranteeing : paying
return (
<>
<FormProvider {...methods}>
@@ -387,11 +383,10 @@ export default function PaymentClient({
<Caption>
{intl.formatMessage<React.ReactNode>(
{
id: "booking.terms",
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
},
{
paymentVerb,
termsLink: (str) => (
termsAndConditionsLink: (str) => (
<Link
className={styles.link}
variant="underscored"
@@ -401,7 +396,7 @@ export default function PaymentClient({
{str}
</Link>
),
privacyLink: (str) => (
privacyPolicyLink: (str) => (
<Link
className={styles.link}
variant="underscored"

View File

@@ -53,7 +53,7 @@ export default function PriceChangeDialog({
<br />
<span className={styles.oldPrice}>
{formatPrice(intl, oldPrice, currency)}
</span>{" "}
</span>
<strong className={styles.newPrice}>
{formatPrice(intl, newPrice, currency)}
</strong>

View File

@@ -28,7 +28,7 @@ export default function ToggleSidePeek({
intent="text"
wrapping
>
{intl.formatMessage({ id: "See room details" })}{" "}
{intl.formatMessage({ id: "See room details" })}
<ChevronRight height="14" />
</Button>
)

View File

@@ -47,8 +47,16 @@ export default function SelectedRoom({
className={styles.description}
color="uiTextHighContrast"
>
{room.roomType}{" "}
<span className={styles.rate}>{rateDescription}</span>
{intl.formatMessage<React.ReactNode>(
{ id: "{roomType} <rate>{rateDescription}</rate>" },
{
roomType: room.roomType,
rateDescription,
rate: (str) => {
return <span className={styles.rate}>{str}</span>
},
}
)}
</Subtitle>
<Link
className={styles.button}

View File

@@ -55,7 +55,7 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
onClick={toggleSummaryOpen}
className={styles.priceDetailsButton}
>
<Caption>{intl.formatMessage({ id: "Total price" })}:</Caption>
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle>
{formatPrice(
intl,

View File

@@ -104,10 +104,17 @@ export default function PriceDetailsTable({
<TableSection>
<TableSectionHeader title={intl.formatMessage({ id: "Breakfast" })} />
<Row
label={intl.formatMessage(
{ id: "booking.adults.breakfasts" },
{ totalAdults: adults, totalBreakfasts: nights.length }
)}
label={`${intl.formatMessage(
{
id: "{totalAdults, plural, one {# voksen} other {# voksne}}",
},
{ totalAdults: adults }
)}, ${intl.formatMessage(
{
id: "{totalBreakfasts, plural, one {# morgenmad} other {# morgenmad}}",
},
{ totalBreakfasts: nights.length }
)}`}
value={formatPrice(
intl,
parseInt(breakfast.localPrice.totalPrice),
@@ -116,13 +123,17 @@ export default function PriceDetailsTable({
/>
{children?.length ? (
<Row
label={intl.formatMessage(
{ id: "booking.children.breakfasts" },
label={`${intl.formatMessage(
{
totalChildren: children.length,
totalBreakfasts: nights.length,
}
)}
id: "{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: children.length }
)}, ${intl.formatMessage(
{
id: "{totalBreakfasts, plural, one {# breakfast} other {# breakfasts}}",
},
{ totalBreakfasts: nights.length }
)}`}
value={formatPrice(intl, 0, breakfast.localPrice.currency)}
/>
) : null}
@@ -131,17 +142,17 @@ export default function PriceDetailsTable({
<TableSection>
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
<Row
label={intl.formatMessage({ id: "booking.vat.excl" })}
label={intl.formatMessage({ id: "Price excluding VAT" })}
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
/>
<Row
label={intl.formatMessage({ id: "booking.vat" }, { vat })}
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
/>
<tr className={styles.row}>
<td>
<Body textTransform="bold">
{intl.formatMessage({ id: "booking.vat.incl" })}
{intl.formatMessage({ id: "Price including VAT" })}
</Body>
</td>
<td className={styles.price}>

View File

@@ -107,7 +107,7 @@ export default function SummaryUI({
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
const nights = intl.formatMessage(
{ id: "booking.nights" },
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff }
)
@@ -123,6 +123,22 @@ export default function SummaryUI({
}
}
const adultsMsg = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: adults }
)
const guestsParts = [adultsMsg]
if (children?.length) {
const childrenMsg = intl.formatMessage(
{
id: "{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: children.length }
)
guestsParts.push(childrenMsg)
}
return (
<section className={styles.summary}>
<header className={styles.header}>
@@ -157,17 +173,7 @@ export default function SummaryUI({
</Body>
</div>
<Caption color="uiTextMediumContrast">
{`${intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: adults }
)}${
children?.length
? `, ${intl.formatMessage(
{ id: "booking.children" },
{ totalChildren: children.length }
)}`
: ""
}`}
{guestsParts.join(", ")}
</Caption>
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
<Modal
@@ -231,7 +237,10 @@ export default function SummaryUI({
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{`${intl.formatMessage({ id: "Crib (child)" })} × ${childBedCrib}`}
{intl.formatMessage(
{ id: "Crib (child) × {count}" },
{ count: childBedCrib }
)}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Based on availability" })}
@@ -246,7 +255,12 @@ export default function SummaryUI({
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{`${intl.formatMessage({ id: "Extra bed (child)" })} × ${childBedExtraBed}`}
{intl.formatMessage(
{ id: "Extra bed (child) × {count}" },
{
count: childBedExtraBed,
}
)}
</Body>
</div>
<Body color="uiTextHighContrast">
@@ -278,7 +292,9 @@ export default function SummaryUI({
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.adults" },
{
id: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: adults }
)}
</Caption>
@@ -294,7 +310,9 @@ export default function SummaryUI({
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.children" },
{
id: "{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: children.length }
)}
</Caption>
@@ -345,11 +363,15 @@ export default function SummaryUI({
</Body>
{totalPrice.requested && (
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}{" "}
{formatPrice(
intl,
totalPrice.requested.price,
totalPrice.requested.currency
{intl.formatMessage(
{ id: "Approx. {value}" },
{
value: formatPrice(
intl,
totalPrice.requested.price,
totalPrice.requested.currency
),
}
)}
</Caption>
)}

View File

@@ -1,4 +1,5 @@
"use client"
import { useParams } from "next/dist/client/components/navigation"
import { memo, useCallback } from "react"
import { useIntl } from "react-intl"
@@ -64,6 +65,8 @@ function HotelCard({
state,
})
const addressStr = `${hotelData.address.streetAddress}, ${hotelData.address.city}`
return (
<article
className={classNames}
@@ -94,9 +97,7 @@ function HotelCard({
</Subtitle>
<div className={styles.addressContainer}>
<address className={styles.address}>
<Caption color="uiTextPlaceholder">
{hotelData.address.streetAddress}, {hotelData.address.city}
</Caption>
<Caption color="uiTextPlaceholder">{addressStr}</Caption>
</address>
<address className={styles.addressMobile}>
<Caption color="burgundy" type="underline" asChild>
@@ -112,7 +113,7 @@ function HotelCard({
color="burgundy"
size="small"
>
{hotelData.address.streetAddress}, {hotelData.address.city}
{addressStr}
</Link>
</Caption>
</address>
@@ -121,7 +122,7 @@ function HotelCard({
</div>
<Caption color="uiTextPlaceholder">
{intl.formatMessage(
{ id: "Distance in km to city centre" },
{ id: "{number} km to city centre" },
{
number: getSingleDecimal(
hotelData.location.distanceToCentre / 1000

View File

@@ -93,7 +93,13 @@ export default function ListingHotelCardDialog({
{publicPrice && memberPrice && <Caption>/</Caption>}
{memberPrice && (
<Subtitle type="two" color="red" className={styles.memberPrice}>
{memberPrice} {currency}
{intl.formatMessage(
{ id: "{price} {currency}" },
{
price: memberPrice,
currency,
}
)}
</Subtitle>
)}
</div>

View File

@@ -88,7 +88,13 @@ export default function StandaloneHotelCardDialog({
</Caption>
{publicPrice && (
<Subtitle type="two">
{publicPrice} {currency}
{intl.formatMessage(
{ id: "{price} {currency}" },
{
price: publicPrice,
currency,
}
)}
<Body asChild>
<span>/{intl.formatMessage({ id: "night" })}</span>
</Body>
@@ -100,7 +106,13 @@ export default function StandaloneHotelCardDialog({
color="red"
className={styles.memberPrice}
>
{memberPrice} {currency}
{intl.formatMessage(
{ id: "{price} {currency}" },
{
price: memberPrice,
currency,
}
)}
<Body asChild color="red">
<span>/{intl.formatMessage({ id: "night" })}</span>
</Body>

View File

@@ -81,8 +81,10 @@ export default function HotelCardListing({
) : activeFilters ? (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "filters.nohotel.heading" })}
text={intl.formatMessage({ id: "filters.nohotel.text" })}
heading={intl.formatMessage({ id: "No hotels match your filters" })}
text={intl.formatMessage({
id: "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
})}
/>
) : null}
{showBackToTop && (

View File

@@ -139,8 +139,10 @@ export default function FilterAndSortModal({
onClick={() => handleApplyFiltersAndSorting(close)}
>
{intl.formatMessage(
{ id: "See results" },
{ count: resultCount }
{ id: "See results ({ count })" },
{
count: resultCount,
}
)}
</Button>
<Button

View File

@@ -1,4 +1,5 @@
"use client"
import { useIntl } from "react-intl"
import { useHotelFilterStore } from "@/stores/hotel-filters"
@@ -13,7 +14,7 @@ export default function HotelCount() {
<Preamble>
{intl.formatMessage(
{
id: "Hotel(s)",
id: "{amount, plural, one {# hotel} other {# hotels}}",
},
{ amount: resultCount }
)}

View File

@@ -1,5 +1,7 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useIntl } from "react-intl"
import SelectionCard from "../SelectionCard"
@@ -11,6 +13,7 @@ export default function BedSelection({
alternatives,
nextPath,
}: BedSelectionProps) {
const intl = useIntl()
const router = useRouter()
const searchParams = useSearchParams()
@@ -46,7 +49,7 @@ export default function BedSelection({
</ul>
<button type="submit" hidden>
Submit
{intl.formatMessage({ id: "Submit" })}
</button>
</form>
</div>

View File

@@ -1,5 +1,7 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useIntl } from "react-intl"
import SelectionCard from "../SelectionCard"
@@ -11,6 +13,7 @@ export default function BreakfastSelection({
alternatives,
nextPath,
}: BreakfastSelectionProps) {
const intl = useIntl()
const router = useRouter()
const searchParams = useSearchParams()
@@ -49,7 +52,7 @@ export default function BreakfastSelection({
</ul>
<button type="submit" hidden>
Submit
{intl.formatMessage({ id: "Submit" })}
</button>
</form>
</div>

View File

@@ -1,5 +1,7 @@
"use client"
import { useSearchParams } from "next/navigation"
import { useIntl } from "react-intl"
import Button from "@/components/TempDesignSystem/Button"
@@ -8,12 +10,13 @@ import styles from "./details.module.css"
import type { DetailsProps } from "@/types/components/hotelReservation/selectRate/section"
export default function Details({ nextPath }: DetailsProps) {
const intl = useIntl()
const searchParams = useSearchParams()
return (
<div className={styles.wrapper}>
<form method="GET" action={`${nextPath}?${searchParams}`}>
<Button type="submit">Submit</Button>
<Button type="submit">{intl.formatMessage({ id: "Submit" })}</Button>
</form>
</div>
)

View File

@@ -70,7 +70,18 @@ export default async function HotelInfoCard({
</Title>
<div className={styles.hotelAddressDescription}>
<Caption color="uiTextMediumContrast">
{`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city}${getSingleDecimal(hotelAttributes.location.distanceToCentre / 1000)} ${intl.formatMessage({ id: "km to city center" })}`}
{intl.formatMessage(
{
id: "{address}, {city} ∙ {distanceToCityCentreInKm} km to city center",
},
{
address: hotelAttributes.address.streetAddress,
city: hotelAttributes.address.city,
distanceToCityCentreInKm: getSingleDecimal(
hotelAttributes.location.distanceToCentre / 1000
),
}
)}
</Caption>
<Body color="uiTextHighContrast">
{hotelAttributes.hotelContent.texts.descriptions.medium}

View File

@@ -84,8 +84,12 @@ export default function RoomFilter({
<div className={styles.infoDesktop}>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Room types available" },
{ numberOfRooms }
{
id: "{numberOfRooms, plural, one {# room type} other {# room types}} available",
},
{
numberOfRooms,
}
)}
</Body>
</div>
@@ -117,8 +121,12 @@ export default function RoomFilter({
</div>
<Caption color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Room types available" },
{ numberOfRooms }
{
id: "{numberOfRooms, plural, one {# room type} other {# room types}} available",
},
{
numberOfRooms,
}
)}
</Caption>
</div>
@@ -138,7 +146,7 @@ export default function RoomFilter({
<CheckboxChip
name={code}
key={code}
label={intl.formatMessage({ id: description })}
label={description}
disabled={isDisabled}
selected={getValues(code)}
Icon={getIconForFeatureCode(code)}

View File

@@ -85,7 +85,7 @@ export default function PriceList({
</div>
) : (
<Subtitle type="two" color="baseTextDisabled">
{intl.formatMessage({ id: "n/a" })}
{intl.formatMessage({ id: "N/A" })}
</Subtitle>
)}
</dd>
@@ -112,7 +112,7 @@ export default function PriceList({
</div>
) : (
<Body textTransform="bold" color="disabled">
- {intl.formatMessage({ id: "Currency Code" })}
-
</Body>
)}
</dd>
@@ -126,9 +126,14 @@ export default function PriceList({
</dt>
<dd>
<Caption color="uiTextMediumContrast">
{totalPublicRequestedPricePerNight}/
{totalMemberRequestedPricePerNight}{" "}
{publicRequestedPrice.currency}
{intl.formatMessage(
{ id: "{publicPrice}/{memberPrice} {currency}" },
{
publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)}
</Caption>
</dd>
</div>

View File

@@ -80,15 +80,15 @@ export default function RateSummary({
const showMemberDiscountBanner = member && !isUserLoggedIn
const summaryPriceText = `${intl.formatMessage(
{ id: "booking.nights" },
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: nights }
)}, ${intl.formatMessage(
{ id: "booking.adults" },
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: roomsAvailability.occupancy?.adults }
)}${
roomsAvailability.occupancy?.children?.length
? `, ${intl.formatMessage(
{ id: "booking.children" },
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
{ totalChildren: roomsAvailability.occupancy.children.length }
)}`
: ""
@@ -136,11 +136,15 @@ export default function RateSummary({
</Subtitle>
{totalPriceToShow?.requestedPrice ? (
<Body color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}{" "}
{formatPrice(
intl,
totalPriceToShow.requestedPrice.price,
totalPriceToShow.requestedPrice.currency
{intl.formatMessage(
{ id: "Approx. {value}" },
{
value: formatPrice(
intl,
totalPriceToShow.requestedPrice.price,
totalPriceToShow.requestedPrice.currency
),
}
)}
</Body>
) : null}

View File

@@ -118,10 +118,12 @@ export default function RoomCard({
{roomConfiguration.roomsLeft > 0 &&
roomConfiguration.roomsLeft < 5 && (
<span className={styles.chip}>
<Footnote
color="burgundy"
textTransform="uppercase"
>{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote>
<Footnote color="burgundy" textTransform="uppercase">
{intl.formatMessage(
{ id: "{amount, number} left" },
{ amount: roomConfiguration.roomsLeft }
)}
</Footnote>
</span>
)}
{roomConfiguration.features
@@ -150,7 +152,7 @@ export default function RoomCard({
<Caption color="uiTextMediumContrast" className={styles.guests}>
{intl.formatMessage(
{
id: "booking.guests",
id: "Max {max, plural, one {{range} guest} other {{range} guests}}",
},
{ max: totalOccupancy.max, range: totalOccupancy.range }
)}
@@ -159,9 +161,21 @@ export default function RoomCard({
{roomSize && (
<Caption color="uiTextMediumContrast">
{roomSize.min === roomSize.max
? roomSize.min
: `${roomSize.min}-${roomSize.max}`}
m²
? intl.formatMessage(
{ id: "{roomSize} m²" },
{
roomSize: roomSize.min,
}
)
: intl.formatMessage(
{
id: "{roomSizeMin} - {roomSizeMax} m²",
},
{
roomSizeMin: roomSize.min,
roomSizeMax: roomSize.max,
}
)}
</Caption>
)}
<div className={styles.toggleSidePeek}>

View File

@@ -1,6 +1,7 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useIntl } from "react-intl"
import RoomFilter from "../RoomFilter"
import RoomSelection from "../RoomSelection"
@@ -27,6 +28,8 @@ export default function Rooms({
hotelType,
isUserLoggedIn,
}: SelectRateProps) {
const intl = useIntl()
const visibleRooms: RoomConfiguration[] = useMemo(() => {
const deduped = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
@@ -59,27 +62,27 @@ export default function Rooms({
() => [
{
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
description: "Accessible Room",
description: intl.formatMessage({ id: "Accessible Room" }),
itemCode: availablePackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
)?.itemCode,
},
{
code: RoomPackageCodeEnum.ALLERGY_ROOM,
description: "Allergy Room",
description: intl.formatMessage({ id: "Allergy Room" }),
itemCode: availablePackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
)?.itemCode,
},
{
code: RoomPackageCodeEnum.PET_ROOM,
description: "Pet Room",
description: intl.formatMessage({ id: "Pet Room" }),
itemCode: availablePackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
)?.itemCode,
},
],
[availablePackages]
[availablePackages, intl]
)
const handleFilter = useCallback(

View File

@@ -30,15 +30,20 @@ export default function SelectionCard({
<div>
<Caption color="burgundy" className={styles.price}>
{formatPrice(intl, price, currency)}/
{intl.formatMessage({ id: "night" })}
{intl.formatMessage(
{ id: "{price}/night" },
{
price: formatPrice(intl, price, currency),
}
)}
</Caption>
{membersPrice && (
<Caption color="burgundy" className={styles.membersPrice}>
{intl.formatMessage({ id: "Members" })}{" "}
{formatPrice(intl, membersPrice, currency)}/
{intl.formatMessage({ id: "night" })}
{intl.formatMessage(
{ id: "Members {price}/night" },
{ price: formatPrice(intl, membersPrice, currency) }
)}
</Caption>
)}
</div>

View File

@@ -77,16 +77,15 @@ export default function LanguageSwitcher({
const classNames = languageSwitcherVariants({ position })
const closeMsg = intl.formatMessage({ id: "Close language menu" })
const openMsg = intl.formatMessage({ id: "Open language menu" })
return (
<div className={classNames} ref={languageSwitcherRef}>
<button
type="button"
className={styles.button}
aria-label={intl.formatMessage({
id: isLanguageSwitcherOpen
? "Close language menu"
: "Open language menu",
})}
aria-label={isLanguageSwitcherOpen ? closeMsg : openMsg}
onClick={handleClick}
>
<GlobeIcon width={globeIconSize} height={globeIconSize} />

View File

@@ -1,4 +1,5 @@
"use client"
import { AnimatePresence, motion } from "framer-motion"
import { useState } from "react"

View File

@@ -100,7 +100,7 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
{/* TODO: Handle when no price is available */}
{hotelPrice
? formatPrice(intl, hotelPrice, pin.currency)
: "N/A"}
: intl.formatMessage({ id: "N/A" })}
</span>
</Body>
</span>

View File

@@ -2,6 +2,7 @@ import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
} from "@vis.gl/react-google-maps"
import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -19,6 +20,8 @@ export default function HotelMapContent({
onActivePoiChange,
activePoi,
}: HotelMapContentProps) {
const intl = useIntl()
function toggleActivePoi(poiName: string) {
onActivePoiChange?.(activePoi === poiName ? null : poiName)
}
@@ -51,7 +54,14 @@ export default function HotelMapContent({
<span>
{poi.name}
<Caption asChild>
<span>{poi.distance} km</span>
<span>
{intl.formatMessage(
{ id: "{distanceInKm} km" },
{
distanceInKm: poi.distance,
}
)}
</span>
</Caption>
</span>
</Body>

View File

@@ -38,6 +38,7 @@ function InnerModal({
subtitle,
}: PropsWithChildren<InnerModalProps>) {
const intl = useIntl()
function modalStateHandler(newAnimationState: AnimationState) {
setAnimation((currentAnimationState) =>
newAnimationState === AnimationStateEnum.hidden &&
@@ -72,7 +73,10 @@ function InnerModal({
animate={animation}
initial={"hidden"}
>
<Dialog className={styles.dialog} aria-label="Dialog">
<Dialog
className={styles.dialog}
aria-label={intl.formatMessage({ id: "Dialog" })}
>
{({ close }) => (
<>
<header className={styles.header}>

View File

@@ -42,7 +42,9 @@ export default function SurprisesNotification({
toast.success(
<>
{intl.formatMessage(
{ id: "Gift(s) added to your benefits" },
{
id: "{amount, plural, one {Gift} other {Gifts}} added to your benefits",
},
{ amount: surprises.length }
)}
<br />
@@ -117,7 +119,10 @@ export default function SurprisesNotification({
}}
onAnimationComplete={confetti}
>
<Dialog aria-label="Surprises" className={styles.dialog}>
<Dialog
aria-label={intl.formatMessage({ id: "Surprises" })}
className={styles.dialog}
>
{({ close }) => {
return (
<>

View File

@@ -18,7 +18,7 @@ export default function Initial({ totalSurprises, onOpen }: InitialProps) {
<>
{intl.formatMessage<React.ReactNode>(
{
id: "You have <b>#</b> gifts waiting for you!",
id: "You have <b>{amount}</b> gifts waiting for you!",
},
{
amount: totalSurprises,
@@ -52,7 +52,7 @@ export default function Initial({ totalSurprises, onOpen }: InitialProps) {
>
{intl.formatMessage(
{
id: "Open gift(s)",
id: "Open {amount, plural, one {gift} other {gifts}}",
},
{ amount: totalSurprises }
)}

View File

@@ -23,25 +23,30 @@ export default function Slide({ surprise, membershipNumber }: SlideProps) {
},
dt()
)
return (
<Card title={surprise.label}>
<Body textAlign="center">{surprise.description}</Body>
<div className={styles.badge}>
<Caption>
{intl.formatMessage(
{ id: "Expires at the earliest" },
{ id: "Expires at the earliest {expirationDate}" },
{
date: dt(earliestExpirationDate)
expirationDate: dt(earliestExpirationDate)
.locale(lang)
.format("D MMM YYYY"),
}
)}
</Caption>
<Caption>
{intl.formatMessage({
id: "Membership ID",
})}{" "}
{membershipNumber}
{intl.formatMessage(
{
id: "Membership ID {id}",
},
{
id: membershipNumber,
}
)}
</Caption>
</div>
</Card>

View File

@@ -16,8 +16,22 @@ export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) {
variant="sidepeek"
>
<Body textTransform="bold">{intl.formatMessage({ id: "Hours" })}</Body>
<Body>{`${intl.formatMessage({ id: "Check in from" })}: ${checkin.checkInTime}`}</Body>
<Body>{`${intl.formatMessage({ id: "Check out at latest" })}: ${checkin.checkOutTime}`}</Body>
<Body>
{intl.formatMessage(
{ id: "Check in from: {checkInTime}" },
{
checkInTime: checkin.checkInTime,
}
)}
</Body>
<Body>
{intl.formatMessage(
{ id: "Check out at latest: {checkOutTime}" },
{
checkOutTime: checkin.checkOutTime,
}
)}
</Body>
</AccordionItem>
)
}

View File

@@ -11,6 +11,7 @@ import { IconName } from "@/types/components/icon"
export default function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({ id: "Parking" })}
@@ -18,39 +19,48 @@ export default function Parking({ parking }: ParkingProps) {
className={styles.parking}
variant="sidepeek"
>
{parking.map((p) => (
<div key={p.name}>
<Subtitle type="two">
{`${intl.formatMessage({ id: p.type })} ${p?.name ? ` (${p.name})` : ""}`}
</Subtitle>
<ul className={styles.list}>
{p?.address && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{`${intl.formatMessage({ id: "Address" })}: ${p.address}`}
</li>
)}
{p?.numberOfParkingSpots !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{intl.formatMessage(
{ id: "Number of parking spots" },
{ number: p.numberOfParkingSpots }
)}
</li>
)}
{p?.numberOfChargingSpaces !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{intl.formatMessage(
{ id: "Number of charging points for electric cars" },
{ number: p.numberOfChargingSpaces }
)}
</li>
)}
</ul>
</div>
))}
{parking.map((p) => {
const title = `${p.type}${p.name ? ` (${p.name})` : ""}`
return (
<div key={p.name}>
<Subtitle type="two">{title}</Subtitle>
<ul className={styles.list}>
{p.address !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />$
{intl.formatMessage(
{ id: "Address: {address}" },
{
address: p.address,
}
)}
</li>
)}
{p.numberOfParkingSpots !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{intl.formatMessage(
{ id: "Number of parking spots: {number}" },
{ number: p.numberOfParkingSpots }
)}
</li>
)}
{p.numberOfChargingSpaces !== undefined && (
<li>
<FilledHeartIcon color="baseIconLowContrast" />
{intl.formatMessage(
{
id: "Number of charging points for electric cars: {number}",
},
{ number: p.numberOfChargingSpaces }
)}
</li>
)}
</ul>
</div>
)
})}
</AccordionItem>
)
}

View File

@@ -13,7 +13,12 @@ export default function Restaurant({
return (
<AccordionItem
title={intl.formatMessage({ id: "Restaurant" }, { count: 1 })}
title={intl.formatMessage(
{
id: "{totalRestaurants, plural, one {Restaurant} other {Restaurants}}",
},
{ totalRestaurants: 1 }
)}
icon={IconName.Restaurant}
variant="sidepeek"
>

Some files were not shown because too many files have changed in this diff Show More