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

View File

@@ -1,12 +1,16 @@
"use client" "use client"
import * as Sentry from "@sentry/nextjs" import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react" import { useEffect } from "react"
import { useIntl } from "react-intl"
export default function Error({ export default function Error({
error, error,
}: { }: {
error: Error & { digest?: string } error: Error & { digest?: string }
}) { }) {
const intl = useIntl()
useEffect(() => { useEffect(() => {
if (!error) return if (!error) return
@@ -17,7 +21,12 @@ export default function Error({
return ( return (
<p> <p>
<strong> <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> </strong>
</p> </p>
) )

View File

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

View File

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

View File

@@ -2,10 +2,13 @@
import * as Sentry from "@sentry/nextjs" import * as Sentry from "@sentry/nextjs"
import { useEffect } from "react" import { useEffect } from "react"
import { useIntl } from "react-intl"
import type { ErrorPage } from "@/types/next/error" import type { ErrorPage } from "@/types/next/error"
export default function ProfileError({ error }: ErrorPage) { export default function ProfileError({ error }: ErrorPage) {
const intl = useIntl()
useEffect(() => { useEffect(() => {
if (!error) return if (!error) return
@@ -13,5 +16,5 @@ export default function ProfileError({ error }: ErrorPage) {
Sentry.captureException(error) Sentry.captureException(error)
}, [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 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 defaultLanguage = languages[params.lang]
const language = languageSelect.find((l) => l.value === user.language) const language = languageSelect.find((l) => l.value === user.language)
return ( return (
@@ -90,18 +108,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) {
<Body color="burgundy" textTransform="bold"> <Body color="burgundy" textTransform="bold">
{intl.formatMessage({ id: "Address" })} {intl.formatMessage({ id: "Address" })}
</Body> </Body>
<Body color="burgundy"> <Body color="burgundy">{addressOutput}</Body>
{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>
</div> </div>
<div className={styles.item}> <div className={styles.item}>
<LockIcon color="burgundy" /> <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 * as Sentry from "@sentry/nextjs"
import { import { useParams, useRouter, useSearchParams } from "next/navigation"
useParams,
usePathname,
useRouter,
useSearchParams,
} from "next/navigation"
import { startTransition, useEffect, useRef } from "react" import { startTransition, useEffect, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { login } from "@/constants/routes/handleAuth" import { login } from "@/constants/routes/handleAuth"
import { SESSION_EXPIRED } from "@/server/errors/trpc" import { SESSION_EXPIRED } from "@/server/errors/trpc"
import { findLang } from "@/utils/languages"
import styles from "./error.module.css" import styles from "./error.module.css"
import type { LangParams } from "@/types/params" import type { LangParams } from "@/types/params"
@@ -63,13 +57,10 @@ export default function Error({
currentSearchParamsRef.current = currentSearchParams currentSearchParamsRef.current = currentSearchParams
}, [searchParams, reset, router]) }, [searchParams, reset, router])
const pathname = usePathname()
const lang = findLang(pathname)
return ( return (
<section className={styles.layout}> <section className={styles.layout}>
<div className={styles.content}> <div className={styles.content}>
{lang}: {intl.formatMessage({ id: "Something went wrong!" })} {intl.formatMessage({ id: "Something went wrong!" })}
</div> </div>
</section> </section>
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,12 +22,19 @@ export default function Row({ transaction }: RowProps) {
const pathName = usePathname() const pathName = usePathname()
const isWebview = webviews.includes(pathName) 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 = let description =
transaction.hotelName && transaction.city transaction.hotelName && transaction.city
? `${transaction.hotelName}, ${transaction.city} ${nightString}` ? `${transaction.hotelName}, ${transaction.city} ${nightsMsg}`
: `${nightString}` : `${nightsMsg}`
switch (transaction.type) { switch (transaction.type) {
case Transactions.rewardType.stay: 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" import type { ClientTableProps } from "@/types/components/myPages/myPage/earnAndBurn"
const tableHeadings = [
"Points",
"Description",
"Booking number",
"Arrival date",
]
export default function ClientTable({ transactions }: ClientTableProps) { export default function ClientTable({ transactions }: ClientTableProps) {
const intl = useIntl() const intl = useIntl()
const tableHeadings = [
intl.formatMessage({ id: "Points" }),
intl.formatMessage({ id: "Description" }),
intl.formatMessage({ id: "Booking number" }),
intl.formatMessage({ id: "Arrival date" }),
]
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Table> <Table>
@@ -28,9 +28,7 @@ export default function ClientTable({ transactions }: ClientTableProps) {
<Table.TR> <Table.TR>
{tableHeadings.map((heading) => ( {tableHeadings.map((heading) => (
<Table.TH key={heading}> <Table.TH key={heading}>
<Body textTransform="bold"> <Body textTransform="bold">{heading}</Body>
{intl.formatMessage({ id: heading })}
</Body>
</Table.TH> </Table.TH>
))} ))}
</Table.TR> </Table.TR>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,8 +5,6 @@ import { getIntl } from "@/i18n"
import styles from "./openingHours.module.css" import styles from "./openingHours.module.css"
import type { OpeningHoursProps } from "@/types/components/hotelPage/sidepeek/openingHours" 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({ export default async function OpeningHours({
openingHours, openingHours,
@@ -16,45 +14,83 @@ export default async function OpeningHours({
}: OpeningHoursProps) { }: OpeningHoursProps) {
const intl = await getIntl() const intl = await getIntl()
const closed = intl.formatMessage({ id: "Closed" }) const closedMsg = intl.formatMessage({ id: "Closed" })
const alwaysOpen = intl.formatMessage({ id: "Always open" }) const alwaysOpenMsg = intl.formatMessage({ id: "Always open" })
const days: (keyof typeof openingHours)[] = [ // In order
DaysEnum.Monday, const weekdayDefinitions = [
DaysEnum.Tuesday, {
DaysEnum.Wednesday, key: "monday",
DaysEnum.Thursday, label: intl.formatMessage({ id: "monday" }),
DaysEnum.Friday, },
DaysEnum.Saturday, {
DaysEnum.Sunday, 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) => { let rangeWeekdays: string[] = []
const today = openingHours[day] as RestaurantOpeningHoursDay let rangeValue = ""
let key: string 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) { if (weekday.alwaysOpen) {
key = closed newValue = alwaysOpenMsg
} else if (today.alwaysOpen) { } else if (weekday.isClosed) {
key = alwaysOpen newValue = closedMsg
} else { } else if (weekday.openingTime && weekday.closingTime) {
key = `${today.openingTime}-${today.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 ( return (
@@ -62,10 +98,10 @@ export default async function OpeningHours({
<Body textTransform="bold" asChild> <Body textTransform="bold" asChild>
<h5>{heading ?? openingHours.name}</h5> <h5>{heading ?? openingHours.name}</h5>
</Body> </Body>
{Object.entries(groupedOpeningHours).map(([time, days]) => { {groupedOpeningHours.map((groupedOpeningHour) => {
return ( return (
<Body color="uiTextHighContrast" key={time}> <Body color="uiTextHighContrast" key={groupedOpeningHour}>
{`${formatDayInterval(days)}: ${time}`} {groupedOpeningHour}
</Body> </Body>
) )
})} })}

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,10 @@ export default function TabNavigation({
hash: HotelHashValues.overview, hash: HotelHashValues.overview,
text: intl.formatMessage({ id: "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, hash: HotelHashValues.restaurant,
text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }), text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }),

View File

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

View File

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

View File

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

View File

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

View File

@@ -164,9 +164,11 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) {
{/* TODO: Update copy once ready */} {/* TODO: Update copy once ready */}
<Body> <Body>
{intl.formatMessage<React.ReactNode>( {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 <Link
variant="underscored" variant="underscored"
color="peach80" color="peach80"

View File

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

View File

@@ -137,6 +137,38 @@ function Trigger({
}) { }) {
const intl = useIntl() 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 ( return (
<Button <Button
className={`${className} ${styles.btn}`} className={`${className} ${styles.btn}`}
@@ -144,29 +176,7 @@ function Trigger({
onPress={triggerFn} onPress={triggerFn}
> >
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
<span> <span>{parts.join(", ")}</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>
</Body> </Body>
</Button> </Button>
) )

View File

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

View File

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

View File

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

View File

@@ -36,10 +36,10 @@ export default function Confirmation({
<Alert <Alert
type={AlertTypeEnum.Info} type={AlertTypeEnum.Info}
heading={intl.formatMessage({ heading={intl.formatMessage({
id: "booking.confirmation.membershipInfo.heading", id: "Failed to verify membership",
})} })}
text={intl.formatMessage({ 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 intl = useIntl()
const text = intl.formatMessage<React.ReactNode>( 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) => ( emailLink: (str) => (
<Link color="burgundy" href="#" textDecoration="underline"> <Link color="burgundy" href="#" textDecoration="underline">
@@ -57,7 +59,7 @@ export default function Header({
<header className={styles.header}> <header className={styles.header}>
<hgroup className={styles.hgroup}> <hgroup className={styles.hgroup}>
<Title as="h2" color="red" textTransform="uppercase" type="h2"> <Title as="h2" color="red" textTransform="uppercase" type="h2">
{intl.formatMessage({ id: "booking.confirmation.title" })} {intl.formatMessage({ id: "Booking confirmation" })}
</Title> </Title>
<Title as="h2" color="burgundy" textTransform="uppercase" type="h1"> <Title as="h2" color="burgundy" textTransform="uppercase" type="h1">
{hotel.name} {hotel.name}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,10 +89,10 @@ export default function JoinScandicFriendsCard({
<Footnote color="uiTextPlaceholder"> <Footnote color="uiTextPlaceholder">
{intl.formatMessage<React.ReactNode>( {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 <Link
variant="default" variant="default"
textDecoration="underline" textDecoration="underline"

View File

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

View File

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

View File

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

View File

@@ -160,7 +160,7 @@ export default function PaymentClient({
(errorMessage: string) => { (errorMessage: string) => {
toast.error( toast.error(
intl.formatMessage({ 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") const currentPaymentMethod = methods.getValues("paymentMethod")
@@ -312,10 +312,6 @@ export default function PaymentClient({
return <LoadingSpinner /> return <LoadingSpinner />
} }
const guaranteeing = intl.formatMessage({ id: "guaranteeing" })
const paying = intl.formatMessage({ id: "paying" })
const paymentVerb = mustBeGuaranteed ? guaranteeing : paying
return ( return (
<> <>
<FormProvider {...methods}> <FormProvider {...methods}>
@@ -387,11 +383,10 @@ export default function PaymentClient({
<Caption> <Caption>
{intl.formatMessage<React.ReactNode>( {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, termsAndConditionsLink: (str) => (
termsLink: (str) => (
<Link <Link
className={styles.link} className={styles.link}
variant="underscored" variant="underscored"
@@ -401,7 +396,7 @@ export default function PaymentClient({
{str} {str}
</Link> </Link>
), ),
privacyLink: (str) => ( privacyPolicyLink: (str) => (
<Link <Link
className={styles.link} className={styles.link}
variant="underscored" variant="underscored"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,18 @@ export default async function HotelInfoCard({
</Title> </Title>
<div className={styles.hotelAddressDescription}> <div className={styles.hotelAddressDescription}>
<Caption color="uiTextMediumContrast"> <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> </Caption>
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{hotelAttributes.hotelContent.texts.descriptions.medium} {hotelAttributes.hotelContent.texts.descriptions.medium}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ export default function Initial({ totalSurprises, onOpen }: InitialProps) {
<> <>
{intl.formatMessage<React.ReactNode>( {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, amount: totalSurprises,
@@ -52,7 +52,7 @@ export default function Initial({ totalSurprises, onOpen }: InitialProps) {
> >
{intl.formatMessage( {intl.formatMessage(
{ {
id: "Open gift(s)", id: "Open {amount, plural, one {gift} other {gifts}}",
}, },
{ amount: totalSurprises } { amount: totalSurprises }
)} )}

View File

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

View File

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

View File

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

View File

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

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