Merged in feat/sw-2333-package-and-sas-i18n (pull request #2538)

feat(SW-2333): I18n for multiple apps and packages

* Set upp i18n in partner-sas

* Adapt lokalise workflow to monorepo

* Fix layout props


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-07-10 07:00:03 +00:00
parent 2c0e8965e2
commit 233c685e52
31 changed files with 48133 additions and 210 deletions

View File

@@ -1,9 +1,12 @@
/* eslint-disable formatjs/no-literal-string-in-jsx */
"use client"
import { useIntl } from "react-intl"
import { Lang } from "@scandic-hotels/common/constants/language"
import { trpc } from "@scandic-hotels/trpc/client"
export function ClientComponent() {
const intl = useIntl()
const { data, isLoading } = trpc.autocomplete.destinations.useQuery({
lang: Lang.en,
includeTypes: ["hotels"],
@@ -15,6 +18,10 @@ export function ClientComponent() {
<p>client component</p>
<p>Data: {JSON.stringify(data?.hits?.hotels[0]?.name)}</p>
<p>Is loading: {isLoading ? "Yes" : "No"}</p>
<p>Translated text: </p>
{intl.formatMessage({
defaultMessage: "All-day breakfast",
})}
</div>
)
}

View File

@@ -3,8 +3,13 @@ import "@scandic-hotels/design-system/fonts.css"
import "@/public/_static/css/design-system-new-deprecated.css"
import "./globals.css"
import { Lang } from "@scandic-hotels/common/constants/language"
import { TrpcProvider } from "@scandic-hotels/trpc/Provider"
import { getMessages } from "../i18n"
import ClientIntlProvider from "../i18n/Provider"
import { setLang } from "../i18n/serverContext"
import type { Metadata } from "next"
export const metadata: Metadata = {
@@ -12,11 +17,24 @@ export const metadata: Metadata = {
description: "Generated by create next app",
}
export default function RootLayout({
children,
}: Readonly<{
type LangParams = {
lang: Lang
}
type RootLayoutProps = {
children: React.ReactNode
}>) {
params: Promise<LangParams>
}
export default async function RootLayout(props: RootLayoutProps) {
// const params = await props.params
const params = { lang: Lang.sv }
const { children } = props
setLang(params.lang)
const messages = await getMessages(params.lang)
return (
<html lang="en">
<head>
@@ -26,8 +44,14 @@ export default function RootLayout({
<link rel="stylesheet" href="/_static/css/scandic.css" />
</head>
<body className="scandic">
{/* TODO handle onError */}
<TrpcProvider>{children}</TrpcProvider>
<ClientIntlProvider
defaultLocale={Lang.en}
locale={params.lang}
messages={messages}
>
{/* TODO handle onError */}
<TrpcProvider>{children}</TrpcProvider>
</ClientIntlProvider>
</body>
</html>
)

View File

@@ -4,11 +4,13 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { serverClient } from "@/lib/trpc"
import { getIntl } from "../i18n"
import { ClientComponent } from "./ClientComponent"
import styles from "./page.module.css"
export default async function Home() {
const intl = await getIntl()
const caller = await serverClient()
const destinations = await caller.autocomplete.destinations({
lang: Lang.en,
@@ -24,6 +26,9 @@ export default async function Home() {
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<p>hello world with data: {hotel}</p>
</Typography>
<Typography>
<p>{intl.formatMessage({ defaultMessage: "Map of the city" })}</p>
</Typography>
<hr />
<ClientComponent />
<hr />

View File

@@ -0,0 +1,43 @@
"use client"
import { type IntlConfig, IntlProvider } from "react-intl"
import { logger } from "@scandic-hotels/common/logger"
type ClientIntlProviderProps = React.PropsWithChildren<
Pick<IntlConfig, "defaultLocale" | "locale" | "messages">
>
const logged: Record<string, boolean> = {}
export default function ClientIntlProvider({
children,
locale,
defaultLocale,
messages,
}: ClientIntlProviderProps) {
return (
<IntlProvider
locale={locale}
defaultLocale={defaultLocale}
messages={messages}
onError={(err) => {
let msg = err.message
if (err.code === "MISSING_TRANSLATION") {
const id = err.descriptor?.id
if (id) {
msg = id
}
}
if (!logged[msg]) {
logged[msg] = true
logger.warn("IntlProvider", err)
}
}}
>
{children}
</IntlProvider>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"WcBpRV": [
{
"type": 1,
"value": "roomSizeMin"
},
{
"type": 0,
"value": "-"
},
{
"type": 1,
"value": "roomSizeMax"
},
{
"type": 0,
"value": " m²"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
import "server-only"
import { createIntl, createIntlCache } from "@formatjs/intl"
import { Lang } from "@scandic-hotels/common/constants/language"
import { getLang } from "./serverContext"
import type { IntlShape } from "react-intl"
const cache = createIntlCache()
const instances: Partial<Record<Lang, IntlShape>> = {}
export async function getMessages(lang: Lang): Promise<Record<string, string>> {
return (await import(`./dictionaries/${lang}.json`)).default
}
export async function getIntl(options?: { lang: Lang | undefined }) {
const lang = options?.lang || (await getLang())
if (!instances[lang]) {
const messages = await getMessages(lang)
instances[lang] = createIntl<React.ReactNode>(
{
defaultLocale: Lang.en,
locale: lang,
messages,
},
cache
)
}
return instances[lang]
}

View File

@@ -0,0 +1,34 @@
import "server-only"
import { headers } from "next/headers"
import { cache } from "react"
import { Lang } from "@scandic-hotels/common/constants/language"
import { languageSchema } from "@scandic-hotels/common/utils/languages"
const getRef = cache(() => ({ current: undefined as Lang | undefined }))
/**
* Set the language for the current request
*
* It works kind of like React's context,
* but on the server side, per request.
*
* @param newLang
*/
export function setLang(newLang: Lang) {
const parseResult = languageSchema.safeParse(newLang)
getRef().current = parseResult.success ? parseResult.data : Lang.en
}
/**
* Get the global language set for the current request
*/
export async function getLang(): Promise<Lang> {
const contextLang = getRef().current
const headersList = await headers()
const headerLang = headersList.get("x-lang") as Lang
const l = contextLang || headerLang || Lang.en
return l
}

View File

@@ -27,6 +27,17 @@ const nextConfig: NextConfig = {
return config
},
experimental: {
swcPlugins: [
[
"@swc/plugin-formatjs",
{
ast: true,
},
],
],
},
}
export default Sentry.withSentryConfig(nextConfig, {

View File

@@ -15,14 +15,18 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@formatjs/intl": "^3.1.6",
"@netlify/plugin-nextjs": "^5.11.2",
"@scandic-hotels/booking-flow": "workspace:*",
"@scandic-hotels/design-system": "workspace:*",
"@scandic-hotels/trpc": "workspace:*",
"@sentry/nextjs": "^8.41.0",
"@swc/plugin-formatjs": "^3.2.2",
"next": "15.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-intl": "^7.1.11",
"server-only": "^0.0.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
@@ -35,6 +39,7 @@
"@types/react-dom": "19.1.0",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"babel-plugin-formatjs": "^10.5.39",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"eslint-plugin-formatjs": "^5.3.1",