├── __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 |
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 |
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 |
23 | {label}
24 |
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 |
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 |
--------------------------------------------------------------------------------