Skip to content

Commit 3785097

Browse files
authored
Merge pull request #43 from jngbng/pr_incremental_build_fix
hot-reload/incremental build JSON language files
2 parents 3ff0d29 + c40fcb1 commit 3785097

File tree

8 files changed

+235
-58
lines changed

8 files changed

+235
-58
lines changed

README.md

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ Easily translate your Gatsby website into multiple languages.
1515

1616
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)
1717

18-
## How is it different from other gatsby i18next plugins?
18+
## :boom: Breaking change since v0.0.27
1919

20-
This plugin does not require fetching translations with graphql query on each page, everything is done automatically. Just use `react-i18next` to translate your pages.
20+
As of v0.0.28, language JSON resources should be loaded by `gatsby-source-filesystem` plugin and than fetched by GraphQL query. It enables incremental build and hot-reload as language JSON files change.
21+
22+
Users who have loaded language JSON files using `path` option will be affected. Please check configuration example on below.
2123

2224
## Demo
2325

@@ -50,9 +52,17 @@ npm install --save gatsby-plugin-react-i18next i18next react-i18next
5052
// In your gatsby-config.js
5153
plugins: [
5254
{
53-
resolve: `gatsby-plugin-react-i18next`,
55+
resolve: `gatsby-source-filesystem`,
5456
options: {
5557
path: `${__dirname}/locales`,
58+
name: `locale`,
59+
ignore: [`**/\.*`, `**/*~`]
60+
}
61+
},
62+
{
63+
resolve: `gatsby-plugin-react-i18next`,
64+
options: {
65+
localeJsonSourceName: `locale`, // name given to `gatsby-source-filesystem` plugin.
5666
languages: [`en`, `es`, `de`],
5767
defaultLanguage: `en`,
5868
// if you are using Helmet, you must include siteUrl, and make sure you add http:https
@@ -149,6 +159,20 @@ const IndexPage = () => {
149159
};
150160

151161
export default IndexPage;
162+
163+
export const query = graphql`
164+
query($language: String!) {
165+
locales: allLocale(filter: {language: {eq: $language}}) {
166+
edges {
167+
node {
168+
ns
169+
data
170+
language
171+
}
172+
}
173+
}
174+
}
175+
`;
152176
```
153177

154178
and in `locales/en/translations.json` you will have
@@ -243,15 +267,16 @@ const Header = ({siteTitle}) => {
243267

244268
## Plugin Options
245269

246-
| Option | Type | Description |
247-
| --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
248-
| path | string | path to the folder with JSON translations |
249-
| languages | string[] | supported language keys |
250-
| defaultLanguage | string | default language when visiting `/page` instead of `/es/page` |
251-
| redirect | boolean | if the value is `true`, `/` or `/page-2` will be redirected to the user's preferred language router. e.g) `/es` or `/es/page-2`. Otherwise, the pages will render `defaultLangugage` language. Default is `true` |
252-
| siteUrl | string | public site url, is used to generate language specific meta tags |
253-
| pages | array | an array of [page options](#page-options) used to modify plugin behaviour for specific pages |
254-
| i18nextOptions | object | [i18next configuration options](https://www.i18next.com/overview/configuration-options) |
270+
| Option | Type | Description |
271+
| -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
272+
| localeJsonSourceName | string | name of JSON translation file nodes that are loaded by `gatsby-source-filesystem` (set by `option.name`). Default is `locale` |
273+
| localeJsonNodeName | string | name of GraphQL node that holds locale data. Default is `locales` |
274+
| languages | string[] | supported language keys |
275+
| defaultLanguage | string | default language when visiting `/page` instead of `/es/page` |
276+
| redirect | boolean | if the value is `true`, `/` or `/page-2` will be redirected to the user's preferred language router. e.g) `/es` or `/es/page-2`. Otherwise, the pages will render `defaultLangugage` language. Default is `true` |
277+
| siteUrl | string | public site url, is used to generate language specific meta tags |
278+
| pages | array | an array of [page options](#page-options) used to modify plugin behaviour for specific pages |
279+
| i18nextOptions | object | [i18next configuration options](https://www.i18next.com/overview/configuration-options) |
255280

256281
## Page options
257282

@@ -371,6 +396,26 @@ export const query = graphql`
371396
`;
372397
```
373398

399+
## How to fetch translations of specific namespaces only
400+
401+
You can use `ns` and `language` field in gatsby page queries to fetch specific namespaces that are being used in the page. This will be useful when you have several big pages with lots of translations.
402+
403+
```javascript
404+
export const query = graphql`
405+
query($language: String!) {
406+
locales: allLocale(filter: {ns: {regex: "/common|about/"}, language: {eq: $language}}) {
407+
edges {
408+
node {
409+
ns
410+
data
411+
language
412+
}
413+
}
414+
}
415+
}
416+
`;
417+
```
418+
374419
## How to add `sitemap.xml` for all language specific pages
375420

376421
You can use [gatsby-plugin-sitemap](https://www.gatsbyjs.org/packages/gatsby-plugin-sitemap/) to automatically generate a sitemap during build time. You need to customize `query` to fetch only original pages and then `serialize` data to build a sitemap. Here is an example:

gatsby-node.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
const {onCreatePage} = require('./dist/plugin/onCreatePage');
2+
const {onCreateNode} = require('./dist/plugin/onCreateNode');
3+
const {onPreBootstrap} = require('./dist/plugin/onPreBootstrap');
4+
25
exports.onCreatePage = onCreatePage;
6+
exports.onCreateNode = onCreateNode;
7+
exports.onPreBootstrap = onPreBootstrap;

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"dependencies": {
6666
"bluebird": "^3.7.2",
6767
"browser-lang": "^0.1.0",
68-
"glob": "^7.1.6",
6968
"path-to-regexp": "^6.1.0"
7069
},
7170
"peerDependencies": {

src/plugin/onCreateNode.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {CreateNodeArgs, Node} from 'gatsby';
2+
import {FileSystemNode, PluginOptions, LocaleNodeInput} from '../types';
3+
4+
export function unstable_shouldOnCreateNode({node}: {node: Node}) {
5+
// We only care about JSON content.
6+
return node.internal.mediaType === `application/json`;
7+
}
8+
9+
export const onCreateNode = async (
10+
{
11+
node,
12+
actions,
13+
loadNodeContent,
14+
createNodeId,
15+
createContentDigest,
16+
reporter
17+
}: CreateNodeArgs<FileSystemNode>,
18+
{localeJsonSourceName = 'locale'}: PluginOptions
19+
) => {
20+
if (!unstable_shouldOnCreateNode({node})) {
21+
return;
22+
}
23+
24+
const {
25+
absolutePath,
26+
internal: {mediaType, type},
27+
sourceInstanceName,
28+
relativeDirectory,
29+
name,
30+
id
31+
} = node;
32+
33+
// Currently only support file resources
34+
if (type !== 'File') {
35+
return;
36+
}
37+
38+
// User is not using this feature
39+
if (localeJsonSourceName == null) {
40+
return;
41+
}
42+
43+
if (sourceInstanceName !== localeJsonSourceName) {
44+
return;
45+
}
46+
47+
const activity = reporter.activityTimer(
48+
`gatsby-plugin-react-i18next: create node: ${relativeDirectory}/${name}`
49+
);
50+
activity.start();
51+
52+
// relativeDirectory name is language name.
53+
const language = relativeDirectory;
54+
const content = await loadNodeContent(node);
55+
56+
// verify & canonicalize indent. (do not care about key order)
57+
let data: string;
58+
try {
59+
data = JSON.stringify(JSON.parse(content), undefined, '');
60+
} catch {
61+
const hint = node.absolutePath ? `file ${node.absolutePath}` : `in node ${node.id}`;
62+
throw new Error(`Unable to parse JSON: ${hint}`);
63+
}
64+
65+
const {createNode, createParentChildLink} = actions;
66+
67+
const localeNode: LocaleNodeInput = {
68+
id: createNodeId(`${id} >>> Locale`),
69+
children: [],
70+
parent: id,
71+
internal: {
72+
content: data,
73+
contentDigest: createContentDigest(data),
74+
type: `Locale`
75+
},
76+
language: language,
77+
ns: name,
78+
data,
79+
fileAbsolutePath: absolutePath
80+
};
81+
82+
createNode(localeNode);
83+
84+
// @ts-ignore
85+
// staled issue: https://github.com/gatsbyjs/gatsby/issues/19993
86+
createParentChildLink({parent: node, child: localeNode});
87+
88+
activity.end();
89+
};

src/plugin/onCreatePage.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,7 @@
1-
import _glob from 'glob';
21
import {CreatePageArgs, Page} from 'gatsby';
32
import BP from 'bluebird';
4-
import fs from 'fs';
5-
import util from 'util';
63
import {match} from 'path-to-regexp';
7-
import {PageContext, PageOptions, PluginOptions, Resources} from '../types';
8-
9-
const readFile = util.promisify(fs.readFile);
10-
const glob = util.promisify(_glob);
11-
12-
const getResources = async (path: string, language: string) => {
13-
const files = await glob(`${path}/${language}/*.json`);
14-
return BP.reduce<string, Resources>(
15-
files,
16-
async (result, file) => {
17-
const [, ns] = /[\/(\w+|\-)]+\/([\w|\-]+)\.json/.exec(file)!;
18-
const content = await readFile(file, 'utf8');
19-
result[language][ns] = JSON.parse(content);
20-
return result;
21-
},
22-
{[language]: {}}
23-
);
24-
};
4+
import {PageContext, PageOptions, PluginOptions} from '../types';
255

266
export const onCreatePage = async (
277
{page, actions}: CreatePageArgs<PageContext>,
@@ -49,7 +29,6 @@ export const onCreatePage = async (
4929
routed = false,
5030
pageOptions
5131
}: GeneratePageParams): Promise<Page<PageContext>> => {
52-
const resources = await getResources(pluginOptions.path, language);
5332
return {
5433
...page,
5534
path,
@@ -61,7 +40,6 @@ export const onCreatePage = async (
6140
languages: pageOptions?.languages || languages,
6241
defaultLanguage,
6342
routed,
64-
resources,
6543
originalPath,
6644
path
6745
}

src/plugin/onPreBootstrap.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {ParentSpanPluginArgs} from 'gatsby';
2+
import {PluginOptions} from '../types';
3+
import report from 'gatsby-cli/lib/reporter';
4+
5+
export const onPreBootstrap = (_args: ParentSpanPluginArgs, pluginOptions: PluginOptions) => {
6+
// Check for deprecated option.
7+
if (pluginOptions.hasOwnProperty('path')) {
8+
report.error(
9+
`gatsby-plugin-react-i18next: 💥💥💥 "path" option is deprecated and won't be working as it was before. Please update setting on your gastby-config.js.\n\nSee detail: https://github.com/microapps/gatsby-plugin-react-i18next\n\n`
10+
);
11+
}
12+
};

src/plugin/wrapPageElement.tsx

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import {withPrefix, WrapPageElementBrowserArgs} from 'gatsby';
33
// @ts-ignore
44
import browserLang from 'browser-lang';
5-
import {I18NextContext, LANGUAGE_KEY, PageContext, PluginOptions} from '../types';
5+
import {I18NextContext, LANGUAGE_KEY, PageContext, PluginOptions, LocaleNode} from '../types';
66
import i18next, {i18n as I18n} from 'i18next';
77
import {I18nextProvider} from 'react-i18next';
88
import {I18nextContext} from '../i18nextContext';
@@ -19,19 +19,11 @@ const withI18next = (i18n: I18n, context: I18NextContext) => (children: any) =>
1919

2020
export const wrapPageElement = (
2121
{element, props}: WrapPageElementBrowserArgs<any, PageContext>,
22-
{i18nextOptions = {}, redirect = true, siteUrl}: PluginOptions
22+
{i18nextOptions = {}, redirect = true, siteUrl, localeJsonNodeName = 'locales'}: PluginOptions
2323
) => {
2424
if (!props) return;
25-
const {pageContext, location} = props;
26-
const {
27-
routed,
28-
language,
29-
languages,
30-
originalPath,
31-
defaultLanguage,
32-
resources,
33-
path
34-
} = pageContext.i18n;
25+
const {data, pageContext, location} = props;
26+
const {routed, language, languages, originalPath, defaultLanguage, path} = pageContext.i18n;
3527
const isRedirect = redirect && !routed;
3628

3729
if (isRedirect) {
@@ -66,18 +58,18 @@ export const wrapPageElement = (
6658
...i18nextOptions,
6759
lng: language,
6860
fallbackLng: defaultLanguage,
69-
resources,
7061
react: {
7162
useSuspense: false
7263
}
7364
});
7465
}
7566

76-
Object.keys(resources[language]).map((ns) => {
77-
if (!i18n.hasResourceBundle(language, ns)) {
78-
i18n.addResourceBundle(language, ns, resources[language][ns]);
79-
}
80-
});
67+
if (data && data[localeJsonNodeName]) {
68+
data[localeJsonNodeName].edges.forEach(({node}: {node: LocaleNode}) => {
69+
const parsedData = JSON.parse(node.data);
70+
i18n.addResourceBundle(node.language, node.ns, parsedData);
71+
});
72+
}
8173

8274
if (i18n.language !== language) {
8375
i18n.changeLanguage(language);

0 commit comments

Comments
 (0)