Skip to content

Commit

Permalink
v5 Release (#755)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
isaachinman committed Jul 27, 2020
1 parent e43a761 commit a43d9c8
Show file tree
Hide file tree
Showing 45 changed files with 4,345 additions and 3,082 deletions.
13 changes: 8 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ commands:
build-and-test:
steps:
- checkout
- run:
name: Install rsync
command: sudo apt install rsync
- restore_cache:
name: Restore cache (main)
keys:
Expand Down Expand Up @@ -38,19 +41,19 @@ commands:
command: yarn test

jobs:
node-v8:
node-v10:
docker:
- image: circleci/node:8-browsers
- image: circleci/node:10-browsers
steps:
- build-and-test
node-v10:
node-v12:
docker:
- image: circleci/node:10-browsers
- image: circleci/node:12-browsers
steps:
- build-and-test

workflows:
node-multi-build:
jobs:
- node-v8
- node-v10
- node-v12
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
**/node_modules
**/out
**/out
src/create-client/package.json
24 changes: 18 additions & 6 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
"browser": true
},
"rules": {
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/indent": [
"error",
2
],
"semi": "off",
"@typescript-eslint/semi": ["error", "never"],
"@typescript-eslint/semi": [
"error",
"never"
],
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-function-return-type": 0,
Expand All @@ -33,11 +39,17 @@
"react/display-name": 0,
"react/jsx-filename-extension": 0,
"react/prefer-stateless-function": 0,
"react/react-in-jsx-scope": 0,
"no-restricted-syntax": 0,
"no-useless-escape": 0,
"no-eval": 0,
"no-console": ["error", {
"allow": ["warn", "error"]
}]
"no-console": [
"error",
{
"allow": [
"warn",
"error"
]
}
]
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ out

# Testing
coverage/
.e2e

# npm
package-lock.json
4 changes: 3 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ runtime.log
__tests__
coverage
jest.*
.e2e

# Build tools
tsconfig.json
Expand All @@ -34,4 +35,5 @@ greenkeeper.json
jest-puppeteer.config.js
.github
Procfile
*.md
*.md
scripts
77 changes: 22 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ If you are using next-i18next in production, please consider [sponsoring the pac

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.

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.
A live demo is [available here](http://next-i18next.com/). This demo app is the [simple example](./examples/simple/) - nothing more, nothing less.

## Setup

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

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.

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.

### 3. Project setup

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:

```jsx
const NextI18Next = require('next-i18next').default
const { localeSubpaths } = require('next/config').default().publicRuntimeConfig
const path = require('path')

module.exports = new NextI18Next({
defaultLanguage: 'en',
otherLanguages: ['de']
otherLanguages: ['de'],
localeSubpaths,
localePath: path.resolve('./public/static/locales')
})
```

Note that `localePath` is required, and must be an absolute path.

[A full list of options can be seen here](#options).

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.

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

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).
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).
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).
3. Update the scripts in `package.json` to:
```
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
```
For more info, see [the NextJs section on custom servers](https://github.com/zeit/next.js#custom-server-and-routing).
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).
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).

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

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:

```jsx
import React from 'react'

// This is our initialised `NextI18Next` instance
import { withTranslation } from '../i18n'

class Footer extends React.Component {
render() {
return (
<footer>{this.props.t('description')}</footer>
)
}
}
const Footer = ({ t }) => (
<footer>
<p>
{t('description')}
</p>
</footer>
)

export default withTranslation('footer')(Footer)
```
Expand Down Expand Up @@ -129,6 +119,8 @@ new NextI18Next({
})
```

The `localeSubpaths` object must also be passed into `next.config.js`, via the `nextI18NextRewrites` util, which you can import from `next-i18next/rewrites`.

The `localeSubpaths` option is a key/value mapping, where keys are the locale itself (case sensitive) and values are the subpath without slashes.

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

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.
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.

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:

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

```jsx
import React from 'react'

// This is our initialised `NextI18Next` instance
import { Link } from '../i18n'

Expand All @@ -174,12 +164,9 @@ const SomeLink = () => (
)
```

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.
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.

```jsx
import React from 'react'

// This is our initialised `NextI18Next` instance
import { Router } from '../i18n'

Expand All @@ -192,25 +179,6 @@ const SomeButton = () => (
)
```

## Custom Routing

Custom routing can be achieved via the `app.render` method:

```jsx
/* First, use middleware */
server.use(nextI18NextMiddleware(nextI18next))

/* Second, declare custom routes */
server.get('/products/:id', (req, res) => {
const { query, params } = req

return app.render(req, res, '/product-page', { ...query, id: params.id })
})

/* Third, add catch-all GET for non-custom routes */
server.get('*', (req, res) => handle(req, res))
```

## Accessing the Current Language

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).
Expand All @@ -236,7 +204,7 @@ MyPage.getInitialProps = async({ req }) => {
| `ignoreRoutes` | `['/_next/', '/static/', '/public/', '/api/']` |
| `otherLanguages` (required) | `[]` |
| `localeExtension` | `'json'` |
| `localePath` | `'public/static/locales'` |
| `localePath` (required) | `'/public/static/locales'` |
| `localeStructure` | `'{{lng}}/{{ns}}'` |
| `localeSubpaths` | `{}` |
| `serverLanguageDetection` | `true` |
Expand All @@ -250,7 +218,6 @@ _This table contains options which are specific to next-i18next. All other [i18n
## Notes

- [`next export` will result in a _client-side only_ React application.](https://github.com/isaachinman/next-i18next/issues/10)
- [Serverless (e.g. Now 2.0) is not currently supported](https://github.com/isaachinman/next-i18next/issues/274).
- [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)
- [Localising `next/head` requires special consideration due to NextJs internals](https://github.com/isaachinman/next-i18next/issues/251#issuecomment-479421852).

Expand Down
44 changes: 24 additions & 20 deletions __tests__/config/create-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ describe('create configuration in non-production environment', () => {
existsSync: jest.fn().mockImplementation(() => false),
}))

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

it('creates default non-production configuration', () => {
isServer.mockReturnValue(true)
const config = createConfig({})
const config = createConfig({
localePath: '/home/user/public/static/locales'
})

expect(config.defaultLanguage).toEqual('en')
expect(config.otherLanguages).toEqual([])
expect(config.fallbackLng).toEqual(false)
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('public/static/locales')
expect(config.localePath).toEqual('/home/user/public/static/locales')
expect(config.localeStructure).toEqual('{{lng}}/{{ns}}')
expect(config.localeSubpaths).toEqual({})
expect(config.use).toEqual([])
Expand Down Expand Up @@ -105,7 +109,7 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual(['fr', 'it'])
expect(config.fallbackLng).toEqual('it')
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('public/static/translations')
expect(config.localePath).toEqual('/home/user/public/static/locales')
expect(config.localeStructure).toEqual('{{ns}}/{{lng}}')
expect(config.localeSubpaths).toEqual(localeSubpathVariations.FOREIGN)
expect(config.defaultNS).toEqual('universal')
Expand All @@ -114,8 +118,8 @@ describe('create configuration in non-production environment', () => {

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

expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json')
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.missing.json')
})

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

describe('localeExtension config option', () => {
it('is set to JSON by default', () => {
const config = createConfig(userConfig)
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.json')
const config = createConfig(userConfigServerSide)
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.missing.json')
})
it('accepts any string and modifies backend paths', () => {
const config = createConfig({
...userConfig,
...userConfigServerSide,
localeExtension: 'test-extension',
})
expect(config.backend.loadPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.test-extension')
expect(config.backend.addPath).toEqual('/home/user/public/static/translations/{{ns}}/{{lng}}.missing.test-extension')
expect(config.backend.loadPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.test-extension')
expect(config.backend.addPath).toEqual('/home/user/public/static/locales/{{ns}}/{{lng}}.missing.test-extension')
})
})
})
Expand All @@ -169,7 +173,7 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual([])
expect(config.fallbackLng).toEqual(false)
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('public/static/locales')
expect(config.localePath).toEqual('/public/static/locales')
expect(config.localeStructure).toEqual('{{lng}}/{{ns}}')
expect(config.localeSubpaths).toEqual(localeSubpathVariations.NONE)
expect(config.use).toEqual([])
Expand Down Expand Up @@ -204,33 +208,33 @@ describe('create configuration in non-production environment', () => {
expect(config.otherLanguages).toEqual(['fr', 'it'])
expect(config.fallbackLng).toEqual('it')
expect(config.load).toEqual('currentOnly')
expect(config.localePath).toEqual('public/static/translations')
expect(config.localePath).toEqual('/public/static/locales')
expect(config.localeStructure).toEqual('{{ns}}/{{lng}}')
expect(config.localeSubpaths).toEqual(localeSubpathVariations.FOREIGN)
expect(config.defaultNS).toEqual('universal')
expect(config.browserLanguageDetection).toEqual(false)

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

expect(config.backend.loadPath).toEqual('/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/static/translations/{{ns}}/{{lng}}.missing.json')
expect(config.backend.loadPath).toEqual('/static/locales/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/static/locales/{{ns}}/{{lng}}.missing.json')

expect(config.shallowRender).toEqual(true)
})

describe('localeExtension config option', () => {
it('is set to JSON by default', () => {
const config = createConfig(userConfig)
expect(config.backend.loadPath).toEqual('/static/translations/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/static/translations/{{ns}}/{{lng}}.missing.json')
expect(config.backend.loadPath).toEqual('/static/locales/{{ns}}/{{lng}}.json')
expect(config.backend.addPath).toEqual('/static/locales/{{ns}}/{{lng}}.missing.json')
})
it('accepts any string and modifies backend paths', () => {
const config = createConfig({
...userConfig,
localeExtension: 'test-extension',
})
expect(config.backend.loadPath).toEqual('/static/translations/{{ns}}/{{lng}}.test-extension')
expect(config.backend.addPath).toEqual('/static/translations/{{ns}}/{{lng}}.missing.test-extension')
expect(config.backend.loadPath).toEqual('/static/locales/{{ns}}/{{lng}}.test-extension')
expect(config.backend.addPath).toEqual('/static/locales/{{ns}}/{{lng}}.missing.test-extension')
})
})
}
Expand Down
Loading

0 comments on commit a43d9c8

Please sign in to comment.