diff --git a/examples/example-app-router-patterns/.gitignore b/examples/example-app-router-patterns/.gitignore new file mode 100644 index 000000000..04239e7d0 --- /dev/null +++ b/examples/example-app-router-patterns/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/.next/ +.DS_Store +tsconfig.tsbuildinfo diff --git a/examples/example-app-router-patterns/README.md b/examples/example-app-router-patterns/README.md new file mode 100644 index 000000000..6c3beabe0 --- /dev/null +++ b/examples/example-app-router-patterns/README.md @@ -0,0 +1,9 @@ +# example-app-router-patterns + +This example demonstrates various use cases and patterns for using `next-intl` with the App Router. + +## Deploy your own + +By deploying to [Vercel](https://vercel.com), you can check out the example in action. Note that you'll be prompted to create a new GitHub repository as part of this, allowing you to make subsequent changes. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/amannn/next-intl/tree/main/examples/example-app-router-patterns) diff --git a/examples/example-app-router-patterns/eslint.config.mjs b/examples/example-app-router-patterns/eslint.config.mjs new file mode 100644 index 000000000..144292198 --- /dev/null +++ b/examples/example-app-router-patterns/eslint.config.mjs @@ -0,0 +1,22 @@ +import {dirname} from 'path'; +import {fileURLToPath} from 'url'; +import {FlatCompat} from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +const eslintConfig = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off' + } + } +]; + +export default eslintConfig; diff --git a/examples/example-app-router-patterns/next-env.d.ts b/examples/example-app-router-patterns/next-env.d.ts new file mode 100644 index 000000000..830fb594c --- /dev/null +++ b/examples/example-app-router-patterns/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/example-app-router-patterns/next.config.mjs b/examples/example-app-router-patterns/next.config.mjs new file mode 100644 index 000000000..4678774e6 --- /dev/null +++ b/examples/example-app-router-patterns/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/examples/example-app-router-patterns/package.json b/examples/example-app-router-patterns/package.json new file mode 100644 index 000000000..f35bd99ba --- /dev/null +++ b/examples/example-app-router-patterns/package.json @@ -0,0 +1,44 @@ +{ + "name": "example-app-router-patterns", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "eslint src && prettier src --check", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "clsx": "^2.1.1", + "next": "^15.5.0", + "next-intl": "^4.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.1.12" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@tailwindcss/postcss": "^4.1.12", + "@types/node": "^20.14.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "eslint": "9.11.1", + "eslint-config-next": "^15.5.0", + "postcss": "^8.5.3", + "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "^4.2.0", + "prettier-plugin-tailwindcss": "^0.6.14", + "typescript": "^5.5.3" + }, + "prettier": { + "singleQuote": true, + "bracketSpacing": false, + "trailingComma": "none", + "plugins": [ + "prettier-plugin-organize-imports", + "prettier-plugin-tailwindcss" + ] + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/example-app-router-patterns/postcss.config.mjs b/examples/example-app-router-patterns/postcss.config.mjs new file mode 100644 index 000000000..313840505 --- /dev/null +++ b/examples/example-app-router-patterns/postcss.config.mjs @@ -0,0 +1,6 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {} + } +}; +export default config; diff --git a/examples/example-app-router-patterns/src/app/design/DesignColorSwatch.tsx b/examples/example-app-router-patterns/src/app/design/DesignColorSwatch.tsx new file mode 100644 index 000000000..7f6222457 --- /dev/null +++ b/examples/example-app-router-patterns/src/app/design/DesignColorSwatch.tsx @@ -0,0 +1,15 @@ +import clsx from 'clsx'; + +type Props = { + className: string; + value: string; +}; + +export default function DesignColorSwatch({className, value}: Props) { + return ( +
+
+
{value}
+
+ ); +} diff --git a/examples/example-app-router-patterns/src/app/design/DesignSection.tsx b/examples/example-app-router-patterns/src/app/design/DesignSection.tsx new file mode 100644 index 000000000..433cbf251 --- /dev/null +++ b/examples/example-app-router-patterns/src/app/design/DesignSection.tsx @@ -0,0 +1,17 @@ +import {ReactNode} from 'react'; + +type Props = { + children: ReactNode; + title: string; +}; + +export default function DesignSection({children, title}: Props) { + return ( +
+

+ {title} +

+ {children} +
+ ); +} diff --git a/examples/example-app-router-patterns/src/app/design/page.tsx b/examples/example-app-router-patterns/src/app/design/page.tsx new file mode 100644 index 000000000..1fcd139d5 --- /dev/null +++ b/examples/example-app-router-patterns/src/app/design/page.tsx @@ -0,0 +1,170 @@ +// Documentation page for the next-intl design system + +import DesignColorSwatch from './DesignColorSwatch'; +import DesignSection from './DesignSection'; + +export default function DesignPage() { + return ( +
+
+

+ Design system +

+
+ +
+
+

+ Blue (primary) +

+
+ + + + + + + + + + +
+
+
+

Gray

+
+ + + + + + + + + + +
+
+
+
+ +
+
+
+

+ Title large +

+

+ Title normal +

+

Title description

+

+ Title caption +

+

+ Title small +

+

+ Headline +

+

+ Headline caption +

+

Body large

+

Body large muted

+

+ Body with{' '} + + a link + +

+

Body muted

+

Label

+

+ Label muted +

+

+ Inline code +

+
+
+
+
+

+ Title large +

+

+ Title normal +

+

Title description

+

+ Title caption +

+

+ Title small +

+

Headline

+

+ Headline caption +

+

Body large

+

Body large muted

+

+ Body with{' '} + + a link + +

+

Body muted

+

Label

+

+ Label muted +

+

+ Inline code +

+
+
+
+
+ +
+
+
+

+ Title caption +

+

+ Title normal +

+

+ Title description +

+
+
+
+
+

+ Title caption +

+

+ Title normal +

+

+ Title description +

+
+
+
+
+
+
+
+ ); +} diff --git a/examples/example-app-router-patterns/src/app/favicon.ico b/examples/example-app-router-patterns/src/app/favicon.ico new file mode 100644 index 000000000..4ddd8fff7 Binary files /dev/null and b/examples/example-app-router-patterns/src/app/favicon.ico differ diff --git a/examples/example-app-router-patterns/src/app/globals.css b/examples/example-app-router-patterns/src/app/globals.css new file mode 100644 index 000000000..73bb5b727 --- /dev/null +++ b/examples/example-app-router-patterns/src/app/globals.css @@ -0,0 +1,23 @@ +@import 'tailwindcss'; + +@theme { + --color-white: #ffffff; + + --color-gray-50: #f7f7f8; + --color-gray-100: #ebebef; + --color-gray-200: #d1d1db; + --color-gray-300: #a9a9bc; + --color-gray-400: #8a8aa3; + --color-gray-500: #6c6c89; + --color-gray-600: #55556d; + --color-gray-700: #3f3f50; + --color-gray-800: #282833; + --color-gray-900: #121217; + + --color-blue-300: #70d2ff; + --color-blue-700: #008fd6; + + --font-mono: + Monaco, ui-monospace, SFMono-Regular, Menlo, Consolas, Liberation Mono, + Courier New, monospace; +} diff --git a/examples/example-app-router-patterns/src/app/layout.tsx b/examples/example-app-router-patterns/src/app/layout.tsx new file mode 100644 index 000000000..f2c77bbad --- /dev/null +++ b/examples/example-app-router-patterns/src/app/layout.tsx @@ -0,0 +1,16 @@ +import {Inter} from 'next/font/google'; +import './globals.css'; +import {clsx} from 'clsx'; + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter' +}); + +export default function RootLayout({children}: LayoutProps<'/'>) { + return ( + + {children} + + ); +} diff --git a/examples/example-app-router-patterns/src/app/page.tsx b/examples/example-app-router-patterns/src/app/page.tsx new file mode 100644 index 000000000..f5638571b --- /dev/null +++ b/examples/example-app-router-patterns/src/app/page.tsx @@ -0,0 +1,5 @@ +import {redirect} from 'next/navigation'; + +export default function HomePage() { + redirect('/design'); +} diff --git a/examples/example-app-router-patterns/tsconfig.json b/examples/example-app-router-patterns/tsconfig.json new file mode 100644 index 000000000..1638156ff --- /dev/null +++ b/examples/example-app-router-patterns/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "allowArbitraryExtensions": true, + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + // See https://github.com/amannn/next-intl/pull/1509 + "declaration": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 000000000..0df02495b --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# local screenshots (not committed; used to share visuals) +/screenshots diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 000000000..e803f8df5 --- /dev/null +++ b/playground/README.md @@ -0,0 +1,25 @@ +# next-intl Playground + +A demo site that documents `next-intl` patterns. Built on Next.js 15 with locale-prefixed routes (`/[locale]/...`) and Code Hike for code samples. + +## Develop + +```bash +pnpm --filter playground dev +``` + +Open http://localhost:3000. + +## Test + +```bash +pnpm --filter playground e2e +``` + +## Add a new page + +1. Create `src/app/[locale]///page.tsx`, `content.mdx`, and a live demo component. +2. Add an entry to `src/lib/nav.ts`. +3. Add string keys to `messages/{en,de}.json`. + +See existing examples under `src/app/[locale]/translations/`. diff --git a/playground/components.json b/playground/components.json new file mode 100644 index 000000000..edcaef267 --- /dev/null +++ b/playground/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/playground/eslint.config.mjs b/playground/eslint.config.mjs new file mode 100644 index 000000000..719cea2b5 --- /dev/null +++ b/playground/eslint.config.mjs @@ -0,0 +1,25 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, +]; + +export default eslintConfig; diff --git a/playground/mdx-components.tsx b/playground/mdx-components.tsx new file mode 100644 index 000000000..49eea7a75 --- /dev/null +++ b/playground/mdx-components.tsx @@ -0,0 +1,6 @@ +import type { MDXComponents } from 'mdx/types'; +import { Code } from '@/components/code/code'; + +export function useMDXComponents(components: MDXComponents): MDXComponents { + return { Code, ...components }; +} diff --git a/playground/messages/de.json b/playground/messages/de.json new file mode 100644 index 000000000..ae960d1dc --- /dev/null +++ b/playground/messages/de.json @@ -0,0 +1,98 @@ +{ + "Layout": { + "title": "next-intl Playground", + "tagline": "Übersetzungen, Formatierung, Routing und Patterns mit Next.js.", + "examplesLabel": "Beispiele" + }, + "Nav": { + "translations": "Übersetzungen", + "formatting": "Formatierung", + "routing": "Routing", + "patterns": "Patterns", + "serverComponents": "Server-Komponenten", + "serverComponentsDesc": "Übersetzte Strings in async Server Components lesen — null Client-JS.", + "clientComponents": "Client-Komponenten", + "clientComponentsDesc": "Übersetzungen aus Client Components für interaktive Inhalte verwenden.", + "dates": "Datum & Uhrzeit", + "datesDesc": "Datum und Uhrzeit für die aktive Sprache mit useFormatter formatieren.", + "numbers": "Zahlen", + "numbersDesc": "Zahlen, Währungen und Prozente formatieren — Trennzeichen wechseln je nach Sprache.", + "explorer": "Explorer", + "explorerDesc": "useFormatter() mit Live-Optionen steuern und das Ergebnis sprachübergreifend vergleichen.", + "mixedRouting": "Gemischtes Routing", + "mixedRoutingDesc": "Mit und ohne Sprachpräfix — die drei localePrefix-Modi nebeneinander.", + "localeSwitcher": "Sprachumschalter", + "localeSwitcherDesc": "Aktuellen Pfad unter einer anderen Sprache erneut aufrufen und Params erhalten.", + "browserLanguage": "Browser-Sprache", + "browserLanguageDesc": "Bevorzugte Sprache der Nutzer:innen aus dem Accept-Language-Header via next-intl-Middleware ermitteln.", + "cacheComponents": "Cache Components", + "cacheComponentsDesc": "next-intl mit Next's Cache Components kombinieren — 'use cache', cacheLife, cacheTag.", + "soon": "demnächst" + }, + "ServerDemo": { + "title": "Server-Komponenten", + "greeting": "Hallo, Welt!" + }, + "ClientDemo": { + "title": "Client-Komponenten", + "label": "Dein Name", + "placeholder": "Frodo", + "greeting": "Hallo, {name}!" + }, + "DatesDemo": { + "dateStyle": "Datumsstil", + "timeStyle": "Uhrzeitstil" + }, + "NumbersDemo": { + "value": "Wert", + "style": "Stil", + "currency": "Währung" + }, + "MixedRoutingDemo": { + "currentLocale": "Aktuelle Sprache", + "alwaysNote": "Jede Sprache, auch die Standardsprache, erhält ein Präfix.", + "asNeededNote": "Die Standardsprache bleibt ohne Präfix; andere bekommen eines.", + "neverNote": "Kein Präfix — die Sprache wird per Cookie oder Header ermittelt." + }, + "LocaleSwitcherDemo": { + "hint": "Wähle eine Sprache — nutzt next-intls typisiertes Link mit locale-Prop; das URL-Präfix wechselt und das Layout rendert in der neuen Sprache neu." + }, + "CacheComponents": { + "boundaryLabel": "Demnächst", + "kicker": "Patterns", + "title": "Cache Components", + "intro": "Demnächst. Hier ist, was diese Seite zeigen wird.", + "useCacheTitle": "'use cache' auf getTranslations", + "useCacheBody": "Sieh dir an, wie eine gecachte Server Component übersetzte Inhalte über Anfragen hinweg mit einem sprachspezifischen Cache-Key wiederverwendet.", + "cacheLifeTitle": "cacheLife-Profile", + "cacheLifeBody": "Pinne einen übersetzten Abschnitt auf Sekunden, Minuten oder Stunden — und beobachte die Invalidierung in Echtzeit.", + "cacheTagTitle": "cacheTag für gezielte Revalidierung", + "cacheTagBody": "Versieh Übersetzungen mit einem Schlüssel und löse updateTag aus einer Server Action aus, um genau diese Fläche neu zu laden.", + "tryExplorer": "Probier in der Zwischenzeit den Explorer", + "readRfc": "use-cache-RFC lesen ↗" + }, + "NotFound": { + "kicker": "Fehler 404 · Seite nicht gefunden", + "title": "Dieser Route fehlt ein Übersetzungsschlüssel.", + "subtitle": "Die aufgerufene URL passt zu keiner Seite im Playground. Solange du hier bist — tippe etwas in die Felder unten, next-intl formatiert es live in deiner aktuellen Sprache.", + "playgroundLabel": "Solange du hier bist", + "takeMeBack": "Bring mich woandershin", + "greeting": "Hi {name},", + "scoreLine": "dein In-404er-stolpern-Score liegt bei {score}.", + "nameLabel": "Dein Name", + "scoreLabel": "404-Score", + "placeholder": "Frodo", + "hint": "Formatiert mit useFormatter() und useTranslations() in der Sprache {locale} — dieselben APIs wie überall sonst im Playground." + }, + "BrowserLanguageDemo": { + "acceptLanguage": "Accept-Language-Header", + "noHeader": "(kein Header gesendet)", + "preferences": "Geparste Präferenzen", + "resolved": "Ermittelte Sprache", + "matchKind": "Trefferart", + "exactMatch": "exakt", + "languageMatch": "Sprache", + "fallbackMatch": "Fallback", + "currentNote": "Du siehst gerade die {locale}-Version, weil das URL-Präfix Vorrang vor dem Header hat. Öffne / ohne Präfix, um die Header-Erkennung in Aktion zu sehen." + } +} diff --git a/playground/messages/en.json b/playground/messages/en.json new file mode 100644 index 000000000..13f0260aa --- /dev/null +++ b/playground/messages/en.json @@ -0,0 +1,98 @@ +{ + "Layout": { + "title": "next-intl Playground", + "tagline": "Translations, formatting, routing, and patterns with Next.js.", + "examplesLabel": "Examples" + }, + "Nav": { + "translations": "Translations", + "formatting": "Formatting", + "routing": "Routing", + "patterns": "Patterns", + "serverComponents": "Server components", + "serverComponentsDesc": "Read translated strings inside async Server Components — zero client JS.", + "clientComponents": "Client components", + "clientComponentsDesc": "Use translations from Client Components for interactive content.", + "dates": "Dates", + "datesDesc": "Format dates and times for the active locale with useFormatter.", + "numbers": "Numbers", + "numbersDesc": "Format numbers, currencies, and percentages — separators flip per locale.", + "explorer": "Explorer", + "explorerDesc": "Drive useFormatter() with live options and compare output across locales.", + "mixedRouting": "Mixed routing", + "mixedRoutingDesc": "Locale-prefixed vs unprefixed paths — see the three localePrefix modes side by side.", + "localeSwitcher": "Locale switcher", + "localeSwitcherDesc": "Replay the current path under a different locale while preserving params.", + "browserLanguage": "Browser language", + "browserLanguageDesc": "Detect the user's preferred locale from the Accept-Language header via next-intl middleware.", + "cacheComponents": "Cache components", + "cacheComponentsDesc": "Combine next-intl with Next's Cache Components — 'use cache', cacheLife, cacheTag.", + "soon": "soon" + }, + "ServerDemo": { + "title": "Server Components", + "greeting": "Hello, world!" + }, + "ClientDemo": { + "title": "Client Components", + "label": "Your name", + "placeholder": "Frodo", + "greeting": "Hello, {name}!" + }, + "DatesDemo": { + "dateStyle": "Date style", + "timeStyle": "Time style" + }, + "NumbersDemo": { + "value": "Value", + "style": "Style", + "currency": "Currency" + }, + "MixedRoutingDemo": { + "currentLocale": "Current locale", + "alwaysNote": "Every locale, including the default, carries a prefix.", + "asNeededNote": "Default locale stays bare; others get a prefix.", + "neverNote": "No prefix anywhere — locale is resolved via cookie or header." + }, + "LocaleSwitcherDemo": { + "hint": "Pick a locale — uses next-intl's typed Link with a locale prop; the URL prefix updates and the layout re-renders with the new locale." + }, + "CacheComponents": { + "boundaryLabel": "Coming soon", + "kicker": "Patterns", + "title": "Cache components", + "intro": "Coming soon. Here's what this page will demo.", + "useCacheTitle": "'use cache' on getTranslations", + "useCacheBody": "See how a cached Server Component reuses translated content across requests with a locale-scoped cache key.", + "cacheLifeTitle": "cacheLife profiles", + "cacheLifeBody": "Pin a translated section to seconds, minutes, or hours — and watch invalidation in real time.", + "cacheTagTitle": "cacheTag for surgical revalidation", + "cacheTagBody": "Tag translations with a key, then trigger updateTag from a Server Action to refresh just that surface.", + "tryExplorer": "Try the Explorer in the meantime", + "readRfc": "Read the use cache RFC ↗" + }, + "NotFound": { + "kicker": "Error 404 · Page not found", + "title": "This route is missing a translation key.", + "subtitle": "The URL you tried doesn't match any page in the playground. While you're here, type something into the boxes below — next-intl will format it live in your current locale.", + "playgroundLabel": "While you're here", + "takeMeBack": "Take me somewhere real", + "greeting": "Hi {name},", + "scoreLine": "your wandering-into-404s score is {score}.", + "nameLabel": "Your name", + "scoreLabel": "404 score", + "placeholder": "Frodo", + "hint": "Formatted via useFormatter() and useTranslations() in the {locale} locale — same APIs the rest of the playground uses." + }, + "BrowserLanguageDemo": { + "acceptLanguage": "Accept-Language header", + "noHeader": "(no header sent)", + "preferences": "Parsed preferences", + "resolved": "Resolved locale", + "matchKind": "Match kind", + "exactMatch": "exact", + "languageMatch": "language", + "fallbackMatch": "fallback", + "currentNote": "You're currently viewing the {locale} version because the URL prefix wins over the header. Visit / without a prefix to see header-based detection take effect." + } +} diff --git a/playground/next.config.ts b/playground/next.config.ts new file mode 100644 index 000000000..8f9a5c733 --- /dev/null +++ b/playground/next.config.ts @@ -0,0 +1,28 @@ +import type { NextConfig } from 'next'; +import createMDX from '@next/mdx'; +import { remarkCodeHike, recmaCodeHike, type CodeHikeConfig } from 'codehike/mdx'; +import createNextIntlPlugin from 'next-intl/plugin'; + +const chConfig: CodeHikeConfig = { + components: { code: 'Code' }, +}; + +const withMDX = createMDX({ + extension: /\.mdx?$/, + options: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + remarkPlugins: [[remarkCodeHike as any, chConfig]], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + recmaPlugins: [[recmaCodeHike as any, chConfig]], + jsx: true, + }, +}); + +const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); + +const nextConfig: NextConfig = { + pageExtensions: ['ts', 'tsx', 'mdx'], +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default withNextIntl(withMDX(nextConfig as any) as any); diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 000000000..b460c3a9c --- /dev/null +++ b/playground/package.json @@ -0,0 +1,46 @@ +{ + "name": "playground", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint", + "e2e": "playwright test" + }, + "dependencies": { + "@mdx-js/loader": "^3.1.0", + "@mdx-js/react": "^3.1.0", + "@next/mdx": "^15.5.4", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "codehike": "^1.0.7", + "lucide-react": "^0.545.0", + "next": "15.5.15", + "next-intl": "^4.9.1", + "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@playwright/test": "^1.50.0", + "@tailwindcss/postcss": "^4", + "@types/mdx": "^2.0.13", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.5.15", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } +} diff --git a/playground/playwright.config.ts b/playground/playwright.config.ts new file mode 100644 index 000000000..4f509126d --- /dev/null +++ b/playground/playwright.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + use: { baseURL: 'http://localhost:3000' }, + webServer: { + command: 'pnpm dev', + port: 3000, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/playground/postcss.config.mjs b/playground/postcss.config.mjs new file mode 100644 index 000000000..c7bcb4b1e --- /dev/null +++ b/playground/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/playground/public/file.svg b/playground/public/file.svg new file mode 100644 index 000000000..004145cdd --- /dev/null +++ b/playground/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/public/globe.svg b/playground/public/globe.svg new file mode 100644 index 000000000..567f17b0d --- /dev/null +++ b/playground/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/public/next.svg b/playground/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/playground/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/public/vercel.svg b/playground/public/vercel.svg new file mode 100644 index 000000000..770539603 --- /dev/null +++ b/playground/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/public/window.svg b/playground/public/window.svg new file mode 100644 index 000000000..b2b2a44f6 --- /dev/null +++ b/playground/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground/src/app/[locale]/formatting/dates/content.mdx b/playground/src/app/[locale]/formatting/dates/content.mdx new file mode 100644 index 000000000..e84a83686 --- /dev/null +++ b/playground/src/app/[locale]/formatting/dates/content.mdx @@ -0,0 +1,22 @@ +import {TwoColumn} from '@/components/playground/two-column'; + + + +- Call `useFormatter()` to get a formatter bound to the active locale. +- `format.dateTime(date, options)` accepts the same options as `Intl.DateTimeFormat` — `dateStyle`, `timeStyle`, `weekday`, `year`, ... +- Use `useNow()` to get a stable "now" reference that hydrates safely across server and client. + +```tsx app/page.tsx +'use client'; +import {useFormatter, useNow} from 'next-intl'; + +export function Today() { + const format = useFormatter(); + const now = useNow(); + + // !mark + return

{format.dateTime(now, {dateStyle: 'long', timeStyle: 'short'})}

; +} +``` + +
diff --git a/playground/src/app/[locale]/formatting/dates/dates-example.tsx b/playground/src/app/[locale]/formatting/dates/dates-example.tsx new file mode 100644 index 000000000..01586c9e6 --- /dev/null +++ b/playground/src/app/[locale]/formatting/dates/dates-example.tsx @@ -0,0 +1,84 @@ +'use client'; + +import {useState} from 'react'; +import {useFormatter, useNow, useTranslations} from 'next-intl'; +import {Label} from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; + +type Style = 'full' | 'long' | 'medium' | 'short'; + +export function DatesExample() { + const t = useTranslations('DatesDemo'); + const format = useFormatter(); + const now = useNow(); + const [dateStyle, setDateStyle] = useState