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

feat: support ignoredPaths and redirectDefaultLanguageToRoot #118

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
74 changes: 36 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Internationalize your Gatsby site.

- Support multi-language url routes in a single page component. This means you don't have to create separate pages such as `pages/en/index.js` or `pages/ko/index.js`.

- Support ignore paths that you don't need to generate locale pages.

## Why?

When you build multilingual sites, Google recommends using different URLs for each language version of a page rather than using cookies or browser settings to adjust the content language on the page. [(read more)](https://support.google.com/webmasters/answer/182192?hl=en&ref_topic=2370587)
Expand All @@ -20,7 +22,6 @@ Demo: [http://gatsby-starter-default-intl.netlify.com](http://gatsby-starter-def

Source: [https://github.com/wiziple/gatsby-plugin-intl/tree/master/examples/gatsby-starter-default-intl](https://github.com/wiziple/gatsby-plugin-intl/tree/master/examples/gatsby-starter-default-intl)


## Showcase

- [https://picpick.app](https://picpick.app)
Expand All @@ -29,7 +30,7 @@ Source: [https://github.com/wiziple/gatsby-plugin-intl/tree/master/examples/gats
- [https://anhek.dev](https://anhek.dev) [(Source)](https://github.com/anhek/anhek-portfolio)
- [https://pkhctech.ineo.vn](https://pkhctech.ineo.vn) [(Source)](https://github.com/hoangbaovu/gatsby-pkhctech)

*Feel free to send us PR to add your project.*
_Feel free to send us PR to add your project._

## How to use

Expand All @@ -53,6 +54,10 @@ plugins: [
defaultLanguage: `ko`,
// option to redirect to `/ko` when connecting `/`
redirect: true,
// option for use / as defaultLangauge root path. if your defaultLanguage is `ko`, when `redirectDefaultLanguageToRoot` is true, then it will not generate `/ko/xxx` pages, instead of `/xxx`
redirectDefaultLanguageToRoot: false,
// paths that you don't want to genereate locale pages, example: ["/dashboard/","/test/**"], string format is from micromatch https://github.com/micromatch/micromatch
ignoredPaths: [],
},
},
]
Expand All @@ -62,12 +67,11 @@ plugins: [

For example,

| language resource file | language |
| --- | --- |
| [src/intl/en.json](https://github.com/wiziple/gatsby-plugin-intl/blob/master/examples/gatsby-starter-default-intl/src/intl/en.json) | English |
| [src/intl/ko.json](https://github.com/wiziple/gatsby-plugin-intl/blob/master/examples/gatsby-starter-default-intl/src/intl/ko.json) | Korean |
| [src/intl/de.json](https://github.com/wiziple/gatsby-plugin-intl/blob/master/examples/gatsby-starter-default-intl/src/intl/de.json) | German |

| language resource file | language |
| ----------------------------------------------------------------------------------------------------------------------------------- | -------- |
| [src/intl/en.json](https://github.com/wiziple/gatsby-plugin-intl/blob/master/examples/gatsby-starter-default-intl/src/intl/en.json) | English |
| [src/intl/ko.json](https://github.com/wiziple/gatsby-plugin-intl/blob/master/examples/gatsby-starter-default-intl/src/intl/ko.json) | Korean |
| [src/intl/de.json](https://github.com/wiziple/gatsby-plugin-intl/blob/master/examples/gatsby-starter-default-intl/src/intl/de.json) | German |

### Change your components

Expand All @@ -80,9 +84,7 @@ import { injectIntl, Link, FormattedMessage } from "gatsby-plugin-intl"
const IndexPage = ({ intl }) => {
return (
<Layout>
<SEO
title={intl.formatMessage({ id: "title" })}
/>
<SEO title={intl.formatMessage({ id: "title" })} />
<Link to="/page-2/">
{intl.formatMessage({ id: "go_page2" })}
{/* OR <FormattedMessage id="go_page2" /> */}
Expand All @@ -92,7 +94,9 @@ const IndexPage = ({ intl }) => {
}
export default injectIntl(IndexPage)
```

Or you can use the new `useIntl` hook.

```jsx
import React from "react"
import { useIntl, Link, FormattedMessage } from "gatsby-plugin-intl"
Expand All @@ -101,9 +105,7 @@ const IndexPage = () => {
const intl = useIntl()
return (
<Layout>
<SEO
title={intl.formatMessage({ id: "title" })}
/>
<SEO title={intl.formatMessage({ id: "title" })} />
<Link to="/page-2/">
{intl.formatMessage({ id: "go_page2" })}
{/* OR <FormattedMessage id="go_page2" /> */}
Expand All @@ -114,48 +116,44 @@ const IndexPage = () => {
export default IndexPage
```


## How It Works

Let's say you have two pages (`index.js` and `page-2.js`) in your `pages` directory. The plugin will create static pages for every language.

file | English | Korean | German | Default*
-- | -- | -- | -- | --
src/pages/index.js | /**en** | /**ko** | /**de** | /
src/pages/page-2.js | /**en**/page-2 | /**ko**/page-2 | /**de**/page-2 | /page-2
| file | English | Korean | German | Default\* |
| ------------------- | -------------- | -------------- | -------------- | --------- |
| src/pages/index.js | /**en** | /**ko** | /**de** | / |
| src/pages/page-2.js | /**en**/page-2 | /**ko**/page-2 | /**de**/page-2 | /page-2 |

**Default Pages and Redirection**

If redirect option is `true`, `/` or `/page-2` will be redirected to the user's preferred language router. e.g) `/ko` or `/ko/page-2`. Otherwise, the pages will render `defaultLangugage` language. You can also specify additional component to be rendered on redirection page by adding `redirectComponent` option.


## Plugin Options

Option | Type | Description
-- | -- | --
path | string | language JSON resource path
languages | string[] | supported language keys
defaultLanguage | string | default language when visiting `/page` instead of `ko/page`
redirect | boolean | if the value is `true`, `/` or `/page-2` will be redirected to the user's preferred language router. e.g) `/ko` or `/ko/page-2`. Otherwise, the pages will render `defaultLangugage` language.
redirectComponent | string (optional) | additional component file path to be rendered on with a redirection component for SEO.

| Option | Type | Description |
| ----------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| path | string | language JSON resource path |
| languages | string[] | supported language keys |
| defaultLanguage | string | default language when visiting `/page` instead of `ko/page` |
| redirect | boolean | if the value is `true`, `/` or `/page-2` will be redirected to the user's preferred language router. e.g) `/ko` or `/ko/page-2`. Otherwise, the pages will render `defaultLangugage` language. |
| redirectComponent | string (optional) | additional component file path to be rendered on with a redirection component for SEO. |

## Components

To make it easy to handle i18n with multi-language url routes, the plugin provides several components.

To use it, simply import it from `gatsby-plugin-intl`.

Component | Type | Description
-- | -- | --
Link | component | This is a wrapper around @gatsby’s Link component that adds useful enhancements for multi-language routes. All props are passed through to @gatsby’s Link component.
navigate | function | This is a wrapper around @gatsby’s navigate function that adds useful enhancements for multi-language routes. All options are passed through to @gatsby’s navigate function.
changeLocale | function | A function that replaces your locale. `changeLocale(locale, to = null)`
IntlContextConsumer | component | A context component to get plugin configuration on the component level.
injectIntl | component | https://github.com/yahoo/react-intl/wiki/API#injection-api
FormattedMessage | component | https://github.com/yahoo/react-intl/wiki/Components#string-formatting-components
and more... | | https://github.com/yahoo/react-intl/wiki/Components

| Component | Type | Description |
| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Link | component | This is a wrapper around @gatsby’s Link component that adds useful enhancements for multi-language routes. All props are passed through to @gatsby’s Link component. |
| navigate | function | This is a wrapper around @gatsby’s navigate function that adds useful enhancements for multi-language routes. All options are passed through to @gatsby’s navigate function. |
| changeLocale | function | A function that replaces your locale. `changeLocale(locale, to = null)` |
| IntlContextConsumer | component | A context component to get plugin configuration on the component level. |
| injectIntl | component | https://github.com/yahoo/react-intl/wiki/API#injection-api |
| FormattedMessage | component | https://github.com/yahoo/react-intl/wiki/Components#string-formatting-components |
| and more... | | https://github.com/yahoo/react-intl/wiki/Components |

## License

Expand Down
103 changes: 103 additions & 0 deletions __tests__/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,109 @@ it(`should read translations from file and create corresponding pages`, async ()
expect(actions.createPage.mock.calls[3][0].path).toBe(`/es/`)
})

it(`it should not generate locale pages when ignoredPaths matched`, async () => {
const pluginOptions = {
languages: [`es`],
defaultLanguage: `es`,
path: `${__dirname}/fixtures/intl`,
ignoredPaths: ["/test/**"],
}

const ignoreMocks = {
actions,
page: {
path: "/test/",
context: {},
},
}
await onCreatePage(ignoreMocks, pluginOptions)

expect(actions.createPage.mock.calls.length).toBe(5)

// assert the pages created match the requested languages
expect(actions.createPage.mock.calls[0][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[1][0].path).toBe(`/en/`)
expect(actions.createPage.mock.calls[2][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[3][0].path).toBe(`/es/`)
expect(actions.createPage.mock.calls[4][0].path).toBe(`/test/`)
})
it(`it should generate locale pages when ignoredPaths not matched`, async () => {
const pluginOptions = {
languages: [`es`],
defaultLanguage: `es`,
path: `${__dirname}/fixtures/intl`,
ignoredPaths: ["/test/**"],
}

const ignoreMocks = {
actions,
page: {
path: "/test2/",
context: {},
},
}
await onCreatePage(ignoreMocks, pluginOptions)

expect(actions.createPage.mock.calls.length).toBe(7)

// assert the pages created match the requested languages
expect(actions.createPage.mock.calls[0][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[1][0].path).toBe(`/en/`)
expect(actions.createPage.mock.calls[2][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[3][0].path).toBe(`/es/`)
expect(actions.createPage.mock.calls[4][0].path).toBe(`/test/`)
expect(actions.createPage.mock.calls[5][0].path).toBe(`/test2/`)
expect(actions.createPage.mock.calls[6][0].path).toBe(`/es/test2/`)
})

it(`it should not generate locale pages when redirectDefaultLanguageToRoot is true`, async () => {
const pluginOptions = {
languages: [`es`],
defaultLanguage: `es`,
path: `${__dirname}/fixtures/intl`,
redirectDefaultLanguageToRoot: true,
}

await onCreatePage(mocks, pluginOptions)

expect(actions.createPage.mock.calls.length).toBe(8)

// assert the pages created match the requested languages
expect(actions.createPage.mock.calls[0][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[1][0].path).toBe(`/en/`)
expect(actions.createPage.mock.calls[2][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[3][0].path).toBe(`/es/`)
expect(actions.createPage.mock.calls[4][0].path).toBe(`/test/`)
expect(actions.createPage.mock.calls[5][0].path).toBe(`/test2/`)
expect(actions.createPage.mock.calls[6][0].path).toBe(`/es/test2/`)
expect(actions.createPage.mock.calls[7][0].path).toBe(`/`)
})

it(`it should generate locale pages when redirectDefaultLanguageToRoot is false`, async () => {
const pluginOptions = {
languages: [`es`],
defaultLanguage: `es`,
path: `${__dirname}/fixtures/intl`,
redirectDefaultLanguageToRoot: false,
}

await onCreatePage(mocks, pluginOptions)

expect(actions.createPage.mock.calls.length).toBe(10)

// assert the pages created match the requested languages
expect(actions.createPage.mock.calls[0][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[1][0].path).toBe(`/en/`)
expect(actions.createPage.mock.calls[2][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[3][0].path).toBe(`/es/`)
expect(actions.createPage.mock.calls[4][0].path).toBe(`/test/`)
expect(actions.createPage.mock.calls[5][0].path).toBe(`/test2/`)
expect(actions.createPage.mock.calls[6][0].path).toBe(`/es/test2/`)
expect(actions.createPage.mock.calls[7][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[8][0].path).toBe(`/`)
expect(actions.createPage.mock.calls[9][0].path).toBe(`/es/`)
})

it(`should crash when translations file doesn't exist`, async () => {
const pluginOptions = {
languages: [`es`, `en`],
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@formatjs/intl-relativetimeformat": "^4.5.9",
"browser-lang": "^0.1.0",
"intl": "^1.2.5",
"micromatch": "^4.0.2",
"react-intl": "^3.12.0"
},
"devDependencies": {
Expand All @@ -21,7 +22,9 @@
"cross-env": "^7.0.0",
"jest": "^25.1.0",
"prettier": "^1.19.1",
"rimraf": "^3.0.2"
"react": "^16.13.1",
"rimraf": "^3.0.2",
"webpack": "^4.43.0"
},
"homepage": "https://github.com/wiziple/gatsby-plugin-intl",
"keywords": [
Expand Down
26 changes: 21 additions & 5 deletions src/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const webpack = require("webpack")
const { isMatch } = require("./util")

function flattenMessages(nestedMessages, prefix = "") {
return Object.keys(nestedMessages).reduce((messages, key) => {
Expand Down Expand Up @@ -49,6 +50,8 @@ exports.onCreatePage = async ({ page, actions }, pluginOptions) => {
languages = ["en"],
defaultLanguage = "en",
redirect = false,
ignoredPaths = [],
redirectDefaultLanguageToRoot = false,
} = pluginOptions

const getMessages = (path, language) => {
Expand Down Expand Up @@ -85,7 +88,9 @@ exports.onCreatePage = async ({ page, actions }, pluginOptions) => {
routed,
originalPath: page.path,
redirect,
redirectDefaultLanguageToRoot,
defaultLanguage,
ignoredPaths,
},
},
}
Expand All @@ -96,11 +101,22 @@ exports.onCreatePage = async ({ page, actions }, pluginOptions) => {
createPage(newPage)

languages.forEach(language => {
const localePage = generatePage(true, language)
const regexp = new RegExp("/404/?$")
if (regexp.test(localePage.path)) {
localePage.matchPath = `/${language}/*`
// check ignore paths, if matched then don't generate locale page
if (!isMatch(ignoredPaths, page.path)) {
if (
redirectDefaultLanguageToRoot === true &&
language === defaultLanguage
) {
// default language will redirect to root, so there is no need to generate default langauge pages
// do nothing
} else {
const localePage = generatePage(true, language)
const regexp = new RegExp("/404/?$")
if (regexp.test(localePage.path)) {
localePage.matchPath = `/${language}/*`
}
createPage(localePage)
}
}
createPage(localePage)
})
}
Loading