Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
59842d7
Set up `example-app-router-patterns`
amannn Aug 29, 2025
5b23c25
Set up design system
amannn Aug 29, 2025
0f3f97c
feat: Add Next.js project files and configurations
Adebesin-Cell Oct 13, 2025
e16db8a
feat: Add 'playground' to packages list
Adebesin-Cell Oct 13, 2025
3ce3957
feat: Add new components and update dependencies
Adebesin-Cell Oct 18, 2025
675e533
style: Update styling in Home component
Adebesin-Cell Oct 18, 2025
37201bd
feat: Add Client Components and Server Components pages and components
Adebesin-Cell Nov 8, 2025
4208d3f
fix: merge conflicts
Adebesin-Cell Nov 8, 2025
adae65c
fix: lockfile
Adebesin-Cell Nov 8, 2025
41ed301
fix: merge conflicts
Adebesin-Cell Nov 8, 2025
b4fb489
docs: Add v1 implementation plan for playground refactor
Adebesin-Cell May 8, 2026
2542c8a
chore(playground): add MDX, Code Hike consumers, next-intl deps
Adebesin-Cell May 8, 2026
5a9c8be
feat(playground): configure @next/mdx and Code Hike plugins
Adebesin-Cell May 8, 2026
5686703
feat(playground): wire MDX components map with stub <Code>
Adebesin-Cell May 8, 2026
337adbc
feat(playground): set up next-intl routing and message catalogs
Adebesin-Cell May 8, 2026
ea3b26f
refactor(playground): move app routes under [locale] segment
Adebesin-Cell May 8, 2026
7e0a9eb
refactor(playground): move chrome components to src/components/playgr…
Adebesin-Cell May 8, 2026
b7f2a9a
refactor(playground): keep client providers as a pure theme wrapper
Adebesin-Cell May 8, 2026
f44bb0c
chore(playground): drop turbopack flag (incompatible with @next/mdx +…
Adebesin-Cell May 8, 2026
3de4122
feat(playground): scaffold filename header
Adebesin-Cell May 8, 2026
fa5c90b
feat(playground): add mark annotation handler
Adebesin-Cell May 8, 2026
c997228
feat(playground): add callout annotation handler
Adebesin-Cell May 8, 2026
d2da339
feat(playground): add focus annotation handler
Adebesin-Cell May 8, 2026
91d9da5
feat(playground): add link annotation handler
Adebesin-Cell May 8, 2026
55730c5
feat(playground): add fold annotation handler
Adebesin-Cell May 8, 2026
be923e3
feat(playground): add line-numbers annotation handler
Adebesin-Cell May 8, 2026
4aac591
feat(playground): export annotation handlers from barrel
Adebesin-Cell May 8, 2026
77ee713
feat(playground): wire <Code> RSC with Code Hike highlight + theme
Adebesin-Cell May 8, 2026
8d90418
refactor(playground): restructure sidebar nav and apply design nits
Adebesin-Cell May 8, 2026
5fad482
feat(playground): add shadcn dropdown-menu primitive
Adebesin-Cell May 8, 2026
c33109c
feat(playground): add locale switcher
Adebesin-Cell May 8, 2026
678d9cf
feat(playground): add shadcn badge primitive
Adebesin-Cell May 8, 2026
dc3cdd1
feat(playground): add TwoColumn MDX layout
Adebesin-Cell May 8, 2026
a8e8837
feat(playground): add DemoCard wrapper
Adebesin-Cell May 8, 2026
d324958
refactor(playground): tighten GitHubLink markup
Adebesin-Cell May 8, 2026
152f45b
refactor(playground): point Byline source link at next-intl repo
Adebesin-Cell May 8, 2026
87bb90c
feat(playground): add MDX content for Server Components page
Adebesin-Cell May 8, 2026
054351a
feat(playground): rewrite Server Components live demo
Adebesin-Cell May 8, 2026
96019f3
feat(playground): wire Server Components page shell
Adebesin-Cell May 8, 2026
2f729ca
docs(playground): slim Server Components README to a pointer
Adebesin-Cell May 8, 2026
b922b42
feat(playground): add MDX content for Client Components page
Adebesin-Cell May 8, 2026
d1ecd8e
feat(playground): rewrite Client Components live demo
Adebesin-Cell May 8, 2026
69f8b54
feat(playground): wire Client Components page shell
Adebesin-Cell May 8, 2026
e0ca0cf
docs(playground): slim Client Components README to a pointer
Adebesin-Cell May 8, 2026
122c8dc
feat(playground): localize landing page and hide empty categories
Adebesin-Cell May 8, 2026
e53baae
test(playground): add Playwright smoke tests + docs + lint cleanup
Adebesin-Cell May 8, 2026
8fe1b94
style(playground): proper page hierarchy, MDX prose, tighter chrome
Adebesin-Cell May 8, 2026
6024bc0
fix(playground): use getTranslations in server components + boundary …
Adebesin-Cell May 8, 2026
c169a34
style(playground): refine layout polish — sidebar icons + dotgrid out…
Adebesin-Cell May 8, 2026
31044e0
feat(playground): make sidebar Playground header a link to home
Adebesin-Cell May 8, 2026
165037b
fix(playground): mobile responsiveness — viewport padding, single-col…
Adebesin-Cell May 8, 2026
c66b795
chore(playground): bump next to 15.5.15 (patches CVE-2025-66478)
Adebesin-Cell May 8, 2026
b28d1e5
chore(playground): gitignore local screenshots dir
Adebesin-Cell May 8, 2026
70fc563
Merge remote-tracking branch 'upstream/main' into docs/refactor-playg…
Adebesin-Cell May 8, 2026
7602243
chore: remove superseded implementation plan
Adebesin-Cell May 8, 2026
a14f68f
Merge branch 'main' into docs/refactor-playground
Adebesin-Cell May 13, 2026
557f5c9
refactor(playground): move into example-app-router-patterns, address …
Adebesin-Cell May 29, 2026
2c8467a
fix(playground): encode dynamic segments in GitHub source links
Adebesin-Cell May 29, 2026
4702b44
Merge remote-tracking branch 'upstream/main' into docs/refactor-playg…
Adebesin-Cell May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/example-app-router-patterns/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules
/.next/
.DS_Store
tsconfig.tsbuildinfo
9 changes: 9 additions & 0 deletions examples/example-app-router-patterns/README.md
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 additions & 0 deletions examples/example-app-router-patterns/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions examples/example-app-router-patterns/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -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 };
}
28 changes: 28 additions & 0 deletions examples/example-app-router-patterns/messages/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"Layout": {
"title": "next-intl Playground",
"tagline": "Übersetzungen, Formatierung, Routing und Patterns mit Next.js.",
"examples": "Beispiele"
},
"Nav": {
"translations": "Übersetzungen",
"serverComponents": "Server-Komponenten",
"serverComponentsDescription": "Übersetzte Strings in Server-Komponenten lesen.",
"clientComponents": "Client-Komponenten",
"clientComponentsDescription": "Übersetzungen in interaktiven Client-Komponenten nutzen."
},
"ServerComponentsPage": {
"title": "Server-Komponenten",
"subtitle": "Übersetzungen",
"output": "Ausgabe",
"greeting": "Hallo, Welt!"
},
"ClientComponentsPage": {
"title": "Client-Komponenten",
"subtitle": "Übersetzungen",
"output": "Ausgabe",
"label": "Dein Name",
"placeholder": "Frodo",
"greeting": "Hallo, {name}!"
}
}
28 changes: 28 additions & 0 deletions examples/example-app-router-patterns/messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"Layout": {
"title": "next-intl playground",
"tagline": "Translations, formatting, routing and patterns with Next.js.",
"examples": "Examples"
},
"Nav": {
"translations": "Translations",
"serverComponents": "Server Components",
"serverComponentsDescription": "Read translated strings inside Server Components.",
"clientComponents": "Client Components",
"clientComponentsDescription": "Use translations in interactive Client Components."
},
"ServerComponentsPage": {
"title": "Server Components",
"subtitle": "Translations",
"output": "Output",
"greeting": "Hello, world!"
},
"ClientComponentsPage": {
"title": "Client Components",
"subtitle": "Translations",
"output": "Output",
"label": "Your name",
"placeholder": "Frodo",
"greeting": "Hello, {name}!"
}
}
6 changes: 6 additions & 0 deletions examples/example-app-router-patterns/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
26 changes: 26 additions & 0 deletions examples/example-app-router-patterns/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import createMDX from '@next/mdx';
import {remarkCodeHike, recmaCodeHike} from 'codehike/mdx';
import createNextIntlPlugin from 'next-intl/plugin';

/** @type {import('codehike/mdx').CodeHikeConfig} */
const chConfig = {
components: {code: 'Code'}
};

const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [[remarkCodeHike, chConfig]],
recmaPlugins: [[recmaCodeHike, chConfig]],
jsx: true
}
});

const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');

/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['ts', 'tsx', 'mdx']
};

export default withNextIntl(withMDX(nextConfig));
51 changes: 51 additions & 0 deletions examples/example-app-router-patterns/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "example-app-router-patterns",
"private": true,
"scripts": {
"dev": "next dev",
"lint": "eslint src && prettier src --check",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.5.4",
"clsx": "^2.1.1",
"codehike": "^1.0.7",
"lucide-react": "^0.545.0",
"next": "^15.5.0",
"next-intl": "^4.0.0",
"next-themes": "^0.4.6",
"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/mdx": "^2.0.13",
"@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"
}
}
6 changes: 6 additions & 0 deletions examples/example-app-router-patterns/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const config = {
plugins: {
'@tailwindcss/postcss': {}
}
};
export default config;
34 changes: 34 additions & 0 deletions examples/example-app-router-patterns/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {PlaygroundByline} from '@/components/playground/byline';
import {PlaygroundSidebar} from '@/components/playground/sidebar';
import {routing} from '@/i18n/routing';
import {hasLocale} from 'next-intl';
import {setRequestLocale} from 'next-intl/server';
import {notFound} from 'next/navigation';
import type {ReactNode} from 'react';

export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}));
}

type Props = {
children: ReactNode;
params: Promise<{locale: string}>;
};

export default async function LocaleLayout({children, params}: Props) {
const {locale} = await params;
if (!hasLocale(routing.locales, locale)) notFound();
setRequestLocale(locale);

return (
<>
<PlaygroundSidebar />
<div className="lg:pl-72">
<div className="mx-auto mt-16 mb-24 max-w-4xl px-4 sm:px-6 lg:mt-0 lg:px-8 lg:py-10">
{children}
<PlaygroundByline />
</div>
</div>
</>
);
}
71 changes: 71 additions & 0 deletions examples/example-app-router-patterns/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {PlaygroundBoundary} from '@/components/playground/boundary';
import {LinkStatus} from '@/components/playground/link-status';
import {Link} from '@/i18n/navigation';
import {sections} from '@/lib/nav';
import {ArrowRight} from 'lucide-react';
import {useTranslations} from 'next-intl';
import {setRequestLocale} from 'next-intl/server';

type Props = {
params: Promise<{locale: string}>;
};

export default async function HomePage({params}: Props) {
const {locale} = await params;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use const {locale} = use(params) to avoid the async/non-async split of components

setRequestLocale(locale);
return <Home />;
}

function Home() {
const t = useTranslations('Layout');
const tNav = useTranslations('Nav');

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #2084 (comment):

I think it's different aspects:

  1. Which patterns to we demonstrate on the pages
  2. How we build this app

We can keep useTranslations for the demonstrated patterns, but for the actual app code I'd switch to useExtracted. It naturally avoids issues like this tNav in this file if you just inline the labels where they are used (you'll need to remove nav.ts then as the labels will be inline in a component. the types from the file can go into src/types.tsx I'd say).


return (
<div className="pb-12">
<div className="mb-12 pt-8 text-center sm:mb-16 sm:pt-12">
<h1 className="text-foreground text-[34px] font-semibold tracking-tight sm:text-5xl">
{t('title')}
</h1>
<p className="text-muted-foreground mx-auto mt-4 max-w-xl text-base sm:text-lg">
{t('tagline')}
</p>
</div>
<PlaygroundBoundary label={t('examples')} className="space-y-10">

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it t('examples')? Maybe a generic "pages" could fit better terminology-wise? (also the rendered labels)

{sections.map((section) => {
const Icon = section.icon;
return (
<div key={section.titleKey} className="flex flex-col gap-3">
<div className="text-muted-foreground flex items-center gap-2 font-mono text-[10px] font-semibold tracking-[0.18em] uppercase">
<Icon className="h-3 w-3" strokeWidth={2} />
{tNav(section.titleKey)}
</div>
<div className="bg-border grid grid-cols-1 gap-px sm:grid-cols-2">
{section.items.map((item) => (
<Link
href={item.slug}
key={item.slug}
className="group bg-background hover:bg-muted flex flex-col gap-1.5 px-5 py-4 transition-colors"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hover:bg-muted: I think that's too dark, can we use a lighter shade?

>
<div className="text-foreground flex items-center justify-between font-medium">
<span className="inline-flex items-center gap-1.5">
{tNav(item.titleKey)}
<LinkStatus />
</span>
<ArrowRight
className="text-muted-foreground group-hover:text-foreground h-4 w-4 transition-transform group-hover:translate-x-0.5"
strokeWidth={1.5}
/>
</div>
<div className="text-muted-foreground line-clamp-3 text-[13px]">
{tNav(item.descriptionKey)}
</div>
</Link>
))}
</div>
</div>
);
})}
</PlaygroundBoundary>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import {useTranslations} from 'next-intl';
import {useState} from 'react';

export function ClientExample() {
const t = useTranslations('ClientComponentsPage');
const [name, setName] = useState('');

return (
<div className="space-y-3">
<label
htmlFor="client-example-name"
className="text-muted-foreground block text-sm font-medium"
>
{t('label')}
</label>
<input
id="client-example-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t('placeholder')}
className="border-border bg-background w-full max-w-xs rounded-md border px-3 py-2 text-sm"
/>
<p className="text-foreground text-2xl font-semibold">
{t('greeting', {name: name || t('placeholder')})}
</p>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
- `useTranslations` works the same in Client Components, so interactive UI can read messages too.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `useTranslations` works the same in Client Components, so interactive UI can read messages too.
- `useTranslations` can be called in Client Components to incorporate client-side state.

- ICU arguments like `{name}` are resolved in the browser as state changes — try typing below.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #2084 (comment): Yeah, I'd switch to inline labels here. Otherwise the locale switcher will appear to be non-working if we have markdown that is not translated. Using inline labels with t should be easier to maintain (esp. with useExtracted).

We can also avoid the @next/mdx dependency then I think if we use codehike directly on a page and pass code samples like below directly as multiline strings. This will also enable Turbopack, since the way next/mdx is used currently causes a switch to webpack.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "try typing below." is not necessary.

There's this phrase I like which I think you already did very well with the input field: Show, don't tell.


```tsx app/greet.tsx

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think app/greet.tsx doesn't add anything here. We could however use a generic "Code" label, related to "Output" below.

'use client';
import {useState} from 'react';
import {useTranslations} from 'next-intl';

export function Greet() {
const t = useTranslations('ClientComponentsPage');
const [name, setName] = useState('Frodo');
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
// !mark

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rendered on the page:

Image

I guess the issue is that // comments don't work in JSX (only /* … */)

<p>{t('greeting', {name})}</p>
</>
);
}
```
Loading
Loading