Adding i18next

This commit is contained in:
Jeff Avallone 2018-02-11 18:37:07 -05:00
parent 63766e84e9
commit 355ef79d20
24 changed files with 195 additions and 66 deletions

View File

@ -65,7 +65,9 @@
"favicons-webpack-plugin-cesco": "^0.0.6", "favicons-webpack-plugin-cesco": "^0.0.6",
"feather-icons": "^4.5.0", "feather-icons": "^4.5.0",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^2.30.1",
"i18next": "^10.3.0",
"jest": "^22.2.2", "jest": "^22.2.2",
"json-loader": "^0.5.7",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"postcss-cssnext": "^3.1.0", "postcss-cssnext": "^3.1.0",
"postcss-loader": "^2.1.0", "postcss-loader": "^2.1.0",
@ -73,12 +75,14 @@
"react": "^16.2.0", "react": "^16.2.0",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"react-ga": "^2.4.1", "react-ga": "^2.4.1",
"react-i18next": "^7.3.6",
"style-loader": "^0.20.1", "style-loader": "^0.20.1",
"svg-react-loader": "^0.4.5", "svg-react-loader": "^0.4.5",
"uglifyjs-webpack-plugin": "^1.1.8", "uglifyjs-webpack-plugin": "^1.1.8",
"webpack": "^3.10.0", "webpack": "^3.10.0",
"webpack-merge": "^4.1.1", "webpack-merge": "^4.1.1",
"workbox-webpack-plugin": "^2.1.2" "workbox-webpack-plugin": "^2.1.2",
"yaml-loader": "^0.5.0"
}, },
"devDependencies": { "devDependencies": {
"http-server": "^0.11.1", "http-server": "^0.11.1",

View File

@ -1,20 +1,24 @@
import React from 'react'; import React from 'react';
import { translate, Trans } from 'react-i18next';
const Footer = () => ( const Footer = () => (
<footer> <footer>
<ul className="inline with-separator"> <ul className="inline with-separator">
<li> <li>
Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a> <Trans>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
</li> </li>
<li> <li>
<Trans i18nKey="Generated images licensed">
Generated images licensed: <a rel="license" href="http://creativecommons.org/licenses/by/3.0/"> Generated images licensed: <a rel="license" href="http://creativecommons.org/licenses/by/3.0/">
<img <img
alt="Creative Commons CC-BY-3.0 License" alt="Creative Commons CC-BY-3.0 License"
src="https://licensebuttons.net/l/by/3.0/80x15.png" /> src="https://licensebuttons.net/l/by/3.0/80x15.png" />
</a> </a>
</Trans>
</li> </li>
</ul> </ul>
</footer> </footer>
); );
export default Footer; export default translate()(Footer);
export { Footer };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import Footer from './Footer'; import { Footer } from './Footer';
test('Footer rendering', () => { test('Footer rendering', () => {
const component = shallow( const component = shallow(

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { translate, Trans } from 'react-i18next';
import GithubIcon from 'feather-icons/dist/icons/github.svg'; import GithubIcon from 'feather-icons/dist/icons/github.svg';
@ -10,10 +11,11 @@ const Header = () => (
<ul className="inline"> <ul className="inline">
<li><a href="https://github.com/javallone/regexper-static"> <li><a href="https://github.com/javallone/regexper-static">
<GithubIcon/>Source on GitHub <GithubIcon/><Trans>Source on GitHub</Trans>
</a></li> </a></li>
</ul> </ul>
</header> </header>
); );
export default Header; export default translate()(Header);
export { Header };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import Header from './Header'; import { Header } from './Header';
const env = { ...process.env }; const env = { ...process.env };

View File

@ -4,9 +4,6 @@ import PropTypes from 'prop-types';
import pkg from '../../package.json'; import pkg from '../../package.json';
import Header from '../components/Header';
import Footer from '../components/Footer';
const PageTemplate = ({ title, children }) => ( const PageTemplate = ({ title, children }) => (
<html> <html>
<head> <head>
@ -20,9 +17,7 @@ const PageTemplate = ({ title, children }) => (
</head> </head>
<body data-build-id={ process.env.BUILD_ID }> <body data-build-id={ process.env.BUILD_ID }>
<div id="root"> <div id="root">
<Header/>
{ children } { children }
<Footer/>
</div> </div>
</body> </body>
</html> </html>

View File

@ -21,7 +21,6 @@ class RavenBoundary extends React.Component {
if (error) { if (error) {
const errorProps = { const errorProps = {
heading: 'An error has occurred.',
details: { extra: errorInfo }, details: { extra: errorInfo },
error error
}; };

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import { Raven } from '../sentry'; import { Raven } from '../sentry';
import Message from './Message'; import Message from './Message';
@ -20,10 +21,12 @@ class RavenError extends React.Component {
} }
render() { render() {
const { heading } = this.props; const { t } = this.props;
return <Message className="error" icon={ AlertIcon } heading={ heading }> return <Message className="error" icon={ AlertIcon } heading={ t('An error has occurred') }>
<p>This error has been logged. You may also <a href="#error-report" onClick={ this.reportError }>fill out a report</a>.</p> <p><Trans i18nKey="This error has been logged">
This error has been logged. You may also <a href="#error-report" onClick={ this.reportError }>fill out a report</a>.
</Trans></p>
</Message>; </Message>;
} }
} }
@ -31,7 +34,8 @@ class RavenError extends React.Component {
RavenError.propTypes = { RavenError.propTypes = {
error: PropTypes.object.isRequired, error: PropTypes.object.isRequired,
details: PropTypes.object.isRequired, details: PropTypes.object.isRequired,
heading: PropTypes.string.isRequired t: PropTypes.func
}; };
export default RavenError; export default translate()(RavenError);
export { RavenError };

View File

@ -3,11 +3,12 @@ import { shallow } from 'enzyme';
jest.mock('../sentry'); jest.mock('../sentry');
import RavenError from './RavenError'; import { RavenError } from './RavenError';
import { Raven } from '../sentry'; import { Raven } from '../sentry';
const testError = { error: 'test error' }; const testError = { error: 'test error' };
const testDetails = { details: 'test details' }; const testDetails = { details: 'test details' };
const translate = v => `translate(${ v })`;
describe('RavenError', () => { describe('RavenError', () => {
test('rendering', () => { test('rendering', () => {
@ -15,7 +16,7 @@ describe('RavenError', () => {
<RavenError <RavenError
error={ testError } error={ testError }
details={ testDetails } details={ testDetails }
heading="Test error"/> t={ translate }/>
); );
expect(component).toMatchSnapshot(); expect(component).toMatchSnapshot();
}); });
@ -25,7 +26,7 @@ describe('RavenError', () => {
<RavenError <RavenError
error={ testError } error={ testError }
details={ testDetails } details={ testDetails }
heading="Test error"/> t={ translate }/>
); );
expect(Raven.captureException).toHaveBeenCalledWith(testError, testDetails); expect(Raven.captureException).toHaveBeenCalledWith(testError, testDetails);
}); });
@ -36,7 +37,7 @@ describe('RavenError', () => {
<RavenError <RavenError
error={ testError } error={ testError }
details={ testDetails } details={ testDetails }
heading="Test error"/> t={ translate }/>
); );
const eventObj = { preventDefault: jest.fn() }; const eventObj = { preventDefault: jest.fn() };
component.find('a').simulate('click', eventObj); component.find('a').simulate('click', eventObj);

View File

@ -6,14 +6,19 @@ exports[`Footer rendering 1`] = `
className="inline with-separator" className="inline with-separator"
> >
<li> <li>
<Trans>
Created by Created by
<a <a
href="mailto:jeff.avallone@gmail.com" href="mailto:jeff.avallone@gmail.com"
> >
Jeff Avallone Jeff Avallone
</a> </a>
</Trans>
</li> </li>
<li> <li>
<Trans
i18nKey="Generated images licensed"
>
Generated images licensed: Generated images licensed:
<a <a
href="http://creativecommons.org/licenses/by/3.0/" href="http://creativecommons.org/licenses/by/3.0/"
@ -24,6 +29,7 @@ exports[`Footer rendering 1`] = `
src="https://licensebuttons.net/l/by/3.0/80x15.png" src="https://licensebuttons.net/l/by/3.0/80x15.png"
/> />
</a> </a>
</Trans>
</li> </li>
</ul> </ul>
</footer> </footer>

View File

@ -20,7 +20,9 @@ exports[`Header rendering 1`] = `
href="https://github.com/javallone/regexper-static" href="https://github.com/javallone/regexper-static"
> >
<Component /> <Component />
<Trans>
Source on GitHub Source on GitHub
</Trans>
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -28,11 +28,9 @@ exports[`PageTemplate rendering 1`] = `
<div <div
id="root" id="root"
> >
<Header />
<p> <p>
Content Content
</p> </p>
<Footer />
</div> </div>
</body> </body>
</html> </html>
@ -67,11 +65,9 @@ exports[`PageTemplate rendering with title 1`] = `
<div <div
id="root" id="root"
> >
<Header />
<p> <p>
Content Content
</p> </p>
<Footer />
</div> </div>
</body> </body>
</html> </html>

View File

@ -3,7 +3,7 @@
exports[`RavenBoundary rendering (with error) 1`] = `<Child />`; exports[`RavenBoundary rendering (with error) 1`] = `<Child />`;
exports[`RavenBoundary rendering (with error) 2`] = ` exports[`RavenBoundary rendering (with error) 2`] = `
<RavenError <Translate(RavenError)
details={ details={
Object { Object {
"extra": Object { "extra": Object {
@ -16,7 +16,6 @@ exports[`RavenBoundary rendering (with error) 2`] = `
"error": "test error", "error": "test error",
} }
} }
heading="An error has occurred."
/> />
`; `;

View File

@ -3,10 +3,13 @@
exports[`RavenError rendering 1`] = ` exports[`RavenError rendering 1`] = `
<Message <Message
className="error" className="error"
heading="Test error" heading="translate(An error has occurred)"
icon={[Function]} icon={[Function]}
> >
<p> <p>
<Trans
i18nKey="This error has been logged"
>
This error has been logged. You may also This error has been logged. You may also
<a <a
href="#error-report" href="#error-report"
@ -15,6 +18,7 @@ exports[`RavenError rendering 1`] = `
fill out a report fill out a report
</a> </a>
. .
</Trans>
</p> </p>
</Message> </Message>
`; `;

14
src/i18n.js Normal file
View File

@ -0,0 +1,14 @@
import i18n from 'i18next';
import { reactI18nextModule } from 'react-i18next';
import locales from './locales';
i18n
.use(reactI18nextModule)
.init({
fallbackLng: 'en',
debug: (process.env.NODE_ENV !== 'production'),
resources: locales
});
export default i18n;

View File

@ -0,0 +1,7 @@
404 Page Not Found: 404 Page Not Found
An error has occurred: An error has occurred
Created by <1>Jeff Avallone</1>: Created by <1>Jeff Avallone</1>
Generated images licensed: "Generated images licensed: <1><0></0></1>"
Source on GitHub: Source on GitHub
The page you have requested could not be found: The page you have requested could not be found
This error has been logged: This error has been logged. You may also <1>fill out a report</1>.

5
src/locales/index.js Normal file
View File

@ -0,0 +1,5 @@
export default {
en: {
translation: require('./en/translation.yaml')
}
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import { translate, Trans } from 'react-i18next';
import Message from '../../components/Message';
import AlertIcon from 'feather-icons/dist/icons/alert-octagon.svg';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
const Component = ({ t }) => ( // eslint-disable-line react/prop-types
<React.Fragment>
<Header/>
<Message className="error" icon={ AlertIcon } heading={ t('404 Page Not Found') }>
<p><Trans>The page you have requested could not be found</Trans></p>
</Message>
<Footer/>
</React.Fragment>
);
export default translate()(Component);

View File

@ -1,6 +1,25 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Component from './Component';
import RavenBoundary from '../../components/RavenBoundary';
import '../../style.css'; import '../../style.css';
import '../../i18n';
import { setupGA } from '../../analytics'; import { setupGA } from '../../analytics';
import { setupRaven } from '../../sentry'; import { Raven, setupRaven } from '../../sentry';
setupRaven(); setupRaven();
setupGA();
try {
setupGA();
ReactDOM.render(
<RavenBoundary>
<Component/>
</RavenBoundary>,
document.getElementById('root'));
}
catch (e) {
Raven.captureException(e);
}

View File

@ -1,14 +1,13 @@
import 'babel-register'; import 'babel-register';
import React from 'react'; import React from 'react';
import '../../i18n';
import PageTemplate, { renderToString } from '../../components/PageTemplate'; import PageTemplate, { renderToString } from '../../components/PageTemplate';
import Message from '../../components/Message'; import Component from './Component';
import AlertIcon from 'feather-icons/dist/icons/alert-octagon.svg';
export default renderToString( export default renderToString(
<PageTemplate> <PageTemplate>
<Message className="error" icon={ AlertIcon } heading="404 Page Not Found"> <Component/>
<p>The page you have requested could not be found</p>
</Message>
</PageTemplate> </PageTemplate>
); );

View File

@ -5,6 +5,7 @@ import App from '../../components/App';
import RavenBoundary from '../../components/RavenBoundary'; import RavenBoundary from '../../components/RavenBoundary';
import '../../style.css'; import '../../style.css';
import '../../i18n';
import { setupServiceWorker } from '../../service-worker'; import { setupServiceWorker } from '../../service-worker';
import { setupGA } from '../../analytics'; import { setupGA } from '../../analytics';
import { Raven, setupRaven } from '../../sentry'; import { Raven, setupRaven } from '../../sentry';

View File

@ -1,12 +1,17 @@
import 'babel-register'; import 'babel-register';
import React from 'react'; import React from 'react';
import '../../i18n';
import PageTemplate, { renderToString } from '../../components/PageTemplate'; import PageTemplate, { renderToString } from '../../components/PageTemplate';
import Message from '../../components/Message'; import Message from '../../components/Message';
import AlertIcon from 'feather-icons/dist/icons/alert-octagon.svg'; import AlertIcon from 'feather-icons/dist/icons/alert-octagon.svg';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
export default renderToString( export default renderToString(
<PageTemplate> <PageTemplate>
<Header/>
<noscript> <noscript>
<Message className="error" icon={ AlertIcon } heading="JavaScript Required"> <Message className="error" icon={ AlertIcon } heading="JavaScript Required">
<p>You need to enable JavaScript to use Regexper.</p> <p>You need to enable JavaScript to use Regexper.</p>
@ -18,5 +23,6 @@ export default renderToString(
<p>Most popular ad blockers will prevent these tools from sending any tracking data, and doing so will <b>not</b> impact the performance of this app. Regexper is not supported by ad revenue or sales of any kind. The information collected by these tools is used to monitor application performance, determine browser support, and collect error reports.</p> <p>Most popular ad blockers will prevent these tools from sending any tracking data, and doing so will <b>not</b> impact the performance of this app. Regexper is not supported by ad revenue or sales of any kind. The information collected by these tools is used to monitor application performance, determine browser support, and collect error reports.</p>
</Message> </Message>
</noscript> </noscript>
<Footer/>
</PageTemplate> </PageTemplate>
); );

View File

@ -139,6 +139,17 @@ module.exports = {
{ {
test: /\.svg$/, test: /\.svg$/,
loader: 'svg-react-loader' loader: 'svg-react-loader'
},
{
test: /\.yaml$/,
use: [
{
loader: 'json-loader'
},
{
loader: 'yaml-loader'
}
]
} }
] ]
} }

View File

@ -3486,6 +3486,10 @@ hoek@4.x.x:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoist-non-react-statics@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
home-or-tmp@^2.0.0: home-or-tmp@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@ -3533,6 +3537,12 @@ html-minifier@^3.2.3:
relateurl "0.2.x" relateurl "0.2.x"
uglify-js "3.3.x" uglify-js "3.3.x"
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"
dependencies:
void-elements "^2.0.1"
html-webpack-plugin@^2.30.1: html-webpack-plugin@^2.30.1:
version "2.30.1" version "2.30.1"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz#7f9c421b7ea91ec460f56527d78df484ee7537d5" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz#7f9c421b7ea91ec460f56527d78df484ee7537d5"
@ -3652,6 +3662,10 @@ husky@^0.14.3:
normalize-path "^1.0.0" normalize-path "^1.0.0"
strip-indent "^2.0.0" strip-indent "^2.0.0"
i18next@^10.3.0:
version "10.3.0"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-10.3.0.tgz#6866d14f6ae7b3629921f78bc028d04914f0654c"
iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
version "0.4.19" version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@ -4437,7 +4451,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1: js-yaml@^3.4.3, js-yaml@^3.5.2, js-yaml@^3.7.0, js-yaml@^3.9.1:
version "3.10.0" version "3.10.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
dependencies: dependencies:
@ -4494,7 +4508,7 @@ jsesc@~0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
json-loader@^0.5.4: json-loader@^0.5.4, json-loader@^0.5.7:
version "0.5.7" version "0.5.7"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
@ -6543,6 +6557,14 @@ react-ga@^2.4.1:
prop-types "^15.6.0" prop-types "^15.6.0"
react "^15.6.2 || ^16.0" react "^15.6.2 || ^16.0"
react-i18next@^7.3.6:
version "7.3.6"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.3.6.tgz#3e4fdd0be7735aa53343720684247ba7f6cfd51a"
dependencies:
hoist-non-react-statics "2.3.1"
html-parse-stringify2 "2.0.1"
prop-types "^15.6.0"
react-reconciler@^0.7.0: react-reconciler@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d" resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d"
@ -8070,6 +8092,10 @@ vm-browserify@0.0.4:
dependencies: dependencies:
indexof "0.0.1" 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"
w3c-hr-time@^1.0.1: w3c-hr-time@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
@ -8415,6 +8441,12 @@ yallist@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
yaml-loader@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/yaml-loader/-/yaml-loader-0.5.0.tgz#86b1982d84a8e429e6647d93de9a0169e1c15827"
dependencies:
js-yaml "^3.5.2"
yargs-parser@^4.2.0: yargs-parser@^4.2.0:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"