Skip to content

Commit

Permalink
Support Next.js 13 app-dir for server/client pages/components (#982)
Browse files Browse the repository at this point in the history
* Support to Next.js 13 app-dir

* Add tests + fix files

* Add docs
  • Loading branch information
aralroca authored Feb 17, 2023
1 parent 1382b03 commit e636125
Show file tree
Hide file tree
Showing 32 changed files with 431 additions and 71 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
!*.ts
*.d.ts
!*.tsx
!src/plugin
!src/app-dir
!examples
!examples/**/*
!src
Expand All @@ -17,6 +17,8 @@
!.husky/*
!.prettierignore

.DS_Store

# dependencies
/node_modules
/examples/*/node_modules
Expand Down
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<div align="center">

[![npm version](https://badge.fury.io/js/next-translate.svg)](https://badge.fury.io/js/next-translate)
![npm](https://img.shields.io/npm/dw/next-translate)
[![size](https://img.shields.io/bundlephobia/minzip/next-translate)](https://bundlephobia.com/package/next-translate)
[![PRs Welcome][badge-prwelcome]][prwelcome]
<a href="https://github.com/aralroca/next-translate/actions?query=workflow%3ACI" alt="Tests status">
<img src="https://github.com/aralroca/next-translate/workflows/CI/badge.svg" /></a>
Expand Down Expand Up @@ -48,10 +50,12 @@
- [11. How to save the user-defined language](#11-how-to-save-the-user-defined-language)
- [12. How to use multi-language in a page](#12-how-to-use-multi-language-in-a-page)
- [13. How to use next-translate in a mono-repo](#13-how-to-use-next-translate-in-a-mono-repo)
- [14. Demos](#14-demos)
- [14. Use Next 13 app directory](#14-use-next-13-app-directory)
- [15. Demos](#15-demos)
- [Demo from Next.js](#demo-from-nextjs)
- [Basic demo](#basic-demo)
- [Complex demo](#complex-demo)
- [With app directory demo](#with-app-directory-demo)
- [Without the webpack loader demo](#without-the-webpack-loader-demo)
- [Contributors ✨](#contributors-)

Expand Down Expand Up @@ -898,7 +902,39 @@ If you want to change it you can use :
- the `NEXT_TRANSLATE_PATH` environment variable. It supports both relative and absolute path
- the native NodeJS function `process.chdir(PATH_TO_NEXT_TRANSLATE)` to move the `process.cwd()`
## 14. Demos
## 14. Use Next 13 app directory
When it comes to server components and client components, it can be challenging to load the same thing on different pages. To simplify this process, we have **extracted all the complexity** using the **[`next-translate-plugin`](https://github.com/aralroca/next-translate-plugin)**.
### Regarding translations:
If you use the "app" folder instead of the "pages" folder, the `next-translate-plugin` will automatically detect the change, and you won't need to touch any of the Next-translate configuration. The only difference is that the "pages" configuration property will reference the pages located within the "app" folder.
**i18n.js**
```js
module.exports = {
locales: ['en', 'ca', 'es'],
defaultLocale: 'en',
pages: {
'*': ['common'],
'/': ['home'], // app/page.tsx
'/second-page': ['home'], // app/second-page/page.tsx
},
}
```
By simply changing the "pages" folder to "app," you can consume translations within your pages using the **`useTranslation`** hook or the **`Trans`** component. You will still see the log (if enabled) to know which namespaces are loaded on each page, and everything else should be the same.
### Regarding routing:
The routing is part of the core of Next.js _(not from this library)_, but direct routing support is not yet available with the beta version of Next 13's app directory. As a workaround, Next.js recommends configuring it as described here:
- https://beta.nextjs.org/docs/guides/internationalization.
The [`next-translate-plugin`](https://github.com/aralroca/next-translate-plugin) automatically detects the **"lang" parameter**. So, without any rewrite, you can test if your translations work by entering your page with the "lang" parameter. For example: `/some-page?lang=en`.
With this in mind, you can do any rewrite as described in the Next documentation, and if the final page has this parameter, everything should work without any additional manual changes. For example, if you rewrite `/en-US/some-page` to `/some-page?lang=en-US`, then the `useTranslation` will look for translations in `en-US` without needing to pass this parameter.
## 15. Demos
### Demo from Next.js
Expand Down Expand Up @@ -932,6 +968,16 @@ This demo is in this repository:
- `cd next-translate`
- `yarn && yarn example:complex`
### With app directory demo
Similar than the complex demo but with some extra: Instead of `pages` folder, we are using the Next.js +13 [app folder with the new layouts system](https://nextjs.org/blog/next-13#new-app-directory-beta).
This demo is in this repository:
- `git clone git@github.com:aralroca/next-translate.git`
- `cd next-translate`
- `yarn && yarn example:with-app-directory`
### Without the webpack loader demo
Similar than the basic example but loading the page namespaces manually deactivating the webpack loader in the i18n.json config file.
Expand Down
27 changes: 27 additions & 0 deletions __tests__/useTranslation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1387,4 +1387,31 @@ describe('useTranslation', () => {
expect(container.textContent).toContain(expected)
})
})

describe('Next.js 13 app-dir', () => {
test('should work without context (with globalThis.__NEXT_TRANSLATE__)', () => {
const Inner = () => {
const { t } = useTranslation()
const text = t('ns:interpolation', {
count: 3,
})
return <>{text}</>
}

const expected = 'There are 3 cats.'

globalThis.__NEXT_TRANSLATE__ = {
namespaces: {
ns: {
interpolation: 'There are {{count}} cats.',
},
},
lang: 'en',
}
globalThis.i18nConfig = {}

const { container } = render(<Inner />)
expect(container.textContent).toContain(expected)
})
})
})
4 changes: 2 additions & 2 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
"build": "next build"
},
"dependencies": {
"next": "13.1.3",
"next": "13.1.7-canary.11",
"next-translate": "link:../../",
"react": "link:../../node_modules/react",
"react-dom": "link:../../node_modules/react-dom"
},
"devDependencies": {
"next-translate-plugin": "2.0.0-canary.6"
"next-translate-plugin": "2.0.0"
}
}
2 changes: 1 addition & 1 deletion examples/complex/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ module.exports = {
suffix: '}',
},
loadLocaleFrom: async (locale, namespace) =>
require(`./src/translations/${namespace}_${locale}`),
import(`./src/translations/${namespace}_${locale}`).then((r) => r.default),
}
16 changes: 8 additions & 8 deletions examples/complex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
"analyze": "ANALYZE=true yarn build"
},
"dependencies": {
"@mdx-js/loader": "2.2.1",
"@mdx-js/react": "2.2.1",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@next/mdx": "13.1.3",
"next": "13.1.3",
"next": "13.1.7-canary.11",
"next-translate": "link:../../",
"react": "link:../../node_modules/react",
"react-dom": "link:../../node_modules/react-dom"
},
"devDependencies": {
"@next/bundle-analyzer": "13.1.3",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"next-translate-plugin": "2.0.0-canary.6",
"typescript": "4.9.4"
"@next/bundle-analyzer": "13.1.6",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
"next-translate-plugin": "2.0.0",
"typescript": "4.9.5"
},
"resolutions": {
"webpack": "5.11.1"
Expand Down
13 changes: 13 additions & 0 deletions examples/with-app-directory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Complex example

Similar than the basic example but with some extras:

- TypeScript
- Webpack 5
- MDX
- With _app.js on top
- Custom interpolation with ${thisFormat} instead of the {{defaultFormat}}
- pages located on src/pages folder
- Loading locales from src/translations with a different structure

![next-translate](../../images/translation-prerendered.gif 'Translations in prerendered pages')
9 changes: 9 additions & 0 deletions examples/with-app-directory/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
locales: ['en', 'ca', 'es'],
defaultLocale: 'en',
pages: {
'*': ['common'],
'/': ['home'],
'/second-page': ['home'],
},
}
4 changes: 4 additions & 0 deletions examples/with-app-directory/locales/ca/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Llibreria next-translate",
"second-page": "Segona <0>pàgina</0>"
}
3 changes: 3 additions & 0 deletions examples/with-app-directory/locales/ca/home.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"client-only": "Només al client"
}
4 changes: 4 additions & 0 deletions examples/with-app-directory/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "next-translate library",
"second-page": "Second <0>page</0>"
}
3 changes: 3 additions & 0 deletions examples/with-app-directory/locales/en/home.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"client-only": "Only client-side"
}
4 changes: 4 additions & 0 deletions examples/with-app-directory/locales/es/common.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "Librería next-translate",
"second-page": "Segunda <0>página</0>"
}
3 changes: 3 additions & 0 deletions examples/with-app-directory/locales/es/home.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"client-only": "Sólo al lado del cliente"
}
5 changes: 5 additions & 0 deletions examples/with-app-directory/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
3 changes: 3 additions & 0 deletions examples/with-app-directory/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const nextTranslate = require('next-translate-plugin')

module.exports = nextTranslate({ experimental: { appDir: true } })
31 changes: 31 additions & 0 deletions examples/with-app-directory/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "-",
"version": "1.0.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"analyze": "ANALYZE=true yarn build"
},
"dependencies": {
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@next/mdx": "13.1.6",
"next": "13.1.7-canary.11",
"next-translate": "link:../../",
"react": "link:../../node_modules/react",
"react-dom": "link:../../node_modules/react-dom"
},
"devDependencies": {
"@next/bundle-analyzer": "13.1.6",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
"next-translate-plugin": "2.0.0",
"typescript": "4.9.5"
},
"resolutions": {
"webpack": "5.11.1"
}
}
Binary file added examples/with-app-directory/public/favicon.ico
Binary file not shown.
9 changes: 9 additions & 0 deletions examples/with-app-directory/src/app/head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function Head() {
return (
<>
<title>App dir</title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link rel="icon" href="/favicon.ico" />
</>
)
}
12 changes: 12 additions & 0 deletions examples/with-app-directory/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body>{children}</body>
</html>
)
}
31 changes: 31 additions & 0 deletions examples/with-app-directory/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Link from 'next/link'
import useTranslation from 'next-translate/useTranslation'
import ClientCode from '../components/client-code'

export default function Page() {
const { t, lang } = useTranslation('common')

return (
<>
<h1>{t('title')}</h1>

<ClientCode />

<div style={{ marginTop: 20 }}>
<Link href="/?lang=en">English</Link>
</div>

<div>
<Link href="/?lang=es">Español</Link>
</div>

<div>
<Link href="/?lang=ca">Català</Link>
</div>

<div>
<Link href={`/second-page?lang=${lang}`}>➡️</Link>
</div>
</>
)
}
16 changes: 16 additions & 0 deletions examples/with-app-directory/src/app/second-page/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Link from 'next/link'
import useTranslation from 'next-translate/useTranslation'
import Trans from 'next-translate/Trans'

export default function Page() {
const { t, lang } = useTranslation('common')
return (
<>
<h1>{t`title`}</h1>
<Trans i18nKey="common:second-page" components={[<b />]} />
<div>
<Link href={`/?lang=${lang}`}>⬅️</Link>
</div>
</>
)
}
17 changes: 17 additions & 0 deletions examples/with-app-directory/src/components/client-code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'
import useTranslation from 'next-translate/useTranslation'
import { useState } from 'react'

export default function ClientCode() {
const [count, setCount] = useState(0)
const { t } = useTranslation('home')

return (
<div>
{t('client-only')}:
<button onClick={() => setCount((v) => v + 1)}>+</button>
{count}
<button onClick={() => setCount((v) => v - 1)}>-</button>
</div>
)
}
27 changes: 27 additions & 0 deletions examples/with-app-directory/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"typeRoots": ["./src/mdx.d.ts"]
}
2 changes: 1 addition & 1 deletion examples/without-loader/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ module.exports = {
},
// When loader === false, then loadLocaleFrom is required
loadLocaleFrom: async (locale, namespace) =>
require(`./locales/${locale}/${namespace}`),
import(`./locales/${locale}/${namespace}`).then((r) => r.default),
}
Loading

0 comments on commit e636125

Please sign in to comment.