Skip to content

Commit a43d9c8

Browse files
authored
v5 Release (#755)
* refactor: Use rewrites instead of custom server (#689) * chore: Add .e2e to npmignore * v5.0.0-beta.0 * chore: Add scripts to npmignore * v5.0.0-beta.1 * 5.0.0-beta.2 * refactor: Require absolute localePath, and refactor out usage of eval * 5.0.0-beta.3 * refactor: Clean up example dir, remove class components * chore: Update core-js to v3 * docs: Update README * v5.0.0-beta.4 * Clean up release
1 parent e43a761 commit a43d9c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+4345
-3082
lines changed

.circleci/config.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ commands:
33
build-and-test:
44
steps:
55
- checkout
6+
- run:
7+
name: Install rsync
8+
command: sudo apt install rsync
69
- restore_cache:
710
name: Restore cache (main)
811
keys:
@@ -38,19 +41,19 @@ commands:
3841
command: yarn test
3942

4043
jobs:
41-
node-v8:
44+
node-v10:
4245
docker:
43-
- image: circleci/node:8-browsers
46+
- image: circleci/node:10-browsers
4447
steps:
4548
- build-and-test
46-
node-v10:
49+
node-v12:
4750
docker:
48-
- image: circleci/node:10-browsers
51+
- image: circleci/node:12-browsers
4952
steps:
5053
- build-and-test
5154

5255
workflows:
5356
node-multi-build:
5457
jobs:
55-
- node-v8
5658
- node-v10
59+
- node-v12

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
**/node_modules
2-
**/out
2+
**/out
3+
src/create-client/package.json

.eslintrc

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@
1313
"browser": true
1414
},
1515
"rules": {
16-
"@typescript-eslint/indent": ["error", 2],
16+
"@typescript-eslint/indent": [
17+
"error",
18+
2
19+
],
1720
"semi": "off",
18-
"@typescript-eslint/semi": ["error", "never"],
21+
"@typescript-eslint/semi": [
22+
"error",
23+
"never"
24+
],
1925
"@typescript-eslint/no-var-requires": 0,
2026
"@typescript-eslint/no-explicit-any": 0,
2127
"@typescript-eslint/explicit-function-return-type": 0,
@@ -33,11 +39,17 @@
3339
"react/display-name": 0,
3440
"react/jsx-filename-extension": 0,
3541
"react/prefer-stateless-function": 0,
42+
"react/react-in-jsx-scope": 0,
3643
"no-restricted-syntax": 0,
3744
"no-useless-escape": 0,
38-
"no-eval": 0,
39-
"no-console": ["error", {
40-
"allow": ["warn", "error"]
41-
}]
45+
"no-console": [
46+
"error",
47+
{
48+
"allow": [
49+
"warn",
50+
"error"
51+
]
52+
}
53+
]
4254
}
4355
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ out
1818

1919
# Testing
2020
coverage/
21+
.e2e
2122

2223
# npm
2324
package-lock.json

.npmignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ runtime.log
2323
__tests__
2424
coverage
2525
jest.*
26+
.e2e
2627

2728
# Build tools
2829
tsconfig.json
@@ -34,4 +35,5 @@ greenkeeper.json
3435
jest-puppeteer.config.js
3536
.github
3637
Procfile
37-
*.md
38+
*.md
39+
scripts

README.md

Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ If you are using next-i18next in production, please consider [sponsoring the pac
1313

1414
While `next-i18next` uses [i18next](https://www.i18next.com/) and [react-i18next](https://github.com/i18next/react-i18next) under the hood, users of `next-i18next` simply need to include their translation content as JSON files and don't have to worry about much else.
1515

16-
A live demo is [available here](http://next-i18next.com/). Please be aware this is hosted on a free Heroku dyno and therefore may go to sleep during periods of inactivity. This demo app is the [simple example](./examples/simple/) - nothing more, nothing less.
16+
A live demo is [available here](http://next-i18next.com/). This demo app is the [simple example](./examples/simple/) - nothing more, nothing less.
1717

1818
## Setup
1919

@@ -43,59 +43,49 @@ This structure can also be seen in the [simple example](./examples/simple).
4343

4444
If you want to structure your translations/namespaces in a custom way, you will need to pass modified `localePath` and `localeStructure` values into the initialisation config.
4545

46-
If translations are not found in `config.localePath` or `public/static/locales` an attempt will be made to find the locales in `static/locales`, if found a deprecation warning will be logged.
47-
4846
### 3. Project setup
4947

5048
The default export of `next-i18next` is a class constructor, into which you pass your config options. The resulting class has all the methods you will need to translate your app:
5149

5250
```jsx
5351
const NextI18Next = require('next-i18next').default
52+
const { localeSubpaths } = require('next/config').default().publicRuntimeConfig
53+
const path = require('path')
5454

5555
module.exports = new NextI18Next({
56-
defaultLanguage: 'en',
57-
otherLanguages: ['de']
56+
otherLanguages: ['de'],
57+
localeSubpaths,
58+
localePath: path.resolve('./public/static/locales')
5859
})
5960
```
6061

62+
Note that `localePath` is required, and must be an absolute path.
63+
6164
[A full list of options can be seen here](#options).
6265

6366
It's recommended to export this `NextI18Next` instance from a single file in your project, where you can continually import it from to use the class methods as needed. You can see this approach in the [examples/simple/i18n.js](./examples/simple/i18n.js) file.
6467

6568
After creating and exporting your `NextI18Next` instance, you need to take the following steps to get things working:
6669

6770
1. Create an `_app.js` file inside your `pages` directory, and wrap it with the `NextI18Next.appWithTranslation` higher order component (HOC). You can see this approach in the [examples/simple/pages/_app.js](./examples/simple/pages/_app.js).
68-
Your app component must either extend `App` if it's a class component or define a `getInitialProps` if it's a function component [(explanation here)](https://github.com/isaachinman/next-i18next/issues/615#issuecomment-575578375).
69-
2. Create a `server.js` file inside your root directory, initialise an [express](https://www.npmjs.com/package/express) server, and use the `nextI18NextMiddleware` middleware with your `nextI18Next` instance passed in. You can see this approach in the [examples/simple/server.js](./examples/simple/server.js).
70-
3. Update the scripts in `package.json` to:
71-
```
72-
{
73-
"scripts": {
74-
"dev": "node server.js",
75-
"build": "next build",
76-
"start": "NODE_ENV=production node server.js"
77-
}
78-
}
79-
```
80-
For more info, see [the NextJs section on custom servers](https://github.com/zeit/next.js#custom-server-and-routing).
71+
Your app component must either extend `App` if it's a class component or define a `getInitialProps` if it's a functional component [(explanation here)](https://github.com/isaachinman/next-i18next/issues/615#issuecomment-575578375).
72+
2. Create a `next.config.js` file inside your root directory if you want to use locale subpaths. You can see this approach in the [examples/simple/next.config.js](./examples/simple/next.config.js).
8173

8274
Note: You can pass `shallowRender: true` into config options to avoid triggering getInitialProps when `changeLanguage` method is invoked.
8375

8476
That's it! Your app is ready to go. You can now use the `NextI18Next.withTranslation` HOC to make your components or pages translatable, based on namespaces:
8577

8678
```jsx
87-
import React from 'react'
88-
8979
// This is our initialised `NextI18Next` instance
9080
import { withTranslation } from '../i18n'
9181

92-
class Footer extends React.Component {
93-
render() {
94-
return (
95-
<footer>{this.props.t('description')}</footer>
96-
)
97-
}
98-
}
82+
const Footer = ({ t }) => (
83+
<footer>
84+
<p>
85+
{t('description')}
86+
</p>
87+
</footer>
88+
)
9989

10090
export default withTranslation('footer')(Footer)
10191
```
@@ -129,6 +119,8 @@ new NextI18Next({
129119
})
130120
```
131121

122+
The `localeSubpaths` object must also be passed into `next.config.js`, via the `nextI18NextRewrites` util, which you can import from `next-i18next/rewrites`.
123+
132124
The `localeSubpaths` option is a key/value mapping, where keys are the locale itself (case sensitive) and values are the subpath without slashes.
133125

134126
Now, all your page routes will be duplicated across all your locale subpaths. Here's an example:
@@ -149,7 +141,7 @@ myapp.com/german
149141
myapp.com/eng
150142
```
151143

152-
When using the localeSubpaths option, our middleware may redirect without calling any subsequent middleware. Therefore, if there are any critical middleware that must run before this redirect, ensure that you place it before the `nextI18NextMiddleware` middleware.
144+
When using the localeSubpaths option, our middleware will redirect as needed in the wrapped `getInitialProps` one level above your `_app`, so none of your code will be called.
153145

154146
The main "gotcha" with locale subpaths is routing. We want to be able to route to "naked" routes, and not have to worry about the locale subpath part of the route:
155147

@@ -162,8 +154,6 @@ With this link, we would expect someone whose language is set to French to autom
162154
To do that, we must import `Link` from your `NextI18Next` instance, **not next/router**:
163155

164156
```jsx
165-
import React from 'react'
166-
167157
// This is our initialised `NextI18Next` instance
168158
import { Link } from '../i18n'
169159

@@ -174,12 +164,9 @@ const SomeLink = () => (
174164
)
175165
```
176166

177-
We can also navigate imperatively with locale subpaths by importing `Router` from your `NextI18Next` instance.
178-
The exported Router shares the same API as the native Next Router. The push, replace, and prefetch functions will automatically prepend locale subpaths.
167+
We can also navigate imperatively with locale subpaths by importing `Router` from your `NextI18Next` instance. The exported Router shares the same API as the native Next Router. The push, replace, and prefetch functions will automatically prepend locale subpaths.
179168

180169
```jsx
181-
import React from 'react'
182-
183170
// This is our initialised `NextI18Next` instance
184171
import { Router } from '../i18n'
185172

@@ -192,25 +179,6 @@ const SomeButton = () => (
192179
)
193180
```
194181

195-
## Custom Routing
196-
197-
Custom routing can be achieved via the `app.render` method:
198-
199-
```jsx
200-
/* First, use middleware */
201-
server.use(nextI18NextMiddleware(nextI18next))
202-
203-
/* Second, declare custom routes */
204-
server.get('/products/:id', (req, res) => {
205-
const { query, params } = req
206-
207-
return app.render(req, res, '/product-page', { ...query, id: params.id })
208-
})
209-
210-
/* Third, add catch-all GET for non-custom routes */
211-
server.get('*', (req, res) => handle(req, res))
212-
```
213-
214182
## Accessing the Current Language
215183

216184
In many cases, you'll need to know the currently active language. Most of the time, to accomplish this, you should use the `withTranslation` HOC, which will pass an `i18n` prop to the wrapped component and further asserts your component will get re-rendered on language change or changes to the translation catalog itself (loaded translations). More info can be found [here](https://react.i18next.com/latest/withtranslation-hoc).
@@ -236,7 +204,7 @@ MyPage.getInitialProps = async({ req }) => {
236204
| `ignoreRoutes` | `['/_next/', '/static/', '/public/', '/api/']` |
237205
| `otherLanguages` (required) | `[]` |
238206
| `localeExtension` | `'json'` |
239-
| `localePath` | `'public/static/locales'` |
207+
| `localePath` (required) | `'/public/static/locales'` |
240208
| `localeStructure` | `'{{lng}}/{{ns}}'` |
241209
| `localeSubpaths` | `{}` |
242210
| `serverLanguageDetection` | `true` |
@@ -250,7 +218,6 @@ _This table contains options which are specific to next-i18next. All other [i18n
250218
## Notes
251219

252220
- [`next export` will result in a _client-side only_ React application.](https://github.com/isaachinman/next-i18next/issues/10)
253-
- [Serverless (e.g. Now 2.0) is not currently supported](https://github.com/isaachinman/next-i18next/issues/274).
254221
- [To add a `lang` attribute to your top-level html DOM node, you must create a `_document.js` file.](https://github.com/isaachinman/next-i18next/issues/20#issuecomment-443461652)
255222
- [Localising `next/head` requires special consideration due to NextJs internals](https://github.com/isaachinman/next-i18next/issues/251#issuecomment-479421852).
256223

__tests__/config/create-config.test.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ describe('create configuration in non-production environment', () => {
3939
existsSync: jest.fn().mockImplementation(() => false),
4040
}))
4141

42-
expect(() => createConfig({})).toThrow(
42+
expect(() => createConfig({
43+
localePath: '/home/user/public/static/locales'
44+
})).toThrow(
4345
'Default namespace not found at /home/user/public/static/locales/en/common.json',
4446
)
4547
})
@@ -58,13 +60,15 @@ describe('create configuration in non-production environment', () => {
5860

5961
it('creates default non-production configuration', () => {
6062
isServer.mockReturnValue(true)
61-
const config = createConfig({})
63+
const config = createConfig({
64+
localePath: '/home/user/public/static/locales'
65+
})
6266

6367
expect(config.defaultLanguage).toEqual('en')
6468
expect(config.otherLanguages).toEqual([])
6569
expect(config.fallbackLng).toEqual(false)
6670
expect(config.load).toEqual('currentOnly')
67-
expect(config.localePath).toEqual('public/static/locales')
71+
expect(config.localePath).toEqual('/home/user/public/static/locales')
6872
expect(config.localeStructure).toEqual('{{lng}}/{{ns}}')
6973
expect(config.localeSubpaths).toEqual({})
7074
expect(config.use).toEqual([])
@@ -105,7 +109,7 @@ describe('create configuration in non-production environment', () => {
105109
expect(config.otherLanguages).toEqual(['fr', 'it'])
106110
expect(config.fallbackLng).toEqual('it')
107111
expect(config.load).toEqual('currentOnly')
108-
expect(config.localePath).toEqual('public/static/translations')
112+
expect(config.localePath).toEqual('/home/user/public/static/locales')
109113
expect(config.localeStructure).toEqual('{{ns}}/{{lng}}')
110114
expect(config.localeSubpaths).toEqual(localeSubpathVariations.FOREIGN)
111115
expect(config.defaultNS).toEqual('universal')
@@ -114,8 +118,8 @@ describe('create configuration in non-production environment', () => {
114118

115119
expect(config.ns).toEqual(['universal', 'file1', 'file2'])
116120

117-
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.json')
118-
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json')
121+
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.json')
122+
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.missing.json')
119123
})
120124

121125
it('falls back to deprecated static folder', () => {
@@ -145,17 +149,17 @@ describe('create configuration in non-production environment', () => {
145149

146150
describe('localeExtension config option', () => {
147151
it('is set to JSON by default', () => {
148-
const config = createConfig(userConfig)
149-
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.json')
150-
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json')
152+
const config = createConfig(userConfigServerSide)
153+
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.json')
154+
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.missing.json')
151155
})
152156
it('accepts any string and modifies backend paths', () => {
153157
const config = createConfig({
154-
...userConfig,
158+
...userConfigServerSide,
155159
localeExtension: 'test-extension',
156160
})
157-
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.test-extension')
158-
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.test-extension')
161+
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.test-extension')
162+
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.missing.test-extension')
159163
})
160164
})
161165
})
@@ -169,7 +173,7 @@ describe('create configuration in non-production environment', () => {
169173
expect(config.otherLanguages).toEqual([])
170174
expect(config.fallbackLng).toEqual(false)
171175
expect(config.load).toEqual('currentOnly')
172-
expect(config.localePath).toEqual('public/static/locales')
176+
expect(config.localePath).toEqual('/public/static/locales')
173177
expect(config.localeStructure).toEqual('{{lng}}/{{ns}}')
174178
expect(config.localeSubpaths).toEqual(localeSubpathVariations.NONE)
175179
expect(config.use).toEqual([])
@@ -204,33 +208,33 @@ describe('create configuration in non-production environment', () => {
204208
expect(config.otherLanguages).toEqual(['fr', 'it'])
205209
expect(config.fallbackLng).toEqual('it')
206210
expect(config.load).toEqual('currentOnly')
207-
expect(config.localePath).toEqual('public/static/translations')
211+
expect(config.localePath).toEqual('/public/static/locales')
208212
expect(config.localeStructure).toEqual('{{ns}}/{{lng}}')
209213
expect(config.localeSubpaths).toEqual(localeSubpathVariations.FOREIGN)
210214
expect(config.defaultNS).toEqual('universal')
211215
expect(config.browserLanguageDetection).toEqual(false)
212216

213217
expect(config.ns).toEqual(['universal'])
214218

215-
expect(config.backend.loadPath).toEqual('/static/translations/{{ns}}/{{lng}}.json')
216-
expect(config.backend.addPath).toEqual('/static/translations/{{ns}}/{{lng}}.missing.json')
219+
expect(config.backend.loadPath).toEqual('/static/locales/{{ns}}/{{lng}}.json')
220+
expect(config.backend.addPath).toEqual('/static/locales/{{ns}}/{{lng}}.missing.json')
217221

218222
expect(config.shallowRender).toEqual(true)
219223
})
220224

221225
describe('localeExtension config option', () => {
222226
it('is set to JSON by default', () => {
223227
const config = createConfig(userConfig)
224-
expect(config.backend.loadPath).toEqual('/static/translations/{{ns}}/{{lng}}.json')
225-
expect(config.backend.addPath).toEqual('/static/translations/{{ns}}/{{lng}}.missing.json')
228+
expect(config.backend.loadPath).toEqual('/static/locales/{{ns}}/{{lng}}.json')
229+
expect(config.backend.addPath).toEqual('/static/locales/{{ns}}/{{lng}}.missing.json')
226230
})
227231
it('accepts any string and modifies backend paths', () => {
228232
const config = createConfig({
229233
...userConfig,
230234
localeExtension: 'test-extension',
231235
})
232-
expect(config.backend.loadPath).toEqual('/static/translations/{{ns}}/{{lng}}.test-extension')
233-
expect(config.backend.addPath).toEqual('/static/translations/{{ns}}/{{lng}}.missing.test-extension')
236+
expect(config.backend.loadPath).toEqual('/static/locales/{{ns}}/{{lng}}.test-extension')
237+
expect(config.backend.addPath).toEqual('/static/locales/{{ns}}/{{lng}}.missing.test-extension')
234238
})
235239
})
236240
}

0 commit comments

Comments
 (0)