diff --git a/examples/nextjs-swc/src/app/[lang]/app-router-demo/page.tsx b/examples/nextjs-swc/src/app/[lang]/app-router-demo/page.tsx
new file mode 100644
index 000000000..842d5515e
--- /dev/null
+++ b/examples/nextjs-swc/src/app/[lang]/app-router-demo/page.tsx
@@ -0,0 +1,4 @@
+import { HomePage } from '../../../components/HomePage'
+import { withLinguiPage } from '../../../withLingui'
+
+export default withLinguiPage(HomePage)
diff --git a/examples/nextjs-swc/src/app/[lang]/layout.tsx b/examples/nextjs-swc/src/app/[lang]/layout.tsx
new file mode 100644
index 000000000..11f1efef5
--- /dev/null
+++ b/examples/nextjs-swc/src/app/[lang]/layout.tsx
@@ -0,0 +1,38 @@
+import linguiConfig from '../../../lingui.config'
+import { allI18nInstances, allMessages } from '../../appRouterI18n'
+import { LinguiClientProvider } from '../../components/LinguiClientProvider'
+import { PageLangParam, withLinguiLayout } from '../../withLingui'
+import React from 'react'
+import { t } from '@lingui/macro'
+
+export async function generateStaticParams() {
+ return linguiConfig.locales.map((lang) => ({ lang }))
+}
+
+export function generateMetadata({ params }: PageLangParam) {
+ const i18n = allI18nInstances[params.lang]!
+
+ return {
+ title: t(i18n)`Translation Demo`
+ }
+}
+
+export default withLinguiLayout(function RootLayout({
+ children,
+ params: { lang }
+}) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+})
diff --git a/examples/nextjs-swc/src/app/[lang]/page.tsx b/examples/nextjs-swc/src/app/[lang]/page.tsx
new file mode 100644
index 000000000..9498097d8
--- /dev/null
+++ b/examples/nextjs-swc/src/app/[lang]/page.tsx
@@ -0,0 +1,9 @@
+export default function Index() {
+ return (
+ <>
+ This is the homepage of the demo app. This page is not localized. You can
+ go to the App router demo or the{' '}
+ Pages router demo.
+ >
+ )
+}
diff --git a/examples/nextjs-swc/src/appRouterI18n.ts b/examples/nextjs-swc/src/appRouterI18n.ts
new file mode 100644
index 000000000..8bd7b24db
--- /dev/null
+++ b/examples/nextjs-swc/src/appRouterI18n.ts
@@ -0,0 +1,37 @@
+import 'server-only'
+
+import linguiConfig from '../lingui.config'
+import { I18n, Messages, setupI18n } from '@lingui/core'
+
+const { locales } = linguiConfig
+// optionally use a stricter union type
+type SupportedLocales = string
+
+async function loadCatalog(locale: SupportedLocales): Promise<{
+ [k: string]: Messages
+}> {
+ const { messages } = await import(`./locales/${locale}.po`)
+ return {
+ [locale]: messages
+ }
+}
+const catalogs = await Promise.all(locales.map(loadCatalog))
+
+// transform array of catalogs into a single object
+export const allMessages = catalogs.reduce((acc, oneCatalog) => {
+ return { ...acc, ...oneCatalog }
+}, {})
+
+type AllI18nInstances = { [K in SupportedLocales]: I18n }
+
+export const allI18nInstances: AllI18nInstances = locales.reduce(
+ (acc, locale) => {
+ const messages = allMessages[locale] ?? {}
+ const i18n = setupI18n({
+ locale,
+ messages: { [locale]: messages }
+ })
+ return { ...acc, [locale]: i18n }
+ },
+ {}
+)
diff --git a/examples/nextjs-swc/src/components/Developers.tsx b/examples/nextjs-swc/src/components/Developers.tsx
index 027f5bf4d..64ef1ed61 100644
--- a/examples/nextjs-swc/src/components/Developers.tsx
+++ b/examples/nextjs-swc/src/components/Developers.tsx
@@ -1,3 +1,6 @@
+'use client'
+// this is a client component because it uses the `useState` hook
+
import { useState } from 'react'
import { Trans, Plural } from '@lingui/macro'
diff --git a/examples/nextjs-swc/src/pages/index.tsx b/examples/nextjs-swc/src/components/HomePage.tsx
similarity index 59%
rename from examples/nextjs-swc/src/pages/index.tsx
rename to examples/nextjs-swc/src/components/HomePage.tsx
index 21c3cc420..59a0a3482 100644
--- a/examples/nextjs-swc/src/pages/index.tsx
+++ b/examples/nextjs-swc/src/components/HomePage.tsx
@@ -1,28 +1,14 @@
-import { t, Trans } from '@lingui/macro'
-import { GetStaticProps, NextPage } from 'next'
+import React from 'react'
+import { useLingui } from '@lingui/react'
import Head from 'next/head'
-import { AboutText } from '../components/AboutText'
-import Developers from '../components/Developers'
-import { Switcher } from '../components/Switcher'
+import { t, Trans } from '@lingui/macro'
+import { Switcher } from './Switcher'
+import { AboutText } from './AboutText'
+import Developers from './Developers'
import styles from '../styles/Index.module.css'
-import { loadCatalog } from '../utils'
-import { useLingui } from '@lingui/react'
-export const getStaticProps: GetStaticProps = async (ctx) => {
- const translation = await loadCatalog(ctx.locale!)
- return {
- props: {
- translation
- }
- }
-}
-
-const Index: NextPage = () => {
- /**
- * This hook is needed to subscribe your
- * component for changes if you use t`` macro
- */
- useLingui()
+export const HomePage = () => {
+ const { i18n } = useLingui()
return (
@@ -32,7 +18,7 @@ const Index: NextPage = () => {
component tree and React Context is not being passed down to the components placed in the .
That means we cannot use the component here and instead have to use `t` macro.
*/}
- {t`Translation Demo`}
+ {t(i18n)`Translation Demo`}
@@ -51,5 +37,3 @@ const Index: NextPage = () => {
)
}
-
-export default Index
diff --git a/examples/nextjs-swc/src/components/LinguiClientProvider.tsx b/examples/nextjs-swc/src/components/LinguiClientProvider.tsx
new file mode 100644
index 000000000..880f952bb
--- /dev/null
+++ b/examples/nextjs-swc/src/components/LinguiClientProvider.tsx
@@ -0,0 +1,25 @@
+'use client'
+
+import { I18nProvider } from '@lingui/react'
+import { type Messages, setupI18n } from '@lingui/core'
+import { useState } from 'react'
+
+type Props = {
+ children: React.ReactNode
+ initialLocale: string
+ initialMessages: Messages
+}
+
+export function LinguiClientProvider({
+ children,
+ initialLocale,
+ initialMessages
+}: Props) {
+ const [i18n] = useState(() => {
+ return setupI18n({
+ locale: initialLocale,
+ messages: { [initialLocale]: initialMessages }
+ })
+ })
+ return {children}
+}
diff --git a/examples/nextjs-swc/src/components/Switcher.tsx b/examples/nextjs-swc/src/components/Switcher.tsx
index 2b43020ba..eee524d57 100644
--- a/examples/nextjs-swc/src/components/Switcher.tsx
+++ b/examples/nextjs-swc/src/components/Switcher.tsx
@@ -1,23 +1,26 @@
-import { useRouter } from 'next/router'
+'use client'
+// this is a client component because it uses the `useState` hook
+
import { useState } from 'react'
-import { t, msg } from '@lingui/macro'
-import { MessageDescriptor } from '@lingui/core/src'
+import { msg } from '@lingui/macro'
import { useLingui } from '@lingui/react'
+import { usePathname, useRouter } from 'next/navigation'
type LOCALES = 'en' | 'sr' | 'es' | 'pseudo'
-const languages: { [key: string]: MessageDescriptor } = {
+const languages = {
en: msg`English`,
sr: msg`Serbian`,
es: msg`Spanish`
-}
+} as const
export function Switcher() {
const router = useRouter()
const { i18n } = useLingui()
+ const pathname = usePathname()
const [locale, setLocale] = useState(
- router.locale!.split('-')[0] as LOCALES
+ pathname?.split('/')[1] as LOCALES
)
// disabled for DEMO - so we can demonstrate the 'pseudo' locale functionality
@@ -28,8 +31,11 @@ export function Switcher() {
function handleChange(event: React.ChangeEvent) {
const locale = event.target.value as LOCALES
+ const pathNameWithoutLocale = pathname?.split('/')?.slice(2) ?? []
+ const newPath = `/${locale}/${pathNameWithoutLocale.join('/')}`
+
setLocale(locale)
- router.push(router.pathname, router.pathname, { locale })
+ router.push(newPath)
}
return (
@@ -37,7 +43,7 @@ export function Switcher() {
{Object.keys(languages).map((locale) => {
return (
)
})}
diff --git a/examples/nextjs-swc/src/locales/en.po b/examples/nextjs-swc/src/locales/en.po
index 8cf61f511..3510f4e73 100644
--- a/examples/nextjs-swc/src/locales/en.po
+++ b/examples/nextjs-swc/src/locales/en.po
@@ -13,11 +13,11 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
-#: src/components/Developers.tsx:20
+#: src/components/Developers.tsx:21
msgid "{selected, plural, one {Developer} other {Developers}}"
msgstr "{selected, plural, one {Developer} other {Developers}}"
-#: src/components/Switcher.tsx:10
+#: src/components/Switcher.tsx:12
msgid "English"
msgstr "English"
@@ -26,22 +26,23 @@ msgstr "English"
msgid "next-explanation"
msgstr "Next.js is an open-source React front-end development web framework that enables functionality such as server-side rendering and generating static websites for React based web applications. It is a production-ready framework that allows developers to quickly create static and dynamic JAMstack websites and is used widely by many large companies."
-#: src/components/Developers.tsx:9
+#: src/components/Developers.tsx:10
msgid "Plural Test: How many developers?"
msgstr "Plural Test: How many developers?"
-#: src/components/Switcher.tsx:11
+#: src/components/Switcher.tsx:13
msgid "Serbian"
msgstr "Serbian"
-#: src/components/Switcher.tsx:12
+#: src/components/Switcher.tsx:14
msgid "Spanish"
msgstr "Spanish"
-#: src/pages/index.tsx:28
+#: src/app/[lang]/layout.tsx:16
+#: src/components/HomePage.tsx:21
msgid "Translation Demo"
msgstr "Translation Demo"
-#: src/pages/index.tsx:35
+#: src/components/HomePage.tsx:28
msgid "Welcome to <0>Next.js!0>"
msgstr "Welcome to <0>Next.js!0>"
diff --git a/examples/nextjs-swc/src/locales/es.po b/examples/nextjs-swc/src/locales/es.po
index 663b3ddd0..330aa7a08 100644
--- a/examples/nextjs-swc/src/locales/es.po
+++ b/examples/nextjs-swc/src/locales/es.po
@@ -13,11 +13,11 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
-#: src/components/Developers.tsx:20
+#: src/components/Developers.tsx:21
msgid "{selected, plural, one {Developer} other {Developers}}"
msgstr "{selected, plural, one {Programador} other {Programadores}}"
-#: src/components/Switcher.tsx:10
+#: src/components/Switcher.tsx:12
msgid "English"
msgstr "Inglés"
@@ -26,22 +26,23 @@ msgstr "Inglés"
msgid "next-explanation"
msgstr "Next.js es un marco de trabajo web de desarrollo front-end de React de código abierto que permite funciones como la representación del lado del servidor y la generación de sitios web estáticos para aplicaciones web basadas en React. Es un marco listo para producción que permite a los desarrolladores crear rápidamente sitios web JAMstack estáticos y dinámicos y es ampliamente utilizado por muchas grandes empresas."
-#: src/components/Developers.tsx:9
+#: src/components/Developers.tsx:10
msgid "Plural Test: How many developers?"
msgstr "Prueba Plural: Cuantos programadores?"
-#: src/components/Switcher.tsx:11
+#: src/components/Switcher.tsx:13
msgid "Serbian"
msgstr "Serbio"
-#: src/components/Switcher.tsx:12
+#: src/components/Switcher.tsx:14
msgid "Spanish"
msgstr "Español"
-#: src/pages/index.tsx:28
+#: src/app/[lang]/layout.tsx:16
+#: src/components/HomePage.tsx:21
msgid "Translation Demo"
msgstr "Demostración de Traducción"
-#: src/pages/index.tsx:35
+#: src/components/HomePage.tsx:28
msgid "Welcome to <0>Next.js!0>"
msgstr "Bienvenido a <0>Next.js!0>"
diff --git a/examples/nextjs-swc/src/locales/pseudo.po b/examples/nextjs-swc/src/locales/pseudo.po
index c2158ba7a..ae5adb3b3 100644
--- a/examples/nextjs-swc/src/locales/pseudo.po
+++ b/examples/nextjs-swc/src/locales/pseudo.po
@@ -13,11 +13,11 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
-#: src/components/Developers.tsx:20
+#: src/components/Developers.tsx:21
msgid "{selected, plural, one {Developer} other {Developers}}"
msgstr ""
-#: src/components/Switcher.tsx:10
+#: src/components/Switcher.tsx:12
msgid "English"
msgstr ""
@@ -26,22 +26,23 @@ msgstr ""
msgid "next-explanation"
msgstr ""
-#: src/components/Developers.tsx:9
+#: src/components/Developers.tsx:10
msgid "Plural Test: How many developers?"
msgstr ""
-#: src/components/Switcher.tsx:11
+#: src/components/Switcher.tsx:13
msgid "Serbian"
msgstr ""
-#: src/components/Switcher.tsx:12
+#: src/components/Switcher.tsx:14
msgid "Spanish"
msgstr ""
-#: src/pages/index.tsx:28
+#: src/app/[lang]/layout.tsx:16
+#: src/components/HomePage.tsx:21
msgid "Translation Demo"
msgstr ""
-#: src/pages/index.tsx:35
+#: src/components/HomePage.tsx:28
msgid "Welcome to <0>Next.js!0>"
msgstr ""
diff --git a/examples/nextjs-swc/src/locales/sr.po b/examples/nextjs-swc/src/locales/sr.po
index 748509afa..fcf445f1b 100644
--- a/examples/nextjs-swc/src/locales/sr.po
+++ b/examples/nextjs-swc/src/locales/sr.po
@@ -13,11 +13,11 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
-#: src/components/Developers.tsx:20
+#: src/components/Developers.tsx:21
msgid "{selected, plural, one {Developer} other {Developers}}"
msgstr "{selected, plural, one {Програмер} other {Програмера}}"
-#: src/components/Switcher.tsx:10
+#: src/components/Switcher.tsx:12
msgid "English"
msgstr "Енглески"
@@ -26,22 +26,23 @@ msgstr "Енглески"
msgid "next-explanation"
msgstr "Некст.јс је отворени изворни Реацт-ов развојни вебоквир који омогућава функционалност као што је приказивање на страни сервера и генерисање статичких веблокација за веб апликације засноване на Реацт-у."
-#: src/components/Developers.tsx:9
+#: src/components/Developers.tsx:10
msgid "Plural Test: How many developers?"
msgstr "Тест за Множину: Колико програмера?"
-#: src/components/Switcher.tsx:11
+#: src/components/Switcher.tsx:13
msgid "Serbian"
msgstr "Српски"
-#: src/components/Switcher.tsx:12
+#: src/components/Switcher.tsx:14
msgid "Spanish"
msgstr "Шпански"
-#: src/pages/index.tsx:28
+#: src/app/[lang]/layout.tsx:16
+#: src/components/HomePage.tsx:21
msgid "Translation Demo"
msgstr "Демо Превод"
-#: src/pages/index.tsx:35
+#: src/components/HomePage.tsx:28
msgid "Welcome to <0>Next.js!0>"
msgstr "Добродошли у <0>Нект.јс!0>"
diff --git a/examples/nextjs-swc/src/middleware.ts b/examples/nextjs-swc/src/middleware.ts
new file mode 100644
index 000000000..bd051c14a
--- /dev/null
+++ b/examples/nextjs-swc/src/middleware.ts
@@ -0,0 +1,52 @@
+/*
+ * For more info see
+ * https://nextjs.org/docs/app/building-your-application/routing/internationalization
+ * */
+import { type NextRequest, NextResponse } from 'next/server'
+
+import Negotiator from 'negotiator'
+import linguiConfig from '../lingui.config'
+
+const { locales } = linguiConfig
+
+export function middleware(request: NextRequest) {
+ const { pathname } = request.nextUrl
+
+ const pathnameHasLocale = locales.some(
+ (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
+ )
+
+ if (pathnameHasLocale) return
+
+ // Redirect if there is no locale
+ const locale = getRequestLocale(request.headers)
+ request.nextUrl.pathname = `/${locale}${pathname}`
+ // e.g. incoming request is /products
+ // The new URL is now /en/products
+ return NextResponse.redirect(request.nextUrl)
+}
+
+function getRequestLocale(requestHeaders: Headers): string {
+ const langHeader = requestHeaders.get('accept-language') || undefined
+ const languages = new Negotiator({
+ headers: { 'accept-language': langHeader }
+ }).languages(locales.slice())
+
+ const activeLocale = languages[0] || locales[0] || 'en'
+
+ return activeLocale
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except:
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ * - images - .svg, .png, .jpg, .jpeg, .gif, .webp
+ * Feel free to modify this pattern to include more paths.
+ */
+ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'
+ ]
+}
diff --git a/examples/nextjs-swc/src/pages/[lang]/pages-router-demo/index.tsx b/examples/nextjs-swc/src/pages/[lang]/pages-router-demo/index.tsx
new file mode 100644
index 000000000..eb8b2a8fe
--- /dev/null
+++ b/examples/nextjs-swc/src/pages/[lang]/pages-router-demo/index.tsx
@@ -0,0 +1,30 @@
+import { GetStaticProps } from 'next'
+import { loadCatalog } from '../../../pagesRouterI18n'
+import { HomePage } from '../../../components/HomePage'
+
+import linguiConfig from '../../../../lingui.config'
+import type { GetStaticPaths } from 'next'
+
+export const getStaticPaths = (async () => {
+ const paths = linguiConfig.locales.map((lang) => ({ params: { lang } }))
+
+ return {
+ paths,
+ fallback: false
+ }
+}) satisfies GetStaticPaths
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const locale = ctx.params?.lang
+ const translation = await loadCatalog(
+ typeof locale === 'string' ? locale : 'en'
+ )
+
+ return {
+ props: {
+ translation
+ }
+ }
+}
+
+export default HomePage
diff --git a/examples/nextjs-swc/src/pages/_app.tsx b/examples/nextjs-swc/src/pages/_app.tsx
index 0883e2eb7..8bed27c61 100644
--- a/examples/nextjs-swc/src/pages/_app.tsx
+++ b/examples/nextjs-swc/src/pages/_app.tsx
@@ -2,17 +2,15 @@ import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import '../styles/globals.css'
import type { AppProps } from 'next/app'
-import { useLinguiInit } from '../utils'
+import { useLinguiInit } from '../pagesRouterI18n'
function MyApp({ Component, pageProps }: AppProps) {
useLinguiInit(pageProps.translation)
return (
- <>
-
-
-
- >
+
+
+
)
}
diff --git a/examples/nextjs-swc/src/utils.ts b/examples/nextjs-swc/src/pagesRouterI18n.ts
similarity index 88%
rename from examples/nextjs-swc/src/utils.ts
rename to examples/nextjs-swc/src/pagesRouterI18n.ts
index e87030fd6..d51f4ba5e 100644
--- a/examples/nextjs-swc/src/utils.ts
+++ b/examples/nextjs-swc/src/pagesRouterI18n.ts
@@ -1,6 +1,6 @@
import { i18n, Messages } from '@lingui/core'
-import { useRouter } from 'next/router'
import { useEffect } from 'react'
+import { usePathname } from 'next/navigation'
export async function loadCatalog(locale: string) {
const catalog = await import(`@lingui/loader!./locales/${locale}.po`)
@@ -8,9 +8,9 @@ export async function loadCatalog(locale: string) {
}
export function useLinguiInit(messages: Messages) {
- const router = useRouter()
- const locale = router.locale || router.defaultLocale!
const isClient = typeof window !== 'undefined'
+ const pathname = usePathname()
+ const locale = pathname?.split('/')[1] ?? 'en'
if (!isClient && locale !== i18n.locale) {
// there is single instance of i18n on the server
diff --git a/examples/nextjs-swc/src/withLingui.tsx b/examples/nextjs-swc/src/withLingui.tsx
new file mode 100644
index 000000000..a6d7b0df1
--- /dev/null
+++ b/examples/nextjs-swc/src/withLingui.tsx
@@ -0,0 +1,45 @@
+import React, { ReactNode } from 'react'
+import { allI18nInstances } from './appRouterI18n'
+import { setI18n } from '@lingui/react/server'
+
+export type PageLangParam = {
+ params: { lang: string }
+}
+
+type PageProps = PageLangParam & {
+ searchParams?: any // in query
+}
+
+type LayoutProps = PageLangParam & {
+ children: React.ReactNode
+}
+
+type PageExposedToNextJS = (props: Props) => ReactNode
+
+export const withLinguiPage = (
+ AppRouterPage: React.ComponentType
+): PageExposedToNextJS => {
+ return function WithLingui(props) {
+ const lang = props.params.lang
+ const i18n = allI18nInstances[lang]!
+ setI18n(i18n)
+
+ return
+ }
+}
+
+type LayoutExposedToNextJS = (
+ props: Props
+) => ReactNode
+
+export const withLinguiLayout = (
+ AppRouterPage: React.ComponentType
+): LayoutExposedToNextJS => {
+ return function WithLingui(props) {
+ const lang = props.params.lang
+ const i18n = allI18nInstances[lang]!
+ setI18n(i18n)
+
+ return
+ }
+}