diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..d012600 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,6 @@ +{ + "general": { + "home": "Home", + "getStarted": "Get started by editing" + } +} diff --git a/next.config.mts b/next.config.mts index 991d1db..ef0f740 100644 --- a/next.config.mts +++ b/next.config.mts @@ -1,8 +1,11 @@ import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; import { withPayload } from "@payloadcms/next/withPayload"; +const withNextIntl = createNextIntlPlugin("./src/i18n/config.ts"); + const nextConfig: NextConfig = { /* config options here */ }; -export default withPayload(nextConfig); +export default withNextIntl(withPayload(nextConfig)); diff --git a/package.json b/package.json index 6db265c..da50c05 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "graphql": "^16.9.0", "lucide-react": "^0.436.0", "next": "15.0.0-canary.132", + "next-intl": "^3.18.0", "payload": "^3.0.0-beta.94", "react": "19.0.0-rc-eb3ad065-20240822", "react-dom": "19.0.0-rc-eb3ad065-20240822", diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/[locale]/layout.tsx similarity index 56% rename from src/app/(frontend)/layout.tsx rename to src/app/(frontend)/[locale]/layout.tsx index dca06ae..a4cd54f 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/[locale]/layout.tsx @@ -1,14 +1,17 @@ +import "../globals.css"; + import type { Metadata } from "next"; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; import localFont from "next/font/local"; -import "./globals.css"; const geistSans = localFont({ - src: "./fonts/GeistVF.woff", + src: "../fonts/GeistVF.woff", variable: "--font-geist-sans", weight: "100 900", }); const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", + src: "../fonts/GeistMonoVF.woff", variable: "--font-geist-mono", weight: "100 900", }); @@ -18,15 +21,21 @@ export const metadata: Metadata = { description: "Generated by create next app", }; -export default function RootLayout({ +export default async function RootLayout({ children, + params: { locale }, }: Readonly<{ children: React.ReactNode; + params: { locale: string }; }>) { + const messages = await getMessages(); + return ( - {children} + + {children} + ); diff --git a/src/app/(frontend)/page.module.css b/src/app/(frontend)/[locale]/page.module.css similarity index 100% rename from src/app/(frontend)/page.module.css rename to src/app/(frontend)/[locale]/page.module.css diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/[locale]/page.tsx similarity index 94% rename from src/app/(frontend)/page.tsx rename to src/app/(frontend)/[locale]/page.tsx index 1c2ca6b..6947d0e 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/[locale]/page.tsx @@ -1,7 +1,10 @@ import Image from "next/image"; import styles from "./page.module.css"; +import { useTranslations } from "next-intl"; export default function Home() { + const t = useTranslations("HomePage"); + return (
@@ -15,7 +18,7 @@ export default function Home() { />
  1. - Get started by editing src/app/page.tsx. + {t("getStarted")} src/app/page.tsx.
  2. Save and see your changes instantly.
diff --git a/src/app/(frontend)/[locale]/params.ts b/src/app/(frontend)/[locale]/params.ts new file mode 100644 index 0000000..54637bd --- /dev/null +++ b/src/app/(frontend)/[locale]/params.ts @@ -0,0 +1,3 @@ +import { Locale } from "@/i18n/config"; + +export type Params = { locale: Locale }; diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index a3bc0e8..73849ab 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,5 +1 @@ - - -export const importMap = { - -} +export const importMap = {}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b449c07..8fbfb6b 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,40 +1,51 @@ -import { forwardRef } from 'react' -import { Center, styled } from 'styled-system/jsx' -import { Spinner } from './spinner' -import { Button as StyledButton, type ButtonProps as StyledButtonProps } from './styled/button' +import { forwardRef } from "react"; +import { Center, styled } from "styled-system/jsx"; +import { Spinner } from "./spinner"; +import { + Button as StyledButton, + type ButtonProps as StyledButtonProps, +} from "./styled/button"; interface ButtonLoadingProps { - loading?: boolean - loadingText?: React.ReactNode + loading?: boolean; + loadingText?: React.ReactNode; } export interface ButtonProps extends StyledButtonProps, ButtonLoadingProps {} -export const Button = forwardRef((props, ref) => { - const { loading, disabled, loadingText, children, ...rest } = props +export const Button = forwardRef( + (props, ref) => { + const { loading, disabled, loadingText, children, ...rest } = props; - const trulyDisabled = loading || disabled + const trulyDisabled = loading || disabled; - return ( - - {loading && !loadingText ? ( - <> - - {children} - - ) : loadingText ? ( - loadingText - ) : ( - children - )} - - ) -}) + return ( + + {loading && !loadingText ? ( + <> + + {children} + + ) : loadingText ? ( + loadingText + ) : ( + children + )} + + ); + } +); -Button.displayName = 'Button' +Button.displayName = "Button"; const ButtonSpinner = () => ( -
+
-) +); diff --git a/src/components/ui/styled/button.tsx b/src/components/ui/styled/button.tsx index 383d5f9..0da7839 100644 --- a/src/components/ui/styled/button.tsx +++ b/src/components/ui/styled/button.tsx @@ -1,7 +1,7 @@ -import { ark } from '@ark-ui/react/factory' -import { styled } from 'styled-system/jsx' -import { button } from 'styled-system/recipes' -import type { ComponentProps } from 'styled-system/types' +import { ark } from "@ark-ui/react/factory"; +import { styled } from "styled-system/jsx"; +import { button } from "styled-system/recipes"; +import type { ComponentProps } from "styled-system/types"; -export type ButtonProps = ComponentProps -export const Button = styled(ark.button, button) +export type ButtonProps = ComponentProps; +export const Button = styled(ark.button, button); diff --git a/src/components/ui/styled/utils/create-style-context.tsx b/src/components/ui/styled/utils/create-style-context.tsx index f264aba..8726e7a 100644 --- a/src/components/ui/styled/utils/create-style-context.tsx +++ b/src/components/ui/styled/utils/create-style-context.tsx @@ -6,53 +6,59 @@ import { createContext, forwardRef, useContext, -} from 'react' -import { cx } from 'styled-system/css' -import { type StyledComponent, isCssProperty, styled } from 'styled-system/jsx' +} from "react"; +import { cx } from "styled-system/css"; +import { type StyledComponent, isCssProperty, styled } from "styled-system/jsx"; -type Props = Record +type Props = Record; type Recipe = { - (props?: Props): Props - splitVariantProps: (props: Props) => [Props, Props] -} -type Slot = keyof ReturnType -type Options = { forwardProps?: string[] } + (props?: Props): Props; + splitVariantProps: (props: Props) => [Props, Props]; +}; +type Slot = keyof ReturnType; +type Options = { forwardProps?: string[] }; -const shouldForwardProp = (prop: string, variantKeys: string[], options: Options = {}) => - options.forwardProps?.includes(prop) || (!variantKeys.includes(prop) && !isCssProperty(prop)) +const shouldForwardProp = ( + prop: string, + variantKeys: string[], + options: Options = {} +) => + options.forwardProps?.includes(prop) || + (!variantKeys.includes(prop) && !isCssProperty(prop)); export const createStyleContext = (recipe: R) => { - const StyleContext = createContext, string> | null>(null) + const StyleContext = createContext, string> | null>(null); const withRootProvider =

(Component: ElementType) => { const StyledComponent = (props: P) => { - const [variantProps, otherProps] = recipe.splitVariantProps(props) - const slotStyles = recipe(variantProps) as Record, string> + const [variantProps, otherProps] = recipe.splitVariantProps(props); + const slotStyles = recipe(variantProps) as Record, string>; return ( - ) - } - return StyledComponent - } + ); + }; + return StyledComponent; + }; const withProvider = ( Component: ElementType, slot: Slot, - options?: Options, + options?: Options ): ForwardRefExoticComponent & RefAttributes> => { const StyledComponent = styled( Component, {}, { - shouldForwardProp: (prop, variantKeys) => shouldForwardProp(prop, variantKeys, options), - }, - ) as StyledComponent + shouldForwardProp: (prop, variantKeys) => + shouldForwardProp(prop, variantKeys, options), + } + ) as StyledComponent; const StyledSlotProvider = forwardRef((props, ref) => { - const [variantProps, otherProps] = recipe.splitVariantProps(props) - const slotStyles = recipe(variantProps) as Record, string> + const [variantProps, otherProps] = recipe.splitVariantProps(props); + const slotStyles = recipe(variantProps) as Record, string>; return ( @@ -62,34 +68,38 @@ export const createStyleContext = (recipe: R) => { className={cx(slotStyles?.[slot], props.className)} /> - ) - }) + ); + }); // @ts-expect-error - StyledSlotProvider.displayName = Component.displayName || Component.name + StyledSlotProvider.displayName = Component.displayName || Component.name; - return StyledSlotProvider - } + return StyledSlotProvider; + }; const withContext = ( Component: ElementType, - slot: Slot, + slot: Slot ): ForwardRefExoticComponent & RefAttributes> => { - const StyledComponent = styled(Component) + const StyledComponent = styled(Component); const StyledSlotComponent = forwardRef((props, ref) => { - const slotStyles = useContext(StyleContext) + const slotStyles = useContext(StyleContext); return ( - - ) - }) + + ); + }); // @ts-expect-error - StyledSlotComponent.displayName = Component.displayName || Component.name + StyledSlotComponent.displayName = Component.displayName || Component.name; - return StyledSlotComponent - } + return StyledSlotComponent; + }; return { withRootProvider, withProvider, withContext, - } -} + }; +}; diff --git a/src/i18n/config.ts b/src/i18n/config.ts new file mode 100644 index 0000000..8862010 --- /dev/null +++ b/src/i18n/config.ts @@ -0,0 +1,13 @@ +import { Locale } from "@/i18n/locales"; +import { getRequestConfig } from "next-intl/server"; +import { notFound } from "next/navigation"; +import { routing } from "@/i18n/routing"; + +export default getRequestConfig(async ({ locale }) => { + // Validate that the incoming `locale` parameter is valid + if (!routing.locales.includes(locale as Locale)) notFound(); + + return { + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/src/i18n/locales.ts b/src/i18n/locales.ts new file mode 100644 index 0000000..bcd9efe --- /dev/null +++ b/src/i18n/locales.ts @@ -0,0 +1,24 @@ +export const locales = [ + { + label: { + de: "Deutsch", + en: "German", + fr: "Allemand", + it: "Tedesco", + }, + code: "de", + }, + { + label: { + de: "Englisch", + en: "English", + it: "Inglese", + fr: "Anglais", + }, + code: "en", + }, +] as const; + +export type Locale = (typeof locales)[number]["code"]; + +export const defaultLocale: Locale = "en"; diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts new file mode 100644 index 0000000..4dfd5f1 --- /dev/null +++ b/src/i18n/navigation.ts @@ -0,0 +1,8 @@ +// Lightweight wrappers around Next.js' navigation APIs + +import { createLocalizedPathnamesNavigation } from "next-intl/navigation"; +import { routing } from "@/i18n/routing"; + +// that will consider the routing configuration +export const { Link, redirect, usePathname, useRouter } = + createLocalizedPathnamesNavigation(routing); diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts new file mode 100644 index 0000000..23d4879 --- /dev/null +++ b/src/i18n/routing.ts @@ -0,0 +1,20 @@ +import { defaultLocale, locales } from "./locales"; + +import { defineRouting } from "next-intl/routing"; + +export const routing = defineRouting({ + // A list of all locales that are supported + locales: locales.map(({ code }) => code), + + // Used when no locale matches + defaultLocale, + + localePrefix: "as-needed", + + pathnames: { + "/about": { + en: "/about", + de: "/ueber-uns", + }, + }, +}); diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..e220572 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,8 @@ +import createMiddleware from "next-intl/middleware"; +import { routing } from "@/i18n/routing"; + +export default createMiddleware(routing); + +export const config = { + matcher: ["/((?!api|admin|static|.*\\..*|_next|favicon.ico|robots.txt).*)"], +}; diff --git a/yarn.lock b/yarn.lock index c0366c6..ce997c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1168,6 +1168,45 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.7.tgz#d0ece53ce99ab5a8e37ebdfe5e32452a2bfc073e" integrity sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA== +"@formatjs/ecma402-abstract@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz#39197ab90b1c78b7342b129a56a7acdb8f512e17" + integrity sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g== + dependencies: + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/fast-memoize@2.2.0", "@formatjs/fast-memoize@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz#33bd616d2e486c3e8ef4e68c99648c196887802b" + integrity sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA== + dependencies: + tslib "^2.4.0" + +"@formatjs/icu-messageformat-parser@2.7.8": + version "2.7.8" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz#f6d7643001e9bb5930d812f1f9a9856f30fa0343" + integrity sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/icu-skeleton-parser" "1.8.2" + tslib "^2.4.0" + +"@formatjs/icu-skeleton-parser@1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz#2252c949ae84ee66930e726130ea66731a123c9f" + integrity sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + tslib "^2.4.0" + +"@formatjs/intl-localematcher@0.5.4", "@formatjs/intl-localematcher@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" + integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== + dependencies: + tslib "^2.4.0" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -5670,6 +5709,16 @@ internal-slot@^1.0.4, internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +intl-messageformat@^10.5.14: + version "10.5.14" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.14.tgz#e5bb373f8a37b88fbe647d7b941f3ab2a37ed00a" + integrity sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/fast-memoize" "2.2.0" + "@formatjs/icu-messageformat-parser" "2.7.8" + tslib "^2.4.0" + ip-address@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" @@ -6416,6 +6465,20 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +next-intl@^3.18.0: + version "3.18.0" + resolved "https://registry.yarnpkg.com/next-intl/-/next-intl-3.18.0.tgz#d8aa33b82a72d0a58e9ccf0a3a667c96b9642465" + integrity sha512-TpSwnBEHA+hEQH/bJJnDL/BkDdkSxABJv8f0rpAV6yPEw7zq/+tH84SHWzKLZYewfBBgI3yTtuGZYIIPk792pw== + dependencies: + "@formatjs/intl-localematcher" "^0.5.4" + negotiator "^0.6.3" + use-intl "^3.18.0" + next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -7836,6 +7899,14 @@ use-context-selector@2.0.0: resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-2.0.0.tgz#3b5dafec7aa947c152d4f0aa7f250e99a205df3d" integrity sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g== +use-intl@^3.18.0: + version "3.18.0" + resolved "https://registry.yarnpkg.com/use-intl/-/use-intl-3.18.0.tgz#f1b448e7da33aac7a0a00acdeb54a872e33fee00" + integrity sha512-38A07Lyu4VapNOckenvL0PhQYetOPz5s7qoAs2wvM4yLCiye+pX8352Jhjr9mxA430zYhrLTK4pilX9VdXlX3g== + dependencies: + "@formatjs/fast-memoize" "^2.2.0" + intl-messageformat "^10.5.14" + use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"