diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +10 diff --git a/package-lock.json b/package-lock.json index 5e64d5b..5659756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3306,6 +3306,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz", + "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -5134,6 +5143,35 @@ "bser": "^2.0.0" } }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "http://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + } + } + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -6732,6 +6770,11 @@ } } }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz", @@ -7076,6 +7119,14 @@ } } }, + "html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "requires": { + "void-elements": "^2.0.1" + } + }, "html-webpack-plugin": { "version": "4.0.0-alpha.2", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-alpha.2.tgz", @@ -7460,6 +7511,16 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "i18next": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-12.0.0.tgz", + "integrity": "sha512-Zy/nFpmBZxgmi6k9HkHbf+MwvAwiY5BDzNjNfvyLPKyalc2YBwwZtblESDlTKLDO8XSv23qYRY2uZcADDlRSjQ==" + }, + "i18next-browser-languagedetector": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.4.tgz", + "integrity": "sha512-wPbtH18FdOuB245I8Bhma5/XSDdN/HpYlX+wga1eMy+slhaFQSnrWX6fp+aYSL2eEuj0RlfHeEVz6Fo/lxAj6A==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -12584,6 +12645,32 @@ "prop-types": "^15.6.2" } }, + "react-i18next": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-8.3.8.tgz", + "integrity": "sha512-ZcSpakSBcDxPJkl34fv/SI0TaoTDvVDrk4WpDF+WElorine+dHUjGMAA6RG5Km2KcLNW1t4GLunHprgKiqDrSw==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "0.2.3", + "hoist-non-react-statics": "3.0.1", + "html-parse-stringify2": "2.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.1.5.tgz", + "integrity": "sha512-xKnPpXG/pvK1B90JkwwxSGii90rQGKtzcMt2gI5G6+M0REXaq6rOHsGC2ay6/d0Uje7zzvSzjEzfR3ENhFlrfA==", + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + } + } + }, "react-is": { "version": "16.6.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.0.tgz", @@ -15304,6 +15391,11 @@ "integrity": "sha512-JZHJtA6ZL15+Q3Dqkbh8iCUmvxD3iJ7ujXS+fVkKnwIVAdHc5BJTDNM0aTrnr2luKulFjU7W+SRhDZvi66Ru7Q==", "dev": true }, + "ua-parser-js": { + "version": "0.7.19", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz", + "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==" + }, "uglify-js": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", @@ -15725,6 +15817,11 @@ "indexof": "0.0.1" } }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "w3c-hr-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", diff --git a/package.json b/package.json index 17ff737..3d3ddb5 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "dependencies": { "classnames": "^2.2.6", "firebase": "^5.5.6", + "i18next": "^12.0.0", + "i18next-browser-languagedetector": "^2.2.4", "lodash.samplesize": "^4.2.0", "node-sass": "^4.9.4", "prop-types": "^15.6.2", @@ -26,6 +28,7 @@ "react-ga": "^2.5.3", "react-google-button": "^0.5.3", "react-grid-system": "^4.3.1", + "react-i18next": "^8.3.8", "react-redux": "^5.1.0", "react-router-dom": "^4.3.1", "react-scripts": "2.1.0", diff --git a/src/i18n.tsx b/src/i18n.tsx new file mode 100644 index 0000000..c99533f --- /dev/null +++ b/src/i18n.tsx @@ -0,0 +1,39 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n.use(LanguageDetector).init({ + // we init with resources + resources: { + en: { + translations: { + LastScore: 'Last score', + SessionsCompleted: 'Sessions completed', + }, + }, + de: { + translations: { + LastScore: 'Letzter Punktestand', + SessionsCompleted: 'Sitzungen abgeschlossen', + }, + }, + }, + fallbackLng: 'en', + debug: false, + + // have a common namespace used around the full app + ns: ['translations'], + defaultNS: 'translations', + + keySeparator: false, // we use content as keys + + interpolation: { + escapeValue: false, // not needed for react!! + formatSeparator: ',', + }, + + react: { + wait: true, + }, +}); + +export default i18n; diff --git a/src/index.tsx b/src/index.tsx index 6ff15a5..e539686 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,6 +26,9 @@ import firebaseCred from './firebase-cred.json'; import ga from './ga-cred.json'; import configureStore from './store/configureStore'; +import { I18nextProvider } from 'react-i18next'; +import i18n from './i18n'; + const config = firebaseCred; const store = configureStore(); @@ -35,24 +38,28 @@ firebase.initializeApp(config); firebase.auth().onAuthStateChanged(user => { user = user || ({} as User); store.dispatch(userPreferencesFetchRequestAction({ uid: user.uid })); - ReactDOM.render( + + const App = () => ( -
-
- - - } /> - - } /> - - - - - -
+ +
+
+ + + } /> + + } /> + + + + + +
+
-
, - document.getElementById('root') + ); + + ReactDOM.render(, document.getElementById('root')); }); diff --git a/src/pages/TypingTest/index.tsx b/src/pages/TypingTest/index.tsx index 8a542c5..975a32a 100644 --- a/src/pages/TypingTest/index.tsx +++ b/src/pages/TypingTest/index.tsx @@ -4,6 +4,7 @@ import ReactGA from 'react-ga'; import { connect } from 'react-redux'; import words from '../../data/words'; import Text from './components/Text'; +import { translate, Trans } from 'react-i18next'; import { User } from 'firebase'; @@ -12,6 +13,7 @@ import * as actionTypes from '../../actions/actionTypes'; import './style.css'; interface TypingTestProps { + t: (key: string) => string; user: User; updateScoreboard: (user: User, score: number) => any; saveScore: (userId: string, score: number) => any; @@ -213,15 +215,19 @@ class TypingTest extends Component { render() { const { letters, index, score, error } = this.state; - const { user, sessionsCompleted } = this.props; + const { t, user, sessionsCompleted } = this.props; const loggedIn = user && user.uid; return (
-

Last score: {score}

-

Sessions completed: {sessionsCompleted}

+

+ {t('LastScore')}: {score} +

+

+ {t('SessionsCompleted')}: {sessionsCompleted} +

{!loggedIn &&
Please log in to save your score!
} {error &&
{error}
} @@ -251,4 +257,4 @@ export const Unwrapped = TypingTest; export default connect( mapStateToProps, mapDispatchToProps -)(TypingTest); +)(translate('translations')(TypingTest));