From e1c4cb9068b05409823fdcb1d218d2484657b6ff Mon Sep 17 00:00:00 2001 From: Jeff Avallone Date: Sun, 6 Jan 2019 12:08:37 -0500 Subject: [PATCH] Adding i18next integration --- gatsby-browser.js | 7 + gatsby-ssr.js | 7 + package.json | 9 +- src/__mocks__/i18n.js | 15 + .../Header/__snapshots__/test.js.snap | 100 ++++++ src/components/Header/index.js | 5 + .../LocaleSwitcher/__snapshots__/test.js.snap | 323 ++++++++++++++++++ src/components/LocaleSwitcher/index.js | 67 ++++ .../LocaleSwitcher/style.module.css | 11 + src/components/LocaleSwitcher/test.js | 27 ++ src/globals.module.css | 47 +++ src/i18n.js | 51 +++ src/locales/en-AC.yaml | 2 + src/locales/en.yaml | 2 + yarn.lock | 45 ++- 15 files changed, 715 insertions(+), 3 deletions(-) create mode 100644 src/__mocks__/i18n.js create mode 100644 src/components/LocaleSwitcher/__snapshots__/test.js.snap create mode 100644 src/components/LocaleSwitcher/index.js create mode 100644 src/components/LocaleSwitcher/style.module.css create mode 100644 src/components/LocaleSwitcher/test.js create mode 100644 src/i18n.js create mode 100644 src/locales/en-AC.yaml create mode 100644 src/locales/en.yaml diff --git a/gatsby-browser.js b/gatsby-browser.js index 34e0714..05aba14 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,6 +1,8 @@ import React from 'react'; import * as Sentry from '@sentry/browser'; +import { I18nextProvider } from 'react-i18next'; +import i18n from 'i18n'; import Layout from 'components/Layout'; import 'site.css'; @@ -14,3 +16,8 @@ export const onClientEntry = () => { export const wrapPageElement = ({ element }) => { return { element }; }; + +// eslint-disable-next-line react/prop-types +export const wrapRootElement = ({ element }) => { + return { element }; +}; diff --git a/gatsby-ssr.js b/gatsby-ssr.js index 75ec661..384d0a5 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.js @@ -1,8 +1,15 @@ import React from 'react'; +import { I18nextProvider } from 'react-i18next'; +import i18n from 'i18n'; import Layout from 'components/Layout'; // eslint-disable-next-line react/prop-types export const wrapPageElement = ({ element }) => { return { element }; }; + +// eslint-disable-next-line react/prop-types +export const wrapRootElement = ({ element }) => { + return { element }; +}; diff --git a/package.json b/package.json index f4bc970..0e496b7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "jest": { "clearMocks": true, "collectCoverageFrom": [ - "src/**/*.js" + "src/**/*.js", + "!src/i18n.js" ], "coverageReporters": [ "text-summary", @@ -77,6 +78,9 @@ "gatsby-plugin-postcss": "^2.0.2", "gatsby-plugin-react-helmet": "^3.0.5", "gatsby-plugin-sentry": "^1.0.0", + "i18next": "^13.1.0", + "i18next-browser-languagedetector": "^2.2.4", + "i18next-xhr-backend": "^1.5.1", "identity-obj-proxy": "^3.0.0", "jest": "^23.6.0", "postcss-cssnext": "^3.1.0", @@ -85,6 +89,7 @@ "react": "^16.7.0", "react-dom": "^16.7.0", "react-feather": "^1.1.5", - "react-helmet": "^5.2.0" + "react-helmet": "^5.2.0", + "react-i18next": "^9.0.2" } } diff --git a/src/__mocks__/i18n.js b/src/__mocks__/i18n.js new file mode 100644 index 0000000..87ced19 --- /dev/null +++ b/src/__mocks__/i18n.js @@ -0,0 +1,15 @@ +const i18n = jest.requireActual('i18n'); + +// Load empty resource bundle to reduce logging output +i18n.default.addResourceBundle('dev', 'translation', {}); +i18n.default.addResourceBundle('en', 'translation', {}); +i18n.default.addResourceBundle('other', 'translation', {}); + +module.exports = { + ...i18n, + locales: [ + { code: 'en', name: 'English' }, + { code: 'other', name: 'Other' } + ], + mockT: str => `TRANSLATE(${ str })` +}; diff --git a/src/components/Header/__snapshots__/test.js.snap b/src/components/Header/__snapshots__/test.js.snap index 1f54fc3..e7e19f7 100644 --- a/src/components/Header/__snapshots__/test.js.snap +++ b/src/components/Header/__snapshots__/test.js.snap @@ -56,6 +56,9 @@ ShallowWrapper { Privacy Policy +
  • + +
  • , ], "className": "header", @@ -115,6 +118,9 @@ ShallowWrapper { Privacy Policy , +
  • + +
  • , ], "className": "list", }, @@ -200,6 +206,25 @@ ShallowWrapper { }, "type": "li", }, + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": , + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": "li", + }, ], "type": "ul", }, @@ -243,6 +268,9 @@ ShallowWrapper { Privacy Policy +
  • + +
  • , ], "className": "header", @@ -302,6 +330,9 @@ ShallowWrapper { Privacy Policy , +
  • + +
  • , ], "className": "list", }, @@ -387,6 +418,25 @@ ShallowWrapper { }, "type": "li", }, + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": , + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": "li", + }, ], "type": "ul", }, @@ -470,6 +520,9 @@ ShallowWrapper { Privacy Policy +
  • + +
  • , ], "className": "header", @@ -529,6 +582,9 @@ ShallowWrapper { Privacy Policy , +
  • + +
  • , ], "className": "list", }, @@ -614,6 +670,25 @@ ShallowWrapper { }, "type": "li", }, + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": , + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": "li", + }, ], "type": "ul", }, @@ -657,6 +732,9 @@ ShallowWrapper { Privacy Policy +
  • + +
  • , ], "className": "header", @@ -716,6 +794,9 @@ ShallowWrapper { Privacy Policy , +
  • + +
  • , ], "className": "list", }, @@ -801,6 +882,25 @@ ShallowWrapper { }, "type": "li", }, + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": , + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": "li", + }, ], "type": "ul", }, diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 506e42b..d4703e2 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -4,6 +4,8 @@ import { Link, StaticQuery, graphql } from 'gatsby'; import GitlabIcon from 'react-feather/dist/icons/gitlab'; +import LocaleSwitcher from 'components/LocaleSwitcher'; + import style from './style.module.css'; const query = graphql` @@ -36,6 +38,9 @@ export const HeaderImpl = ({ site: { siteMetadata } }) => (
  • Privacy Policy
  • +
  • + +
  • ); diff --git a/src/components/LocaleSwitcher/__snapshots__/test.js.snap b/src/components/LocaleSwitcher/__snapshots__/test.js.snap new file mode 100644 index 0000000..8def336 --- /dev/null +++ b/src/components/LocaleSwitcher/__snapshots__/test.js.snap @@ -0,0 +1,323 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LocaleSwitcher rendering 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + Language + , +
    + + +
    , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": "Language", + }, + "ref": null, + "rendered": "Language", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + , + , + ], + "className": "switcher", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + , + , + ], + "onChange": [Function], + "value": "en", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": "en", + "nodeType": "host", + "props": Object { + "children": "English", + "value": "en", + }, + "ref": null, + "rendered": "English", + "type": "option", + }, + Object { + "instance": null, + "key": "other", + "nodeType": "host", + "props": Object { + "children": "Other", + "value": "other", + }, + "ref": null, + "rendered": "Other", + "type": "option", + }, + ], + "type": "select", + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "color": "currentColor", + "size": "24", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": "div", + }, + ], + "type": "label", + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + Language + , +
    + + +
    , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": "Language", + }, + "ref": null, + "rendered": "Language", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + , + , + ], + "className": "switcher", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + , + , + ], + "onChange": [Function], + "value": "en", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": "en", + "nodeType": "host", + "props": Object { + "children": "English", + "value": "en", + }, + "ref": null, + "rendered": "English", + "type": "option", + }, + Object { + "instance": null, + "key": "other", + "nodeType": "host", + "props": Object { + "children": "Other", + "value": "other", + }, + "ref": null, + "rendered": "Other", + "type": "option", + }, + ], + "type": "select", + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "color": "currentColor", + "size": "24", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": "div", + }, + ], + "type": "label", + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/LocaleSwitcher/index.js b/src/components/LocaleSwitcher/index.js new file mode 100644 index 0000000..c3255ce --- /dev/null +++ b/src/components/LocaleSwitcher/index.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { withNamespaces, Trans } from 'react-i18next'; + +import ExpandIcon from 'react-feather/dist/icons/chevrons-down'; + +import i18n, { locales } from 'i18n'; + +import style from './style.module.css'; + +const localeToAvailable = (locale, available, defaultLocale) => { + if (available.includes(locale)) { + return locale; + } + + const parts = locale.split('-'); + + if (parts.length > 0 && available.includes(parts[0])) { + return parts[0]; + } + + return defaultLocale; +}; + +export class LocaleSwitcherImpl extends React.PureComponent { + state = { + current: localeToAvailable( + i18n.language || '', + locales.map(l => l.code), + 'en') + } + + componentDidMount() { + i18n.on('languageChanged', this.handleLanguageChange); + } + + componentWillUnmount() { + i18n.off('languageChanged', this.handleLanguageChange); + } + + handleSelectChange = ({ target }) => { + i18n.changeLanguage(target.value); + } + + handleLanguageChange = lang => { + this.setState({ current: lang }); + } + + render() { + const { current } = this.state; + + return ; + } +} + +export default withNamespaces()(LocaleSwitcherImpl); diff --git a/src/components/LocaleSwitcher/style.module.css b/src/components/LocaleSwitcher/style.module.css new file mode 100644 index 0000000..2e2f3dc --- /dev/null +++ b/src/components/LocaleSwitcher/style.module.css @@ -0,0 +1,11 @@ +@import url('../../globals.module.css'); + +:root { + --control-gradient: var(--color-tan) var(--gradient-tan); + --select-height: 2.4rem; + --select-width: 10rem; +} + +.switcher { + composes: fancy-select; +} diff --git a/src/components/LocaleSwitcher/test.js b/src/components/LocaleSwitcher/test.js new file mode 100644 index 0000000..6ec1231 --- /dev/null +++ b/src/components/LocaleSwitcher/test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import i18n from 'i18n'; +import { LocaleSwitcherImpl } from 'components/LocaleSwitcher'; + +describe('LocaleSwitcher', () => { + test('rendering', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + test('changing language', () => { + jest.spyOn(i18n, 'changeLanguage'); + + const component = shallow( + + ); + const selectInput = component.find('select'); + selectInput.value = 'other'; + selectInput.simulate('change', { target: { value: 'other' } }); + + expect(i18n.changeLanguage).toHaveBeenCalledWith('other'); + }); +}); diff --git a/src/globals.module.css b/src/globals.module.css index aa230f7..1c00488 100644 --- a/src/globals.module.css +++ b/src/globals.module.css @@ -11,12 +11,20 @@ --gradient-green: linear-gradient(to bottom, var(--color-green) 0%, color(var(--color-green) shade(40%)) 100%); + --gradient-tan: linear-gradient(to bottom, + var(--color-tan) 0%, + color(var(--color-tan) shade(40%)) 100%); --header-height: 6rem; --content-margin: 2rem; --spacing-margin: 1rem; --list-separator-width: 2ex; + + --control-gradient: var(--color-green) var(--gradient-green); + --select-height: 2.8rem; + --select-width: 12rem; + --select-separator-inset: 0.2rem; } .inline-list { @@ -80,3 +88,42 @@ visibility: hidden; } } + +.fancy-select { + margin-left: 1ex; + position: relative; + vertical-align: bottom; + display: inline-block; + height: var(--select-height); + width: var(--select-width); + background: var(--control-gradient); + color: var(--color-black); + overflow: hidden; + + & select { + background: transparent; + border: 0 none; + font-size: inherit; + height: var(--select-height); + width: calc(var(--select-width) + 2rem); + } + + & svg { + position: absolute; + top: 0; + right: 0; + height: var(--select-height); + width: var(--select-height); + pointer-events: none; + background: var(--control-gradient); + } + + &:after { + content: ''; + position: absolute; + top: var(--select-separator-inset); + right: var(--select-height); + height: calc(var(--select-height) - var(--select-separator-inset) * 2); + border-right: 0.1rem solid color(var(--color-black) alpha(0.2)); + } +} diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..4aac2c3 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,51 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-xhr-backend'; + +export const locales = [ + { code: 'en', name: 'English' }, + { code: 'en-AC', name: 'English (ALL CAPS)' } +]; + +i18n + .use(LanguageDetector) + .use(Backend) + .init({ + fallbackLng: 'en', + debug: false, + keySeparator: null, + nsSeparator: null, + saveMissing: true, + saveMissingTo: 'current', + missingKeyHandler: (lng, ns, key, fallback) => { + const escapedKey = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + // eslint-disable-next-line no-console + if (console && console.group && console.log) { + // eslint-disable-next-line no-console + console.group(`Missing key in ${ lng }`); + // eslint-disable-next-line no-console + console.log(`"${ escapedKey }": |\n ${ fallback }`); + // eslint-disable-next-line no-console + console.groupEnd(); + } + }, + interpolation: { + escapeValue: false + }, + backend: { + loadPath: '{{lng}}', + parse: data => data, + ajax: (lng, options, callback) => { + try { + import(`locales/${ lng }.yaml`).then( + locale => callback(locale, { status: '200' }), + () => callback(null, { status: '404' })); + } + catch (e) { + callback(null, { status: '404' }); + } + } + } + }); + +export default i18n; diff --git a/src/locales/en-AC.yaml b/src/locales/en-AC.yaml new file mode 100644 index 0000000..a0213f6 --- /dev/null +++ b/src/locales/en-AC.yaml @@ -0,0 +1,2 @@ +"Language": | + LANGUAGE diff --git a/src/locales/en.yaml b/src/locales/en.yaml new file mode 100644 index 0000000..b3bad5d --- /dev/null +++ b/src/locales/en.yaml @@ -0,0 +1,2 @@ +"Language": | + Language diff --git a/yarn.lock b/yarn.lock index 49171bd..b8b985b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5649,6 +5649,13 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== +hoist-non-react-statics@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#fba3e7df0210eb9447757ca1a7cb607162f0a364" + integrity sha512-1kXwPsOi0OGQIZNVMPvgWJ9tSnGMiMfJdihqEzrPEXlHOBh9AAHXX/QYmAJTXztnz/K+PQ8ryCb4eGaN6HlGbQ== + dependencies: + react-is "^16.3.2" + hoist-non-react-statics@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -5711,6 +5718,13 @@ html-entities@^1.2.0: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= +html-parse-stringify2@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a" + integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o= + dependencies: + void-elements "^2.0.1" + htmlparser2@^3.9.1: version "3.10.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" @@ -5797,6 +5811,21 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +i18next-browser-languagedetector@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.4.tgz#b02412d7ab15d7d74e1b1317d67d8a244b219ee3" + integrity sha512-wPbtH18FdOuB245I8Bhma5/XSDdN/HpYlX+wga1eMy+slhaFQSnrWX6fp+aYSL2eEuj0RlfHeEVz6Fo/lxAj6A== + +i18next-xhr-backend@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-1.5.1.tgz#50282610780c6a696d880dfa7f4ac1d01e8c3ad5" + integrity sha512-9OLdC/9YxDvTFcgsH5t2BHCODHEotHCa6h7Ly0EUlUC7Y2GS09UeoHOGj3gWKQ3HCqXz8NlH4gOrK3NNc9vPuw== + +i18next@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-13.1.0.tgz#d8656bbc53216ffcd428364fbfb75f75647846a5" + integrity sha512-BuDhYRFReCXJiUJ8GdC2m0wXw4vC/BS6e7UJO+wTrkE3K+92VmJn5p/wxqEE+pdffquh9GYYAMfK9rUlb48pcg== + iconv-lite@0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" @@ -9542,7 +9571,16 @@ react-hot-loader@^4.6.2: shallowequal "^1.0.2" source-map "^0.7.3" -react-is@^16.6.1, react-is@^16.7.0: +react-i18next@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-9.0.2.tgz#6c524c01402549ca010e90539dbe40996e213702" + integrity sha512-gR+GelJzwDUWYyOEnIGnbBA96ZHqkWgHg9t0wVyBAiYVhTrMIyOWX6tBL7zpETwodAXcQKj/Gd4NlgRZqfQnzA== + dependencies: + "@babel/runtime" "^7.1.2" + hoist-non-react-statics "3.0.1" + html-parse-stringify2 "2.0.1" + +react-is@^16.3.2, react-is@^16.6.1, react-is@^16.7.0: version "16.7.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa" integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g== @@ -11419,6 +11457,11 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" +void-elements@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"