├── __mocks__ ├── file-mock.js ├── gatsby.js └── react-intl.js ├── loadershim.js ├── src ├── images │ ├── gatsby-icon.png │ └── gatsby-astronaut.png ├── i18n │ ├── translations │ │ ├── index.js │ │ ├── pl │ │ │ ├── 404.js │ │ │ ├── page-2.js │ │ │ ├── index.js │ │ │ └── home.js │ │ └── en │ │ │ ├── 404.js │ │ │ ├── page-2.js │ │ │ ├── index.js │ │ │ └── home.js │ ├── index.js │ ├── languages.js │ ├── detectLocale.js │ └── redirect.jsx ├── layout │ ├── PageContext.js │ ├── index.js │ ├── withLayout.jsx │ └── layout.css ├── __pages-tests__ │ ├── __snapshots__ │ │ └── index.test.js.snap │ └── index.test.js ├── components │ ├── Link.jsx │ ├── Header │ │ ├── header.test.js │ │ ├── __snapshots__ │ │ │ └── header.test.js.snap │ │ └── index.jsx │ ├── Image.jsx │ ├── Langs.jsx │ └── SEO.jsx └── pages │ ├── 404.jsx │ ├── page-2.jsx │ └── index.jsx ├── .prettierrc ├── jest-preprocess.js ├── gatsby-browser.js ├── gatsby-ssr.js ├── jest.config.js ├── LICENSE ├── gatsby-node.js ├── gatsby-config.js ├── .gitignore ├── package.json └── README.md /__mocks__/file-mock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /loadershim.js: -------------------------------------------------------------------------------- 1 | global.___loader = { 2 | enqueue: jest.fn(), 3 | }; 4 | -------------------------------------------------------------------------------- /src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomekskuta/gatsby-starter-intl/HEAD/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /src/i18n/translations/index.js: -------------------------------------------------------------------------------- 1 | import pl from './pl'; 2 | import en from './en'; 3 | 4 | export default { pl, en }; 5 | -------------------------------------------------------------------------------- /src/images/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomekskuta/gatsby-starter-intl/HEAD/src/images/gatsby-astronaut.png -------------------------------------------------------------------------------- /src/layout/PageContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext({ custom: {}, page: {} }); 4 | -------------------------------------------------------------------------------- /src/__pages-tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`HomePage renders correctly 1`] = `null`; 4 | -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | import withLayout from './withLayout'; 2 | import PageContext from './PageContext'; 3 | 4 | export { withLayout as default, PageContext }; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "tslintIntegration": true, 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /jest-preprocess.js: -------------------------------------------------------------------------------- 1 | const babelOptions = { 2 | presets: ['babel-preset-gatsby'], 3 | }; 4 | 5 | module.exports = require('babel-jest').createTransformer(babelOptions); 6 | -------------------------------------------------------------------------------- /src/i18n/translations/pl/404.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'NOT FOUND': 'NIE ZNALEZIONO', 3 | 'You just hit a route that doesnt exist the sadness': 'Zszedłeś na złą drogę... przykre bardzo.', 4 | }; 5 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import translations from './translations'; 2 | import languages from './languages'; 3 | import detectLocale from './detectLocale'; 4 | 5 | export { translations, languages, detectLocale }; 6 | -------------------------------------------------------------------------------- /src/i18n/languages.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | locale: 'pl', 4 | label: 'Polski', 5 | }, 6 | { 7 | locale: 'en', 8 | label: 'English', 9 | default: true, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /src/i18n/translations/en/404.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'NOT FOUND': 'NOT FOUND', 3 | 'You just hit a route that doesnt exist the sadness': 4 | "You just hit a route that doesn't exist... the sadness.", 5 | }; 6 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /src/i18n/translations/pl/page-2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Strona druga', 3 | 'Hi from the second page': 'No elo', 4 | 'Welcome to page 2': 'Witaj na drugiej stronie', 5 | 'Go back to the homepage': 'Wróć do strony głównej', 6 | }; 7 | -------------------------------------------------------------------------------- /src/i18n/translations/en/page-2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Page two', 3 | 'Hi from the second page': 'Hi from the second page', 4 | 'Welcome to page 2': 'Welcome to page 2', 5 | 'Go back to the homepage': 'Go back to the homepage', 6 | }; 7 | -------------------------------------------------------------------------------- /src/i18n/translations/pl/index.js: -------------------------------------------------------------------------------- 1 | import flatten from 'flat'; 2 | 3 | import error404 from './404'; 4 | import home from './home'; 5 | import page2 from './page-2'; 6 | 7 | const message = { error404, home, page2 }; 8 | 9 | export default flatten(message); 10 | -------------------------------------------------------------------------------- /src/i18n/translations/en/index.js: -------------------------------------------------------------------------------- 1 | import flatten from 'flat'; 2 | 3 | import error404 from './404'; 4 | import home from './home'; 5 | import page2 from './page-2'; 6 | 7 | const messages = { error404, home, page2 }; 8 | 9 | export default flatten(messages); 10 | -------------------------------------------------------------------------------- /src/components/Link.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'gatsby'; 3 | import { injectIntl } from 'react-intl'; 4 | 5 | const LocalizedLink = ({ to, intl: { locale }, ...props }) => ( 6 | 7 | ); 8 | 9 | export default injectIntl(LocalizedLink); 10 | -------------------------------------------------------------------------------- /src/i18n/translations/en/home.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Home', 3 | 'Hi people': 'Hi people', 4 | 'Welcome to your new Gatsby site': 'Welcome to your new Gatsby site.', 5 | 'Now go build something great': 'Now go build something great.', 6 | 'or learn more': '...or learn more.', 7 | 'Go to page 2': 'Go to page 2', 8 | }; 9 | -------------------------------------------------------------------------------- /__mocks__/gatsby.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const gatsby = jest.requireActual('gatsby'); 3 | 4 | module.exports = { 5 | ...gatsby, 6 | graphql: jest.fn(), 7 | Link: jest.fn().mockImplementation(({ to, ...rest }) => 8 | React.createElement('a', { 9 | ...rest, 10 | href: to, 11 | }), 12 | ), 13 | StaticQuery: jest.fn(), 14 | }; 15 | -------------------------------------------------------------------------------- /src/i18n/translations/pl/home.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Strona główna', 3 | 'Hi people': 'No cześć', 4 | 'Welcome to your new Gatsby site': 'Witaj na Twojej nowej super stronie Gatsby.', 5 | 'Now go build something great': 'Teraz zbuduj coś wielkiego królewiczu złoty.', 6 | 'or learn more': '...albo dowiedz się więcej.', 7 | 'Go to page 2': 'Chodź na 2 stronę', 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Header/header.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | import Header from './index'; 5 | 6 | describe('Header', () => { 7 | it('renders correctly', () => { 8 | const tree = renderer.create(
).toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/i18n/detectLocale.js: -------------------------------------------------------------------------------- 1 | import browserLang from 'browser-lang'; 2 | import languages from './languages'; 3 | 4 | const fallbackLang = languages.find(language => language.default).locale; 5 | 6 | export default () => 7 | localStorage.getItem('language') || 8 | browserLang({ 9 | languages: languages.map(language => language.locale), 10 | fallback: fallbackLang, 11 | }); 12 | -------------------------------------------------------------------------------- /src/i18n/redirect.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import detectLocale from './detectLocale'; 4 | 5 | const Redirect = ({ navigate, pageContext: { redirectPage } }) => { 6 | useEffect(() => { 7 | const detected = detectLocale(); 8 | const newUrl = `/${detected}${redirectPage}`; 9 | 10 | localStorage.setItem('language', detected); 11 | navigate(newUrl); 12 | }, []); 13 | 14 | return
; 15 | }; 16 | 17 | export default Redirect; 18 | -------------------------------------------------------------------------------- /__mocks__/react-intl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Intl = jest.genMockFromModule('react-intl'); 3 | 4 | // Here goes intl context injected into component, feel free to extend 5 | const intl = { 6 | formatMessage: ({ defaultMessage }) => defaultMessage, 7 | }; 8 | 9 | Intl.injectIntl = Node => { 10 | const renderWrapped = props => ; 11 | renderWrapped.displayName = Node.displayName || Node.name || 'Component'; 12 | return renderWrapped; 13 | }; 14 | 15 | module.exports = Intl; 16 | -------------------------------------------------------------------------------- /src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import withLayout from '../layout'; 5 | 6 | const NotFoundPage = () => ( 7 | <> 8 |

9 | 10 |

11 |

12 | 13 |

14 | 15 | ); 16 | 17 | const customProps = { 18 | localeKey: 'error404', 19 | hideLangs: true, 20 | }; 21 | 22 | export default withLayout(customProps)(NotFoundPage); 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.jsx?$': `/jest-preprocess.js`, 4 | }, 5 | moduleNameMapper: { 6 | '.+\\.(css|styl|less|sass|scss)$': `identity-obj-proxy`, 7 | '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `/__mocks__/file-mock.js`, 8 | }, 9 | testPathIgnorePatterns: [`node_modules`, `.cache`], 10 | transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`], 11 | globals: { 12 | __PATH_PREFIX__: ``, 13 | }, 14 | testURL: `http://localhost`, 15 | setupFiles: [`/loadershim.js`], 16 | }; 17 | -------------------------------------------------------------------------------- /src/pages/page-2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import withLayout from '../layout'; 5 | import Link from '../components/Link'; 6 | 7 | const SecondPage = () => ( 8 | <> 9 |

10 | 11 |

12 |

13 | 14 |

15 | 16 | 17 | 18 | 19 | ); 20 | 21 | const customProps = { 22 | localeKey: 'page2', 23 | }; 24 | 25 | export default withLayout(customProps)(SecondPage); 26 | -------------------------------------------------------------------------------- /src/__pages-tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { StaticQuery } from 'gatsby'; 4 | 5 | import HomePage from '../pages/index'; 6 | 7 | beforeEach(() => { 8 | StaticQuery.mockImplementationOnce(({ render }) => 9 | render({ 10 | site: { 11 | siteMetadata: { 12 | title: `Gatsby Starter Intl`, 13 | }, 14 | }, 15 | }), 16 | ); 17 | }); 18 | 19 | describe('HomePage', () => { 20 | it('renders correctly', () => { 21 | const tree = renderer.create().toJSON(); 22 | expect(tree).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/Header/__snapshots__/header.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Header renders correctly 1`] = ` 4 |
12 |
24 |

31 | 40 | Default Starter 41 | 42 |

43 |
44 |
45 | `; 46 | -------------------------------------------------------------------------------- /src/components/Image.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { StaticQuery, graphql } from "gatsby" 3 | import Img from "gatsby-image" 4 | 5 | /* 6 | * This component is built using `gatsby-image` to automatically serve optimized 7 | * images with lazy loading and reduced file sizes. The image is loaded using a 8 | * `StaticQuery`, which allows us to load the image from directly within this 9 | * component, rather than having to pass the image data down from pages. 10 | * 11 | * For more information, see the docs: 12 | * - `gatsby-image`: https://gatsby.app/gatsby-image 13 | * - `StaticQuery`: https://gatsby.app/staticquery 14 | */ 15 | 16 | const Image = () => ( 17 | } 30 | /> 31 | ) 32 | export default Image 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 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 | -------------------------------------------------------------------------------- /src/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Link from '../Link'; 4 | import Langs from '../Langs'; 5 | 6 | const Header = ({ siteTitle, hideLangs }) => ( 7 |
13 |
23 |

24 | 31 | {siteTitle} 32 | 33 |

34 | {!hideLangs && } 35 |
36 |
37 | ); 38 | 39 | Header.propTypes = { 40 | siteTitle: PropTypes.string, 41 | hideLangs: PropTypes.bool, 42 | }; 43 | 44 | Header.defaultProps = { 45 | siteTitle: ``, 46 | hideLangs: false, 47 | }; 48 | 49 | export default Header; 50 | -------------------------------------------------------------------------------- /src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import withLayout from '../layout'; 5 | import Link from '../components/Link'; 6 | import Image from '../components/Image'; 7 | 8 | const IndexPage = () => ( 9 | <> 10 |

11 | 12 |

13 |

14 | 15 |

16 |

17 | 18 |

19 |

20 | 25 | 26 | 27 |

28 |
29 | 30 |
31 | 32 | 33 | 34 | 35 | ); 36 | 37 | const customProps = { 38 | localeKey: 'home', // same as file name in src/i18n/translations/your-lang/index.js 39 | }; 40 | 41 | export default withLayout(customProps)(IndexPage); 42 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Node APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/node-apis/ 5 | */ 6 | 7 | const path = require('path'); 8 | const languages = require('./src/i18n/languages'); 9 | 10 | exports.onCreatePage = ({ page, actions }) => { 11 | const { createPage, deletePage } = actions; 12 | 13 | return new Promise(resolve => { 14 | const Redirect = path.resolve('src/i18n/redirect.jsx'); 15 | const redirectPage = { 16 | ...page, 17 | component: Redirect, 18 | context: { 19 | languages, 20 | locale: '', 21 | routed: false, 22 | redirectPage: page.path, 23 | }, 24 | }; 25 | 26 | deletePage(page); 27 | createPage(redirectPage); 28 | 29 | languages.forEach(lang => { 30 | const localizedPath = `/${lang.locale}${page.path}`; 31 | const localePage = { 32 | ...page, 33 | originalPath: page.path, 34 | path: localizedPath, 35 | context: { 36 | languages, 37 | locale: lang.locale, 38 | routed: true, 39 | originalPath: page.path, 40 | }, 41 | }; 42 | createPage(localePage); 43 | }); 44 | 45 | resolve(); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: `Gatsby Starter Intl`, 4 | description: `Kick off your next, great Gatsby project with this i18n starter. This barebones starter ships with the main Gatsby configuration files you might need... and i18n too!`, 5 | author: `@gatsbyjs, @tomekskuta`, 6 | keywords: [`gatsby`, `application`, `react`], 7 | }, 8 | plugins: [ 9 | `gatsby-plugin-react-helmet`, 10 | { 11 | resolve: `gatsby-source-filesystem`, 12 | options: { 13 | name: `images`, 14 | path: `${__dirname}/src/images`, 15 | }, 16 | }, 17 | `gatsby-transformer-sharp`, 18 | `gatsby-plugin-sharp`, 19 | { 20 | resolve: `gatsby-plugin-manifest`, 21 | options: { 22 | name: `gatsby-starter-default`, 23 | short_name: `starter`, 24 | start_url: `/`, 25 | background_color: `#663399`, 26 | theme_color: `#663399`, 27 | display: `minimal-ui`, 28 | icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. 29 | }, 30 | }, 31 | // this (optional) plugin enables Progressive Web App + Offline functionality 32 | // To learn more, visit: https://gatsby.app/offline 33 | // 'gatsby-plugin-offline', 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | yarn.lock 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 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 variables file 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 | yarn-lock -------------------------------------------------------------------------------- /src/components/Langs.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { navigate } from 'gatsby'; 3 | import { injectIntl } from 'react-intl'; 4 | 5 | import languages from '../i18n/languages'; 6 | import PageContext from '../layout/PageContext'; 7 | 8 | const buttonStyle = { 9 | margin: '0.5rem', 10 | borderRadius: '10px', 11 | border: 0, 12 | cursor: 'pointer', 13 | }; 14 | 15 | const LangButton = ({ label, chosen, onClick }) => ( 16 | 25 | ); 26 | 27 | const Langs = ({ intl: { locale } }) => { 28 | const pageContext = useContext(PageContext); 29 | 30 | const handleSetLang = language => { 31 | const { originalPath } = pageContext.page; 32 | const newPathname = `/${language}${originalPath}`; 33 | 34 | localStorage.setItem('language', language); 35 | navigate(newPathname); 36 | }; 37 | 38 | if (!pageContext.custom.localeKey) return null; 39 | return ( 40 |
41 | {languages.map(language => ( 42 | handleSetLang(language.locale)} 47 | /> 48 | ))} 49 |
50 | ); 51 | }; 52 | 53 | export default injectIntl(Langs); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-intl", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby and i18n", 5 | "version": "1.0.0", 6 | "author": "Tomek Skuta ", 7 | "dependencies": { 8 | "browser-lang": "^0.0.1", 9 | "flat": "^4.1.0", 10 | "gatsby": "^2.1.24", 11 | "gatsby-image": "^2.0.31", 12 | "gatsby-plugin-manifest": "^2.0.22", 13 | "gatsby-plugin-offline": "^2.0.24", 14 | "gatsby-plugin-react-helmet": "^3.0.8", 15 | "gatsby-plugin-sharp": "^2.0.25", 16 | "gatsby-source-filesystem": "^2.0.23", 17 | "gatsby-transformer-sharp": "^2.1.15", 18 | "path": "^0.12.7", 19 | "prop-types": "^15.7.2", 20 | "react": "^16.8.4", 21 | "react-dom": "^16.8.4", 22 | "react-helmet": "^5.2.0", 23 | "react-intl": "^2.8.0" 24 | }, 25 | "keywords": [ 26 | "gatsby" 27 | ], 28 | "license": "MIT", 29 | "scripts": { 30 | "build": "gatsby build", 31 | "develop": "gatsby develop", 32 | "start": "npm run develop", 33 | "serve": "gatsby serve", 34 | "test": "jest" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/gatsbyjs/gatsby/issues" 42 | }, 43 | "devDependencies": { 44 | "babel-jest": "^24.3.0", 45 | "babel-preset-gatsby": "^0.1.8", 46 | "identity-obj-proxy": "^3.0.0", 47 | "jest": "^24.3.0", 48 | "react-test-renderer": "^16.8.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/layout/withLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { StaticQuery, graphql } from 'gatsby'; 4 | import { IntlProvider, addLocaleData } from 'react-intl'; 5 | 6 | import PageContext from './PageContext'; 7 | 8 | import plData from 'react-intl/locale-data/pl'; 9 | import enData from 'react-intl/locale-data/en'; 10 | import { translations, languages } from '../i18n'; 11 | 12 | import Header from '../components/Header'; 13 | import SEO from '../components/SEO'; 14 | import './layout.css'; 15 | 16 | addLocaleData([...plData, ...enData]); 17 | 18 | const withLayout = customProps => PageComponent => props => { 19 | const { locale } = props.pageContext; 20 | const { localeKey, hideLangs } = customProps; 21 | 22 | const pageContextValue = { custom: customProps, page: props.pageContext }; 23 | 24 | const defaultLocale = languages.find(language => language.default).locale; 25 | const pageLocale = locale || defaultLocale; 26 | const pageTitle = locale ? translations[locale][`${localeKey}.title`] : ''; 27 | 28 | return ( 29 | ( 40 | 41 | 42 | 43 |
44 |
52 |
53 | 54 |
55 |
56 | © {new Date().getFullYear()}, Built with 57 | {` `} 58 | Gatsby 59 |
60 |
61 | 62 | 63 | )} 64 | /> 65 | ); 66 | }; 67 | 68 | withLayout.propTypes = { 69 | children: PropTypes.node.isRequired, 70 | }; 71 | 72 | export default withLayout; 73 | -------------------------------------------------------------------------------- /src/components/SEO.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import { StaticQuery, graphql } from 'gatsby'; 5 | 6 | function SEO({ description, lang, meta, keywords, title }) { 7 | return ( 8 | { 11 | const pageTitle = title || data.site.siteMetadata.title; 12 | const metaDescription = description || data.site.siteMetadata.description; 13 | const metaKeywords = keywords.length ? keywords : data.site.siteMetadata.keywords; 14 | 15 | return ( 16 | 0 58 | ? { 59 | name: `keywords`, 60 | content: metaKeywords.join(`, `), 61 | } 62 | : [], 63 | ) 64 | .concat(meta)} 65 | /> 66 | ); 67 | }} 68 | /> 69 | ); 70 | } 71 | 72 | SEO.defaultProps = { 73 | title: '', 74 | meta: [], 75 | keywords: [], 76 | }; 77 | 78 | SEO.propTypes = { 79 | description: PropTypes.string, 80 | lang: PropTypes.string.isRequired, 81 | meta: PropTypes.array, 82 | keywords: PropTypes.arrayOf(PropTypes.string), 83 | title: PropTypes.string, 84 | }; 85 | 86 | export default SEO; 87 | 88 | const detailsQuery = graphql` 89 | query DefaultSEOQuery { 90 | site { 91 | siteMetadata { 92 | title 93 | description 94 | author 95 | keywords 96 | } 97 | } 98 | } 99 | `; 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Gatsby 4 | 5 |

6 | 7 | # :bullettrain_side: Gatsby starter intl :airplane: 8 | 9 | Gatsby v2 i18n starter based on [Gatsby Starter Default](https://github.com/gatsbyjs/gatsby-starter-default) and [gatsby-starter-default-intl](https://github.com/wiziple/gatsby-starter-default-intl). 10 | Gatsby creates static pages for every language and detects your browser locale. :guitar: 11 | 12 | ...and you know, it's React 16.8 - you can use **hooks**! 13 | 14 | [Checkout demo!](https://gatsby-starter-intl.tomekskuta.pl/) 15 | 16 | ## How to start 17 | 18 | To use **Gatsby starter intl** you have to install Gatsby CLI 19 | 20 | ```sh 21 | npm install global gatsby-cli 22 | or 23 | yarn global add gatsby-cli 24 | ``` 25 | 26 | Then use it to start new project based on **Gatsby starter intl** 27 | 28 | ```sh 29 | gatsby new your-project-name https://github.com/tomekskuta/gatsby-starter-intl 30 | cd your-project-name/ 31 | gatsby develop 32 | ``` 33 | 34 | Your site is running at `localhost:8000`. 35 | 36 | If you want to compile production build just run `gatsby build`. 37 | 38 | Start build your great i18n Gatsby app! :rocket: 39 | 40 | ## How it works 41 | 42 | Gatsby creates **static pages** for every language sets in `src/i18n/languages.js`. 43 | It looks like `/en/page-2` and `/pl/page-2`. Google loves it - they said. 44 | 45 | ### Translations 46 | 47 | Translations are set in `src/i18n/translations/`. For better navigation I prefer create files for every page. Unfortunately react-intl supports only flat objects so in `src/i18n/translations/chosen-lang/index.js` I used [flat](https://github.com/hughsk/flat). Don't forget to import your translate file into `index.js` in the same directory. 48 | Then if I want to get translation for `Welcome to page 2` on `page-2` I do it this way: 49 | 50 | ```jsx 51 | 52 | ``` 53 | 54 | Translation is in `src/i18n/translations/en/page-2.js` and looks like: 55 | 56 | ```js 57 | export default { 58 | 'Welcome to page 2': 'Welcome to page 2', 59 | }; 60 | ``` 61 | 62 | ### Languages 63 | 64 | Languages list is in `src/i18n/languages.js`. Elements of array has attrs: 65 | 66 | - locale - key to identify your locale, 67 | - label - pretty name of your locale to display in list or buttons in your UI. 68 | - default - if true it is fallback language for app. It is used in `src/i18n/detectLocale` and `src/layout/withLayout.jsx`. 69 | 70 | ### Redirect 71 | 72 | Redirect is a component declared in `src/i18n/redirect.jsx`. 73 | It is used in `gatsby-node.js` as a default component for every root without locale. 74 | It detects language from localStorage or browser langs and redirect to correct locale route. 75 | 76 | `/page-2/` :point_right: `/en/page-2` 77 | 78 | ### Layout 79 | 80 | There are every necessary components and properties used by every page. I moved it from `Layout.js` to a HOC `withLayout`. I did it to minimize amount of props manually sets from page to Layouts components. :boy::point_left: 81 | In standard way you have to set `locale` and `pageContext` on every page. Now if you don't need to set any custom props just import `withLayout` to your page and use it in export: 82 | 83 | ```jsx 84 | // src/pages/my-page.jsx 85 | 86 | import withLayout from '../layout'; 87 | 88 | const MyPage = () =>
Hello guys!
; 89 | 90 | export default withLayout()(MyPage); 91 | ``` 92 | 93 | Of course you can set your custom props to layout: 94 | 95 | ```jsx 96 | const customProps = { 97 | mySuperCustomProp: 'oh yeah Im his super custom prop', 98 | }; 99 | 100 | export default withLayout(customProps)(MyPage); 101 | ``` 102 | 103 | In this example I used 2 customProps: 104 | 105 | - localeKey - it can tells your layout the page key in `src/i18n/translations/[locale]/index.js` to set individual properties to every language page (ex. site title, description or keywords). 106 | - hideLangs - to hide possibility of changing languages (ex. in 404). 107 | 108 | witLayout use [**Context API**](https://reactjs.org/docs/context.html) to share `pageContext` and `customProps` to every component you want in your page. 109 | PageContext object looks like: 110 | 111 | ```js 112 | PageContext = { 113 | custom, // customProps 114 | page, // pageContext from Gatsby magic 115 | }; 116 | ``` 117 | 118 | To get context you can use hooks: 119 | 120 | ```jsx 121 | // src/components/MyComponent.jsx 122 | 123 | import React, { useContext } from 'react'; 124 | import PageContext from '../layout/PageContext'; 125 | 126 | const MyComponent = () => { 127 | const pageContext = useContext(PageContext); 128 | return
{pageContext.page.locale}
; 129 | }; 130 | ``` 131 | 132 | ### Change Languages 133 | 134 | Inside `src/components/Langs.jsx` is possibility to change language. 135 | Set language function set chosen locale to `localStorage` and use Gatsby's `navigate` method to redirect to chosen locale page. 136 | 137 | ### 404 138 | 139 | Gatsby makes locale pages for 404 too but it doesn't work properly on local develop server. 140 | To see your error page hit `gatsby build && gatsby serve` in bash and then you can get all your production build on `localhost:9000`. 141 | 142 | I'm not sure but 404 constructed this way can not work properly with some static page servers (I didn't use any of them with this starter). Starter's Demo is served by [Express](https://expressjs.com/) and function to serve error page looks like this: 143 | 144 | ```js 145 | app.use((req, res, next) => { 146 | fs.readFile(__dirname + './public/404.html', 'utf-8', (err, page) => { 147 | res.writeHead(404, { 'Content-Type': 'text/html' }); 148 | res.write(page); 149 | res.end(); 150 | }); 151 | return; 152 | }); 153 | ``` 154 | 155 | If you don't need i18n inside your 404 you can put this condition at the top of `onCreatePage` method in `gatsby-node.js`: 156 | 157 | ```js 158 | if (page.path.includes('404')) { 159 | return Promise.resolve(); 160 | } 161 | ``` 162 | 163 | Gatsby will skip building locale pages for 404 and you can see error page during developing with `gatsby develop`. 164 | 165 | ## Tests 166 | 167 | :point_right: :bug: 168 | 169 | Unit tests are configured with [Jest](https://jestjs.io/) followed [official Gatsby documentation](https://www.gatsbyjs.org/docs/unit-testing/). 170 | 171 | There is `src/__pages-tests__` directory prepared for pages tests where I set example snapshot test for homepage. 172 | Test for `
` is in `src/components/Header/header.test.js` next to file with the component. 173 | 174 | [Test react-intl.](https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl#jest) 175 | 176 | [More info about testing Gatsby components and Graphql.](https://github.com/gatsbyjs/gatsby/blob/master/docs/docs/testing-components-with-graphql.md) 177 | 178 | ## Contributing 179 | 180 | If you have any question, see bugs or you think some feature can be written better - just open pull request or issue. I will be happy to help and learn from you. 181 | 182 | ## License 183 | 184 | [MIT](https://opensource.org/licenses/MIT) 185 | -------------------------------------------------------------------------------- /src/layout/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 | } 9 | article, 10 | aside, 11 | details, 12 | figcaption, 13 | figure, 14 | footer, 15 | header, 16 | main, 17 | menu, 18 | nav, 19 | section, 20 | summary { 21 | display: block; 22 | } 23 | audio, 24 | canvas, 25 | progress, 26 | video { 27 | display: inline-block; 28 | } 29 | audio:not([controls]) { 30 | display: none; 31 | height: 0; 32 | } 33 | progress { 34 | vertical-align: baseline; 35 | } 36 | [hidden], 37 | template { 38 | display: none; 39 | } 40 | a { 41 | background-color: transparent; 42 | -webkit-text-decoration-skip: objects; 43 | } 44 | a:active, 45 | a:hover { 46 | outline-width: 0; 47 | } 48 | abbr[title] { 49 | border-bottom: none; 50 | text-decoration: underline; 51 | text-decoration: underline dotted; 52 | } 53 | b, 54 | strong { 55 | font-weight: inherit; 56 | font-weight: bolder; 57 | } 58 | dfn { 59 | font-style: italic; 60 | } 61 | h1 { 62 | font-size: 2em; 63 | margin: 0.67em 0; 64 | } 65 | mark { 66 | background-color: #ff0; 67 | color: #000; 68 | } 69 | small { 70 | font-size: 80%; 71 | } 72 | sub, 73 | sup { 74 | font-size: 75%; 75 | line-height: 0; 76 | position: relative; 77 | vertical-align: baseline; 78 | } 79 | sub { 80 | bottom: -0.25em; 81 | } 82 | sup { 83 | top: -0.5em; 84 | } 85 | img { 86 | border-style: none; 87 | } 88 | svg:not(:root) { 89 | overflow: hidden; 90 | } 91 | code, 92 | kbd, 93 | pre, 94 | samp { 95 | font-family: monospace, monospace; 96 | font-size: 1em; 97 | } 98 | figure { 99 | margin: 1em 40px; 100 | } 101 | hr { 102 | box-sizing: content-box; 103 | height: 0; 104 | overflow: visible; 105 | } 106 | button, 107 | input, 108 | optgroup, 109 | select, 110 | textarea { 111 | font: inherit; 112 | margin: 0; 113 | } 114 | optgroup { 115 | font-weight: 700; 116 | } 117 | button, 118 | input { 119 | overflow: visible; 120 | } 121 | button, 122 | select { 123 | text-transform: none; 124 | } 125 | [type="reset"], 126 | [type="submit"], 127 | button, 128 | html [type="button"] { 129 | -webkit-appearance: button; 130 | } 131 | [type="button"]::-moz-focus-inner, 132 | [type="reset"]::-moz-focus-inner, 133 | [type="submit"]::-moz-focus-inner, 134 | button::-moz-focus-inner { 135 | border-style: none; 136 | padding: 0; 137 | } 138 | [type="button"]:-moz-focusring, 139 | [type="reset"]:-moz-focusring, 140 | [type="submit"]:-moz-focusring, 141 | button:-moz-focusring { 142 | outline: 1px dotted ButtonText; 143 | } 144 | fieldset { 145 | border: 1px solid silver; 146 | margin: 0 2px; 147 | padding: 0.35em 0.625em 0.75em; 148 | } 149 | legend { 150 | box-sizing: border-box; 151 | color: inherit; 152 | display: table; 153 | max-width: 100%; 154 | padding: 0; 155 | white-space: normal; 156 | } 157 | textarea { 158 | overflow: auto; 159 | } 160 | [type="checkbox"], 161 | [type="radio"] { 162 | box-sizing: border-box; 163 | padding: 0; 164 | } 165 | [type="number"]::-webkit-inner-spin-button, 166 | [type="number"]::-webkit-outer-spin-button { 167 | height: auto; 168 | } 169 | [type="search"] { 170 | -webkit-appearance: textfield; 171 | outline-offset: -2px; 172 | } 173 | [type="search"]::-webkit-search-cancel-button, 174 | [type="search"]::-webkit-search-decoration { 175 | -webkit-appearance: none; 176 | } 177 | ::-webkit-input-placeholder { 178 | color: inherit; 179 | opacity: 0.54; 180 | } 181 | ::-webkit-file-upload-button { 182 | -webkit-appearance: button; 183 | font: inherit; 184 | } 185 | html { 186 | font: 112.5%/1.45em georgia, serif; 187 | box-sizing: border-box; 188 | overflow-y: scroll; 189 | } 190 | * { 191 | box-sizing: inherit; 192 | } 193 | *:before { 194 | box-sizing: inherit; 195 | } 196 | *:after { 197 | box-sizing: inherit; 198 | } 199 | body { 200 | color: hsla(0, 0%, 0%, 0.8); 201 | font-family: georgia, serif; 202 | font-weight: normal; 203 | word-wrap: break-word; 204 | font-kerning: normal; 205 | -moz-font-feature-settings: "kern", "liga", "clig", "calt"; 206 | -ms-font-feature-settings: "kern", "liga", "clig", "calt"; 207 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt"; 208 | font-feature-settings: "kern", "liga", "clig", "calt"; 209 | } 210 | img { 211 | max-width: 100%; 212 | margin-left: 0; 213 | margin-right: 0; 214 | margin-top: 0; 215 | padding-bottom: 0; 216 | padding-left: 0; 217 | padding-right: 0; 218 | padding-top: 0; 219 | margin-bottom: 1.45rem; 220 | } 221 | h1 { 222 | margin-left: 0; 223 | margin-right: 0; 224 | margin-top: 0; 225 | padding-bottom: 0; 226 | padding-left: 0; 227 | padding-right: 0; 228 | padding-top: 0; 229 | margin-bottom: 1.45rem; 230 | color: inherit; 231 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 232 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 233 | font-weight: bold; 234 | text-rendering: optimizeLegibility; 235 | font-size: 2.25rem; 236 | line-height: 1.1; 237 | } 238 | h2 { 239 | margin-left: 0; 240 | margin-right: 0; 241 | margin-top: 0; 242 | padding-bottom: 0; 243 | padding-left: 0; 244 | padding-right: 0; 245 | padding-top: 0; 246 | margin-bottom: 1.45rem; 247 | color: inherit; 248 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 249 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 250 | font-weight: bold; 251 | text-rendering: optimizeLegibility; 252 | font-size: 1.62671rem; 253 | line-height: 1.1; 254 | } 255 | h3 { 256 | margin-left: 0; 257 | margin-right: 0; 258 | margin-top: 0; 259 | padding-bottom: 0; 260 | padding-left: 0; 261 | padding-right: 0; 262 | padding-top: 0; 263 | margin-bottom: 1.45rem; 264 | color: inherit; 265 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 266 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 267 | font-weight: bold; 268 | text-rendering: optimizeLegibility; 269 | font-size: 1.38316rem; 270 | line-height: 1.1; 271 | } 272 | h4 { 273 | margin-left: 0; 274 | margin-right: 0; 275 | margin-top: 0; 276 | padding-bottom: 0; 277 | padding-left: 0; 278 | padding-right: 0; 279 | padding-top: 0; 280 | margin-bottom: 1.45rem; 281 | color: inherit; 282 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 283 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 284 | font-weight: bold; 285 | text-rendering: optimizeLegibility; 286 | font-size: 1rem; 287 | line-height: 1.1; 288 | } 289 | h5 { 290 | margin-left: 0; 291 | margin-right: 0; 292 | margin-top: 0; 293 | padding-bottom: 0; 294 | padding-left: 0; 295 | padding-right: 0; 296 | padding-top: 0; 297 | margin-bottom: 1.45rem; 298 | color: inherit; 299 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 300 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 301 | font-weight: bold; 302 | text-rendering: optimizeLegibility; 303 | font-size: 0.85028rem; 304 | line-height: 1.1; 305 | } 306 | h6 { 307 | margin-left: 0; 308 | margin-right: 0; 309 | margin-top: 0; 310 | padding-bottom: 0; 311 | padding-left: 0; 312 | padding-right: 0; 313 | padding-top: 0; 314 | margin-bottom: 1.45rem; 315 | color: inherit; 316 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 317 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 318 | font-weight: bold; 319 | text-rendering: optimizeLegibility; 320 | font-size: 0.78405rem; 321 | line-height: 1.1; 322 | } 323 | hgroup { 324 | margin-left: 0; 325 | margin-right: 0; 326 | margin-top: 0; 327 | padding-bottom: 0; 328 | padding-left: 0; 329 | padding-right: 0; 330 | padding-top: 0; 331 | margin-bottom: 1.45rem; 332 | } 333 | ul { 334 | margin-left: 1.45rem; 335 | margin-right: 0; 336 | margin-top: 0; 337 | padding-bottom: 0; 338 | padding-left: 0; 339 | padding-right: 0; 340 | padding-top: 0; 341 | margin-bottom: 1.45rem; 342 | list-style-position: outside; 343 | list-style-image: none; 344 | } 345 | ol { 346 | margin-left: 1.45rem; 347 | margin-right: 0; 348 | margin-top: 0; 349 | padding-bottom: 0; 350 | padding-left: 0; 351 | padding-right: 0; 352 | padding-top: 0; 353 | margin-bottom: 1.45rem; 354 | list-style-position: outside; 355 | list-style-image: none; 356 | } 357 | dl { 358 | margin-left: 0; 359 | margin-right: 0; 360 | margin-top: 0; 361 | padding-bottom: 0; 362 | padding-left: 0; 363 | padding-right: 0; 364 | padding-top: 0; 365 | margin-bottom: 1.45rem; 366 | } 367 | dd { 368 | margin-left: 0; 369 | margin-right: 0; 370 | margin-top: 0; 371 | padding-bottom: 0; 372 | padding-left: 0; 373 | padding-right: 0; 374 | padding-top: 0; 375 | margin-bottom: 1.45rem; 376 | } 377 | p { 378 | margin-left: 0; 379 | margin-right: 0; 380 | margin-top: 0; 381 | padding-bottom: 0; 382 | padding-left: 0; 383 | padding-right: 0; 384 | padding-top: 0; 385 | margin-bottom: 1.45rem; 386 | } 387 | figure { 388 | margin-left: 0; 389 | margin-right: 0; 390 | margin-top: 0; 391 | padding-bottom: 0; 392 | padding-left: 0; 393 | padding-right: 0; 394 | padding-top: 0; 395 | margin-bottom: 1.45rem; 396 | } 397 | pre { 398 | margin-left: 0; 399 | margin-right: 0; 400 | margin-top: 0; 401 | margin-bottom: 1.45rem; 402 | font-size: 0.85rem; 403 | line-height: 1.42; 404 | background: hsla(0, 0%, 0%, 0.04); 405 | border-radius: 3px; 406 | overflow: auto; 407 | word-wrap: normal; 408 | padding: 1.45rem; 409 | } 410 | table { 411 | margin-left: 0; 412 | margin-right: 0; 413 | margin-top: 0; 414 | padding-bottom: 0; 415 | padding-left: 0; 416 | padding-right: 0; 417 | padding-top: 0; 418 | margin-bottom: 1.45rem; 419 | font-size: 1rem; 420 | line-height: 1.45rem; 421 | border-collapse: collapse; 422 | width: 100%; 423 | } 424 | fieldset { 425 | margin-left: 0; 426 | margin-right: 0; 427 | margin-top: 0; 428 | padding-bottom: 0; 429 | padding-left: 0; 430 | padding-right: 0; 431 | padding-top: 0; 432 | margin-bottom: 1.45rem; 433 | } 434 | blockquote { 435 | margin-left: 1.45rem; 436 | margin-right: 1.45rem; 437 | margin-top: 0; 438 | padding-bottom: 0; 439 | padding-left: 0; 440 | padding-right: 0; 441 | padding-top: 0; 442 | margin-bottom: 1.45rem; 443 | } 444 | form { 445 | margin-left: 0; 446 | margin-right: 0; 447 | margin-top: 0; 448 | padding-bottom: 0; 449 | padding-left: 0; 450 | padding-right: 0; 451 | padding-top: 0; 452 | margin-bottom: 1.45rem; 453 | } 454 | noscript { 455 | margin-left: 0; 456 | margin-right: 0; 457 | margin-top: 0; 458 | padding-bottom: 0; 459 | padding-left: 0; 460 | padding-right: 0; 461 | padding-top: 0; 462 | margin-bottom: 1.45rem; 463 | } 464 | iframe { 465 | margin-left: 0; 466 | margin-right: 0; 467 | margin-top: 0; 468 | padding-bottom: 0; 469 | padding-left: 0; 470 | padding-right: 0; 471 | padding-top: 0; 472 | margin-bottom: 1.45rem; 473 | } 474 | hr { 475 | margin-left: 0; 476 | margin-right: 0; 477 | margin-top: 0; 478 | padding-bottom: 0; 479 | padding-left: 0; 480 | padding-right: 0; 481 | padding-top: 0; 482 | margin-bottom: calc(1.45rem - 1px); 483 | background: hsla(0, 0%, 0%, 0.2); 484 | border: none; 485 | height: 1px; 486 | } 487 | address { 488 | margin-left: 0; 489 | margin-right: 0; 490 | margin-top: 0; 491 | padding-bottom: 0; 492 | padding-left: 0; 493 | padding-right: 0; 494 | padding-top: 0; 495 | margin-bottom: 1.45rem; 496 | } 497 | b { 498 | font-weight: bold; 499 | } 500 | strong { 501 | font-weight: bold; 502 | } 503 | dt { 504 | font-weight: bold; 505 | } 506 | th { 507 | font-weight: bold; 508 | } 509 | li { 510 | margin-bottom: calc(1.45rem / 2); 511 | } 512 | ol li { 513 | padding-left: 0; 514 | } 515 | ul li { 516 | padding-left: 0; 517 | } 518 | li > ol { 519 | margin-left: 1.45rem; 520 | margin-bottom: calc(1.45rem / 2); 521 | margin-top: calc(1.45rem / 2); 522 | } 523 | li > ul { 524 | margin-left: 1.45rem; 525 | margin-bottom: calc(1.45rem / 2); 526 | margin-top: calc(1.45rem / 2); 527 | } 528 | blockquote *:last-child { 529 | margin-bottom: 0; 530 | } 531 | li *:last-child { 532 | margin-bottom: 0; 533 | } 534 | p *:last-child { 535 | margin-bottom: 0; 536 | } 537 | li > p { 538 | margin-bottom: calc(1.45rem / 2); 539 | } 540 | code { 541 | font-size: 0.85rem; 542 | line-height: 1.45rem; 543 | } 544 | kbd { 545 | font-size: 0.85rem; 546 | line-height: 1.45rem; 547 | } 548 | samp { 549 | font-size: 0.85rem; 550 | line-height: 1.45rem; 551 | } 552 | abbr { 553 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 554 | cursor: help; 555 | } 556 | acronym { 557 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 558 | cursor: help; 559 | } 560 | abbr[title] { 561 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 562 | cursor: help; 563 | text-decoration: none; 564 | } 565 | thead { 566 | text-align: left; 567 | } 568 | td, 569 | th { 570 | text-align: left; 571 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12); 572 | font-feature-settings: "tnum"; 573 | -moz-font-feature-settings: "tnum"; 574 | -ms-font-feature-settings: "tnum"; 575 | -webkit-font-feature-settings: "tnum"; 576 | padding-left: 0.96667rem; 577 | padding-right: 0.96667rem; 578 | padding-top: 0.725rem; 579 | padding-bottom: calc(0.725rem - 1px); 580 | } 581 | th:first-child, 582 | td:first-child { 583 | padding-left: 0; 584 | } 585 | th:last-child, 586 | td:last-child { 587 | padding-right: 0; 588 | } 589 | tt, 590 | code { 591 | background-color: hsla(0, 0%, 0%, 0.04); 592 | border-radius: 3px; 593 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono", 594 | "Liberation Mono", Menlo, Courier, monospace; 595 | padding: 0; 596 | padding-top: 0.2em; 597 | padding-bottom: 0.2em; 598 | } 599 | pre code { 600 | background: none; 601 | line-height: 1.42; 602 | } 603 | code:before, 604 | code:after, 605 | tt:before, 606 | tt:after { 607 | letter-spacing: -0.2em; 608 | content: " "; 609 | } 610 | pre code:before, 611 | pre code:after, 612 | pre tt:before, 613 | pre tt:after { 614 | content: ""; 615 | } 616 | @media only screen and (max-width: 480px) { 617 | html { 618 | font-size: 100%; 619 | } 620 | } 621 | --------------------------------------------------------------------------------