feat: add next-intl and configure middleware, locales

feature/next-intl
RaviAnand Mohabir 4 months ago
parent c68f2934da
commit bae8182970

@ -0,0 +1,6 @@
{
"general": {
"home": "Home",
"getStarted": "Get started by editing"
}
}

@ -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));

@ -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",

@ -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 (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);

@ -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 (
<div className={styles.page}>
<main className={styles.main}>
@ -15,7 +18,7 @@ export default function Home() {
/>
<ol>
<li>
Get started by editing <code>src/app/page.tsx</code>.
{t("getStarted")} <code>src/app/page.tsx</code>.
</li>
<li>Save and see your changes instantly.</li>
</ol>

@ -0,0 +1,3 @@
import { Locale } from "@/i18n/config";
export type Params = { locale: Locale };

@ -1,5 +1 @@
export const importMap = {
}
export const importMap = {};

@ -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<HTMLButtonElement, ButtonProps>((props, ref) => {
const { loading, disabled, loadingText, children, ...rest } = props
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { loading, disabled, loadingText, children, ...rest } = props;
const trulyDisabled = loading || disabled
const trulyDisabled = loading || disabled;
return (
<StyledButton disabled={trulyDisabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<ButtonSpinner />
<styled.span opacity={0}>{children}</styled.span>
</>
) : loadingText ? (
loadingText
) : (
children
)}
</StyledButton>
)
})
return (
<StyledButton disabled={trulyDisabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<ButtonSpinner />
<styled.span opacity={0}>{children}</styled.span>
</>
) : loadingText ? (
loadingText
) : (
children
)}
</StyledButton>
);
}
);
Button.displayName = 'Button'
Button.displayName = "Button";
const ButtonSpinner = () => (
<Center inline position="absolute" transform="translate(-50%, -50%)" top="50%" insetStart="50%">
<Center
inline
position="absolute"
transform="translate(-50%, -50%)"
top="50%"
insetStart="50%"
>
<Spinner colorPalette="gray" />
</Center>
)
);

@ -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<typeof Button>
export const Button = styled(ark.button, button)
export type ButtonProps = ComponentProps<typeof Button>;
export const Button = styled(ark.button, button);

@ -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<string, unknown>
type Props = Record<string, unknown>;
type Recipe = {
(props?: Props): Props
splitVariantProps: (props: Props) => [Props, Props]
}
type Slot<R extends Recipe> = keyof ReturnType<R>
type Options = { forwardProps?: string[] }
(props?: Props): Props;
splitVariantProps: (props: Props) => [Props, Props];
};
type Slot<R extends Recipe> = keyof ReturnType<R>;
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 = <R extends Recipe>(recipe: R) => {
const StyleContext = createContext<Record<Slot<R>, string> | null>(null)
const StyleContext = createContext<Record<Slot<R>, string> | null>(null);
const withRootProvider = <P extends {}>(Component: ElementType) => {
const StyledComponent = (props: P) => {
const [variantProps, otherProps] = recipe.splitVariantProps(props)
const slotStyles = recipe(variantProps) as Record<Slot<R>, string>
const [variantProps, otherProps] = recipe.splitVariantProps(props);
const slotStyles = recipe(variantProps) as Record<Slot<R>, string>;
return (
<StyleContext.Provider value={slotStyles}>
<Component {...otherProps} />
</StyleContext.Provider>
)
}
return StyledComponent
}
);
};
return StyledComponent;
};
const withProvider = <T, P extends { className?: string | undefined }>(
Component: ElementType,
slot: Slot<R>,
options?: Options,
options?: Options
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> => {
const StyledComponent = styled(
Component,
{},
{
shouldForwardProp: (prop, variantKeys) => shouldForwardProp(prop, variantKeys, options),
},
) as StyledComponent<ElementType>
shouldForwardProp: (prop, variantKeys) =>
shouldForwardProp(prop, variantKeys, options),
}
) as StyledComponent<ElementType>;
const StyledSlotProvider = forwardRef<T, P>((props, ref) => {
const [variantProps, otherProps] = recipe.splitVariantProps(props)
const slotStyles = recipe(variantProps) as Record<Slot<R>, string>
const [variantProps, otherProps] = recipe.splitVariantProps(props);
const slotStyles = recipe(variantProps) as Record<Slot<R>, string>;
return (
<StyleContext.Provider value={slotStyles}>
@ -62,34 +68,38 @@ export const createStyleContext = <R extends Recipe>(recipe: R) => {
className={cx(slotStyles?.[slot], props.className)}
/>
</StyleContext.Provider>
)
})
);
});
// @ts-expect-error
StyledSlotProvider.displayName = Component.displayName || Component.name
StyledSlotProvider.displayName = Component.displayName || Component.name;
return StyledSlotProvider
}
return StyledSlotProvider;
};
const withContext = <T, P extends { className?: string | undefined }>(
Component: ElementType,
slot: Slot<R>,
slot: Slot<R>
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> => {
const StyledComponent = styled(Component)
const StyledComponent = styled(Component);
const StyledSlotComponent = forwardRef<T, P>((props, ref) => {
const slotStyles = useContext(StyleContext)
const slotStyles = useContext(StyleContext);
return (
<StyledComponent {...props} ref={ref} className={cx(slotStyles?.[slot], props.className)} />
)
})
<StyledComponent
{...props}
ref={ref}
className={cx(slotStyles?.[slot], props.className)}
/>
);
});
// @ts-expect-error
StyledSlotComponent.displayName = Component.displayName || Component.name
StyledSlotComponent.displayName = Component.displayName || Component.name;
return StyledSlotComponent
}
return StyledSlotComponent;
};
return {
withRootProvider,
withProvider,
withContext,
}
}
};
};

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

@ -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";

@ -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);

@ -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",
},
},
});

@ -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).*)"],
};

@ -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"

Loading…
Cancel
Save