Skip to content

Commit

Permalink
docs(examples): nextjs app router example (#1944)
Browse files Browse the repository at this point in the history
* chore: update nextjs example deps

* feat: add nextjs app router example
  • Loading branch information
vonovak authored Jun 11, 2024
1 parent ec49d0c commit 30dab4e
Show file tree
Hide file tree
Showing 24 changed files with 1,956 additions and 845 deletions.
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

0 comments on commit 30dab4e

Please sign in to comment.