Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(examples): nextjs app router example #1944

Merged
merged 2 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions examples/nextjs-swc/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
## Example project using Next 13 SWC Compiler with LinguiJS Plugin
## Example project using Next 14 and SWC Compiler with LinguiJS Plugin

This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). It showcases use with app router (in `src/app`) as well as with pages router (in `src/pages`).

## SWC Compatibility

SWC Plugin support is still experimental. Semver backwards compatibility between different `next-swc` versions is not guaranteed.

Therefore, you need to select an appropriate version of the Lingui plugin to match compatible `NextJs` version.
Expand All @@ -11,12 +12,12 @@ You also need to add the `@lingui/swc-plugin` dependency with strict version wit
```json
{
"devDependencies": {
"@lingui/swc-plugin": "4.0.5"
"@lingui/swc-plugin": "see-below"
}
}
```

For more information on compatibility, please refer to the [Compatibility section](https://github.com/lingui/swc-plugin#compatibility).
For version compatibility table, please refer to the [Compatibility section](https://github.com/lingui/swc-plugin#compatibility).

## Getting Started

Expand All @@ -33,10 +34,10 @@ pnpm dev
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

## LinguiJS Integration
LinguiJs integrated with standard nextjs i18n support. Nextjs do routing for every language,
LinguiJs activated with `router.locale`.

Open [http://localhost:3000/cs](http://localhost:3000/cs) with your browser to prerender page in different language.
LinguiJs is integrated with standard Next.js i18n support for using [middleware](https://nextjs.org/docs/app/building-your-application/routing/internationalization).

Open [http://localhost:3000/es](http://localhost:3000/es) with your browser to prerender page in different language.

## LinguiJS Related Commands

Expand Down
6 changes: 2 additions & 4 deletions examples/nextjs-swc/lingui.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const nextConfig = require('./next.config')

/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: nextConfig.i18n.locales,
locales: ['en', 'sr', 'es', 'pseudo'],
pseudoLocale: 'pseudo',
sourceLocale: nextConfig.i18n.defaultLocale,
sourceLocale: 'en',
fallbackLocales: {
default: 'en'
},
Expand Down
1 change: 1 addition & 0 deletions examples/nextjs-swc/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
23 changes: 14 additions & 9 deletions examples/nextjs-swc/next.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
/** @type {import('next').NextConfig} */
module.exports = {
i18n: {
// These are all the locales you want to support in
// your application
locales: ['en', 'sr', 'es', 'pseudo'],
defaultLocale: 'en'
// i18n: {
// this option has been replaced by the middleware in src/
// when migrating to support app router
// },
webpack: (config) => {
config.module.rules.push({
test: /\.po$/,
use: {
loader: '@lingui/loader'
}
})
return config
},
experimental: {
swcPlugins: [
['@lingui/swc-plugin', {}],
],
},
swcPlugins: [['@lingui/swc-plugin', {}]]
}
}
28 changes: 16 additions & 12 deletions examples/nextjs-swc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@
"build": "yarn lingui:extract && next build",
"start": "next start",
"lingui:extract": "lingui extract --clean",
"test": "yarn build"
"test": "yarn build",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@lingui/core": "^4.1.2",
"@lingui/react": "^4.1.2",
"next": "13.5.6",
"@lingui/core": "^4.11.0",
"@lingui/react": "^4.11.0",
"negotiator": "^0.6.3",
"next": "^14.2.3",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@lingui/cli": "^4.1.2",
"@lingui/loader": "^4.1.2",
"@lingui/macro": "^4.1.2",
"@lingui/swc-plugin": "4.0.5",
"@types/react": "^18.0.14",
"eslint": "8.35.0",
"eslint-config-next": "12.3.4",
"typescript": "^4.7.4"
"@lingui/cli": "^4.11.0",
"@lingui/loader": "^4.11.0",
"@lingui/macro": "^4.11.0",
"@lingui/swc-plugin": "4.0.7",
"@types/negotiator": "^0.6.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.3",
"typescript": "^5.4.5"
}
}
4 changes: 4 additions & 0 deletions examples/nextjs-swc/src/app/[lang]/app-router-demo/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { HomePage } from '../../../components/HomePage'
import { withLinguiPage } from '../../../withLingui'

export default withLinguiPage(HomePage)
38 changes: 38 additions & 0 deletions examples/nextjs-swc/src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang={lang}>
<body className="bg-background text-foreground">
<main className="min-h-screen flex flex-col">
<LinguiClientProvider
initialLocale={lang}
initialMessages={allMessages[lang]!}
>
{children}
</LinguiClientProvider>
</main>
</body>
</html>
)
})
9 changes: 9 additions & 0 deletions examples/nextjs-swc/src/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <a href="/app-router-demo">App router demo</a> or the{' '}
<a href="/pages-router-demo">Pages router demo</a>.
</>
)
}
37 changes: 37 additions & 0 deletions examples/nextjs-swc/src/appRouterI18n.ts
Original file line number Diff line number Diff line change
@@ -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 }
},
{}
)
3 changes: 3 additions & 0 deletions examples/nextjs-swc/src/components/Developers.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.container}>
Expand All @@ -32,7 +18,7 @@ const Index: NextPage = () => {
component tree and React Context is not being passed down to the components placed in the <Head>.
That means we cannot use the <Trans> component here and instead have to use `t` macro.
*/}
<title>{t`Translation Demo`}</title>
<title>{t(i18n)`Translation Demo`}</title>
<link rel="icon" href="/favicon.ico" />
</Head>

Expand All @@ -51,5 +37,3 @@ const Index: NextPage = () => {
</div>
)
}

export default Index
25 changes: 25 additions & 0 deletions examples/nextjs-swc/src/components/LinguiClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 <I18nProvider i18n={i18n}>{children}</I18nProvider>
}
22 changes: 14 additions & 8 deletions examples/nextjs-swc/src/components/Switcher.tsx
Original file line number Diff line number Diff line change
@@ -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<LOCALES>(
router.locale!.split('-')[0] as LOCALES
pathname?.split('/')[1] as LOCALES
)

// disabled for DEMO - so we can demonstrate the 'pseudo' locale functionality
Expand All @@ -28,16 +31,19 @@ export function Switcher() {
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
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 (
<select value={locale} onChange={handleChange}>
{Object.keys(languages).map((locale) => {
return (
<option value={locale} key={locale}>
{i18n._(languages[locale as unknown as LOCALES])}
{i18n._(languages[locale as keyof typeof languages])}
</option>
)
})}
Expand Down
Loading
Loading