├── .babelrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── example ├── babel-extract.config.js ├── gatsby-config.js ├── gatsby-node.js ├── languages.js ├── locales │ ├── de │ │ ├── 404.json │ │ ├── common.json │ │ ├── index.json │ │ └── page-2.json │ ├── en │ │ ├── 404.json │ │ ├── common.json │ │ ├── index.json │ │ └── page-2.json │ ├── es │ │ ├── 404.json │ │ ├── common.json │ │ ├── index.json │ │ └── page-2.json │ ├── fr │ │ ├── 404.json │ │ ├── common.json │ │ ├── index.json │ │ └── page-2.json │ └── it │ │ ├── 404.json │ │ ├── common.json │ │ ├── index.json │ │ └── page-2.json ├── package.json ├── src │ ├── components │ │ ├── header.css │ │ ├── header.js │ │ ├── layout.css │ │ ├── layout.js │ │ └── seo.js │ ├── images │ │ ├── gatsby-astronaut.png │ │ └── gatsby-icon.png │ └── pages │ │ ├── 404.js │ │ ├── ignored-page.js │ │ ├── index.js │ │ └── page-2.js ├── translate.js └── yarn.lock ├── gatsby-browser.js ├── gatsby-node.js ├── gatsby-ssr.js ├── index.d.ts ├── index.js ├── package.json ├── src ├── Link.tsx ├── i18nextContext.ts ├── index.ts ├── plugin │ ├── onCreateNode.ts │ ├── onCreatePage.ts │ ├── onPreBootstrap.ts │ └── wrapPageElement.tsx ├── types.ts └── useI18next.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["babel-preset-gatsby-package", { "browser": true }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 18 15 | registry-url: https://registry.npmjs.org/ 16 | - run: yarn install 17 | - run: npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | **/node_modules/ 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # dotenv environment variable files 56 | .env* 57 | 58 | # gatsby files 59 | /**/.cache 60 | /**/public 61 | 62 | # Mac files 63 | .DS_Store 64 | 65 | # Yarn 66 | yarn-error.log 67 | .pnp/ 68 | .pnp.js 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | .idea 73 | dist/ 74 | 75 | **/.yalc 76 | **/yalc.lock 77 | 78 | # Package-lock.json 79 | **/*/package-lock.json 80 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | 4 | # Runtime data 5 | pids 6 | *.pid 7 | *.seed 8 | 9 | # Directory for instrumented libs generated by jscoverage/JSCover 10 | lib-cov 11 | 12 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 13 | .grunt 14 | 15 | # node-waf configuration 16 | .lock-wscript 17 | 18 | # Compiled binary addons (http://nodejs.org/api/addons.html) 19 | build/Release 20 | 21 | # Dependency directory 22 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 23 | node_modules 24 | yarn.lock 25 | src 26 | flow-typed 27 | decls 28 | example 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": false, 4 | "jsxBracketSameLine": true, 5 | "printWidth": 100, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 microapps SL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gatsby-plugin-react-i18next 2 | 3 | Easily translate your Gatsby website into multiple languages. 4 | 5 | ## Features 6 | 7 | - Seamless integration with [react-i18next](https://react.i18next.com/) - a powerful internationalization framework for React. 8 | - Code splitting. Load translations for each page separately. 9 | - Automatic redirection based on the user's preferred language in browser provided by [browser-lang](https://github.com/wiziple/browser-lang). 10 | - Support multi-language url routes in a single page component. You don't have to create separate pages such as `pages/en/index.js` or `pages/es/index.js`. 11 | - SEO friendly 12 | - Support for [gatsby-plugin-layout](https://www.gatsbyjs.org/packages/gatsby-plugin-layout/) 13 | 14 | ## Why? 15 | 16 | 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) 17 | 18 | ## :boom: Breaking change since v2.0.0 19 | 20 | As of V2.0.0, this plugin only supports Gatsby 4.16.0+ and React 18+. 21 | 22 | ## :boom: Breaking change since v1.0.0 23 | 24 | As of v1.0.0, language JSON resources should be loaded by `gatsby-source-filesystem` plugin and then fetched by GraphQL query. It enables incremental build and hot-reload as language JSON files change. 25 | 26 | Users who have loaded language JSON files using `path` option will be affected. Please check configuration example on below. 27 | 28 | ## Demo 29 | 30 | - [View demo online](https://kind-lichterman-5edcb4.netlify.app/) 31 | - [Source code](/example) 32 | 33 | ## Used by 34 | 35 | - [monei.com](https://monei.com/) - The digital payment gateway with best rates. 36 | - [moonmail.io](https://moonmail.io/) - OmniChannel Communication Platform used by more than 100,000 businesses worldwide. 37 | - [nyxo.app](https://nyxo.app) – Sleep tracking and coaching [(source code)](https://github.com/hello-nyxo/nyxo-website) 38 | 39 | ### Tutorials 40 | 41 | - [Best internationalization for Gatsby](https://dev.to/adrai/best-internationalization-for-gatsby-mkf) by Adriano Raiano 42 | 43 | ## How to use 44 | 45 | ### Install package 46 | 47 | ``` 48 | yarn add gatsby-plugin-react-i18next i18next react-i18next 49 | ``` 50 | 51 | or 52 | 53 | ``` 54 | npm install --save gatsby-plugin-react-i18next i18next react-i18next 55 | ``` 56 | 57 | ### Configure the plugin 58 | 59 | ```javascript 60 | // In your gatsby-config.js 61 | plugins: [ 62 | { 63 | resolve: `gatsby-source-filesystem`, 64 | options: { 65 | path: `${__dirname}/locales`, 66 | name: `locale` 67 | } 68 | }, 69 | { 70 | resolve: `gatsby-plugin-react-i18next`, 71 | options: { 72 | localeJsonSourceName: `locale`, // name given to `gatsby-source-filesystem` plugin. 73 | languages: [`en`, `es`, `de`], 74 | defaultLanguage: `en`, 75 | siteUrl: `https://example.com`, 76 | // if you are using trailingSlash gatsby config include it here, as well (the default is 'always') 77 | trailingSlash: 'always', 78 | // you can pass any i18next options 79 | i18nextOptions: { 80 | interpolation: { 81 | escapeValue: false // not needed for react as it escapes by default 82 | }, 83 | keySeparator: false, 84 | nsSeparator: false 85 | }, 86 | pages: [ 87 | { 88 | matchPath: '/:lang?/blog/:uid', 89 | getLanguageFromPath: true, 90 | excludeLanguages: ['es'] 91 | }, 92 | { 93 | matchPath: '/preview', 94 | languages: ['en'] 95 | } 96 | ] 97 | } 98 | } 99 | ]; 100 | ``` 101 | 102 | This example is not using semantic keys instead the entire message will be used as a key. [Read more](https://www.i18next.com/principles/fallback#key-fallback). 103 | 104 | **NOTE:** If you want nested translation keys do not set `keySeparator: false`. [More configuration options](https://www.i18next.com/overview/configuration-options). 105 | 106 | ### You'll also need to add language JSON resources to the project. 107 | 108 | For example, 109 | 110 | | language resource files | language | 111 | | ---------------------------------------------------------------------------------------------------------------------------- | -------- | 112 | | [/locales/en/index.json](https://github.com/microapps/gatsby-plugin-react-i18next/blob/master/example/locales/en/index.json) | English | 113 | | [/locales/es/index.json](https://github.com/microapps/gatsby-plugin-react-i18next/blob/master/example/locales/es/index.json) | Spanish | 114 | | [/locales/de/index.json](https://github.com/microapps/gatsby-plugin-react-i18next/blob/master/example/locales/de/index.json) | German | 115 | 116 | You can use different namespaces to organize your translations. Use the following file structure: 117 | 118 | ``` 119 | |-- language 120 | |-- namespace.json 121 | ``` 122 | 123 | For example: 124 | 125 | ``` 126 | |-- en 127 | |-- common.json 128 | |-- index.json 129 | ``` 130 | 131 | The default namespace is `translation`. [Read more about i18next namespaces](https://www.i18next.com/principles/namespaces) 132 | 133 | ### Change your components 134 | 135 | Use react i18next [`useTranslation`](https://react.i18next.com/latest/usetranslation-hook) react hook and [`Trans`](https://react.i18next.com/latest/trans-component) component to translate your pages. 136 | 137 | `gatsby-plugin-react-i18next` exposes all [`react-i18next`](https://react.i18next.com/) methods and components. 138 | 139 | Replace [Gatsby `Link`](https://www.gatsbyjs.org/docs/gatsby-link) component with the `Link` component exported from `gatsby-plugin-react-i18next` 140 | 141 | ```javascript 142 | import {graphql} from 'gatsby'; 143 | import React from 'react'; 144 | import {Link, Trans, useTranslation} from 'gatsby-plugin-react-i18next'; 145 | import Layout from '../components/layout'; 146 | import Image from '../components/image'; 147 | import SEO from '../components/seo'; 148 | 149 | const IndexPage = () => { 150 | const {t} = useTranslation(); 151 | return ( 152 | 153 | 154 | 155 | Hi people 156 | 157 | 158 | Welcome to your new Gatsby site. 159 | 160 | 161 | Now go build something great. 162 | 163 | 164 | 165 | 166 | 167 | Go to page 2 168 | 169 | 170 | ); 171 | }; 172 | 173 | export default IndexPage; 174 | 175 | export const query = graphql` 176 | query ($language: String!) { 177 | locales: allLocale(filter: {language: {eq: $language}}) { 178 | edges { 179 | node { 180 | ns 181 | data 182 | language 183 | } 184 | } 185 | } 186 | } 187 | `; 188 | ``` 189 | 190 | and in `locales/en/translations.json` you will have 191 | 192 | ```json 193 | { 194 | "Home": "Home", 195 | "Hi people": "Hi people", 196 | "Welcome to your new Gatsby site.": "Welcome to your new Gatsby site.", 197 | "Now go build something great.": "Now go build something great.", 198 | "Go to page 2": "Go to page 2" 199 | } 200 | ``` 201 | 202 | This example is not using semantic keys instead the entire message will be used as a key. [Read more](https://www.i18next.com/principles/fallback#key-fallback). 203 | 204 | ### Changing the language 205 | 206 | `gatsby-plugin-react-i18next` exposes `useI18next` hook 207 | 208 | ```javascript 209 | import {Link, useI18next} from 'gatsby-plugin-react-i18next'; 210 | import React from 'react'; 211 | 212 | const Header = ({siteTitle}) => { 213 | const {languages, changeLanguage} = useI18next(); 214 | return ( 215 | 216 | 217 | 223 | {siteTitle} 224 | 225 | 226 | 227 | {languages.map((lng) => ( 228 | 229 | { 232 | e.preventDefault(); 233 | changeLanguage(lng); 234 | }}> 235 | {lng} 236 | 237 | 238 | ))} 239 | 240 | 241 | ); 242 | }; 243 | ``` 244 | 245 | Or a more SEO friendly version using `Link` component 246 | 247 | ```javascript 248 | import {Link, useI18next} from 'gatsby-plugin-react-i18next'; 249 | import PropTypes from 'prop-types'; 250 | import React from 'react'; 251 | 252 | const Header = ({siteTitle}) => { 253 | const {languages, originalPath} = useI18next(); 254 | return ( 255 | 256 | 257 | 263 | {siteTitle} 264 | 265 | 266 | 267 | {languages.map((lng) => ( 268 | 269 | 270 | {lng} 271 | 272 | 273 | ))} 274 | 275 | 276 | ); 277 | }; 278 | ``` 279 | 280 | ## Plugin Options 281 | 282 | | Option | Type | Description | 283 | | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 284 | | localeJsonSourceName | string | name of JSON translation file nodes that are loaded by `gatsby-source-filesystem` (set by `option.name`). Default is `locale` | 285 | | localeJsonNodeName | string | name of GraphQL node that holds locale data. Default is `locales` | 286 | | languages | string[] | supported language keys | 287 | | defaultLanguage | string | default language when visiting `/page` instead of `/es/page` | 288 | | fallbackLanguage | string | optionally fallback to a different language than the defaultLanguage | 289 | | generateDefaultLanguagePage | boolean | generate dedicated page for default language. e.g) `/en/page`. It is useful when you need page urls for all languages. For example, server-side [redirect](https://www.gatsbyjs.com/docs/reference/config-files/actions/#createRedirect) using `Accept-Language` header. Default is `false`. | 290 | | 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` | 291 | | siteUrl | string | public site url, is used to generate language specific meta tags | 292 | | pages | array | an array of [page options](#page-options) used to modify plugin behaviour for specific pages | 293 | | i18nextOptions | object | [i18next configuration options](https://www.i18next.com/overview/configuration-options) | 294 | | verbose | boolean | Verbose output. Default is true | 295 | 296 | ## Page options 297 | 298 | | Option | Type | Description | 299 | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 300 | | matchPath | string | a path pattern like `/:lang?/blog/:uid`, check [path-to-regexp](https://github.com/pillarjs/path-to-regexp) for more info | 301 | | getLanguageFromPath | boolean | if set to `true` the language will be taken from the `:lang` param in the path instead of automatically generating a new page for each language | 302 | | excludeLanguages | array | an array of languages to exclude, if specified the plugin will not automatically generate pages for those languages, this option can be used to replace pages in some languages with custom ones | 303 | | languages | array | an array of languages, if specified the plugin will automatically generate pages only for those languages | 304 | 305 | ## Plugin API 306 | 307 | ### `Link` 308 | 309 | `Link` component is identical to [Gatsby Link component](https://www.gatsbyjs.org/docs/gatsby-link/) except that you can provide additional `language` prop to create a link to a page with different language 310 | 311 | ```javascript 312 | import {Link} from 'gatsby-plugin-react-i18next'; 313 | 314 | const SpanishAboutLink = () => ( 315 | 316 | About page in Spanish 317 | 318 | ); 319 | ``` 320 | 321 | ### `I18nextContext` 322 | 323 | Use this react context to access language information about the page 324 | 325 | ```javascript 326 | const context = React.useContext(I18nextContext); 327 | ``` 328 | 329 | Content of the context object 330 | 331 | | Attribute | Type | Description | 332 | | --------------- | -------- | -------------------------------------------------------- | 333 | | language | string | current language | 334 | | languages | string[] | supported language keys | 335 | | routed | boolean | if `false` it means that the page is in default language | 336 | | defaultLanguage | string | default language provided in plugin options | 337 | | originalPath | string | page path in default language | 338 | | path | string | page path | 339 | | siteUrl | string | public site url provided in plugin options | 340 | 341 | The same context will be also available in the Gatsby `pageContext.i18n` object 342 | 343 | ### `useI18next` 344 | 345 | This react hook returns `I18nextContext`, object and additional helper functions 346 | 347 | | Function | Description | 348 | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 349 | | navigate | This is a wrapper around [Gatsby navigate helper function](https://www.gatsbyjs.org/docs/gatsby-link/#how-to-use-the-navigate-helper-function) that will navigate to the page in selected language | 350 | | changeLanguage | A helper function to change language. The first parameter is a language code. Signature: `(language: string, to?: string, options?: NavigateOptions) => Promise`. You can pass additional parameters to navigate to different page. | 351 | 352 | `useI18next` also exposes the output of react i18next [`useTranslation`](https://react.i18next.com/latest/usetranslation-hook) so you can use 353 | 354 | ```javascript 355 | const {t} = useI18next(); 356 | ``` 357 | 358 | ## How to exclude pages that already have language key in path 359 | 360 | For example if you have some other plugin or script that generates your blog posts from headless CRM like [prismic.io](https://prismic.io/) in different languages you would like to exclude those pages, to not generate duplicates for each language key. You can do that by providing `pages` option. 361 | 362 | ```js 363 | pages: [ 364 | { 365 | matchPath: '/:lang?/blog/:uid', 366 | getLanguageFromPath: true, 367 | excludeLanguages: ['es'] 368 | } 369 | ]; 370 | ``` 371 | 372 | You have to specify a `:lang` url param, so the plugin knows what part of the path should be treated as language key. 373 | In this example the plugin will automatically generate language pages for all languages except `es`. Assuming that you have `['en', 'es', 'de']` languages the blog post with the path `/blog/hello-world` you will have the following pages generated: 374 | 375 | - `/blog/hello-world` - the English version (if you have `en` as a `defaultLanguage`) 376 | - `/es/blog/hello-world` - the Spanish version that should exist before you run the plugin (created manually or at build time with a plugin or api call) 377 | - `/de/blog/hello-world` - the German version that is generated automatically 378 | 379 | Omit `excludeLanguages` to get all the languages form the path. Make sure that you have pages for all the languages that you specify in the plugin, otherwise you might have broken links. 380 | 381 | You may also need to add a pages config for the 404 page, if it uses the same path pattern as your excluded pages. Note that the order of these rules is important. 382 | 383 | ```js 384 | pages: [ 385 | { 386 | matchPath: '/:lang?/404', 387 | getLanguageFromPath: false 388 | }, 389 | { 390 | matchPath: '/:lang?/:uid', 391 | getLanguageFromPath: true, 392 | excludeLanguages: ['es'] 393 | } 394 | ]; 395 | ``` 396 | 397 | ## How to exclude a page that should not be translated 398 | 399 | You can limit the languages used to generate versions of a specific page, for example to limit `/preview` page to only English version: 400 | 401 | ```js 402 | pages: [ 403 | { 404 | matchPath: '/preview', 405 | languages: ['en'] 406 | } 407 | ]; 408 | ``` 409 | 410 | ## How to fetch translations of specific namespaces only 411 | 412 | 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. 413 | 414 | ```javascript 415 | export const query = graphql` 416 | query ($language: String!) { 417 | locales: allLocale(filter: {ns: {in: ["common", "index"]}, language: {eq: $language}}) { 418 | edges { 419 | node { 420 | ns 421 | data 422 | language 423 | } 424 | } 425 | } 426 | } 427 | `; 428 | ``` 429 | 430 | Note that in this case only files `common.json` and `index.json` will be loaded. 431 | This plugin will automatically add all loaded namespaces as fallback namespaces so if you don't specify a namespace in your translations they will still work. 432 | 433 | ## How to fetch language specific data 434 | 435 | You can use `language` variable in gatsby page queries to fetch additional data for each language. For example if you're using [gatsby-transformer-json](https://www.gatsbyjs.org/packages/gatsby-transformer-json/) your query might look like: 436 | 437 | ```javascript 438 | export const query = graphql` 439 | query ($language: String!) { 440 | dataJson(language: {eq: $language}) { 441 | ...DataFragment 442 | } 443 | } 444 | `; 445 | ``` 446 | 447 | ## How to add `sitemap.xml` for all language specific pages 448 | 449 | 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: 450 | 451 | ```javascript 452 | // In your gatsby-config.js 453 | plugins: [ 454 | { 455 | resolve: 'gatsby-plugin-sitemap', 456 | options: { 457 | excludes: ['/**/404', '/**/404.html'], 458 | query: ` 459 | { 460 | site { 461 | siteMetadata { 462 | siteUrl 463 | } 464 | } 465 | allSitePage(filter: {context: {i18n: {routed: {eq: false}}}}) { 466 | edges { 467 | node { 468 | context { 469 | i18n { 470 | defaultLanguage 471 | languages 472 | originalPath 473 | } 474 | } 475 | path 476 | } 477 | } 478 | } 479 | } 480 | `, 481 | serialize: ({site, allSitePage}) => { 482 | return allSitePage.edges.map((edge) => { 483 | const {languages, originalPath, defaultLanguage} = edge.node.context.i18n; 484 | const {siteUrl} = site.siteMetadata; 485 | const url = siteUrl + originalPath; 486 | const links = [ 487 | {lang: defaultLanguage, url}, 488 | {lang: 'x-default', url} 489 | ]; 490 | languages.forEach((lang) => { 491 | if (lang === defaultLanguage) return; 492 | links.push({lang, url: `${siteUrl}/${lang}${originalPath}`}); 493 | }); 494 | return { 495 | url, 496 | changefreq: 'daily', 497 | priority: originalPath === '/' ? 1.0 : 0.7, 498 | links 499 | }; 500 | }); 501 | } 502 | } 503 | } 504 | ]; 505 | ``` 506 | 507 | ## How to use a fallback language with semantic keys (vs. message strings) 508 | 509 | By default this plugin is setup to fallback on the entire **message string**, that is used as language key. 510 | 511 | In order to use **semantic keys**, so the fallback message string is the default's language value (instead of the key), it is possible to do the following; 512 | 513 | In `/gatsby-config.js`, setup the plugin as usual, and add the key `options.i18nextOptions.fallbackLng` (i18next documentation, [configuration options](https://www.i18next.com/overview/configuration-options#languages-namespaces-resources), and [fallback options](https://www.i18next.com/principles/fallback#fallback)); 514 | 515 | ``` 516 | { 517 | resolve: `gatsby-plugin-react-i18next`, 518 | options: { 519 | localeJsonSourceName: `locale`, 520 | languages: [`en`, `de`, `fr`], 521 | defaultLanguage: `en`, 522 | siteUrl: `https://example.com/`, 523 | i18nextOptions: { 524 | fallbackLng: 'en', // here we provide the fallback language to i18next 525 | interpolation: { 526 | escapeValue: false 527 | }, 528 | keySeparator: false, 529 | nsSeparator: false 530 | } 531 | } 532 | } 533 | ``` 534 | 535 | Then in a page query, we avoid to specify a `$language` variable used as filter, so i18next gets access to the available locales, used as fallback. 536 | 537 | ``` 538 | // /pages/index.js 539 | export const query = graphql` 540 | query { // no $language variable defined, no filters on allLocale 541 | locales: allLocale { 542 | edges { 543 | node { 544 | ns 545 | data 546 | language 547 | } 548 | } 549 | } 550 | } 551 | `; 552 | ``` 553 | 554 | ## How to extract translations from pages 555 | 556 | You can use [babel-plugin-i18next-extract](https://i18next-extract.netlify.app) automatically extract translations inside `t` function and `Trans` component from you pages and save them in JSON. 557 | 558 | 1. Install 559 | 560 | ``` 561 | yarn add @babel/cli @babel/plugin-transform-typescript babel-plugin-i18next-extract -D 562 | ``` 563 | 564 | 2. create `babel-extract.config.js` file (don't name it `babel.config.js`, or it will be used by gatsby) 565 | 566 | ```javascript 567 | module.exports = { 568 | presets: ['babel-preset-gatsby'], 569 | plugins: [ 570 | [ 571 | 'i18next-extract', 572 | { 573 | keySeparator: null, 574 | nsSeparator: null, 575 | keyAsDefaultValue: ['en'], 576 | useI18nextDefaultValue: ['en'], 577 | discardOldKeys: true, 578 | outputPath: 'locales/{{locale}}/{{ns}}.json', 579 | customTransComponents: [['gatsby-plugin-react-i18next', 'Trans']] 580 | } 581 | ] 582 | ], 583 | overrides: [ 584 | { 585 | test: [`**/*.ts`, `**/*.tsx`], 586 | plugins: [[`@babel/plugin-transform-typescript`, {isTSX: true}]] 587 | } 588 | ] 589 | }; 590 | ``` 591 | 592 | 3. add a script to your `package.json` 593 | 594 | ```json 595 | { 596 | "scripts": { 597 | "extract": "yarn run babel --config-file ./babel-extract.config.js -o tmp/chunk.js 'src/**/*.{js,jsx,ts,tsx}' && rm -rf tmp" 598 | } 599 | } 600 | ``` 601 | 602 | If you want to extract translations per page, you can add a special comment at the beginning of the page: 603 | 604 | ``` 605 | // i18next-extract-mark-ns-start about-page 606 | ``` 607 | 608 | This will create a file `about-page.json` with all the translations on this page. 609 | 610 | To load this file you need to specify a namespace like this: 611 | 612 | ```javascript 613 | export const query = graphql` 614 | query ($language: String!) { 615 | locales: allLocale( 616 | filter: {ns: {in: ["translation", "about-page"]}, language: {eq: $language}} 617 | ) { 618 | edges { 619 | node { 620 | ns 621 | data 622 | language 623 | } 624 | } 625 | } 626 | } 627 | `; 628 | ``` 629 | 630 | ### Automatically translate to different languages 631 | 632 | After your messages had been extracted you can use [AWS Translate](https://aws.amazon.com/translate/) to automatically translate messages to different languages. 633 | 634 | This functionality is out of the scope of this plugin, but you can get the idea from [this script](/example/translate.js). 635 | 636 | ## How to fallback to a different language than the defaultLanguage 637 | 638 | By default, on first load, this plugin will fallback to the defaultLanguage if the browser's detected language is not included in the array of languages. 639 | 640 | If you want to fallback to a different language in the languages array, you can set the `fallbackLanguage` option. 641 | 642 | For example, if the default language of your site is Japanese, you only have English as another language, and you want all other browser-detected languages to fallback to English, not Japanese. 643 | 644 | ```javascript 645 | module.exports = { 646 | plugins: [ 647 | { 648 | resolve: 'gatsby-plugin-react-i18next', 649 | options: { 650 | defaultLanguage: 'ja', 651 | fallbackLanguage: 'en' 652 | } 653 | } 654 | ] 655 | }; 656 | ``` 657 | 658 | ## Mentions 659 | 660 | - [Best internationalization for Gatsby](https://dev.to/adrai/best-internationalization-for-gatsby-mkf) by 661 | Adriano Raiano 662 | 663 | ## Credits 664 | 665 | This package is based on: 666 | 667 | - [gatsby-plugin-intl](https://github.com/wiziple/gatsby-plugin-intl) by Daewoong Moon 668 | - [gatsby-i18n-plugin](https://github.com/ikhudo/gatsby-i18n-plugin) by ikhudo 669 | 670 | ## License 671 | 672 | MIT © [microapps](https://github.com/microapps) 673 | -------------------------------------------------------------------------------- /example/babel-extract.config.js: -------------------------------------------------------------------------------- 1 | const {defaultLanguage} = require('./languages'); 2 | 3 | process.env.NODE_ENV = 'test'; 4 | 5 | module.exports = { 6 | presets: ['babel-preset-gatsby'], 7 | plugins: [ 8 | [ 9 | 'i18next-extract', 10 | { 11 | keySeparator: null, 12 | nsSeparator: null, 13 | keyAsDefaultValue: [defaultLanguage], 14 | useI18nextDefaultValue: [defaultLanguage], 15 | discardOldKeys: true, 16 | defaultNS: 'common', 17 | outputPath: 'locales/{{locale}}/{{ns}}.json', 18 | customTransComponents: [['gatsby-plugin-react-i18next', 'Trans']] 19 | } 20 | ] 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /example/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const {languages, defaultLanguage} = require('./languages'); 2 | const siteUrl = process.env.URL || `https://fallback.net`; 3 | 4 | module.exports = { 5 | siteMetadata: { 6 | title: `Gatsby Default Starter`, 7 | description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`, 8 | author: `@gatsbyjs`, 9 | siteUrl: 'https://kind-lichterman-5edcb4.netlify.app' 10 | }, 11 | plugins: [ 12 | `gatsby-plugin-image`, 13 | { 14 | resolve: `gatsby-source-filesystem`, 15 | options: { 16 | name: `images`, 17 | path: `${__dirname}/src/images` 18 | } 19 | }, 20 | { 21 | resolve: `gatsby-source-filesystem`, 22 | options: { 23 | path: `${__dirname}/locales`, 24 | name: `locale` 25 | } 26 | }, 27 | `gatsby-transformer-sharp`, 28 | `gatsby-plugin-sharp`, 29 | { 30 | resolve: `gatsby-plugin-manifest`, 31 | options: { 32 | name: `gatsby-starter-default`, 33 | short_name: `starter`, 34 | start_url: `/`, 35 | background_color: `#663399`, 36 | theme_color: `#663399`, 37 | display: `minimal-ui`, 38 | icon: `src/images/gatsby-icon.png` // This path is relative to the root of the site. 39 | } 40 | }, 41 | { 42 | resolve: `gatsby-plugin-react-i18next`, 43 | options: { 44 | languages, 45 | defaultLanguage, 46 | siteUrl: 'https://kind-lichterman-5edcb4.netlify.app', 47 | i18nextOptions: { 48 | defaultNS: 'common', 49 | //debug: true, 50 | lowerCaseLng: true, 51 | saveMissing: false, 52 | interpolation: { 53 | escapeValue: false // not needed for react as it escapes by default 54 | }, 55 | keySeparator: false, 56 | nsSeparator: false 57 | }, 58 | pages: [ 59 | { 60 | matchPath: '/ignored-page', 61 | languages: ['en'] 62 | } 63 | ] 64 | } 65 | }, 66 | { 67 | resolve: 'gatsby-plugin-sitemap', 68 | options: { 69 | excludes: ['/**/404', '/**/404.html'], 70 | query: ` 71 | { 72 | site { 73 | siteMetadata { 74 | siteUrl 75 | } 76 | } 77 | allSitePage(filter: {context: {i18n: {routed: {eq: false}}}}) { 78 | nodes { 79 | context { 80 | i18n { 81 | defaultLanguage 82 | languages 83 | originalPath 84 | } 85 | } 86 | path 87 | } 88 | } 89 | } 90 | `, 91 | serialize: (node) => { 92 | const {languages, originalPath, defaultLanguage} = node.context.i18n; 93 | const url = siteUrl + originalPath; 94 | const links = [ 95 | {lang: defaultLanguage, url}, 96 | {lang: 'x-default', url} 97 | ]; 98 | languages.forEach((lang) => { 99 | if (lang === defaultLanguage) return; 100 | links.push({lang, url: `${siteUrl}/${lang}${originalPath}`}); 101 | }); 102 | return { 103 | url, 104 | changefreq: 'daily', 105 | priority: originalPath === '/' ? 1.0 : 0.7, 106 | links 107 | }; 108 | } 109 | } 110 | } 111 | // this (optional) plugin enables Progressive Web App + Offline functionality 112 | // To learn more, visit: https://gatsby.dev/offline 113 | // `gatsby-plugin-offline`, 114 | ] 115 | }; 116 | -------------------------------------------------------------------------------- /example/gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Workaround for missing sitePage.context: 3 | * Used for generating sitemap with `gatsby-plugin-react-i18next` and `gatsby-plugin-sitemap` plugins 4 | * https://www.gatsbyjs.com/docs/reference/release-notes/migrating-from-v3-to-v4/#field-sitepagecontext-is-no-longer-available-in-graphql-queries 5 | */ 6 | exports.createSchemaCustomization = ({actions}) => { 7 | const {createTypes} = actions; 8 | createTypes(` 9 | type SitePage implements Node { 10 | context: SitePageContext 11 | } 12 | type SitePageContext { 13 | i18n: i18nContext 14 | } 15 | type i18nContext { 16 | language: String, 17 | languages: [String], 18 | defaultLanguage: String, 19 | originalPath: String 20 | routed: Boolean 21 | } 22 | `); 23 | }; 24 | -------------------------------------------------------------------------------- /example/languages.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | languages: ['en', 'es', 'de', 'it', 'fr'], 3 | defaultLanguage: 'en' 4 | }; 5 | -------------------------------------------------------------------------------- /example/locales/de/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "404: Not found": "404: Nicht gefunden", 3 | "NOT FOUND": "NICHT GEFUNDEN", 4 | "You just hit a route that doesn't exist... the sadness.": "Du hast gerade eine Route angefahren, die es nicht gibt... die Traurigkeit." 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/de/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to {{siteTitle}}": "Willkommen bei {{siteTitle}}" 3 | } 4 | -------------------------------------------------------------------------------- /example/locales/de/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hi people": "Hallo Leute", 3 | "Home": "Nach Hause", 4 | "Welcome to your new Gatsby site.": "Willkommen auf deiner neuen Gatsby Seite.", 5 | "Go to ignored page": "Gehe zur ignorierten Seite", 6 | "Go to page 2": "Gehe zu Seite 2", 7 | "Now go build something great.": "Jetzt baue etwas Großartiges." 8 | } 9 | -------------------------------------------------------------------------------- /example/locales/de/page-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Page two": "Seite zwei", 3 | "Go back to the homepage": "Geh zurück zur Homepage", 4 | "Welcome to page 2": "Willkommen auf Seite 2" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/en/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "404: Not found": "404: Not found", 3 | "NOT FOUND": "NOT FOUND", 4 | "You just hit a route that doesn't exist... the sadness.": "You just hit a route that doesn't exist... the sadness." 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to {{siteTitle}}": "Welcome to {{siteTitle}}" 3 | } 4 | -------------------------------------------------------------------------------- /example/locales/en/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "Go to ignored page": "Go to ignored page", 3 | "Go to page 2": "Go to page 2", 4 | "Hi people": "Hi people", 5 | "Home": "Home", 6 | "Now go build something great.": "Now go build something great.", 7 | "Welcome to your new Gatsby site.": "Welcome to your new Gatsby site." 8 | } 9 | -------------------------------------------------------------------------------- /example/locales/en/page-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Go back to the homepage": "Go back to the homepage", 3 | "Page two": "Page two", 4 | "Welcome to page 2": "Welcome to page 2" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/es/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "404: Not found": "404: No encontrado", 3 | "You just hit a route that doesn't exist... the sadness.": "Acabas de llegar a una ruta que no existe... la tristeza.", 4 | "NOT FOUND": "NO ENCONTRADO" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/es/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to {{siteTitle}}": "Bienvenido a {{siteTitle}}" 3 | } 4 | -------------------------------------------------------------------------------- /example/locales/es/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to your new Gatsby site.": "Bienvenido a su nuevo sitio de Gatsby.", 3 | "Home": "Casa", 4 | "Go to page 2": "Ir a la página 2", 5 | "Hi people": "Hola gente", 6 | "Go to ignored page": "Ir a la página ignorada", 7 | "Now go build something great.": "Ahora ve a construir algo genial." 8 | } 9 | -------------------------------------------------------------------------------- /example/locales/es/page-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Page two": "Página dos", 3 | "Welcome to page 2": "Bienvenido a la página 2", 4 | "Go back to the homepage": "Volver a la página principal" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/fr/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "404: Not found": "404 : Non trouvé", 3 | "You just hit a route that doesn't exist... the sadness.": "Vous venez de prendre une route qui n'existe pas... la tristesse.", 4 | "NOT FOUND": "INTROUVABLE" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to {{siteTitle}}": "Bienvenue sur {{siteTitle}}" 3 | } 4 | -------------------------------------------------------------------------------- /example/locales/fr/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "Home": "Accueil", 3 | "Hi people": "Salut les gens", 4 | "Go to ignored page": "Aller à la page ignorée", 5 | "Go to page 2": "Aller à la page 2", 6 | "Now go build something great.": "Maintenant va construire quelque chose de génial.", 7 | "Welcome to your new Gatsby site.": "Bienvenue sur votre nouveau site Gatsby." 8 | } 9 | -------------------------------------------------------------------------------- /example/locales/fr/page-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Go back to the homepage": "Retournez à la page d'accueil", 3 | "Page two": "Page deux", 4 | "Welcome to page 2": "Bienvenue à la page 2" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/it/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOT FOUND": "NON TROVATO", 3 | "You just hit a route that doesn't exist... the sadness.": "Hai appena toccato una strada che non esiste... la tristezza.", 4 | "404: Not found": "404: Non trovato" 5 | } 6 | -------------------------------------------------------------------------------- /example/locales/it/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to {{siteTitle}}": "Benvenuto su {{siteTitle}}" 3 | } 4 | -------------------------------------------------------------------------------- /example/locales/it/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "Go to ignored page": "Vai alla pagina ignorata", 3 | "Go to page 2": "Vai alla pagina 2", 4 | "Welcome to your new Gatsby site.": "Benvenuto nel tuo nuovo sito Gatsby.", 5 | "Hi people": "Ciao gente", 6 | "Now go build something great.": "Ora vai a costruire qualcosa di grande.", 7 | "Home": "Casa" 8 | } 9 | -------------------------------------------------------------------------------- /example/locales/it/page-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to page 2": "Benvenuto a pagina 2", 3 | "Page two": "Pagina due", 4 | "Go back to the homepage": "Torna alla homepage" 5 | } 6 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Dmitriy Nevzorov ", 7 | "dependencies": { 8 | "gatsby": "^5.2.0", 9 | "gatsby-plugin-image": "^3.2.0", 10 | "gatsby-plugin-manifest": "^5.2.0", 11 | "gatsby-plugin-offline": "^6.2.0", 12 | "gatsby-plugin-react-i18next": "^3.0.1", 13 | "gatsby-plugin-sharp": "^5.2.0", 14 | "gatsby-plugin-sitemap": "^6.2.0", 15 | "gatsby-source-filesystem": "^5.2.0", 16 | "gatsby-transformer-sharp": "^5.2.0", 17 | "i18next": "^22.0.6", 18 | "prop-types": "^15.8.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-i18next": "^12.0.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.19.3", 25 | "@babel/core": "^7.20.5", 26 | "aws-sdk": "^2.1266.0", 27 | "babel-plugin-i18next-extract": "^0.9.0", 28 | "babel-preset-gatsby": "^3.2.0", 29 | "mkdirp": "^1.0.4", 30 | "prettier": "^2.8.0" 31 | }, 32 | "keywords": [ 33 | "gatsby" 34 | ], 35 | "license": "MIT", 36 | "scripts": { 37 | "build": "gatsby build", 38 | "develop": "gatsby develop", 39 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 40 | "start": "npm run develop", 41 | "serve": "gatsby serve", 42 | "clean": "gatsby clean", 43 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1", 44 | "extract": "yarn run babel --config-file ./babel-extract.config.js -o tmp/chunk.js 'src/**/*.{js,jsx,ts,tsx}' && rm -rf tmp", 45 | "translate": "yarn run extract && node translate.js" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/gatsbyjs/gatsby/issues" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/src/components/header.css: -------------------------------------------------------------------------------- 1 | .main-header { 2 | background: rebeccapurple; 3 | margin-bottom: 1.45rem; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding: 1.45rem 1.0875rem; 8 | } 9 | 10 | .main-header a { 11 | color: white; 12 | text-decoration: none; 13 | } 14 | 15 | .languages { 16 | list-style: none; 17 | display: flex; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | .languages > li { 23 | padding: 0 0.45rem; 24 | margin: 0; 25 | } 26 | -------------------------------------------------------------------------------- /example/src/components/header.js: -------------------------------------------------------------------------------- 1 | import {Link, useI18next} from 'gatsby-plugin-react-i18next'; 2 | import PropTypes from 'prop-types'; 3 | import './header.css'; 4 | import React from 'react'; 5 | 6 | const Header = ({siteTitle}) => { 7 | const {languages, originalPath, t} = useI18next(); 8 | return ( 9 | 10 | 11 | 17 | {t('Welcome to {{siteTitle}}', {siteTitle})} 18 | 19 | 20 | 21 | {languages.map((lng) => ( 22 | 23 | 24 | {lng} 25 | 26 | 27 | ))} 28 | 29 | 30 | ); 31 | }; 32 | 33 | Header.propTypes = { 34 | siteTitle: PropTypes.string 35 | }; 36 | 37 | Header.defaultProps = { 38 | siteTitle: `` 39 | }; 40 | 41 | export default Header; 42 | -------------------------------------------------------------------------------- /example/src/components/layout.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | -ms-text-size-adjust: 100%; 4 | -webkit-text-size-adjust: 100%; 5 | } 6 | body { 7 | margin: 0; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | main, 19 | menu, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | audio, 26 | canvas, 27 | progress, 28 | video { 29 | display: inline-block; 30 | } 31 | audio:not([controls]) { 32 | display: none; 33 | height: 0; 34 | } 35 | progress { 36 | vertical-align: baseline; 37 | } 38 | [hidden], 39 | template { 40 | display: none; 41 | } 42 | a { 43 | background-color: transparent; 44 | -webkit-text-decoration-skip: objects; 45 | } 46 | a:active, 47 | a:hover { 48 | outline-width: 0; 49 | } 50 | abbr[title] { 51 | border-bottom: none; 52 | text-decoration: underline; 53 | text-decoration: underline dotted; 54 | } 55 | b, 56 | strong { 57 | font-weight: inherit; 58 | font-weight: bolder; 59 | } 60 | dfn { 61 | font-style: italic; 62 | } 63 | h1 { 64 | font-size: 2em; 65 | margin: 0.67em 0; 66 | } 67 | mark { 68 | background-color: #ff0; 69 | color: #000; 70 | } 71 | small { 72 | font-size: 80%; 73 | } 74 | sub, 75 | sup { 76 | font-size: 75%; 77 | line-height: 0; 78 | position: relative; 79 | vertical-align: baseline; 80 | } 81 | sub { 82 | bottom: -0.25em; 83 | } 84 | sup { 85 | top: -0.5em; 86 | } 87 | img { 88 | border-style: none; 89 | } 90 | svg:not(:root) { 91 | overflow: hidden; 92 | } 93 | code, 94 | kbd, 95 | pre, 96 | samp { 97 | font-family: monospace, monospace; 98 | font-size: 1em; 99 | } 100 | figure { 101 | margin: 1em 40px; 102 | } 103 | hr { 104 | box-sizing: content-box; 105 | height: 0; 106 | overflow: visible; 107 | } 108 | button, 109 | input, 110 | optgroup, 111 | select, 112 | textarea { 113 | font: inherit; 114 | margin: 0; 115 | } 116 | optgroup { 117 | font-weight: 700; 118 | } 119 | button, 120 | input { 121 | overflow: visible; 122 | } 123 | button, 124 | select { 125 | text-transform: none; 126 | } 127 | [type="reset"], 128 | [type="submit"], 129 | button, 130 | html [type="button"] { 131 | -webkit-appearance: button; 132 | } 133 | [type="button"]::-moz-focus-inner, 134 | [type="reset"]::-moz-focus-inner, 135 | [type="submit"]::-moz-focus-inner, 136 | button::-moz-focus-inner { 137 | border-style: none; 138 | padding: 0; 139 | } 140 | [type="button"]:-moz-focusring, 141 | [type="reset"]:-moz-focusring, 142 | [type="submit"]:-moz-focusring, 143 | button:-moz-focusring { 144 | outline: 1px dotted ButtonText; 145 | } 146 | fieldset { 147 | border: 1px solid silver; 148 | margin: 0 2px; 149 | padding: 0.35em 0.625em 0.75em; 150 | } 151 | legend { 152 | box-sizing: border-box; 153 | color: inherit; 154 | display: table; 155 | max-width: 100%; 156 | padding: 0; 157 | white-space: normal; 158 | } 159 | textarea { 160 | overflow: auto; 161 | } 162 | [type="checkbox"], 163 | [type="radio"] { 164 | box-sizing: border-box; 165 | padding: 0; 166 | } 167 | [type="number"]::-webkit-inner-spin-button, 168 | [type="number"]::-webkit-outer-spin-button { 169 | height: auto; 170 | } 171 | [type="search"] { 172 | -webkit-appearance: textfield; 173 | outline-offset: -2px; 174 | } 175 | [type="search"]::-webkit-search-cancel-button, 176 | [type="search"]::-webkit-search-decoration { 177 | -webkit-appearance: none; 178 | } 179 | ::-webkit-input-placeholder { 180 | color: inherit; 181 | opacity: 0.54; 182 | } 183 | ::-webkit-file-upload-button { 184 | -webkit-appearance: button; 185 | font: inherit; 186 | } 187 | html { 188 | font: 112.5%/1.45em georgia, serif; 189 | box-sizing: border-box; 190 | overflow-y: scroll; 191 | } 192 | * { 193 | box-sizing: inherit; 194 | } 195 | *:before { 196 | box-sizing: inherit; 197 | } 198 | *:after { 199 | box-sizing: inherit; 200 | } 201 | body { 202 | color: hsla(0, 0%, 0%, 0.8); 203 | font-family: georgia, serif; 204 | font-weight: normal; 205 | word-wrap: break-word; 206 | font-kerning: normal; 207 | -moz-font-feature-settings: "kern", "liga", "clig", "calt"; 208 | -ms-font-feature-settings: "kern", "liga", "clig", "calt"; 209 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt"; 210 | font-feature-settings: "kern", "liga", "clig", "calt"; 211 | } 212 | img { 213 | max-width: 100%; 214 | margin-left: 0; 215 | margin-right: 0; 216 | margin-top: 0; 217 | padding-bottom: 0; 218 | padding-left: 0; 219 | padding-right: 0; 220 | padding-top: 0; 221 | margin-bottom: 1.45rem; 222 | } 223 | h1 { 224 | margin-left: 0; 225 | margin-right: 0; 226 | margin-top: 0; 227 | padding-bottom: 0; 228 | padding-left: 0; 229 | padding-right: 0; 230 | padding-top: 0; 231 | margin-bottom: 1.45rem; 232 | color: inherit; 233 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 234 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 235 | font-weight: bold; 236 | text-rendering: optimizeLegibility; 237 | font-size: 2.25rem; 238 | line-height: 1.1; 239 | } 240 | h2 { 241 | margin-left: 0; 242 | margin-right: 0; 243 | margin-top: 0; 244 | padding-bottom: 0; 245 | padding-left: 0; 246 | padding-right: 0; 247 | padding-top: 0; 248 | margin-bottom: 1.45rem; 249 | color: inherit; 250 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 251 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 252 | font-weight: bold; 253 | text-rendering: optimizeLegibility; 254 | font-size: 1.62671rem; 255 | line-height: 1.1; 256 | } 257 | h3 { 258 | margin-left: 0; 259 | margin-right: 0; 260 | margin-top: 0; 261 | padding-bottom: 0; 262 | padding-left: 0; 263 | padding-right: 0; 264 | padding-top: 0; 265 | margin-bottom: 1.45rem; 266 | color: inherit; 267 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 268 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 269 | font-weight: bold; 270 | text-rendering: optimizeLegibility; 271 | font-size: 1.38316rem; 272 | line-height: 1.1; 273 | } 274 | h4 { 275 | margin-left: 0; 276 | margin-right: 0; 277 | margin-top: 0; 278 | padding-bottom: 0; 279 | padding-left: 0; 280 | padding-right: 0; 281 | padding-top: 0; 282 | margin-bottom: 1.45rem; 283 | color: inherit; 284 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 285 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 286 | font-weight: bold; 287 | text-rendering: optimizeLegibility; 288 | font-size: 1rem; 289 | line-height: 1.1; 290 | } 291 | h5 { 292 | margin-left: 0; 293 | margin-right: 0; 294 | margin-top: 0; 295 | padding-bottom: 0; 296 | padding-left: 0; 297 | padding-right: 0; 298 | padding-top: 0; 299 | margin-bottom: 1.45rem; 300 | color: inherit; 301 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 302 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 303 | font-weight: bold; 304 | text-rendering: optimizeLegibility; 305 | font-size: 0.85028rem; 306 | line-height: 1.1; 307 | } 308 | h6 { 309 | margin-left: 0; 310 | margin-right: 0; 311 | margin-top: 0; 312 | padding-bottom: 0; 313 | padding-left: 0; 314 | padding-right: 0; 315 | padding-top: 0; 316 | margin-bottom: 1.45rem; 317 | color: inherit; 318 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 319 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 320 | font-weight: bold; 321 | text-rendering: optimizeLegibility; 322 | font-size: 0.78405rem; 323 | line-height: 1.1; 324 | } 325 | hgroup { 326 | margin-left: 0; 327 | margin-right: 0; 328 | margin-top: 0; 329 | padding-bottom: 0; 330 | padding-left: 0; 331 | padding-right: 0; 332 | padding-top: 0; 333 | margin-bottom: 1.45rem; 334 | } 335 | ul { 336 | margin-left: 1.45rem; 337 | margin-right: 0; 338 | margin-top: 0; 339 | padding-bottom: 0; 340 | padding-left: 0; 341 | padding-right: 0; 342 | padding-top: 0; 343 | margin-bottom: 1.45rem; 344 | list-style-position: outside; 345 | list-style-image: none; 346 | } 347 | ol { 348 | margin-left: 1.45rem; 349 | margin-right: 0; 350 | margin-top: 0; 351 | padding-bottom: 0; 352 | padding-left: 0; 353 | padding-right: 0; 354 | padding-top: 0; 355 | margin-bottom: 1.45rem; 356 | list-style-position: outside; 357 | list-style-image: none; 358 | } 359 | dl { 360 | margin-left: 0; 361 | margin-right: 0; 362 | margin-top: 0; 363 | padding-bottom: 0; 364 | padding-left: 0; 365 | padding-right: 0; 366 | padding-top: 0; 367 | margin-bottom: 1.45rem; 368 | } 369 | dd { 370 | margin-left: 0; 371 | margin-right: 0; 372 | margin-top: 0; 373 | padding-bottom: 0; 374 | padding-left: 0; 375 | padding-right: 0; 376 | padding-top: 0; 377 | margin-bottom: 1.45rem; 378 | } 379 | p { 380 | margin-left: 0; 381 | margin-right: 0; 382 | margin-top: 0; 383 | padding-bottom: 0; 384 | padding-left: 0; 385 | padding-right: 0; 386 | padding-top: 0; 387 | margin-bottom: 1.45rem; 388 | } 389 | figure { 390 | margin-left: 0; 391 | margin-right: 0; 392 | margin-top: 0; 393 | padding-bottom: 0; 394 | padding-left: 0; 395 | padding-right: 0; 396 | padding-top: 0; 397 | margin-bottom: 1.45rem; 398 | } 399 | pre { 400 | margin-left: 0; 401 | margin-right: 0; 402 | margin-top: 0; 403 | margin-bottom: 1.45rem; 404 | font-size: 0.85rem; 405 | line-height: 1.42; 406 | background: hsla(0, 0%, 0%, 0.04); 407 | border-radius: 3px; 408 | overflow: auto; 409 | word-wrap: normal; 410 | padding: 1.45rem; 411 | } 412 | table { 413 | margin-left: 0; 414 | margin-right: 0; 415 | margin-top: 0; 416 | padding-bottom: 0; 417 | padding-left: 0; 418 | padding-right: 0; 419 | padding-top: 0; 420 | margin-bottom: 1.45rem; 421 | font-size: 1rem; 422 | line-height: 1.45rem; 423 | border-collapse: collapse; 424 | width: 100%; 425 | } 426 | fieldset { 427 | margin-left: 0; 428 | margin-right: 0; 429 | margin-top: 0; 430 | padding-bottom: 0; 431 | padding-left: 0; 432 | padding-right: 0; 433 | padding-top: 0; 434 | margin-bottom: 1.45rem; 435 | } 436 | blockquote { 437 | margin-left: 1.45rem; 438 | margin-right: 1.45rem; 439 | margin-top: 0; 440 | padding-bottom: 0; 441 | padding-left: 0; 442 | padding-right: 0; 443 | padding-top: 0; 444 | margin-bottom: 1.45rem; 445 | } 446 | form { 447 | margin-left: 0; 448 | margin-right: 0; 449 | margin-top: 0; 450 | padding-bottom: 0; 451 | padding-left: 0; 452 | padding-right: 0; 453 | padding-top: 0; 454 | margin-bottom: 1.45rem; 455 | } 456 | noscript { 457 | margin-left: 0; 458 | margin-right: 0; 459 | margin-top: 0; 460 | padding-bottom: 0; 461 | padding-left: 0; 462 | padding-right: 0; 463 | padding-top: 0; 464 | margin-bottom: 1.45rem; 465 | } 466 | iframe { 467 | margin-left: 0; 468 | margin-right: 0; 469 | margin-top: 0; 470 | padding-bottom: 0; 471 | padding-left: 0; 472 | padding-right: 0; 473 | padding-top: 0; 474 | margin-bottom: 1.45rem; 475 | } 476 | hr { 477 | margin-left: 0; 478 | margin-right: 0; 479 | margin-top: 0; 480 | padding-bottom: 0; 481 | padding-left: 0; 482 | padding-right: 0; 483 | padding-top: 0; 484 | margin-bottom: calc(1.45rem - 1px); 485 | background: hsla(0, 0%, 0%, 0.2); 486 | border: none; 487 | height: 1px; 488 | } 489 | address { 490 | margin-left: 0; 491 | margin-right: 0; 492 | margin-top: 0; 493 | padding-bottom: 0; 494 | padding-left: 0; 495 | padding-right: 0; 496 | padding-top: 0; 497 | margin-bottom: 1.45rem; 498 | } 499 | b { 500 | font-weight: bold; 501 | } 502 | strong { 503 | font-weight: bold; 504 | } 505 | dt { 506 | font-weight: bold; 507 | } 508 | th { 509 | font-weight: bold; 510 | } 511 | li { 512 | margin-bottom: calc(1.45rem / 2); 513 | } 514 | ol li { 515 | padding-left: 0; 516 | } 517 | ul li { 518 | padding-left: 0; 519 | } 520 | li > ol { 521 | margin-left: 1.45rem; 522 | margin-bottom: calc(1.45rem / 2); 523 | margin-top: calc(1.45rem / 2); 524 | } 525 | li > ul { 526 | margin-left: 1.45rem; 527 | margin-bottom: calc(1.45rem / 2); 528 | margin-top: calc(1.45rem / 2); 529 | } 530 | blockquote *:last-child { 531 | margin-bottom: 0; 532 | } 533 | li *:last-child { 534 | margin-bottom: 0; 535 | } 536 | p *:last-child { 537 | margin-bottom: 0; 538 | } 539 | li > p { 540 | margin-bottom: calc(1.45rem / 2); 541 | } 542 | code { 543 | font-size: 0.85rem; 544 | line-height: 1.45rem; 545 | } 546 | kbd { 547 | font-size: 0.85rem; 548 | line-height: 1.45rem; 549 | } 550 | samp { 551 | font-size: 0.85rem; 552 | line-height: 1.45rem; 553 | } 554 | abbr { 555 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 556 | cursor: help; 557 | } 558 | acronym { 559 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 560 | cursor: help; 561 | } 562 | abbr[title] { 563 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 564 | cursor: help; 565 | text-decoration: none; 566 | } 567 | thead { 568 | text-align: left; 569 | } 570 | td, 571 | th { 572 | text-align: left; 573 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12); 574 | font-feature-settings: "tnum"; 575 | -moz-font-feature-settings: "tnum"; 576 | -ms-font-feature-settings: "tnum"; 577 | -webkit-font-feature-settings: "tnum"; 578 | padding-left: 0.96667rem; 579 | padding-right: 0.96667rem; 580 | padding-top: 0.725rem; 581 | padding-bottom: calc(0.725rem - 1px); 582 | } 583 | th:first-child, 584 | td:first-child { 585 | padding-left: 0; 586 | } 587 | th:last-child, 588 | td:last-child { 589 | padding-right: 0; 590 | } 591 | tt, 592 | code { 593 | background-color: hsla(0, 0%, 0%, 0.04); 594 | border-radius: 3px; 595 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono", 596 | "Liberation Mono", Menlo, Courier, monospace; 597 | padding: 0; 598 | padding-top: 0.2em; 599 | padding-bottom: 0.2em; 600 | } 601 | pre code { 602 | background: none; 603 | line-height: 1.42; 604 | } 605 | code:before, 606 | code:after, 607 | tt:before, 608 | tt:after { 609 | letter-spacing: -0.2em; 610 | content: " "; 611 | } 612 | pre code:before, 613 | pre code:after, 614 | pre tt:before, 615 | pre tt:after { 616 | content: ""; 617 | } 618 | @media only screen and (max-width: 480px) { 619 | html { 620 | font-size: 100%; 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /example/src/components/layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout component that queries for data 3 | * with Gatsby's useStaticQuery component 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import {useStaticQuery, graphql} from 'gatsby'; 11 | 12 | import Header from './header'; 13 | import './layout.css'; 14 | 15 | const Layout = ({children}) => { 16 | const data = useStaticQuery(graphql` 17 | query SiteTitleQuery { 18 | site { 19 | siteMetadata { 20 | title 21 | } 22 | } 23 | } 24 | `); 25 | 26 | return ( 27 | <> 28 | 29 | 35 | {children} 36 | 41 | 42 | > 43 | ); 44 | }; 45 | 46 | Layout.propTypes = { 47 | children: PropTypes.node.isRequired 48 | }; 49 | 50 | export default Layout; 51 | -------------------------------------------------------------------------------- /example/src/components/seo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 6 | */ 7 | 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import {useStaticQuery, graphql} from 'gatsby'; 11 | 12 | function Seo({description, title}) { 13 | const {site} = useStaticQuery( 14 | graphql` 15 | query { 16 | site { 17 | siteMetadata { 18 | title 19 | description 20 | author 21 | } 22 | } 23 | } 24 | ` 25 | ); 26 | 27 | const metaDescription = description || site.siteMetadata.description; 28 | 29 | return ( 30 | <> 31 | {title} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | > 41 | ); 42 | } 43 | 44 | Seo.defaultProps = { 45 | description: `` 46 | }; 47 | 48 | Seo.propTypes = { 49 | description: PropTypes.string, 50 | lang: PropTypes.string, 51 | title: PropTypes.string.isRequired 52 | }; 53 | 54 | export default Seo; 55 | -------------------------------------------------------------------------------- /example/src/images/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microapps/gatsby-plugin-react-i18next/0cb31fe4e48dd5b1771efaf24c85ece5540aa084/example/src/images/gatsby-astronaut.png -------------------------------------------------------------------------------- /example/src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microapps/gatsby-plugin-react-i18next/0cb31fe4e48dd5b1771efaf24c85ece5540aa084/example/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /example/src/pages/404.js: -------------------------------------------------------------------------------- 1 | // i18next-extract-mark-ns-start 404 2 | 3 | import {graphql} from 'gatsby'; 4 | import React from 'react'; 5 | import {useTranslation, Trans} from 'gatsby-plugin-react-i18next'; 6 | import Layout from '../components/layout'; 7 | import Seo from '../components/seo'; 8 | 9 | const NotFoundPage = () => { 10 | return ( 11 | 12 | 13 | NOT FOUND 14 | 15 | 16 | You just hit a route that doesn't exist... the sadness. 17 | 18 | 19 | ); 20 | }; 21 | 22 | export const Head = () => { 23 | const {t} = useTranslation(); 24 | return ; 25 | }; 26 | 27 | export default NotFoundPage; 28 | 29 | export const query = graphql` 30 | query ($language: String!) { 31 | locales: allLocale(filter: {ns: {in: ["common", "404"]}, language: {eq: $language}}) { 32 | edges { 33 | node { 34 | ns 35 | data 36 | language 37 | } 38 | } 39 | } 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /example/src/pages/ignored-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../components/layout'; 3 | import Seo from '../components/seo'; 4 | import {Link} from 'gatsby-plugin-react-i18next'; 5 | 6 | const IgnoredPage = (props) => { 7 | return ( 8 | 9 | Ignored page 10 | This page does not have language prefix 11 | Go back to the homepage 12 | 13 | ); 14 | }; 15 | 16 | export const Head = () => ; 17 | 18 | export default IgnoredPage; 19 | -------------------------------------------------------------------------------- /example/src/pages/index.js: -------------------------------------------------------------------------------- 1 | // i18next-extract-mark-ns-start index 2 | 3 | import React from 'react'; 4 | import {Link, Trans, useTranslation} from 'gatsby-plugin-react-i18next'; 5 | import {graphql, Link as GatsbyLink} from 'gatsby'; 6 | import {StaticImage} from 'gatsby-plugin-image'; 7 | import Layout from '../components/layout'; 8 | import Seo from '../components/seo'; 9 | 10 | const IndexPage = () => { 11 | return ( 12 | 13 | 14 | Hi people 15 | 16 | 17 | Welcome to your new Gatsby site. 18 | 19 | 20 | Now go build something great. 21 | 22 | 23 | 30 | 31 | 32 | 33 | Go to page 2 34 | 35 | 36 | 37 | 38 | Go to ignored page 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export const Head = () => { 46 | const {t} = useTranslation(); 47 | return ; 48 | }; 49 | 50 | export default IndexPage; 51 | 52 | export const query = graphql` 53 | query ($language: String!) { 54 | locales: allLocale(filter: {ns: {in: ["common", "index"]}, language: {eq: $language}}) { 55 | edges { 56 | node { 57 | ns 58 | data 59 | language 60 | } 61 | } 62 | } 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /example/src/pages/page-2.js: -------------------------------------------------------------------------------- 1 | // i18next-extract-mark-ns-start page-2 2 | 3 | import {graphql} from 'gatsby'; 4 | import React from 'react'; 5 | import Layout from '../components/layout'; 6 | import Seo from '../components/seo'; 7 | import {Link, useTranslation, Trans} from 'gatsby-plugin-react-i18next'; 8 | 9 | const SecondPage = (props) => { 10 | return ( 11 | 12 | 13 | Page two 14 | 15 | 16 | Welcome to page 2 ({props.path}) 17 | 18 | 19 | Go back to the homepage 20 | 21 | 22 | ); 23 | }; 24 | 25 | export const Head = () => { 26 | const {t} = useTranslation(); 27 | return ; 28 | }; 29 | 30 | export default SecondPage; 31 | 32 | export const query = graphql` 33 | query ($language: String!) { 34 | locales: allLocale(filter: {ns: {in: ["common", "page-2"]}, language: {eq: $language}}) { 35 | edges { 36 | node { 37 | ns 38 | data 39 | language 40 | } 41 | } 42 | } 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /example/translate.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const BP = require('bluebird'); 3 | const fs = require('fs'); 4 | const util = require('util'); 5 | const mkdirp = require('mkdirp'); 6 | const getDirName = require('path').dirname; 7 | const {languages, defaultLanguage} = require('./languages'); 8 | 9 | const readFile = util.promisify(fs.readFile); 10 | const readdir = util.promisify(fs.readdir); 11 | const writeFile = util.promisify(fs.writeFile); 12 | 13 | const write = async (path, content) => { 14 | await mkdirp(getDirName(path)); 15 | return writeFile(path, JSON.stringify(content, null, 2), 'utf8'); 16 | }; 17 | 18 | AWS.config.region = process.env.REGION || 'us-east-1'; 19 | const translate = new AWS.Translate(); 20 | 21 | async function main() { 22 | const files = await readdir(`locales/${defaultLanguage}`); 23 | await BP.map(files, async (filename) => { 24 | const data = await readFile(`locales/${defaultLanguage}/${filename}`, 'utf8'); 25 | const translations = JSON.parse(data); 26 | 27 | const resultMap = {}; 28 | await BP.map( 29 | languages, 30 | async (lng) => { 31 | if (lng === defaultLanguage) return; 32 | try { 33 | const localeData = await readFile(`locales/${lng}/${filename}`, 'utf8'); 34 | resultMap[lng] = JSON.parse(localeData); 35 | } catch (error) { 36 | resultMap[lng] = {}; 37 | } 38 | await BP.map( 39 | Object.keys(translations), 40 | async (key) => { 41 | if (key === '_t' || resultMap[lng][key]) return; 42 | const {TranslatedText} = await translate 43 | .translateText({ 44 | Text: translations[key], 45 | SourceLanguageCode: defaultLanguage, 46 | TargetLanguageCode: lng 47 | }) 48 | .promise(); 49 | resultMap[lng][key] = TranslatedText; 50 | }, 51 | {concurrency: 10} 52 | ); 53 | console.log(`Translated: ${lng}`); 54 | }, 55 | {concurrency: 10} 56 | ); 57 | 58 | BP.map(Object.keys(resultMap), async (lng) => { 59 | return write(`locales/${lng}/${filename}`, resultMap[lng]); 60 | }); 61 | }); 62 | 63 | console.log('All done!'); 64 | } 65 | 66 | main(); 67 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | const {wrapPageElement} = require('./dist/plugin/wrapPageElement'); 2 | exports.wrapPageElement = wrapPageElement; 3 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const {onCreatePage} = require('./dist/plugin/onCreatePage'); 2 | const {onCreateNode} = require('./dist/plugin/onCreateNode'); 3 | const {onPreBootstrap} = require('./dist/plugin/onPreBootstrap'); 4 | 5 | exports.onCreatePage = onCreatePage; 6 | exports.onCreateNode = onCreateNode; 7 | exports.onPreBootstrap = onPreBootstrap; 8 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | const {wrapPageElement} = require('./dist/plugin/wrapPageElement'); 2 | exports.wrapPageElement = wrapPageElement; 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-plugin-react-i18next", 3 | "version": "3.0.1", 4 | "description": "Easily translate your Gatsby website into multiple languages", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "author": "Dmitriy Nevzorov ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "react", 11 | "gatsby", 12 | "gatsbyjs", 13 | "gatsby-plugin", 14 | "gatsby-component", 15 | "i18next", 16 | "react-i18next", 17 | "gatsby-i18n", 18 | "i18n", 19 | "i18next", 20 | "localization", 21 | "localisation", 22 | "translation", 23 | "translate" 24 | ], 25 | "repository": { 26 | "url": "https://github.com/microapps/gatsby-plugin-react-i18next" 27 | }, 28 | "files": [ 29 | "dist", 30 | "index.js", 31 | "index.d.ts", 32 | "gatsby-browser.js", 33 | "gatsby-node.js", 34 | "gatsby-ssr.js" 35 | ], 36 | "publishConfig": { 37 | "access": "public", 38 | "registry": "https://registry.npmjs.org/" 39 | }, 40 | "scripts": { 41 | "build:ts": "babel src --out-dir dist --extensions .ts,.tsx", 42 | "build:defs": "tsc --declaration --outDir dist --emitDeclarationOnly", 43 | "dev": "babel -w src --out-dir dist --extensions .ts,.tsx", 44 | "prepare": "NODE_ENV=production yarn run build:ts && yarn run build:defs", 45 | "format": "prettier --write 'src/**/*.{js,jsx,json,ts,tsx,md}'", 46 | "release": "release-it" 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.19.3", 50 | "@types/bluebird": "^3.5.38", 51 | "@types/react": "^18.0.25", 52 | "babel-preset-gatsby-package": "^3.2.0", 53 | "gatsby": "^5.2.0", 54 | "husky": "^4.3.8", 55 | "i18next": "^22.0.6", 56 | "prettier": "^2.8.0", 57 | "pretty-quick": "^3.1.3", 58 | "react": "^18.2.0", 59 | "react-dom": "^18.2.0", 60 | "react-i18next": "^12.0.0", 61 | "release-it": "^15.5.1", 62 | "typescript": "^4.9.3" 63 | }, 64 | "dependencies": { 65 | "bluebird": "^3.7.2", 66 | "browser-lang": "^0.2.1", 67 | "outdent": "^0.8.0", 68 | "path-to-regexp": "^6.2.1" 69 | }, 70 | "peerDependencies": { 71 | "gatsby": "^5.2.0", 72 | "i18next": "^22.0.6", 73 | "react": "^18.x", 74 | "react-i18next": "^12.0.0" 75 | }, 76 | "husky": { 77 | "hooks": { 78 | "pre-commit": "pretty-quick --staged" 79 | } 80 | }, 81 | "release-it": { 82 | "git": { 83 | "tagName": "v${version}", 84 | "commitMessage": "chore: release v${version}" 85 | }, 86 | "github": { 87 | "release": true 88 | }, 89 | "npm": { 90 | "publish": false 91 | }, 92 | "hooks": { 93 | "before:init": [ 94 | "yarn run format", 95 | "yarn run prepare" 96 | ] 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {I18nextContext} from './i18nextContext'; 3 | import {Link as GatsbyLink, GatsbyLinkProps} from 'gatsby'; 4 | import {LANGUAGE_KEY} from './types'; 5 | 6 | type Props = GatsbyLinkProps & {language?: string}; 7 | 8 | export const Link = React.forwardRef( 9 | ({language, to, onClick, ...rest}, ref) => { 10 | const context = useContext(I18nextContext); 11 | const urlLanguage = language || context.language; 12 | const getLanguagePath = (language: string) => { 13 | return context.generateDefaultLanguagePage || language !== context.defaultLanguage 14 | ? `/${language}` 15 | : ''; 16 | }; 17 | const link = `${getLanguagePath(urlLanguage)}${to}`; 18 | 19 | return ( 20 | // @ts-ignore 21 | { 27 | if (language) { 28 | localStorage.setItem(LANGUAGE_KEY, language); 29 | } 30 | if (onClick) { 31 | onClick(e); 32 | } 33 | }} 34 | /> 35 | ); 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /src/i18nextContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {I18NextContext} from './types'; 3 | 4 | export const I18nextContext = React.createContext({ 5 | language: 'en', 6 | languages: ['en'], 7 | routed: false, 8 | defaultLanguage: 'en', 9 | generateDefaultLanguagePage: false, 10 | originalPath: '/', 11 | path: '/' 12 | }); 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'react-i18next'; 2 | export * from './i18nextContext'; 3 | export * from './useI18next'; 4 | export * from './Link'; 5 | -------------------------------------------------------------------------------- /src/plugin/onCreateNode.ts: -------------------------------------------------------------------------------- 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 | }: // @ts-ignore 18 | CreateNodeArgs, 19 | {localeJsonSourceName = 'locale', verbose = true}: PluginOptions 20 | ) => { 21 | if (!unstable_shouldOnCreateNode({node})) { 22 | return; 23 | } 24 | 25 | const { 26 | absolutePath, 27 | internal: {type}, 28 | sourceInstanceName, 29 | relativeDirectory, 30 | name, 31 | id 32 | } = node; 33 | 34 | // Currently only support file resources 35 | if (type !== 'File') { 36 | return; 37 | } 38 | 39 | // User is not using this feature 40 | if (localeJsonSourceName == null) { 41 | return; 42 | } 43 | 44 | if (sourceInstanceName !== localeJsonSourceName) { 45 | return; 46 | } 47 | 48 | let activity; 49 | if (verbose) { 50 | activity = reporter.activityTimer( 51 | `gatsby-plugin-react-i18next: create node: ${relativeDirectory}/${name}` 52 | ); 53 | activity.start(); 54 | } 55 | 56 | // relativeDirectory name is language name. 57 | const language = relativeDirectory; 58 | const content = await loadNodeContent(node); 59 | 60 | // verify & canonicalize indent. (do not care about key order) 61 | let data: string; 62 | try { 63 | data = JSON.stringify(JSON.parse(content), undefined, ''); 64 | } catch { 65 | const hint = node.absolutePath ? `file ${node.absolutePath}` : `in node ${node.id}`; 66 | throw new Error(`Unable to parse JSON: ${hint}`); 67 | } 68 | 69 | const {createNode, createParentChildLink} = actions; 70 | 71 | const localeNode: LocaleNodeInput = { 72 | id: createNodeId(`${id} >>> Locale`), 73 | children: [], 74 | parent: id, 75 | internal: { 76 | content: data, 77 | contentDigest: createContentDigest(data), 78 | type: `Locale` 79 | }, 80 | language: language, 81 | ns: name, 82 | data, 83 | fileAbsolutePath: absolutePath 84 | }; 85 | 86 | createNode(localeNode); 87 | 88 | // @ts-ignore 89 | // staled issue: https://github.com/gatsbyjs/gatsby/issues/19993 90 | createParentChildLink({parent: node, child: localeNode}); 91 | 92 | if (verbose && activity) { 93 | activity.end(); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/plugin/onCreatePage.ts: -------------------------------------------------------------------------------- 1 | import {CreatePageArgs, Page} from 'gatsby'; 2 | import BP from 'bluebird'; 3 | import {match} from 'path-to-regexp'; 4 | import {PageContext, PageOptions, PluginOptions} from '../types'; 5 | 6 | export const onCreatePage = async ( 7 | {page, actions}: CreatePageArgs, 8 | pluginOptions: PluginOptions 9 | ) => { 10 | //Exit if the page has already been processed. 11 | if (typeof page.context?.i18n === 'object') { 12 | return; 13 | } 14 | 15 | const {createPage, deletePage} = actions; 16 | const { 17 | defaultLanguage = 'en', 18 | generateDefaultLanguagePage = false, 19 | languages = ['en'], 20 | pages = [] 21 | } = pluginOptions; 22 | 23 | type GeneratePageParams = { 24 | language: string; 25 | path?: string; 26 | originalPath?: string; 27 | routed?: boolean; 28 | matchPath?: string; 29 | pageOptions?: PageOptions; 30 | }; 31 | const generatePage = async ({ 32 | language, 33 | path = page.path, 34 | originalPath = page.path, 35 | routed = false, 36 | matchPath = page.matchPath, 37 | pageOptions 38 | }: GeneratePageParams): Promise> => { 39 | return { 40 | ...page, 41 | matchPath, 42 | path, 43 | context: { 44 | ...page.context, 45 | language, 46 | i18n: { 47 | language, 48 | languages: pageOptions?.languages || languages, 49 | defaultLanguage, 50 | generateDefaultLanguagePage, 51 | routed, 52 | originalPath, 53 | path 54 | } 55 | } 56 | }; 57 | }; 58 | 59 | const pageOptions = pages.find((opt) => match(opt.matchPath)(page.path)); 60 | 61 | let newPage; 62 | let alternativeLanguages = generateDefaultLanguagePage 63 | ? languages 64 | : languages.filter((lng) => lng !== defaultLanguage); 65 | 66 | if (pageOptions?.excludeLanguages) { 67 | alternativeLanguages = alternativeLanguages.filter( 68 | (lng) => !pageOptions?.excludeLanguages?.includes(lng) 69 | ); 70 | } 71 | 72 | if (pageOptions?.languages) { 73 | alternativeLanguages = generateDefaultLanguagePage 74 | ? pageOptions.languages 75 | : pageOptions.languages.filter((lng) => lng !== defaultLanguage); 76 | } 77 | 78 | if (pageOptions?.getLanguageFromPath) { 79 | const result = match<{lang: string}>(pageOptions.matchPath)(page.path); 80 | if (!result) return; 81 | const language = languages.find((lng) => lng === result.params.lang) || defaultLanguage; 82 | const originalPath = page.path.replace(`/${language}`, ''); 83 | const routed = Boolean(result.params.lang); 84 | newPage = await generatePage({language, originalPath, routed, pageOptions}); 85 | if (routed || !pageOptions.excludeLanguages) { 86 | alternativeLanguages = []; 87 | } 88 | } else { 89 | newPage = await generatePage({language: defaultLanguage, pageOptions}); 90 | } 91 | 92 | try { 93 | deletePage(page); 94 | } catch {} 95 | createPage(newPage); 96 | 97 | await BP.map(alternativeLanguages, async (lng) => { 98 | const localePage = await generatePage({ 99 | language: lng, 100 | path: `${lng}${page.path}`, 101 | matchPath: page.matchPath ? `/${lng}${page.matchPath}` : undefined, 102 | routed: true 103 | }); 104 | const regexp = new RegExp('/404/?$'); 105 | if (regexp.test(localePage.path)) { 106 | localePage.matchPath = `/${lng}/*`; 107 | } 108 | if (localePage.matchPath !== undefined) { 109 | localePage.matchPath = `/${lng}${localePage.matchPath}`; 110 | } 111 | createPage(localePage); 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /src/plugin/onPreBootstrap.ts: -------------------------------------------------------------------------------- 1 | import {ParentSpanPluginArgs} from 'gatsby'; 2 | import {PluginOptions} from '../types'; 3 | 4 | export const onPreBootstrap = (_args: ParentSpanPluginArgs, pluginOptions: PluginOptions) => { 5 | // Check for deprecated option. 6 | if (pluginOptions.hasOwnProperty('path')) { 7 | console.error( 8 | `gatsby-plugin-react-i18next: "path" option is deprecated. Please remove it from config in your gastby-config.js. As of v1.0.0, language JSON resources should be loaded by gatsby-source-filesystem plugin and then fetched by GraphQL query. It enables incremental build and hot-reload as language JSON files change.\nSee details: https://github.com/microapps/gatsby-plugin-react-i18next` 9 | ); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/plugin/wrapPageElement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {withPrefix, WrapPageElementBrowserArgs} from 'gatsby'; 3 | // @ts-ignore 4 | import browserLang from 'browser-lang'; 5 | import { 6 | I18NextContext, 7 | LANGUAGE_KEY, 8 | PageContext, 9 | PluginOptions, 10 | LocaleNode, 11 | Resource, 12 | ResourceKey 13 | } from '../types'; 14 | import i18next, {i18n as I18n} from 'i18next'; 15 | import {I18nextProvider} from 'react-i18next'; 16 | import {I18nextContext} from '../i18nextContext'; 17 | import outdent from 'outdent'; 18 | 19 | const withI18next = (i18n: I18n, context: I18NextContext) => (children: any) => { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | const removePathPrefix = (pathname: string, stripTrailingSlash: boolean) => { 28 | const pathPrefix = withPrefix('/'); 29 | let result = pathname; 30 | 31 | if (pathname.startsWith(pathPrefix)) { 32 | result = pathname.replace(pathPrefix, '/'); 33 | } 34 | 35 | if (stripTrailingSlash && result.endsWith('/')) { 36 | return result.slice(0, -1); 37 | } 38 | 39 | return result; 40 | }; 41 | 42 | export const wrapPageElement = ( 43 | {element, props}: WrapPageElementBrowserArgs, 44 | { 45 | i18nextOptions = {}, 46 | redirect = true, 47 | generateDefaultLanguagePage = false, 48 | siteUrl, 49 | localeJsonNodeName = 'locales', 50 | fallbackLanguage, 51 | trailingSlash 52 | }: PluginOptions 53 | ) => { 54 | if (!props) return; 55 | const {data, pageContext, location} = props; 56 | const {routed, language, languages, originalPath, defaultLanguage, path} = pageContext.i18n; 57 | const isRedirect = redirect && !routed; 58 | 59 | if (isRedirect) { 60 | const {search} = location; 61 | 62 | // Skip build, Browsers only 63 | if (typeof window !== 'undefined') { 64 | let detected = 65 | window.localStorage.getItem(LANGUAGE_KEY) || 66 | browserLang({ 67 | languages, 68 | fallback: fallbackLanguage || language 69 | }); 70 | 71 | if (!languages.includes(detected)) { 72 | detected = language; 73 | } 74 | 75 | window.localStorage.setItem(LANGUAGE_KEY, detected); 76 | 77 | if (detected !== defaultLanguage) { 78 | const queryParams = search || ''; 79 | const stripTrailingSlash = trailingSlash === 'never'; 80 | const newUrl = withPrefix( 81 | `/${detected}${removePathPrefix(location.pathname, stripTrailingSlash)}${queryParams}${ 82 | location.hash 83 | }` 84 | ); 85 | // @ts-ignore 86 | window.___replace(newUrl); 87 | return null; 88 | } 89 | } 90 | } 91 | 92 | const localeNodes: Array<{node: LocaleNode}> = data?.[localeJsonNodeName]?.edges || []; 93 | 94 | if (languages.length > 1 && localeNodes.length === 0 && process.env.NODE_ENV === 'development') { 95 | console.error( 96 | outdent` 97 | No translations were found in "${localeJsonNodeName}" key for "${originalPath}". 98 | You need to add a graphql query to every page like this: 99 | 100 | export const query = graphql\` 101 | query($language: String!) { 102 | ${localeJsonNodeName}: allLocale(language: {eq: $language}) { 103 | edges { 104 | node { 105 | ns 106 | data 107 | language 108 | } 109 | } 110 | } 111 | } 112 | \`; 113 | ` 114 | ); 115 | } 116 | 117 | const namespaces = localeNodes.map(({node}) => node.ns); 118 | 119 | // We want to set default namespace to a page namespace if it exists 120 | // and use other namespaces as fallback 121 | // this way you dont need to specify namespaces in pages 122 | let defaultNS = i18nextOptions.defaultNS?.toString() || 'translation'; 123 | defaultNS = namespaces.find((ns) => ns !== defaultNS) || defaultNS; 124 | const fallbackNS = namespaces.filter((ns) => ns !== defaultNS); 125 | 126 | const resources: Resource = localeNodes.reduce((res: Resource, {node}) => { 127 | const parsedData: ResourceKey = 128 | typeof node.data === 'object' ? node.data : JSON.parse(node.data); 129 | 130 | if (!(node.language in res)) res[node.language] = {}; 131 | 132 | res[node.language][node.ns || defaultNS] = parsedData; 133 | 134 | return res; 135 | }, {}); 136 | 137 | const i18n = i18next.createInstance(); 138 | 139 | i18n.init({ 140 | ...i18nextOptions, 141 | resources, 142 | lng: language, 143 | fallbackLng: defaultLanguage, 144 | defaultNS, 145 | fallbackNS, 146 | react: { 147 | ...i18nextOptions.react, 148 | useSuspense: false 149 | } 150 | }); 151 | 152 | if (i18n.language !== language) { 153 | i18n.changeLanguage(language); 154 | } 155 | 156 | const context = { 157 | routed, 158 | language, 159 | languages, 160 | originalPath, 161 | defaultLanguage, 162 | generateDefaultLanguagePage, 163 | siteUrl, 164 | path 165 | }; 166 | 167 | return withI18next(i18n, context)(element); 168 | }; 169 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {InitOptions} from 'i18next'; 2 | import {NodeInput} from 'gatsby'; 3 | 4 | export const LANGUAGE_KEY = 'gatsby-i18next-language'; 5 | 6 | export type {Resource, ResourceLanguage, ResourceKey} from 'i18next'; 7 | 8 | export type PageOptions = { 9 | matchPath: string; 10 | getLanguageFromPath?: boolean; 11 | excludeLanguages?: string[]; 12 | languages?: string[]; 13 | }; 14 | 15 | export type PluginOptions = { 16 | languages: string[]; 17 | defaultLanguage: string; 18 | generateDefaultLanguagePage: boolean; 19 | redirect: boolean; 20 | siteUrl?: string; 21 | i18nextOptions: InitOptions; 22 | pages: Array; 23 | localeJsonSourceName?: string; 24 | localeJsonNodeName?: string; 25 | fallbackLanguage?: string; 26 | trailingSlash?: 'always' | 'never' | 'ignore'; 27 | verbose?: boolean; 28 | }; 29 | 30 | export type I18NextContext = { 31 | language: string; 32 | routed: boolean; 33 | languages: string[]; 34 | defaultLanguage: string; 35 | generateDefaultLanguagePage: boolean; 36 | originalPath: string; 37 | path: string; 38 | siteUrl?: string; 39 | }; 40 | 41 | export type PageContext = { 42 | path?: string; 43 | language: string; 44 | i18n: I18NextContext; 45 | }; 46 | 47 | // Taken from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-filesystem/index.d.ts 48 | // No way to refer it without directly depending on gatsby-source-filesystem. 49 | export interface FileSystemNode extends Node { 50 | absolutePath: string; 51 | accessTime: string; 52 | birthTime: Date; 53 | changeTime: string; 54 | extension: string; 55 | modifiedTime: string; 56 | prettySize: string; 57 | relativeDirectory: string; 58 | relativePath: string; 59 | sourceInstanceName: string; 60 | 61 | // parsed path typings 62 | base: string; 63 | dir: string; 64 | ext: string; 65 | name: string; 66 | root: string; 67 | 68 | // stats 69 | atime: Date; 70 | atimeMs: number; 71 | /** 72 | * @deprecated Use `birthTime` instead 73 | */ 74 | birthtime: Date; 75 | /** 76 | * @deprecated Use `birthTime` instead 77 | */ 78 | birthtimeMs: number; 79 | ctime: Date; 80 | ctimeMs: number; 81 | gid: number; 82 | mode: number; 83 | mtime: Date; 84 | mtimeMs: number; 85 | size: number; 86 | uid: number; 87 | } 88 | 89 | export interface LocaleNodeInput extends NodeInput { 90 | language: string; 91 | ns: string; 92 | data: string; 93 | fileAbsolutePath: string; 94 | } 95 | 96 | export interface LocaleNode extends LocaleNodeInput { 97 | parent: string; 98 | children: string[]; 99 | internal: NodeInput['internal'] & { 100 | owner: string; 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /src/useI18next.ts: -------------------------------------------------------------------------------- 1 | import {useTranslation, UseTranslationOptions} from 'react-i18next'; 2 | import {Namespace} from 'i18next'; 3 | import {useContext} from 'react'; 4 | import {navigate as gatsbyNavigate} from 'gatsby'; 5 | import {I18nextContext} from './i18nextContext'; 6 | import {NavigateOptions} from '@reach/router'; 7 | import {LANGUAGE_KEY} from './types'; 8 | 9 | declare var __BASE_PATH__: string | undefined; 10 | declare var __PATH_PREFIX__: string | undefined; 11 | 12 | export const useI18next = (ns?: Namespace, options?: UseTranslationOptions) => { 13 | const {i18n, t, ready} = useTranslation(ns, options); 14 | const context = useContext(I18nextContext); 15 | 16 | const {routed, defaultLanguage, generateDefaultLanguagePage} = context; 17 | 18 | const getLanguagePath = (language: string) => { 19 | return generateDefaultLanguagePage || language !== defaultLanguage ? `/${language}` : ''; 20 | }; 21 | 22 | const removePrefix = (pathname: string) => { 23 | const base = typeof __BASE_PATH__ !== `undefined` ? __BASE_PATH__ : __PATH_PREFIX__; 24 | if (base && pathname.indexOf(base) === 0) { 25 | pathname = pathname.slice(base.length); 26 | } 27 | return pathname; 28 | }; 29 | 30 | const removeLocalePart = (pathname: string) => { 31 | if (!routed) return pathname; 32 | const i = pathname.indexOf(`/`, 1); 33 | return pathname.substring(i); 34 | }; 35 | 36 | const navigate = (to: string, options?: NavigateOptions<{}>) => { 37 | const languagePath = getLanguagePath(context.language); 38 | const link = routed ? `${languagePath}${to}` : `${to}`; 39 | return gatsbyNavigate(link, options); 40 | }; 41 | 42 | const changeLanguage = (language: string, to?: string, options?: NavigateOptions<{}>) => { 43 | const languagePath = getLanguagePath(language); 44 | const pathname = to || removeLocalePart(removePrefix(window.location.pathname)); 45 | const link = `${languagePath}${pathname}${window.location.search}`; 46 | localStorage.setItem(LANGUAGE_KEY, language); 47 | return gatsbyNavigate(link, options); 48 | }; 49 | 50 | return { 51 | ...context, 52 | i18n, 53 | t, 54 | ready, 55 | navigate, 56 | changeLanguage 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "isolatedModules": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------
158 | Welcome to your new Gatsby site. 159 |
161 | Now go build something great. 162 |
16 | You just hit a route that doesn't exist... the sadness. 17 |
This page does not have language prefix
17 | Welcome to your new Gatsby site. 18 |
20 | Now go build something great. 21 |
32 | 33 | Go to page 2 34 | 35 |
37 | 38 | Go to ignored page 39 | 40 |
16 | Welcome to page 2 ({props.path}) 17 |