├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 01_bug_report.yml │ ├── 02_feature_request.yml │ ├── 03_update_docs.yml │ └── config.yml └── workflows │ ├── close-stale.yml │ ├── issue-commenter.yml │ ├── lint-pr.yml │ ├── main.yml │ ├── prerelease-canary.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── README.md ├── eslint.config.mjs ├── lintLinks.mjs ├── media │ └── assets.sketch ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.mjs ├── package.json ├── postcss.config.js ├── public │ ├── crowdin-editor-schematic.png │ ├── crowdin-repo-mapping.png │ ├── crowdin-repo-sync.png │ ├── crowdin-workflow.png │ ├── example-app-router-mixed-routing.png │ ├── example-app-router-without-i18n-routing.png │ ├── example-app-router.png │ ├── example-street-photography.png │ ├── external-link.svg │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ ├── google2a52d47b4884506c.html │ ├── i18n-ally-demo.mp4 │ ├── rsc-pr-participants.png │ ├── storybook-integration.png │ ├── testimonial-bg-daybridge-bright.png │ ├── testimonial-bg-daybridge-dark.png │ ├── testimonial-bg-nodejs-bright.png │ ├── testimonial-bg-nodejs-dark.png │ ├── testimonial-bg-todoist-bright.png │ ├── testimonial-bg-todoist-dark.png │ ├── testimonial-bg-watershed-bright.png │ ├── testimonial-bg-watershed-dark.png │ ├── twitter-image.png │ ├── user-alvar-lagerlof.jpeg │ ├── user-cali-castle.png │ ├── user-claudio-wunder.jpeg │ ├── user-colin-sidoti.jpg │ ├── user-dawid-gawel.jpeg │ ├── user-jokull-solberg.jpeg │ ├── user-kieran-mchugh.jpg │ └── user-lachlan-campbell.jpeg ├── src │ ├── app │ │ ├── redirect │ │ │ └── route.tsx │ │ └── robots.tsx │ ├── components │ │ ├── AlgoliaSearch.css │ │ ├── AlgoliaSearch.tsx │ │ ├── BlogPostLink.tsx │ │ ├── Button.tsx │ │ ├── Callout.tsx │ │ ├── Card.tsx │ │ ├── Cards.tsx │ │ ├── Chip.tsx │ │ ├── CodeSnippet.module.css │ │ ├── CodeSnippet.tsx │ │ ├── CodeSnippets.tsx │ │ ├── CopyToClipboard.tsx │ │ ├── Details.tsx │ │ ├── Example.tsx │ │ ├── FeaturePanel.tsx │ │ ├── Footer.tsx │ │ ├── FooterLink.tsx │ │ ├── FooterSeparator.tsx │ │ ├── FooterVersionSelector.tsx │ │ ├── GetStartedBackground.module.css │ │ ├── GetStartedBackground.tsx │ │ ├── Hero.tsx │ │ ├── HeroAnnouncement.tsx │ │ ├── HeroCode.tsx │ │ ├── Link.tsx │ │ ├── LinkButton.tsx │ │ ├── Logo.tsx │ │ ├── Partner.tsx │ │ ├── PartnerBanner.tsx │ │ ├── PartnerContentLink.tsx │ │ ├── PartnerLink.tsx │ │ ├── PartnerSidebar.tsx │ │ ├── Pre.tsx │ │ ├── Screenshot.tsx │ │ ├── Section.tsx │ │ ├── StayUpdated.mdx │ │ ├── Steps.module.css │ │ ├── Steps.tsx │ │ ├── UserTestimonial.tsx │ │ ├── Wrapper.tsx │ │ └── icons │ │ │ ├── CheckIcon.tsx │ │ │ └── CopyIcon.tsx │ ├── config.js │ ├── hooks │ │ └── useLocationHash.tsx │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── _meta.tsx │ │ ├── api │ │ │ ├── Inter-Regular.otf │ │ │ ├── Inter-SemiBold.otf │ │ │ └── og-image.tsx │ │ ├── blog │ │ │ ├── _meta.tsx │ │ │ ├── date-formatting-nextjs.mdx │ │ │ ├── index.mdx │ │ │ ├── next-intl-3-0.mdx │ │ │ ├── next-intl-3-22.mdx │ │ │ ├── next-intl-4-0.mdx │ │ │ └── translations-outside-of-react-components.mdx │ │ ├── docs │ │ │ ├── _meta.tsx │ │ │ ├── design-principles.mdx │ │ │ ├── environments.mdx │ │ │ ├── environments │ │ │ │ ├── _meta.tsx │ │ │ │ ├── actions-metadata-route-handlers.mdx │ │ │ │ ├── core-library.mdx │ │ │ │ ├── error-files.mdx │ │ │ │ ├── mdx.mdx │ │ │ │ ├── runtime-requirements.mdx │ │ │ │ ├── server-client-components.mdx │ │ │ │ └── testing.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── getting-started │ │ │ │ ├── _meta.tsx │ │ │ │ ├── app-router.mdx │ │ │ │ ├── app-router │ │ │ │ │ ├── _meta.tsx │ │ │ │ │ ├── with-i18n-routing.mdx │ │ │ │ │ └── without-i18n-routing.mdx │ │ │ │ └── pages-router.mdx │ │ │ ├── routing.mdx │ │ │ ├── routing │ │ │ │ ├── _meta.tsx │ │ │ │ ├── middleware.mdx │ │ │ │ └── navigation.mdx │ │ │ ├── usage.mdx │ │ │ ├── usage │ │ │ │ ├── _meta.tsx │ │ │ │ ├── configuration.mdx │ │ │ │ ├── dates-times.mdx │ │ │ │ ├── lists.mdx │ │ │ │ ├── messages.mdx │ │ │ │ └── numbers.mdx │ │ │ ├── workflows.mdx │ │ │ └── workflows │ │ │ │ ├── _meta.tsx │ │ │ │ ├── linting.mdx │ │ │ │ ├── localization-management.mdx │ │ │ │ ├── messages.mdx │ │ │ │ ├── storybook.mdx │ │ │ │ ├── typescript.mdx │ │ │ │ └── vscode-integration.mdx │ │ ├── examples.mdx │ │ └── index.mdx │ ├── services │ │ ├── BrowserTracker.tsx │ │ └── ServerTracker.tsx │ ├── styles.css │ └── theme.config.tsx ├── tailwind.config.js └── tsconfig.json ├── examples ├── example-app-router-migration │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── [locale] │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── not-found.tsx │ │ ├── components │ │ │ ├── LocaleSwitcher.tsx │ │ │ └── PageLayout.tsx │ │ ├── i18n │ │ │ ├── navigation.ts │ │ │ ├── request.ts │ │ │ └── routing.ts │ │ ├── middleware.ts │ │ └── pages │ │ │ ├── [locale] │ │ │ └── about.tsx │ │ │ └── _app.tsx │ └── tsconfig.json ├── example-app-router-mixed-routing │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── global.ts │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── src │ │ ├── app │ │ │ ├── (public) │ │ │ │ └── [locale] │ │ │ │ │ ├── Login.tsx │ │ │ │ │ ├── NavLink.tsx │ │ │ │ │ ├── PublicNavigation.tsx │ │ │ │ │ ├── PublicNavigationLocaleSwitcher.tsx │ │ │ │ │ ├── about │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── app │ │ │ │ ├── AppNavigation.tsx │ │ │ │ ├── AppNavigationLocaleButton.tsx │ │ │ │ ├── Logout.tsx │ │ │ │ ├── NavLink.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── profile │ │ │ │ │ └── page.tsx │ │ │ ├── favicon.ico │ │ │ ├── layout.tsx │ │ │ └── not-found.tsx │ │ ├── components │ │ │ ├── Document.tsx │ │ │ ├── PageTitle.tsx │ │ │ └── globals.css │ │ ├── config.ts │ │ ├── i18n │ │ │ ├── navigation.public.ts │ │ │ ├── request.ts │ │ │ └── routing.public.ts │ │ └── middleware.ts │ ├── tailwind.config.ts │ ├── tests │ │ └── main.spec.ts │ └── tsconfig.json ├── example-app-router-next-auth │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── global.ts │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── [locale] │ │ │ │ ├── Index.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── login │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── secret │ │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── auth │ │ │ │ │ └── [...nextauth] │ │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ └── not-found.tsx │ │ ├── auth.ts │ │ ├── components │ │ │ ├── LocaleSwitcher.tsx │ │ │ └── PageLayout.tsx │ │ ├── i18n │ │ │ ├── navigation.ts │ │ │ ├── request.ts │ │ │ └── routing.ts │ │ └── middleware.ts │ ├── tests │ │ └── main.spec.ts │ └── tsconfig.json ├── example-app-router-playground │ ├── .gitignore │ ├── .storybook │ │ ├── main.ts │ │ ├── next-intl.ts │ │ └── preview.ts │ ├── README.md │ ├── eslint.config.mjs │ ├── global.ts │ ├── jest.config.js │ ├── messages │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ └── ja.json │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── assets │ │ │ └── image.jpg │ │ └── favicon.ico │ ├── runPlaywright.mjs │ ├── src │ │ ├── app │ │ │ ├── [locale] │ │ │ │ ├── [...rest] │ │ │ │ │ └── page.tsx │ │ │ │ ├── about │ │ │ │ │ ├── de.mdx │ │ │ │ │ ├── en.mdx │ │ │ │ │ ├── es.mdx │ │ │ │ │ └── page.tsx │ │ │ │ ├── actions │ │ │ │ │ ├── List.tsx │ │ │ │ │ ├── ListItem.tsx │ │ │ │ │ ├── ListItemAsync.tsx │ │ │ │ │ ├── ListItemClient.tsx │ │ │ │ │ ├── ZodForm.tsx │ │ │ │ │ ├── ZodFormExample.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── api │ │ │ │ │ └── route.ts │ │ │ │ ├── client │ │ │ │ │ ├── ClientContent.tsx │ │ │ │ │ ├── DelayedServerContent.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── redirect │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── nested │ │ │ │ │ ├── UnlocalizedPathname.tsx │ │ │ │ │ ├── page.spec.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── news │ │ │ │ │ ├── [articleId] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── just-in │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── redirect │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── not-found.tsx │ │ ├── components │ │ │ ├── AsyncComponent.tsx │ │ │ ├── AsyncComponentWithNamespaceAndLocale.tsx │ │ │ ├── AsyncComponentWithoutNamespace.tsx │ │ │ ├── AsyncComponentWithoutNamespaceAndLocale.tsx │ │ │ ├── ClientLink.tsx │ │ │ ├── ClientRouterWithoutProvider.tsx │ │ │ ├── CoreLibrary.tsx │ │ │ ├── DropdownMenu.tsx │ │ │ ├── LocaleSwitcher.tsx │ │ │ ├── Navigation.stories.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── NavigationLink.tsx │ │ │ ├── PageLayout.tsx │ │ │ ├── RichText.tsx │ │ │ ├── TypePortabilityTest.ts │ │ │ ├── UseFormatterTypeTests.tsx │ │ │ ├── UseLocaleTypeTests.tsx │ │ │ ├── UseMessagesTypeTests.tsx │ │ │ ├── UseTranslationsTypeTests.tsx │ │ │ └── client │ │ │ │ ├── 01-MessagesAsPropsCounter │ │ │ │ ├── ClientCounter.tsx │ │ │ │ ├── Counter.tsx │ │ │ │ └── index.tsx │ │ │ │ └── 02-MessagesOnClientCounter │ │ │ │ ├── ClientCounter.tsx │ │ │ │ ├── Counter.tsx │ │ │ │ └── index.tsx │ │ ├── i18n │ │ │ ├── navigation.ts │ │ │ ├── request.tsx │ │ │ └── routing.ts │ │ ├── mdx-components.tsx │ │ └── middleware.ts │ ├── tests │ │ ├── base-path.spec.ts │ │ ├── domains.spec.ts │ │ ├── locale-cookie-false.spec.ts │ │ ├── locale-prefix-never.spec.ts │ │ ├── main.spec.ts │ │ ├── trailing-slash.spec.ts │ │ └── utils.ts │ └── tsconfig.json ├── example-app-router-single-locale │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── messages │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── next.config.ts │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── about │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ └── PageLayout.tsx │ │ └── i18n │ │ │ └── request.ts │ ├── tests │ │ └── main.spec.ts │ └── tsconfig.json ├── example-app-router-without-i18n-routing │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── next.config.ts │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── app │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── profile │ │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── login │ │ │ │ ├── LoginForm.tsx │ │ │ │ ├── LoginFormErrors.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── Button.tsx │ │ │ ├── FormField.tsx │ │ │ ├── LocaleSwitcher.tsx │ │ │ ├── LocaleSwitcherSelect.tsx │ │ │ └── NavLink.tsx │ │ ├── i18n │ │ │ ├── config.ts │ │ │ └── request.ts │ │ └── services │ │ │ ├── locale.ts │ │ │ └── session.ts │ ├── tailwind.config.js │ ├── tests │ │ └── main.spec.ts │ └── tsconfig.json ├── example-app-router │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── global.ts │ ├── jest.config.js │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── [locale] │ │ │ │ ├── [...rest] │ │ │ │ │ └── page.tsx │ │ │ │ ├── error.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── pathnames │ │ │ │ │ └── page.tsx │ │ │ │ └── styles.css │ │ │ ├── layout.tsx │ │ │ ├── manifest.ts │ │ │ ├── not-found.tsx │ │ │ ├── page.tsx │ │ │ ├── robots.txt │ │ │ └── sitemap.ts │ │ ├── components │ │ │ ├── ExternalLink.tsx │ │ │ ├── LocaleSwitcher.tsx │ │ │ ├── LocaleSwitcherSelect.tsx │ │ │ ├── Navigation.spec.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── NavigationLink.tsx │ │ │ ├── NotFoundPage.tsx │ │ │ └── PageLayout.tsx │ │ ├── config.ts │ │ ├── i18n │ │ │ ├── navigation.ts │ │ │ ├── request.ts │ │ │ └── routing.ts │ │ └── middleware.ts │ ├── tailwind.config.js │ ├── tests │ │ └── main.spec.ts │ └── tsconfig.json ├── example-pages-router-advanced │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── global.ts │ ├── jest.config.js │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── components │ │ │ ├── Code.tsx │ │ │ ├── Navigation.spec.tsx │ │ │ ├── Navigation.tsx │ │ │ └── PageLayout.tsx │ │ └── pages │ │ │ ├── 404.tsx │ │ │ ├── _app.tsx │ │ │ ├── about.tsx │ │ │ ├── api │ │ │ └── hello.tsx │ │ │ ├── index.tsx │ │ │ └── strict-types.tsx │ └── tsconfig.json ├── example-pages-router-legacy │ ├── .gitignore │ ├── README.md │ ├── messages │ │ └── en.json │ ├── next.config.js │ ├── package.json │ ├── public │ │ └── favicon.ico │ └── src │ │ ├── components │ │ └── PageLayout.js │ │ └── pages │ │ ├── _app.js │ │ └── index.js ├── example-pages-router │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── global.ts │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── components │ │ │ ├── LocaleSwitcher.tsx │ │ │ └── PageLayout.tsx │ │ └── pages │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ └── tsconfig.json ├── example-react-native │ ├── .gitignore │ ├── App.js │ ├── README.md │ ├── app.json │ ├── assets │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash.png │ ├── babel.config.js │ └── package.json ├── example-remix │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── root.tsx │ │ ├── routes │ │ │ └── _index.tsx │ │ └── utils.tsx │ ├── messages │ │ ├── de.json │ │ └── en.json │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── remix.config.js │ └── tsconfig.json └── example-use-intl │ ├── .gitignore │ ├── README.md │ ├── global.ts │ ├── index.html │ ├── messages │ └── en.json │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.tsx │ ├── config.tsx │ ├── main.tsx │ └── vite-env.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── lerna.json ├── media ├── assets.sketch ├── logo-dark-mode.svg ├── logo.png ├── logo.svg ├── og-image.png ├── partner.svg └── twitter-image.png ├── package.json ├── packages ├── next-intl │ ├── .gitignore │ ├── .size-limit.ts │ ├── CHANGELOG.md │ ├── __mocks__ │ │ └── react.tsx │ ├── config.d.ts │ ├── eslint.config.mjs │ ├── middleware.d.ts │ ├── navigation.d.ts │ ├── next-env.d.ts │ ├── package.json │ ├── plugin.d.cts │ ├── plugin.d.ts │ ├── rollup.config.js │ ├── routing.d.ts │ ├── server.d.ts │ ├── src │ │ ├── config.tsx │ │ ├── index.react-client.tsx │ │ ├── index.react-server.tsx │ │ ├── middleware.tsx │ │ ├── middleware │ │ │ ├── getAlternateLinksHeaderValue.test.tsx │ │ │ ├── getAlternateLinksHeaderValue.tsx │ │ │ ├── index.tsx │ │ │ ├── middleware.test.tsx │ │ │ ├── middleware.tsx │ │ │ ├── resolveLocale.test.tsx │ │ │ ├── resolveLocale.tsx │ │ │ ├── syncCookie.tsx │ │ │ ├── utils.test.tsx │ │ │ └── utils.tsx │ │ ├── navigation.react-client.tsx │ │ ├── navigation.react-server.tsx │ │ ├── navigation │ │ │ ├── createNavigation.test.tsx │ │ │ ├── react-client │ │ │ │ ├── createNavigation.test.tsx │ │ │ │ ├── createNavigation.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── useBasePathname.test.tsx │ │ │ │ └── useBasePathname.tsx │ │ │ ├── react-server │ │ │ │ ├── createNavigation.test.tsx │ │ │ │ ├── createNavigation.tsx │ │ │ │ ├── getServerLocale.tsx │ │ │ │ └── index.tsx │ │ │ └── shared │ │ │ │ ├── BaseLink.tsx │ │ │ │ ├── StrictParams.tsx │ │ │ │ ├── createSharedNavigationFns.tsx │ │ │ │ ├── syncLocaleCookie.tsx │ │ │ │ ├── utils.test.tsx │ │ │ │ └── utils.tsx │ │ ├── plugin.tsx │ │ ├── plugin │ │ │ ├── createMessagesDeclaration.tsx │ │ │ ├── createNextIntlPlugin.tsx │ │ │ ├── getNextConfig.tsx │ │ │ ├── hasStableTurboConfig.tsx │ │ │ ├── index.tsx │ │ │ ├── types.tsx │ │ │ ├── utils.tsx │ │ │ └── watchFile.tsx │ │ ├── react-client │ │ │ ├── index.tsx │ │ │ ├── useFormatter.test.tsx │ │ │ ├── useNow.test.tsx │ │ │ ├── useTimeZone.test.tsx │ │ │ └── useTranslations.test.tsx │ │ ├── react-server │ │ │ ├── NextIntlClientProviderServer.test.tsx │ │ │ ├── NextIntlClientProviderServer.tsx │ │ │ ├── index.test.tsx │ │ │ ├── index.tsx │ │ │ ├── testUtils.tsx │ │ │ ├── useConfig.tsx │ │ │ ├── useFormatter.test.tsx │ │ │ ├── useFormatter.tsx │ │ │ ├── useLocale.tsx │ │ │ ├── useMessages.tsx │ │ │ ├── useNow.tsx │ │ │ ├── useTimeZone.tsx │ │ │ ├── useTranslations.test.tsx │ │ │ └── useTranslations.tsx │ │ ├── routing.tsx │ │ ├── routing │ │ │ ├── config.tsx │ │ │ ├── defineRouting.test.tsx │ │ │ ├── defineRouting.tsx │ │ │ ├── index.tsx │ │ │ ├── types.test.tsx │ │ │ └── types.tsx │ │ ├── server.react-client.tsx │ │ ├── server.react-server.tsx │ │ ├── server │ │ │ ├── react-client │ │ │ │ ├── index.test.tsx │ │ │ │ └── index.tsx │ │ │ └── react-server │ │ │ │ ├── RequestLocale.tsx │ │ │ │ ├── RequestLocaleCache.tsx │ │ │ │ ├── createRequestConfig.tsx │ │ │ │ ├── getConfig.tsx │ │ │ │ ├── getConfigNow.tsx │ │ │ │ ├── getDefaultNow.tsx │ │ │ │ ├── getFormats.tsx │ │ │ │ ├── getFormatter.test.tsx │ │ │ │ ├── getFormatter.tsx │ │ │ │ ├── getLocale.tsx │ │ │ │ ├── getMessages.tsx │ │ │ │ ├── getNow.tsx │ │ │ │ ├── getRequestConfig.tsx │ │ │ │ ├── getServerFormatter.tsx │ │ │ │ ├── getServerTranslator.tsx │ │ │ │ ├── getTimeZone.tsx │ │ │ │ ├── getTranslations.test.tsx │ │ │ │ ├── getTranslations.tsx │ │ │ │ ├── index.test.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── validateLocale.test.tsx │ │ │ │ └── validateLocale.tsx │ │ └── shared │ │ │ ├── NextIntlClientProvider.tsx │ │ │ ├── constants.tsx │ │ │ ├── types.tsx │ │ │ ├── use.tsx │ │ │ ├── utils.test.tsx │ │ │ └── utils.tsx │ ├── test │ │ └── setup.tsx │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── types │ │ └── index.d.ts │ └── vitest.config.mts └── use-intl │ ├── .size-limit.ts │ ├── CHANGELOG.md │ ├── README.md │ ├── core.d.ts │ ├── eslint.config.mjs │ ├── package.json │ ├── react.d.ts │ ├── rollup.config.js │ ├── src │ ├── core.tsx │ ├── core │ │ ├── AbstractIntlMessages.tsx │ │ ├── AppConfig.tsx │ │ ├── DateTimeFormatOptions.tsx │ │ ├── Formats.tsx │ │ ├── ICUArgs.tsx │ │ ├── ICUTags.tsx │ │ ├── IntlConfig.tsx │ │ ├── IntlError.tsx │ │ ├── IntlErrorCode.tsx │ │ ├── MessageKeys.tsx │ │ ├── NumberFormatOptions.tsx │ │ ├── RelativeTimeFormatOptions.tsx │ │ ├── TimeZone.tsx │ │ ├── TranslationValues.tsx │ │ ├── convertFormatsToIntlMessageFormat.tsx │ │ ├── createBaseTranslator.tsx │ │ ├── createFormatter.test.tsx │ │ ├── createFormatter.tsx │ │ ├── createTranslator.test.tsx │ │ ├── createTranslator.tsx │ │ ├── createTranslatorImpl.tsx │ │ ├── defaults.tsx │ │ ├── formatters.tsx │ │ ├── hasLocale.test.tsx │ │ ├── hasLocale.tsx │ │ ├── index.tsx │ │ ├── initializeConfig.tsx │ │ ├── joinPath.tsx │ │ ├── resolveNamespace.tsx │ │ ├── types.tsx │ │ └── validateMessages.tsx │ ├── index.tsx │ ├── react.tsx │ └── react │ │ ├── IntlContext.tsx │ │ ├── IntlProvider.test.tsx │ │ ├── IntlProvider.tsx │ │ ├── index.test.tsx │ │ ├── index.tsx │ │ ├── useFormatter.test.tsx │ │ ├── useFormatter.tsx │ │ ├── useIntlContext.tsx │ │ ├── useLocale.test.tsx │ │ ├── useLocale.tsx │ │ ├── useMessages.test.tsx │ │ ├── useMessages.tsx │ │ ├── useNow.test.tsx │ │ ├── useNow.tsx │ │ ├── useTimeZone.test.tsx │ │ ├── useTimeZone.tsx │ │ ├── useTranslations.test.tsx │ │ ├── useTranslations.tsx │ │ └── useTranslationsImpl.tsx │ ├── test │ └── setup.tsx │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── types │ └── index.d.ts │ └── vitest.config.mts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tools ├── eslint.config.mjs ├── package.json └── src │ ├── getBuildConfig.js │ └── index.js └── turbo.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: amannn 2 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: lint-pr 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Lint PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v3.1.0 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .vscode 5 | .vercel 6 | .turbo/ 7 | dist/ 8 | .next/ 9 | tsconfig.tsbuildinfo 10 | .turbo 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=true 2 | prefer-workspace-packages=true 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": false, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | .vercel 5 | public/.nextra 6 | tsconfig.tsbuildinfo 7 | .env 8 | public/robots.txt 9 | public/sitemap.xml 10 | public/sitemap-0.xml 11 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | Website for `next-intl`. 4 | 5 | You can run it locally like this: 6 | 7 | ``` 8 | pnpm install 9 | pnpm dev 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {getPresets} from 'eslint-config-molindo'; 2 | import globals from 'globals'; 3 | 4 | export default (await getPresets('typescript', 'react', 'tailwind')).concat({ 5 | languageOptions: { 6 | globals: { 7 | ...globals.browser, 8 | ...globals.node 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /docs/lintLinks.mjs: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob'; 2 | import {printErrors, scanURLs, validateFiles} from 'next-validate-link'; 3 | 4 | const scanned = await scanURLs(); 5 | 6 | printErrors( 7 | await validateFiles(await glob('src/pages/docs/**/*.{md,mdx}'), { 8 | scanned 9 | }), 10 | true 11 | ); 12 | -------------------------------------------------------------------------------- /docs/media/assets.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/media/assets.sketch -------------------------------------------------------------------------------- /docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 7 | -------------------------------------------------------------------------------- /docs/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | const config = require('./src/config'); 2 | 3 | /** @type {import('next-sitemap').IConfig} */ 4 | module.exports = { 5 | siteUrl: config.baseUrl, 6 | generateRobotsTxt: true, 7 | exclude: ['*/_meta'] 8 | }; 9 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {} 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /docs/public/crowdin-editor-schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/crowdin-editor-schematic.png -------------------------------------------------------------------------------- /docs/public/crowdin-repo-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/crowdin-repo-mapping.png -------------------------------------------------------------------------------- /docs/public/crowdin-repo-sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/crowdin-repo-sync.png -------------------------------------------------------------------------------- /docs/public/crowdin-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/crowdin-workflow.png -------------------------------------------------------------------------------- /docs/public/example-app-router-mixed-routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/example-app-router-mixed-routing.png -------------------------------------------------------------------------------- /docs/public/example-app-router-without-i18n-routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/example-app-router-without-i18n-routing.png -------------------------------------------------------------------------------- /docs/public/example-app-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/example-app-router.png -------------------------------------------------------------------------------- /docs/public/example-street-photography.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/example-street-photography.png -------------------------------------------------------------------------------- /docs/public/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /docs/public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-intl", 3 | "short_name": "next-intl", 4 | "icons": [ 5 | { 6 | "src": "/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/public/google2a52d47b4884506c.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google2a52d47b4884506c.html 2 | -------------------------------------------------------------------------------- /docs/public/i18n-ally-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/i18n-ally-demo.mp4 -------------------------------------------------------------------------------- /docs/public/rsc-pr-participants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/rsc-pr-participants.png -------------------------------------------------------------------------------- /docs/public/storybook-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/storybook-integration.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-daybridge-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-daybridge-bright.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-daybridge-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-daybridge-dark.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-nodejs-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-nodejs-bright.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-nodejs-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-nodejs-dark.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-todoist-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-todoist-bright.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-todoist-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-todoist-dark.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-watershed-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-watershed-bright.png -------------------------------------------------------------------------------- /docs/public/testimonial-bg-watershed-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/testimonial-bg-watershed-dark.png -------------------------------------------------------------------------------- /docs/public/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/twitter-image.png -------------------------------------------------------------------------------- /docs/public/user-alvar-lagerlof.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-alvar-lagerlof.jpeg -------------------------------------------------------------------------------- /docs/public/user-cali-castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-cali-castle.png -------------------------------------------------------------------------------- /docs/public/user-claudio-wunder.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-claudio-wunder.jpeg -------------------------------------------------------------------------------- /docs/public/user-colin-sidoti.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-colin-sidoti.jpg -------------------------------------------------------------------------------- /docs/public/user-dawid-gawel.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-dawid-gawel.jpeg -------------------------------------------------------------------------------- /docs/public/user-jokull-solberg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-jokull-solberg.jpeg -------------------------------------------------------------------------------- /docs/public/user-kieran-mchugh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-kieran-mchugh.jpg -------------------------------------------------------------------------------- /docs/public/user-lachlan-campbell.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/public/user-lachlan-campbell.jpeg -------------------------------------------------------------------------------- /docs/src/app/robots.tsx: -------------------------------------------------------------------------------- 1 | import {MetadataRoute} from 'next'; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | if (process.env.VERCEL_ENV !== 'production') { 5 | return { 6 | rules: { 7 | userAgent: '*', 8 | disallow: '/' 9 | } 10 | }; 11 | } else { 12 | return { 13 | rules: { 14 | userAgent: '*', 15 | allow: '/' 16 | } 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/components/Cards.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'clsx'; 2 | import type {ComponentProps} from 'react'; 3 | 4 | type Props = ComponentProps<'div'>; 5 | 6 | export default function Cards({children, className, ...props}: Props) { 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/components/CodeSnippet.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import {ReactNode} from 'react'; 3 | import styles from './CodeSnippet.module.css'; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | }; 8 | 9 | export default function CodeSnippet({children}: Props) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /docs/src/components/FooterLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import {ComponentProps} from 'react'; 3 | 4 | type Props = ComponentProps; 5 | 6 | export default function FooterLink({children, ...rest}: Props) { 7 | return ( 8 | 12 |

{children}

13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /docs/src/components/FooterSeparator.tsx: -------------------------------------------------------------------------------- 1 | export default function FooterSeparator() { 2 | return ( 3 | 4 |  ·  5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/components/FooterVersionSelector.tsx: -------------------------------------------------------------------------------- 1 | import {ChangeEvent} from 'react'; 2 | 3 | export default function FooterVersionSelector() { 4 | function onChange(event: ChangeEvent) { 5 | const version = event.target.value; 6 | window.location.href = `https://${version}.next-intl.dev`; 7 | } 8 | 9 | return ( 10 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/components/GetStartedBackground.module.css: -------------------------------------------------------------------------------- 1 | @keyframes pulsate { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 50% { 6 | opacity: 1; 7 | } 8 | 100% { 9 | opacity: 0; 10 | } 11 | } 12 | 13 | .dot { 14 | --GetStartedBackground-dot: rgba(0, 0, 0, 0.1); 15 | 16 | :global(.dark) & { 17 | --GetStartedBackground-dot: rgba(255, 255, 255, 0.1); 18 | } 19 | } 20 | 21 | .dot1 { 22 | animation: pulsate 4s infinite; 23 | } 24 | 25 | .dot2 { 26 | animation: pulsate 4s infinite 1s; 27 | } 28 | 29 | .dot3 { 30 | animation: pulsate 4s infinite 4s; 31 | } 32 | 33 | .dot4 { 34 | animation: pulsate 4s infinite 3s; 35 | } 36 | 37 | .dot5 { 38 | animation: pulsate 4s infinite 2s; 39 | } 40 | -------------------------------------------------------------------------------- /docs/src/components/HeroAnnouncement.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import {ReactNode} from 'react'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | href: string; 7 | }; 8 | 9 | export default function HeroAnnouncement({children, href}: Props) { 10 | return ( 11 | 15 | 📣 {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /docs/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link'; 2 | import {ComponentProps} from 'react'; 3 | 4 | type Props = Omit, 'className'>; 5 | 6 | export default function Link(props: Props) { 7 | return ( 8 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /docs/src/components/PartnerContentLink.tsx: -------------------------------------------------------------------------------- 1 | import {useMDXComponents} from 'nextra/mdx'; 2 | import {ComponentProps, ElementType} from 'react'; 3 | import PartnerLink from './PartnerLink'; 4 | 5 | type Props = Omit, 'as'>; 6 | 7 | export default function PartnerContentLink(props: Props) { 8 | const components = useMDXComponents(); 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/components/PartnerLink.tsx: -------------------------------------------------------------------------------- 1 | import {HTMLAttributes} from 'react'; 2 | import BrowserTracker from '@/services/BrowserTracker'; 3 | 4 | type Props = HTMLAttributes & { 5 | as?: React.ElementType; 6 | href: string; 7 | name: string; 8 | }; 9 | 10 | export default function PartnerLink({ 11 | as: Component = 'a', 12 | href, 13 | name, 14 | ...rest 15 | }: Props) { 16 | function onClick() { 17 | BrowserTracker.trackEvent({ 18 | name: 'partner-referral', 19 | data: {href, name} 20 | }); 21 | } 22 | 23 | return ; 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/components/PartnerSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Partner from './Partner'; 2 | import PartnerLink from './PartnerLink'; 3 | 4 | type Props = { 5 | intro?: string; 6 | }; 7 | 8 | export default function PartnerSidebar({intro = 'Presented by'}: Props) { 9 | return ( 10 | 15 |

16 | {intro} 17 |

18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/components/Screenshot.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Image from 'next/image'; 3 | import {ComponentProps} from 'react'; 4 | 5 | type Props = ComponentProps; 6 | 7 | export default function Screenshot({className, ...rest}: Props) { 8 | return ( 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/StayUpdated.mdx: -------------------------------------------------------------------------------- 1 |
2 | 3 | **Let's keep in touch:** 4 | 5 | - [Bluesky (Jan Amann)](https://bsky.app/profile/amann.work) 6 | - [X (Jan Amann)](https://x.com/jamannnnnn) 7 | -------------------------------------------------------------------------------- /docs/src/components/Steps.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply ml-4 border-l border-slate-200 pl-8; 3 | counter-reset: step; 4 | } 5 | 6 | .root h3 { 7 | counter-increment: step; 8 | @apply text-lg; 9 | } 10 | .root h3:before { 11 | content: counter(step); 12 | @apply absolute mt-[-6px] ml-[-52px] inline-block h-10 w-10 rounded-full border-4 border-white bg-slate-100 pt-[4px] text-center text-base font-bold text-slate-500; 13 | } 14 | 15 | :global(.dark) .root { 16 | @apply border-slate-800; 17 | } 18 | :global(.dark) .root h3:before { 19 | @apply bg-slate-800 text-white/75; 20 | border-color: rgba(17, 17, 17, var(--tw-bg-opacity)); /* bg-dark */ 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/components/Steps.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | import styles from './Steps.module.css'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | }; 7 | 8 | export default function Steps({children}: Props) { 9 | return
{children}
; 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import {ReactNode} from 'react'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | className?: string; 7 | }; 8 | 9 | export default function Wrapper({children, className}: Props) { 10 | return ( 11 |
17 | {children} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/components/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import {ComponentProps} from 'react'; 2 | 3 | type Props = ComponentProps<'svg'>; 4 | 5 | export default function CheckIcon(props: Props) { 6 | return ( 7 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/components/icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import {ComponentProps} from 'react'; 2 | 3 | type Props = ComponentProps<'svg'>; 4 | 5 | export default function CopyIcon(props: Props) { 6 | return ( 7 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'next-intl', 3 | description: 'Internationalization (i18n) for Next.js', 4 | baseUrl: process.env.VERCEL_PROJECT_PRODUCTION_URL 5 | ? 'https://' + process.env.VERCEL_PROJECT_PRODUCTION_URL 6 | : 'http://localhost:3000', 7 | githubUrl: 'https://github.com/amannn/next-intl', 8 | blueskyUrl: 'https://bsky.app/profile/amann.work', 9 | xUrl: 'https://x.com/jamannnnnn' 10 | }; 11 | -------------------------------------------------------------------------------- /docs/src/hooks/useLocationHash.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | 3 | function getHash() { 4 | return decodeURIComponent(window.location.hash.replace('#', '')); 5 | } 6 | 7 | export default function useLocationHash() { 8 | const [hash, setHash] = useState(); 9 | 10 | useEffect(() => { 11 | function updateHash() { 12 | setHash(getHash()); 13 | } 14 | 15 | window.addEventListener('hashchange', updateHash); 16 | 17 | updateHash(); 18 | 19 | return () => { 20 | window.removeEventListener('hashchange', updateHash); 21 | }; 22 | }, []); 23 | 24 | return hash; 25 | } 26 | -------------------------------------------------------------------------------- /docs/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import {Head, Html, Main, NextScript} from 'next/document'; 2 | import {SkipNavLink} from 'nextra-theme-docs'; 3 | import React from 'react'; 4 | 5 | export default function Document() { 6 | return ( 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/pages/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | title: 'Introduction', 4 | type: 'page', 5 | display: 'hidden', 6 | theme: {layout: 'raw'} 7 | }, 8 | docs: { 9 | title: 'Docs', 10 | type: 'page' 11 | }, 12 | examples: { 13 | title: 'Examples', 14 | type: 'page', 15 | theme: { 16 | sidebar: false, 17 | toc: false 18 | } 19 | }, 20 | blog: { 21 | title: 'Blog', 22 | type: 'page', 23 | theme: { 24 | sidebar: false, 25 | toc: false 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /docs/src/pages/api/Inter-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/src/pages/api/Inter-Regular.otf -------------------------------------------------------------------------------- /docs/src/pages/api/Inter-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/docs/src/pages/api/Inter-SemiBold.otf -------------------------------------------------------------------------------- /docs/src/pages/blog/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | title: 'Overview' 4 | }, 5 | 'next-intl-4-0': { 6 | title: 'next-intl 4.0', 7 | display: 'hidden' 8 | }, 9 | 'next-intl-3-22': { 10 | title: 'next-intl 3.22', 11 | display: 'hidden' 12 | }, 13 | 'date-formatting-nextjs': { 14 | title: 'Reliable date formatting in Next.js', 15 | display: 'hidden' 16 | }, 17 | 'next-intl-3-0': { 18 | title: 'next-intl 3.0', 19 | display: 'hidden' 20 | }, 21 | 'translations-outside-of-react-components': { 22 | title: 'How (not) to use translations outside of React components', 23 | display: 'hidden' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /docs/src/pages/docs/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'getting-started': 'Getting started', 3 | usage: 'Usage guide', 4 | routing: 'Routing', 5 | environments: 'Environments', 6 | workflows: 'Workflows & integrations', 7 | 'design-principles': 'Design principles' 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/docs/environments/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'server-client-components': 'Server & Client Components', 3 | 'actions-metadata-route-handlers': 4 | 'Server Actions, Metadata & Route Handlers', 5 | 'error-files': 'Error files (e.g. not-found)', 6 | mdx: 'Markdown (MDX)', 7 | 'core-library': 'Core library', 8 | 'runtime-requirements': 'Runtime requirements', 9 | testing: 'Testing' 10 | }; 11 | -------------------------------------------------------------------------------- /docs/src/pages/docs/getting-started/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app-router': 'App Router', 3 | 'pages-router': 'Pages Router' 4 | }; 5 | -------------------------------------------------------------------------------- /docs/src/pages/docs/getting-started/app-router/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'with-i18n-routing': 'With i18n routing', 3 | 'without-i18n-routing': 'Without i18n routing' 4 | }; 5 | -------------------------------------------------------------------------------- /docs/src/pages/docs/routing/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | middleware: 'Middleware', 3 | navigation: 'Navigation' 4 | }; 5 | -------------------------------------------------------------------------------- /docs/src/pages/docs/usage.mdx: -------------------------------------------------------------------------------- 1 | import Callout from '@/components/Callout'; 2 | import Card from '@/components/Card'; 3 | import Cards from '@/components/Cards'; 4 | 5 | # Usage guide 6 | 7 | This guide explains how `next-intl` can be used in React components. 8 | 9 | Note that while it might be helpful to get an overview of the capabilities of `next-intl`, you don't have to read the whole usage guide upfront. Rather, you can come back to it as necessary. 10 | 11 | Let's get started: 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/src/pages/docs/usage/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | messages: 'Messages', 3 | numbers: 'Numbers', 4 | 'dates-times': 'Dates and times', 5 | lists: 'Lists', 6 | configuration: 'Global configuration' 7 | }; 8 | -------------------------------------------------------------------------------- /docs/src/pages/docs/workflows.mdx: -------------------------------------------------------------------------------- 1 | import Card from '@/components/Card'; 2 | import Cards from '@/components/Cards'; 3 | 4 | # Workflows & integrations 5 | 6 | To get the most out of `next-intl`, you can choose from these integrations to improve your workflow. 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/src/pages/docs/workflows/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | typescript: 'TypeScript augmentation', 3 | 'localization-management': 'Localization management with Crowdin', 4 | 'vscode-integration': 'VSCode integration', 5 | linting: 'ESLint', 6 | storybook: 'Storybook', 7 | messages: 'Validating messages' 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/services/BrowserTracker.tsx: -------------------------------------------------------------------------------- 1 | import * as vercel from '@vercel/analytics/react'; 2 | 3 | type Event = { 4 | name: 'partner-referral'; 5 | data: {href: string; name: string}; 6 | }; 7 | 8 | export default class BrowserTracker { 9 | public static trackEvent({data, name}: Event) { 10 | return vercel.track(name, data); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/services/ServerTracker.tsx: -------------------------------------------------------------------------------- 1 | import * as vercel from '@vercel/analytics/server'; 2 | 3 | export default class ServerTracker { 4 | public static async trackEvent({ 5 | data, 6 | name 7 | }: { 8 | data?: Record; 9 | name: string; 10 | }) { 11 | return vercel.track(name, data).catch((error) => { 12 | throw new Error('Vercel tracking failed', {cause: error}); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | tsconfig.tsbuildinfo 5 | /test-results/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/README.md: -------------------------------------------------------------------------------- 1 | # example-app-router-migration 2 | 3 | An example that showcases a basic installation of `next-intl` in a Next.js app that uses both the Pages as well as the App Router. 4 | 5 | **Important**: Do not add an `i18n` config to `next.config.js`, as it will cause the `app` directory to be ignored. 6 | 7 | ## Deploy your own 8 | 9 | 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. 10 | 11 | [![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-migration) 12 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {FlatCompat} from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/no-explicit-any': 'off' 17 | } 18 | } 19 | ]; 20 | 21 | export default eslintConfig; 22 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/messages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Index": { 3 | "title": "Start", 4 | "description": "Das ist die Startseite.", 5 | "navigateToAbout": "Zu Über uns navigieren" 6 | }, 7 | "About": { 8 | "title": "Über", 9 | "description": "Das ist die Über-uns-Seite." 10 | }, 11 | "LocaleSwitcher": { 12 | "switchLocale": "Zu {locale, select, de {Deutsch} en {Englisch} other {Unbekannt}} wechseln" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Index": { 3 | "title": "Home", 4 | "description": "This is the home page.", 5 | "navigateToAbout": "Navigate to about" 6 | }, 7 | "About": { 8 | "title": "About", 9 | "description": "This is the about page." 10 | }, 11 | "LocaleSwitcher": { 12 | "switchLocale": "Switch to {locale, select, de {German} en {English} other {Unknown}}" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 7 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/next.config.ts: -------------------------------------------------------------------------------- 1 | import {NextConfig} from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const config: NextConfig = {}; 7 | 8 | export default withNextIntl(config); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-migration/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import {useLocale, useTranslations} from 'next-intl'; 2 | import LocaleSwitcher from '@/components/LocaleSwitcher'; 3 | import PageLayout from '@/components/PageLayout'; 4 | import {Link} from '@/i18n/navigation'; 5 | 6 | export default function Index() { 7 | const t = useTranslations('Index'); 8 | const locale = useLocale(); 9 | 10 | return ( 11 | 12 |

{t('description')}

13 | 14 |

15 | 16 | {t('navigateToAbout')} 17 | 18 |

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | // Since we have a `not-found.tsx` page on the root, a layout file 8 | // is required, even if it's just passing children through. 9 | export default function RootLayout({children}: Props) { 10 | return children; 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Error from 'next/error'; 4 | 5 | // Render the default Next.js 404 page when a route 6 | // is requested that doesn't match the middleware and 7 | // therefore doesn't have a locale associated with it. 8 | 9 | export default function NotFound() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import {useLocale, useTranslations} from 'next-intl'; 3 | 4 | export default function LocaleSwitcher() { 5 | const t = useTranslations('LocaleSwitcher'); 6 | const locale = useLocale(); 7 | const otherLocale = locale === 'en' ? 'de' : 'en'; 8 | 9 | return ( 10 | 11 | {t('switchLocale', {locale: otherLocale})} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/components/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children?: ReactNode; 5 | title: string; 6 | }; 7 | 8 | export default function PageLayout({children, title}: Props) { 9 | return ( 10 | <> 11 |
18 |
19 |

{title}

20 | {children} 21 |
22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/i18n/navigation.ts: -------------------------------------------------------------------------------- 1 | import {createNavigation} from 'next-intl/navigation'; 2 | import {routing} from './routing'; 3 | 4 | export const {Link, redirect, usePathname, useRouter} = 5 | createNavigation(routing); 6 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import {hasLocale} from 'next-intl'; 2 | import {getRequestConfig} from 'next-intl/server'; 3 | import {routing} from './routing'; 4 | 5 | export default getRequestConfig(async ({requestLocale}) => { 6 | // Typically corresponds to the `[locale]` segment 7 | const requested = await requestLocale; 8 | const locale = hasLocale(routing.locales, requested) 9 | ? requested 10 | : routing.defaultLocale; 11 | 12 | return { 13 | locale, 14 | messages: (await import(`../../messages/${locale}.json`)).default 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/i18n/routing.ts: -------------------------------------------------------------------------------- 1 | import {defineRouting} from 'next-intl/routing'; 2 | 3 | export const routing = defineRouting({ 4 | locales: ['en', 'de'], 5 | defaultLocale: 'en' 6 | }); 7 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import {routing} from './i18n/routing'; 3 | 4 | export default createMiddleware(routing); 5 | 6 | export const config = { 7 | // Match all pathnames except for 8 | // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel` 9 | // - … the ones containing a dot (e.g. `favicon.ico`) 10 | matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)' 11 | }; 12 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import {AppProps} from 'next/app'; 2 | import {NextRouter, withRouter} from 'next/router'; 3 | import {NextIntlClientProvider} from 'next-intl'; 4 | 5 | type Props = AppProps & { 6 | router: NextRouter; 7 | }; 8 | 9 | function App({Component, pageProps, router}: Props) { 10 | return ( 11 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default withRouter(App); 22 | -------------------------------------------------------------------------------- /examples/example-app-router-migration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "strict": true, 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | }, 24 | "strictNullChecks": true 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | tsconfig.tsbuildinfo 5 | test-results 6 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {FlatCompat} from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/no-explicit-any': 'off' 17 | } 18 | } 19 | ]; 20 | 21 | export default eslintConfig; 22 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/global.ts: -------------------------------------------------------------------------------- 1 | import {locales} from '@/config'; 2 | import messages from './messages/en.json'; 3 | 4 | declare module 'next-intl' { 5 | interface AppConfig { 6 | Locale: (typeof locales)[number]; 7 | Messages: typeof messages; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/messages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "About": { 3 | "title": "Über uns" 4 | }, 5 | "App": { 6 | "title": "Startseite" 7 | }, 8 | "AppNavigation": { 9 | "home": "Start", 10 | "profile": "Profil" 11 | }, 12 | "Index": { 13 | "title": "Start" 14 | }, 15 | "Login": { 16 | "label": "Anmelden →" 17 | }, 18 | "Logout": { 19 | "label": "Abmelden →" 20 | }, 21 | "PublicNavigation": { 22 | "about": "Über uns", 23 | "home": "Start" 24 | }, 25 | "Profile": { 26 | "title": "Profil" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "About": { 3 | "title": "About" 4 | }, 5 | "App": { 6 | "title": "Home" 7 | }, 8 | "AppNavigation": { 9 | "home": "Home", 10 | "profile": "Profile" 11 | }, 12 | "Index": { 13 | "title": "Home" 14 | }, 15 | "Login": { 16 | "label": "Login →" 17 | }, 18 | "Logout": { 19 | "label": "Logout →" 20 | }, 21 | "PublicNavigation": { 22 | "about": "About", 23 | "home": "Home" 24 | }, 25 | "Profile": { 26 | "title": "Profile" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/next.config.ts: -------------------------------------------------------------------------------- 1 | import {NextConfig} from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const config: NextConfig = {}; 7 | 8 | export default withNextIntl(config); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import type {PlaywrightTestConfig} from '@playwright/test'; 3 | import {devices} from '@playwright/test'; 4 | 5 | // Use a distinct port on CI to avoid conflicts during concurrent tests 6 | const PORT = process.env.CI ? 3002 : 3000; 7 | 8 | const config: PlaywrightTestConfig = { 9 | retries: process.env.CI ? 1 : 0, 10 | testDir: './tests', 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: devices['Desktop Chrome'] 15 | } 16 | ], 17 | fullyParallel: true, 18 | webServer: { 19 | command: `PORT=${PORT} pnpm start`, 20 | port: PORT, 21 | reuseExistingServer: true 22 | } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/(public)/[locale]/NavLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useSelectedLayoutSegment} from 'next/navigation'; 4 | import {ComponentProps} from 'react'; 5 | import {Link} from '@/i18n/navigation.public'; 6 | 7 | export default function NavLink({href, ...rest}: ComponentProps) { 8 | const selectedLayoutSegment = useSelectedLayoutSegment(); 9 | const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; 10 | const isActive = pathname === href; 11 | 12 | return ( 13 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/(public)/[locale]/PublicNavigation.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import Login from './Login'; 3 | import NavLink from './NavLink'; 4 | 5 | export default function PublicNavigation() { 6 | const t = useTranslations('PublicNavigation'); 7 | 8 | return ( 9 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/(public)/[locale]/about/page.tsx: -------------------------------------------------------------------------------- 1 | import {Locale, useTranslations} from 'next-intl'; 2 | import {setRequestLocale} from 'next-intl/server'; 3 | import {use} from 'react'; 4 | import PageTitle from '@/components/PageTitle'; 5 | 6 | type Props = { 7 | params: Promise<{locale: Locale}>; 8 | }; 9 | 10 | export default function About({params}: Props) { 11 | const {locale} = use(params); 12 | 13 | // Enable static rendering 14 | setRequestLocale(locale); 15 | 16 | const t = useTranslations('About'); 17 | return {t('title')}; 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/(public)/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import {Locale, useTranslations} from 'next-intl'; 2 | import {setRequestLocale} from 'next-intl/server'; 3 | import {use} from 'react'; 4 | import PageTitle from '@/components/PageTitle'; 5 | 6 | type Props = { 7 | params: Promise<{locale: Locale}>; 8 | }; 9 | 10 | export default function Index({params}: Props) { 11 | const {locale} = use(params); 12 | 13 | // Enable static rendering 14 | setRequestLocale(locale); 15 | 16 | const t = useTranslations('Index'); 17 | return {t('title')}; 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/app/AppNavigation.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import NavLink from './NavLink'; 3 | 4 | export default function AppNavigation() { 5 | const t = useTranslations('AppNavigation'); 6 | 7 | return ( 8 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/app/AppNavigationLocaleButton.tsx: -------------------------------------------------------------------------------- 1 | import {Locale, useLocale} from 'next-intl'; 2 | 3 | type Props = { 4 | locale: Locale; 5 | }; 6 | 7 | export default function AppNavigationLocaleButton({locale}: Props) { 8 | const curLocale = useLocale(); 9 | 10 | return ( 11 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/app/Logout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import {useTranslations} from 'next-intl'; 3 | 4 | export default function Logout() { 5 | const t = useTranslations('Logout'); 6 | return {t('label')}; 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/app/NavLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import {usePathname} from 'next/navigation'; 5 | import {ComponentProps} from 'react'; 6 | 7 | export default function NavLink({href, ...rest}: ComponentProps) { 8 | const pathname = usePathname(); 9 | const isActive = pathname === href; 10 | 11 | return ( 12 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import PageTitle from '@/components/PageTitle'; 3 | 4 | export default function App() { 5 | const t = useTranslations('App'); 6 | return {t('title')}; 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import PageTitle from '@/components/PageTitle'; 3 | 4 | export default function Profile() { 5 | const t = useTranslations('Profile'); 6 | return {t('title')}; 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-mixed-routing/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | export default function RootLayout({children}: Props) { 8 | return children; 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Error from 'next/error'; 4 | import Document from '@/components/Document'; 5 | 6 | export default function NotFound() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/components/Document.tsx: -------------------------------------------------------------------------------- 1 | import {Inter} from 'next/font/google'; 2 | import {ReactNode} from 'react'; 3 | import './globals.css'; 4 | 5 | const inter = Inter({ 6 | subsets: ['latin'], 7 | display: 'swap' 8 | }); 9 | 10 | type Props = { 11 | children: ReactNode; 12 | locale: string; 13 | }; 14 | 15 | export default function Document({children, locale}: Props) { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/components/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | export default function PageTitle({children}: Props) { 8 | return

{children}

; 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/components/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/config.ts: -------------------------------------------------------------------------------- 1 | import {Locale} from 'next-intl'; 2 | 3 | export const locales = ['en', 'de'] as const; 4 | 5 | export const defaultLocale: Locale = 'en'; 6 | 7 | // This cookie name is used by `next-intl` on the public pages too. By 8 | // reading/writing to this locale, we can ensure that the user's locale 9 | // is consistent across public and private pages. In case you save the 10 | // locale of registered users in a database, you can of course also use 11 | // that instead when the user is logged in. 12 | export const COOKIE_NAME = 'NEXT_LOCALE'; 13 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/i18n/navigation.public.ts: -------------------------------------------------------------------------------- 1 | import {createNavigation} from 'next-intl/navigation'; 2 | import {routing} from './routing.public'; 3 | 4 | // Should only be used on public routes in the `[locale]` segment 5 | export const {Link, usePathname} = createNavigation(routing); 6 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/i18n/routing.public.ts: -------------------------------------------------------------------------------- 1 | import {defineRouting} from 'next-intl/routing'; 2 | import {defaultLocale, locales} from '../config'; 3 | 4 | export const routing = defineRouting({ 5 | locales, 6 | defaultLocale 7 | }); 8 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import {routing} from './i18n/routing.public'; 3 | 4 | export default createMiddleware(routing); 5 | 6 | export const config = { 7 | // Match all pathnames except for 8 | // - … if they start with `/app`, `/_next` or `/_vercel` 9 | // - … the ones containing a dot (e.g. `favicon.ico`) 10 | matcher: '/((?!app|_next|_vercel|.*\\..*).*)' 11 | }; 12 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type {Config} from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}' 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' 15 | } 16 | } 17 | }, 18 | plugins: [] 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/example-app-router-mixed-routing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "strict": true, 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "Bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/.env: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET=secret 2 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | tsconfig.tsbuildinfo 5 | /test-results/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/README.md: -------------------------------------------------------------------------------- 1 | # example-app-router-next-auth 2 | 3 | An example that showcases the usage of `next-intl` together with Auth.js and the App Router. 4 | 5 | **Credentials**: admin / admin 6 | 7 | See [the corresponding docs for this example](https://next-intl.dev/docs/routing/middleware#example-auth-js). 8 | 9 | ## Deploy your own 10 | 11 | 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. 12 | 13 | [![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-next-auth) 14 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {FlatCompat} from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/no-explicit-any': 'off' 17 | } 18 | } 19 | ]; 20 | 21 | export default eslintConfig; 22 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/global.ts: -------------------------------------------------------------------------------- 1 | import {routing} from '@/i18n/routing'; 2 | import messages from './messages/en.json'; 3 | 4 | declare module 'next-intl' { 5 | interface AppConfig { 6 | Locale: (typeof routing.locales)[number]; 7 | Messages: typeof messages; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/next.config.ts: -------------------------------------------------------------------------------- 1 | import {NextConfig} from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const config: NextConfig = {}; 7 | 8 | export default withNextIntl(config); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import type {PlaywrightTestConfig} from '@playwright/test'; 3 | import {devices} from '@playwright/test'; 4 | 5 | // Use a distinct port on CI to avoid conflicts during concurrent tests 6 | const PORT = process.env.CI ? 3003 : 3000; 7 | 8 | const config: PlaywrightTestConfig = { 9 | retries: process.env.CI ? 1 : 0, 10 | testDir: './tests', 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: devices['Desktop Chrome'] 15 | } 16 | ], 17 | fullyParallel: true, 18 | webServer: { 19 | command: `PORT=${PORT} pnpm start`, 20 | port: PORT, 21 | reuseExistingServer: true 22 | } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-next-auth/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import {getServerSession} from 'next-auth'; 2 | import auth from '@/auth'; 3 | import Index from './Index'; 4 | 5 | export default async function IndexPage() { 6 | const session = await getServerSession(auth); 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/app/[locale]/secret/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useTranslations} from 'next-intl'; 4 | import PageLayout from '@/components/PageLayout'; 5 | 6 | export default function Secret() { 7 | const t = useTranslations('Secret'); 8 | 9 | return ( 10 | 11 |

{t('description')}

12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import auth from '../../../../auth'; 3 | 4 | const handler = NextAuth(auth); 5 | 6 | export {handler as GET, handler as POST}; 7 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | // Since we have a `not-found.tsx` page on the root, a layout file 8 | // is required, even if it's just passing children through. 9 | export default function RootLayout({children}: Props) { 10 | return children; 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Error from 'next/error'; 4 | 5 | // Render the default Next.js 404 page when a route 6 | // is requested that doesn't match the middleware and 7 | // therefore doesn't have a locale associated with it. 8 | 9 | export default function NotFound() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/auth.ts: -------------------------------------------------------------------------------- 1 | import {AuthOptions} from 'next-auth'; 2 | import CredentialsProvider from 'next-auth/providers/credentials'; 3 | 4 | const auth: AuthOptions = { 5 | providers: [ 6 | CredentialsProvider({ 7 | name: 'Credentials', 8 | credentials: { 9 | username: {type: 'text'}, 10 | password: {type: 'password'} 11 | }, 12 | authorize(credentials) { 13 | if ( 14 | credentials?.username === 'admin' && 15 | credentials.password === 'admin' 16 | ) { 17 | return {id: '1', name: 'admin'}; 18 | } 19 | 20 | return null; 21 | } 22 | }) 23 | ] 24 | }; 25 | 26 | export default auth; 27 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import {useLocale, useTranslations} from 'next-intl'; 2 | import {Link, usePathname} from '@/i18n/navigation'; 3 | 4 | export default function LocaleSwitcher() { 5 | const t = useTranslations('LocaleSwitcher'); 6 | const locale = useLocale(); 7 | const otherLocale = locale === 'en' ? 'de' : 'en'; 8 | const pathname = usePathname(); 9 | 10 | return ( 11 | 12 | {t('switchLocale', {locale: otherLocale})} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/components/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | import LocaleSwitcher from './LocaleSwitcher'; 3 | 4 | type Props = { 5 | children?: ReactNode; 6 | title: string; 7 | }; 8 | 9 | export default function PageLayout({children, title}: Props) { 10 | return ( 11 | <> 12 |
20 |
21 |

{title}

22 | {children} 23 |
24 | 25 |
26 |
27 |
28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/i18n/navigation.ts: -------------------------------------------------------------------------------- 1 | import {createNavigation} from 'next-intl/navigation'; 2 | import {routing} from './routing'; 3 | 4 | export const {Link, redirect, usePathname, useRouter} = 5 | createNavigation(routing); 6 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import {hasLocale} from 'next-intl'; 2 | import {getRequestConfig} from 'next-intl/server'; 3 | import {routing} from './routing'; 4 | 5 | export default getRequestConfig(async ({requestLocale}) => { 6 | // Typically corresponds to the `[locale]` segment 7 | const requested = await requestLocale; 8 | const locale = hasLocale(routing.locales, requested) 9 | ? requested 10 | : routing.defaultLocale; 11 | 12 | return { 13 | locale, 14 | messages: (await import(`../../messages/${locale}.json`)).default 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/src/i18n/routing.ts: -------------------------------------------------------------------------------- 1 | import {defineRouting} from 'next-intl/routing'; 2 | 3 | export const routing = defineRouting({ 4 | locales: ['en', 'de'], 5 | defaultLocale: 'en', 6 | localePrefix: 'as-needed' 7 | }); 8 | -------------------------------------------------------------------------------- /examples/example-app-router-next-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "strict": true, 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | tsconfig.tsbuildinfo 5 | *storybook.log 6 | storybook-static 7 | test-results 8 | messages/*.d.json.ts 9 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type {StorybookConfig} from '@storybook/nextjs'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.stories.tsx'], 5 | addons: ['storybook-next-intl'], 6 | framework: { 7 | name: '@storybook/nextjs', 8 | options: {} 9 | }, 10 | staticDirs: ['../public'] 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/.storybook/next-intl.ts: -------------------------------------------------------------------------------- 1 | import en from '../messages/en.json'; 2 | import de from '../messages/de.json'; 3 | import es from '../messages/es.json'; 4 | import ja from '../messages/ja.json'; 5 | 6 | const messagesByLocale: Record = {en, de, es, ja}; 7 | 8 | const nextIntl = { 9 | defaultLocale: 'en', 10 | messagesByLocale 11 | }; 12 | 13 | export default nextIntl; 14 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type {Preview} from '@storybook/react'; 2 | import nextIntl from './next-intl'; 3 | 4 | const preview: Preview = { 5 | initialGlobals: { 6 | locale: 'en', 7 | locales: { 8 | en: 'English', 9 | de: 'Deutsch', 10 | es: 'Español', 11 | ja: '日本語' 12 | } 13 | }, 14 | parameters: { 15 | nextIntl 16 | } 17 | }; 18 | 19 | export default preview; 20 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/README.md: -------------------------------------------------------------------------------- 1 | # example-app-router-playground 2 | 3 | An example that showcases various advanced use cases of using `next-intl` with the App Router. 4 | 5 | ## Deploy your own 6 | 7 | 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. 8 | 9 | [![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-playground) 10 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {FlatCompat} from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/no-explicit-any': 'off', 17 | '@typescript-eslint/ban-ts-comment': 'off' 18 | } 19 | } 20 | ]; 21 | 22 | export default eslintConfig; 23 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/global.ts: -------------------------------------------------------------------------------- 1 | import {formats} from '@/i18n/request'; 2 | import {routing} from '@/i18n/routing'; 3 | import messages from './messages/en.json'; 4 | 5 | declare module 'next-intl' { 6 | interface AppConfig { 7 | Locale: (typeof routing.locales)[number]; 8 | Formats: typeof formats; 9 | Messages: typeof messages; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const nextJest = require('next/jest'); 3 | 4 | const createJestConfig = nextJest({dir: './'}); 5 | 6 | module.exports = async () => ({ 7 | ...(await createJestConfig({ 8 | testEnvironment: 'jsdom', 9 | rootDir: 'src' 10 | })()), 11 | transformIgnorePatterns: ['node_modules/(?!next-intl)/'] 12 | }); 13 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/public/assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-playground/public/assets/image.jpg -------------------------------------------------------------------------------- /examples/example-app-router-playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-playground/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-app-router-playground/runPlaywright.mjs: -------------------------------------------------------------------------------- 1 | import {execSync} from 'child_process'; 2 | 3 | const useCases = [ 4 | 'main', 5 | 'locale-prefix-never', 6 | 'trailing-slash', 7 | 'base-path', 8 | 'domains', 9 | 'locale-cookie-false' 10 | ]; 11 | 12 | for (const useCase of useCases) { 13 | // eslint-disable-next-line no-console 14 | console.log(`Running tests for use case: ${useCase}`); 15 | 16 | const command = `NEXT_PUBLIC_USE_CASE=${useCase} pnpm build && NEXT_PUBLIC_USE_CASE=${useCase} TEST_MATCH=${useCase}.spec.ts playwright test`; 17 | execSync(command, {stdio: 'inherit'}); 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/[...rest]/page.tsx: -------------------------------------------------------------------------------- 1 | import {notFound} from 'next/navigation'; 2 | 3 | export default function CatchAll() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/about/de.mdx: -------------------------------------------------------------------------------- 1 | import AsyncComponent from '@/components/AsyncComponent'; 2 | import Counter from '@/components/client/02-MessagesOnClientCounter/Counter'; 3 | 4 | # Über uns 5 | 6 | Ein bisschen Text … 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/about/en.mdx: -------------------------------------------------------------------------------- 1 | import AsyncComponent from '@/components/AsyncComponent'; 2 | import Counter from '@/components/client/02-MessagesOnClientCounter/Counter'; 3 | 4 | # About 5 | 6 | Some text … 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/about/es.mdx: -------------------------------------------------------------------------------- 1 | import AsyncComponent from '@/components/AsyncComponent'; 2 | import Counter from '@/components/client/02-MessagesOnClientCounter/Counter'; 3 | 4 | # Acerca de 5 | 6 | Algun texto ... 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/about/page.tsx: -------------------------------------------------------------------------------- 1 | import {Locale} from 'next-intl'; 2 | 3 | type Props = { 4 | params: Promise<{ 5 | locale: Locale; 6 | }>; 7 | }; 8 | 9 | export default async function AboutPage({params}: Props) { 10 | const {locale} = await params; 11 | const Content = (await import(`./${locale}.mdx`)).default; 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/actions/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | 3 | export default function ListItem({id}: {id: number}) { 4 | const t = useTranslations('ServerActions'); 5 | return t('item', {id: String(id)}); 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/actions/ListItemAsync.tsx: -------------------------------------------------------------------------------- 1 | import {getTranslations} from 'next-intl/server'; 2 | 3 | export default async function ListItemAsync({id}: {id: number}) { 4 | const t = await getTranslations('ServerActions'); 5 | return t('item', {id: String(id)}); 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/actions/ListItemClient.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useTranslations} from 'next-intl'; 4 | 5 | export default function ListItemClient({id}: {id: number}) { 6 | const t = useTranslations('ServerActions'); 7 | return t('item', {id: String(id)}); 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/actions/ZodForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {ReactNode, useActionState} from 'react'; 4 | import {FormResult} from './ZodFormExample'; 5 | 6 | type Props = { 7 | action(prev: unknown, data: FormData): Promise; 8 | children: ReactNode; 9 | }; 10 | 11 | export default function ZodForm({action, children}: Props) { 12 | const [state, formAction] = useActionState(action, null); 13 | const hasErrors = state && !state.success; 14 | 15 | return ( 16 |
17 | {children} 18 | {hasErrors &&

{state.fieldErrors.task?.join(', ')}

} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/api/route.ts: -------------------------------------------------------------------------------- 1 | import {NextRequest, NextResponse} from 'next/server'; 2 | import {Locale} from 'next-intl'; 3 | import {getTranslations} from 'next-intl/server'; 4 | 5 | type Props = { 6 | params: Promise<{ 7 | locale: Locale; 8 | }>; 9 | }; 10 | 11 | export async function GET(request: NextRequest, props: Props) { 12 | const params = await props.params; 13 | const {locale} = params; 14 | 15 | const name = request.nextUrl.searchParams.get('name'); 16 | if (!name) { 17 | return new Response('Search param `name` was not provided.', {status: 400}); 18 | } 19 | 20 | const t = await getTranslations({locale, namespace: 'ApiRoute'}); 21 | return NextResponse.json({message: t('hello', {name})}); 22 | } 23 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/client/ClientContent.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useLocale, useNow, useTimeZone} from 'next-intl'; 4 | import {Link, usePathname} from '@/i18n/navigation'; 5 | 6 | export default function ClientContent() { 7 | const now = useNow(); 8 | const timeZone = useTimeZone(); 9 | const locale = useLocale(); 10 | 11 | return ( 12 | <> 13 |

{now.toISOString()}

14 | Go to home 15 |

{usePathname()}

16 |

{timeZone}

17 |

{locale}

18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/client/DelayedServerContent.tsx: -------------------------------------------------------------------------------- 1 | import {useNow} from 'next-intl'; 2 | import {use} from 'react'; 3 | 4 | export default function DelayedServerContent() { 5 | use(new Promise((resolve) => setTimeout(resolve, 50))); 6 | const now = useNow(); 7 | 8 | return ( 9 | <> 10 |

{now.toISOString()}

11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/client/page.tsx: -------------------------------------------------------------------------------- 1 | import {NextIntlClientProvider, useNow, useTranslations} from 'next-intl'; 2 | import PageLayout from '../../../components/PageLayout'; 3 | import ClientContent from './ClientContent'; 4 | import DelayedServerContent from './DelayedServerContent'; 5 | 6 | export default function Client() { 7 | const t = useTranslations('Client'); 8 | const now = useNow(); 9 | 10 | return ( 11 | 12 |

{t('description')}

13 |

{now.toISOString()}

14 | 15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/client/redirect/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useLocale} from 'next-intl'; 4 | import {redirect} from '@/i18n/navigation'; 5 | 6 | export default function ClientRedirectPage() { 7 | const locale = useLocale(); 8 | redirect({href: '/client', locale}); 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/nested/UnlocalizedPathname.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {usePathname} from '@/i18n/navigation'; 4 | 5 | export default function UnlocalizedPathname() { 6 | return

{usePathname()}

; 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/nested/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import {render, screen} from '@testing-library/react'; 2 | import pick from 'lodash/pick'; 3 | import {NextIntlClientProvider} from 'next-intl'; 4 | import messages from '../../../../messages/en.json'; 5 | import Nested, {generateMetadata} from './page'; 6 | 7 | it('renders', () => { 8 | render( 9 | 10 | 11 | 12 | ); 13 | screen.getByText('Nested'); 14 | }); 15 | 16 | it("can't generate metadata in a test", async () => { 17 | await expect(generateMetadata()).rejects.toThrow( 18 | '`getTranslations` is not supported in Client Components.' 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/nested/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import {getTranslations} from 'next-intl/server'; 3 | import PageLayout from '../../../components/PageLayout'; 4 | import UnlocalizedPathname from './UnlocalizedPathname'; 5 | 6 | export async function generateMetadata() { 7 | const t = await getTranslations('Nested'); 8 | return { 9 | title: t('title') 10 | }; 11 | } 12 | 13 | export default function Nested() { 14 | const t = useTranslations('Nested'); 15 | 16 | return ( 17 | 18 |

{t('description')}

19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/news/just-in/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | 3 | export default function NewsArticle() { 4 | const t = useTranslations('JustIn'); 5 | return

{t('title')}

; 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import PageLayout from '../../components/PageLayout'; 3 | 4 | export default function NotFound() { 5 | const t = useTranslations('NotFound'); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import {ImageResponse} from 'next/og'; 2 | import {Locale} from 'next-intl'; 3 | import {getTranslations} from 'next-intl/server'; 4 | 5 | type Props = { 6 | params: { 7 | locale: Locale; 8 | }; 9 | }; 10 | 11 | export default async function Image({params: {locale}}: Props) { 12 | const t = await getTranslations({locale, namespace: 'OpenGraph'}); 13 | return new ImageResponse(
{t('title')}
); 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/[locale]/redirect/page.tsx: -------------------------------------------------------------------------------- 1 | import {useLocale} from 'next-intl'; 2 | import {redirect} from '@/i18n/navigation'; 3 | 4 | export default function Redirect() { 5 | const locale = useLocale(); 6 | redirect({href: '/client', locale}); 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | // Since we have a `not-found.tsx` page on the root, a layout file 8 | // is required, even if it's just passing children through. 9 | export default function RootLayout({children}: Props) { 10 | return children; 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Error from 'next/error'; 4 | 5 | // Render the default Next.js 404 page when a route 6 | // is requested that doesn't match the middleware and 7 | // therefore doesn't have a locale associated with it. 8 | 9 | export default function NotFound() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/AsyncComponentWithNamespaceAndLocale.tsx: -------------------------------------------------------------------------------- 1 | import {getLocale, getTranslations} from 'next-intl/server'; 2 | 3 | export default async function AsyncComponentWithNamespaceAndLocale() { 4 | const locale = await getLocale(); 5 | const t = await getTranslations({locale, namespace: 'AsyncComponent'}); 6 | 7 | return ( 8 |
9 | {t('basic')} 10 |
11 | ); 12 | } 13 | 14 | export async function TypeTest() { 15 | const locale = await getLocale(); 16 | const t = await getTranslations({locale}); 17 | 18 | // @ts-expect-error 19 | await getTranslations({locale, namespace: 'Unknown'}); 20 | 21 | // @ts-expect-error 22 | t('AsyncComponent.unknown'); 23 | } 24 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/AsyncComponentWithoutNamespace.tsx: -------------------------------------------------------------------------------- 1 | import {getTranslations} from 'next-intl/server'; 2 | 3 | export default async function AsyncComponentWithoutNamespace() { 4 | const t = await getTranslations(); 5 | 6 | return ( 7 |
8 | {t('AsyncComponent.basic')} 9 |
10 | ); 11 | } 12 | 13 | export async function TypeTest() { 14 | const t = await getTranslations(); 15 | 16 | // @ts-expect-error 17 | t('AsyncComponent.unknown'); 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/AsyncComponentWithoutNamespaceAndLocale.tsx: -------------------------------------------------------------------------------- 1 | import {getLocale, getTranslations} from 'next-intl/server'; 2 | 3 | export default async function AsyncComponentWithoutNamespaceAndLocale() { 4 | const locale = await getLocale(); 5 | const t = await getTranslations({locale}); 6 | 7 | return ( 8 |
9 | {t('AsyncComponent.basic')} 10 |
11 | ); 12 | } 13 | 14 | export async function TypeTest() { 15 | const locale = await getLocale(); 16 | const t = await getTranslations({locale}); 17 | 18 | // @ts-expect-error 19 | t('AsyncComponent.unknown'); 20 | } 21 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/ClientLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {ComponentProps} from 'react'; 4 | import {Link} from '@/i18n/navigation'; 5 | 6 | export default function NavigationLink(props: ComponentProps) { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/ClientRouterWithoutProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useRouter} from '@/i18n/navigation'; 4 | 5 | export default function ClientRouterWithoutProvider() { 6 | const router = useRouter(); 7 | 8 | function onClick() { 9 | router.push('/nested'); 10 | } 11 | 12 | return ( 13 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/CoreLibrary.tsx: -------------------------------------------------------------------------------- 1 | import {createFormatter, createTranslator} from 'next-intl'; 2 | 3 | export default function CoreLibrary() { 4 | const t = createTranslator({ 5 | locale: 'en', 6 | messages: {Index: {title: 'Relative time:'}} as any 7 | }); 8 | 9 | const now = new Date(2022, 10, 6, 20, 20, 0, 0); 10 | const format = createFormatter({locale: 'en', now}); 11 | const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); 12 | 13 | return ( 14 |

15 | {t('Index.title')} {format.relativeTime(tomorrow)} 16 |

17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as Dropdown from '@radix-ui/react-dropdown-menu'; 2 | import {Link as NextIntlLink} from '@/i18n/navigation'; 3 | 4 | export default function DropdownMenu() { 5 | return ( 6 | 7 | Toggle dropdown 8 | 9 | 10 | Bar 11 | 12 | Link to about 13 | 14 | Foo 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import {useLocale, useTranslations} from 'next-intl'; 2 | import {Link} from '@/i18n/navigation'; 3 | 4 | export default function LocaleSwitcher() { 5 | const t = useTranslations('LocaleSwitcher'); 6 | 7 | const locale = useLocale(); 8 | const otherLocale = locale === 'en' ? 'de' : 'en'; 9 | 10 | return ( 11 | 12 | {t('switchLocale', {locale: otherLocale})} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/Navigation.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from '@storybook/react'; 2 | import Navigation from './Navigation'; 3 | 4 | const meta = { 5 | title: 'components/Navigation', 6 | component: Navigation 7 | } satisfies Meta; 8 | export default meta; 9 | type Story = StoryObj; 10 | 11 | export const Default: Story = {}; 12 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/NavigationLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useSelectedLayoutSegment} from 'next/navigation'; 4 | import {ComponentProps} from 'react'; 5 | import {Link} from '@/i18n/navigation'; 6 | 7 | export default function NavigationLink({ 8 | href, 9 | ...rest 10 | }: ComponentProps) { 11 | const selectedLayoutSegment = useSelectedLayoutSegment(); 12 | const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/'; 13 | const isActive = pathname === href; 14 | 15 | return ( 16 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children?: ReactNode; 5 | title: string; 6 | }; 7 | 8 | export default function PageLayout({children, title}: Props) { 9 | return ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/RichText.tsx: -------------------------------------------------------------------------------- 1 | import {ComponentProps, ReactNode} from 'react'; 2 | 3 | type Tag = 'b' | 'i'; 4 | 5 | type Props = { 6 | children(tags: Record ReactNode>): ReactNode; 7 | } & Omit, 'children'>; 8 | 9 | export default function RichText({children, ...rest}: Props) { 10 | return ( 11 |

12 | {children({ 13 | b: (chunks: ReactNode) => {chunks}, 14 | i: (chunks: ReactNode) => {chunks} 15 | })} 16 |

17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/client/01-MessagesAsPropsCounter/ClientCounter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useState} from 'react'; 4 | 5 | type Props = { 6 | messages: { 7 | count: string; 8 | increment: string; 9 | }; 10 | }; 11 | 12 | export default function ClientCounter({messages}: Props) { 13 | const [count, setCount] = useState(0); 14 | 15 | function onIncrement() { 16 | setCount(count + 1); 17 | } 18 | 19 | return ( 20 |
21 |

22 | {messages.count} {count} 23 |

24 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/client/01-MessagesAsPropsCounter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import ClientCounter from './ClientCounter'; 3 | 4 | export default function Counter() { 5 | const t = useTranslations('Counter'); 6 | 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/client/01-MessagesAsPropsCounter/index.tsx: -------------------------------------------------------------------------------- 1 | export {default} from './Counter'; 2 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/client/02-MessagesOnClientCounter/ClientCounter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {useTranslations} from 'next-intl'; 4 | import {useState} from 'react'; 5 | 6 | export default function ClientCounter() { 7 | const t = useTranslations('ClientCounter'); 8 | const [count, setCount] = useState(0); 9 | 10 | function onIncrement() { 11 | setCount(count + 1); 12 | } 13 | 14 | return ( 15 |
16 |

{t('count', {count: String(count)})}

17 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/client/02-MessagesOnClientCounter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick'; 2 | import {NextIntlClientProvider, useMessages} from 'next-intl'; 3 | import ClientCounter from './ClientCounter'; 4 | 5 | export default function Counter() { 6 | const messages = useMessages(); 7 | 8 | return ( 9 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/components/client/02-MessagesOnClientCounter/index.tsx: -------------------------------------------------------------------------------- 1 | export {default} from './Counter'; 2 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/i18n/navigation.ts: -------------------------------------------------------------------------------- 1 | import {createNavigation} from 'next-intl/navigation'; 2 | import {routing} from './routing'; 3 | 4 | export const {Link, getPathname, redirect, usePathname, useRouter} = 5 | createNavigation(routing); 6 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import type {MDXComponents} from 'mdx/types'; 2 | 3 | export function useMDXComponents(components: MDXComponents): MDXComponents { 4 | return components; 5 | } 6 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import {routing} from './i18n/routing'; 3 | 4 | export default createMiddleware(routing); 5 | 6 | export const config = { 7 | matcher: [ 8 | // Skip all paths that should not be internationalized 9 | '/((?!_next|.*/opengraph-image|.*\\..*).*)', 10 | 11 | // Necessary for base path to work 12 | '/' 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /examples/example-app-router-playground/tests/locale-cookie-false.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect, test as it} from '@playwright/test'; 2 | 3 | it('never sets a cookie', async ({page}) => { 4 | async function expectNoCookie() { 5 | expect( 6 | (await page.context().cookies()).find((cur) => cur.name === 'NEXT_LOCALE') 7 | ).toBeUndefined(); 8 | } 9 | 10 | await page.goto('/'); 11 | await expectNoCookie(); 12 | 13 | await page.getByRole('link', {name: 'Switch to German'}).click(); 14 | await expect(page).toHaveURL('/de'); 15 | await expectNoCookie(); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | tsconfig.tsbuildinfo 5 | /test-results/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/README.md: -------------------------------------------------------------------------------- 1 | # example-app-router-single-locale 2 | 3 | An example that showcases an app that only supports a single language and doesn't utilize i18n routing. 4 | 5 | ## Deploy your own 6 | 7 | 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. 8 | 9 | [![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-single-locale) 10 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {FlatCompat} from '@eslint/eslintrc'; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | const compat = new FlatCompat({ 7 | baseDirectory: __dirname 8 | }); 9 | 10 | const eslintConfig = [ 11 | ...compat.extends('next/core-web-vitals', 'next/typescript') 12 | ]; 13 | export default eslintConfig; 14 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Index": { 3 | "title": "Home", 4 | "description": "This is the home page.", 5 | "navigateToAbout": "Navigate to about" 6 | }, 7 | "About": { 8 | "title": "About", 9 | "description": "This is the about page." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | 3 | const withNextIntl = createNextIntlPlugin(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = {}; 7 | 8 | export default withNextIntl(nextConfig); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/next.config.ts: -------------------------------------------------------------------------------- 1 | import {NextConfig} from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const config: NextConfig = {}; 7 | 8 | export default withNextIntl(config); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import type {PlaywrightTestConfig} from '@playwright/test'; 3 | import {devices} from '@playwright/test'; 4 | 5 | // Use a distinct port on CI to avoid conflicts during concurrent tests 6 | const PORT = process.env.CI ? 3005 : 3000; 7 | 8 | const config: PlaywrightTestConfig = { 9 | retries: process.env.CI ? 1 : 0, 10 | testDir: './tests', 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: devices['Desktop Chrome'] 15 | } 16 | ], 17 | fullyParallel: true, 18 | webServer: { 19 | command: `PORT=${PORT} pnpm start`, 20 | port: PORT, 21 | reuseExistingServer: true 22 | } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-single-locale/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | import PageLayout from '@/components/PageLayout'; 3 | 4 | export default function About() { 5 | const t = useTranslations('About'); 6 | 7 | return ( 8 | 9 |

{t('description')}

10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import {NextIntlClientProvider} from 'next-intl'; 2 | import {getLocale} from 'next-intl/server'; 3 | import {ReactNode} from 'react'; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | }; 8 | 9 | export default async function LocaleLayout({children}: Props) { 10 | const locale = await getLocale(); 11 | 12 | return ( 13 | 14 | 15 | next-intl 16 | 17 | 18 | {children} 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import {useTranslations} from 'next-intl'; 3 | import PageLayout from '@/components/PageLayout'; 4 | 5 | export default function Index() { 6 | const t = useTranslations('Index'); 7 | 8 | return ( 9 | 10 |

{t('description')}

11 | {t('navigateToAbout')} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/src/components/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactNode} from 'react'; 2 | 3 | type Props = { 4 | children?: ReactNode; 5 | title: string; 6 | }; 7 | 8 | export default function PageLayout({children, title}: Props) { 9 | return ( 10 | <> 11 |
18 |
19 |

{title}

20 | {children} 21 |
22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import {getRequestConfig} from 'next-intl/server'; 2 | 3 | export default getRequestConfig(async () => { 4 | const locale = 'en'; 5 | 6 | return { 7 | locale, 8 | messages: (await import(`../../messages/${locale}.json`)).default 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/tests/main.spec.ts: -------------------------------------------------------------------------------- 1 | import {test as it} from '@playwright/test'; 2 | 3 | it('uses the provided locale', async ({page}) => { 4 | await page.goto('/'); 5 | page.getByRole('heading', {name: 'Home'}); 6 | 7 | await page.goto('/'); 8 | page.getByRole('heading', {name: 'About'}); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/example-app-router-single-locale/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "strict": true, 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | }, 24 | "strictNullChecks": true 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.next/ 3 | .DS_Store 4 | tsconfig.tsbuildinfo 5 | /test-results/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {FlatCompat} from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript') 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | 3 | const withNextIntl = createNextIntlPlugin(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = {}; 7 | 8 | export default withNextIntl(nextConfig); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/next.config.ts: -------------------------------------------------------------------------------- 1 | import {NextConfig} from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const config: NextConfig = {}; 7 | 8 | export default withNextIntl(config); 9 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import type {PlaywrightTestConfig} from '@playwright/test'; 3 | import {devices} from '@playwright/test'; 4 | 5 | // Use a distinct port on CI to avoid conflicts during concurrent tests 6 | const PORT = process.env.CI ? 3006 : 3000; 7 | 8 | const config: PlaywrightTestConfig = { 9 | retries: process.env.CI ? 1 : 0, 10 | testDir: './tests', 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: devices['Desktop Chrome'] 15 | } 16 | ], 17 | fullyParallel: true, 18 | webServer: { 19 | command: `PORT=${PORT} pnpm start`, 20 | port: PORT, 21 | reuseExistingServer: true 22 | } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amannn/next-intl/9c837d654703507a510278a616490db4bdd0469b/examples/example-app-router-without-i18n-routing/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/src/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | 3 | export default function HomePage() { 4 | const t = useTranslations('HomePage'); 5 | return ( 6 |

{t('title')}

7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/src/app/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslations} from 'next-intl'; 2 | 3 | export default function ProfilePage() { 4 | const t = useTranslations('ProfilePage'); 5 | return ( 6 |

{t('title')}

7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | -webkit-font-smoothing: antialiased; 7 | @apply outline-sky-600; 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import {redirect} from 'next/navigation'; 2 | 3 | export default function IndexPage() { 4 | redirect('/login'); 5 | } 6 | -------------------------------------------------------------------------------- /examples/example-app-router-without-i18n-routing/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {ComponentProps} from 'react'; 4 | import {useFormStatus} from 'react-dom'; 5 | 6 | export default function Button(props: ComponentProps<'button'>) { 7 | const {pending} = useFormStatus(); 8 | 9 | return ( 10 |