diff --git a/scm-ui/package.json b/scm-ui/package.json index d5a642099f..7439e76011 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -7,8 +7,12 @@ "bulma": "^0.7.1", "classnames": "^2.2.5", "history": "^4.7.2", + "i18next": "^11.4.0", + "i18next-browser-languagedetector": "^2.2.2", + "i18next-fetch-backend": "^0.1.0", "react": "^16.4.1", "react-dom": "^16.4.1", + "react-i18next": "^7.9.0", "react-jss": "^8.6.0", "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", @@ -60,7 +64,10 @@ "coveragePathIgnorePatterns": [ "src/tests/.*" ], - "reporters": [ "default", "jest-junit" ] + "reporters": [ + "default", + "jest-junit" + ] }, "jest-junit": { "output": "./target/jest-reports/TEST-all.xml" diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json new file mode 100644 index 0000000000..f436658763 --- /dev/null +++ b/scm-ui/public/locales/en/commons.json @@ -0,0 +1,35 @@ +{ + "login": { + "title": "Login", + "subtitle": "Please login to proceed.", + "logo-alt": "SCM-Manager", + "username-placeholder": "Your Username", + "password-placeholder": "Your Password", + "submit": "Login" + }, + "logout": { + "error": { + "title": "Logout failed", + "subtitle": "Something went wrong during logout" + } + }, + "app": { + "error": { + "title": "Error", + "subtitle": "Unknown error occurred" + } + }, + "error-notification": { + "prefix": "Error" + }, + "loading": { + "alt": "Loading ..." + }, + "logo": { + "alt": "SCM-Manager" + }, + "primary-navigation": { + "users": "Users", + "logout": "Logout" + } +} diff --git a/scm-ui/src/components/ErrorNotification.js b/scm-ui/src/components/ErrorNotification.js index d90caf22a2..9ef3b58653 100644 --- a/scm-ui/src/components/ErrorNotification.js +++ b/scm-ui/src/components/ErrorNotification.js @@ -1,18 +1,20 @@ //@flow import React from "react"; +import { translate } from "react-i18next"; import Notification from "./Notification"; type Props = { + t: string => string, error?: Error }; class ErrorNotification extends React.Component { render() { - const { error } = this.props; + const { t, error } = this.props; if (error) { return ( - Error: {error.message} + {t("error-notification.prefix")}: {error.message} ); } @@ -20,4 +22,4 @@ class ErrorNotification extends React.Component { } } -export default ErrorNotification; +export default translate("commons")(ErrorNotification); diff --git a/scm-ui/src/components/Loading.js b/scm-ui/src/components/Loading.js index d9b370b40f..88fe427941 100644 --- a/scm-ui/src/components/Loading.js +++ b/scm-ui/src/components/Loading.js @@ -1,5 +1,6 @@ //@flow import React from "react"; +import { translate } from "react-i18next"; import injectSheet from "react-jss"; import Image from "../images/loading.svg"; @@ -24,20 +25,21 @@ const styles = { }; type Props = { + t: string => string, classes: any }; class Loading extends React.Component { render() { - const { classes } = this.props; + const { t, classes } = this.props; return (
- Loading ... + {t("loading.alt")}
); } } -export default injectSheet(styles)(Loading); +export default injectSheet(styles)(translate("commons")(Loading)); diff --git a/scm-ui/src/components/Logo.js b/scm-ui/src/components/Logo.js index 3ffdb3139e..8dac21309f 100644 --- a/scm-ui/src/components/Logo.js +++ b/scm-ui/src/components/Logo.js @@ -1,13 +1,17 @@ //@flow import React from "react"; +import { translate } from "react-i18next"; import Image from "../images/logo.png"; -type Props = {}; +type Props = { + t: string => string +}; class Logo extends React.Component { render() { - return SCM-Manager logo; + const { t } = this.props; + return {t("logo.alt")}; } } -export default Logo; +export default translate("commons")(Logo); diff --git a/scm-ui/src/components/PrimaryNavigation.js b/scm-ui/src/components/PrimaryNavigation.js index 3eda2a2937..83be320395 100644 --- a/scm-ui/src/components/PrimaryNavigation.js +++ b/scm-ui/src/components/PrimaryNavigation.js @@ -1,20 +1,30 @@ //@flow import React from "react"; +import { translate } from "react-i18next"; import PrimaryNavigationLink from "./PrimaryNavigationLink"; -type Props = {}; +type Props = { + t: string => string +}; class PrimaryNavigation extends React.Component { render() { + const { t } = this.props; return ( ); } } -export default PrimaryNavigation; +export default translate("commons")(PrimaryNavigation); diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index 4ff9b245f7..f15f0371a1 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -1,6 +1,7 @@ import React, { Component } from "react"; import Main from "./Main"; import { connect } from "react-redux"; +import { translate } from "react-i18next"; import { withRouter } from "react-router-dom"; import { fetchMe } from "../modules/auth"; @@ -17,6 +18,7 @@ type Props = { error: Error, loading: boolean, authenticated?: boolean, + t: string => string, fetchMe: () => void }; @@ -26,7 +28,7 @@ class App extends Component { } render() { - const { entry, loading, error, authenticated } = this.props; + const { entry, loading, error, t, authenticated } = this.props; let content; const navigation = authenticated ? : ""; @@ -36,8 +38,8 @@ class App extends Component { } else if (error) { content = ( ); @@ -72,5 +74,5 @@ export default withRouter( connect( mapStateToProps, mapDispatchToProps - )(App) + )(translate("commons")(App)) ); diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index c8e3775d44..d661ef2b19 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -2,6 +2,7 @@ import React from "react"; import { Redirect, withRouter } from "react-router-dom"; import injectSheet from "react-jss"; +import { translate } from "react-i18next"; import { login } from "../modules/auth"; import { connect } from "react-redux"; @@ -35,6 +36,7 @@ type Props = { loading?: boolean, error?: Error, + t: string => string, classes: any, from: any, @@ -83,7 +85,7 @@ class Login extends React.Component { }; render() { - const { authenticated, loading, error, classes } = this.props; + const { authenticated, loading, error, t, classes } = this.props; if (authenticated) { return this.renderRedirect(); @@ -94,30 +96,30 @@ class Login extends React.Component {
-

Login

-

Please login to proceed.

+

{t("login.title")}

+

{t("login.subtitle")}

SCM-Manager
string, loading: boolean, authenticated: boolean, error?: Error, @@ -20,13 +22,13 @@ class Logout extends React.Component { } render() { - const { authenticated, loading, error } = this.props; + const { authenticated, loading, t, error } = this.props; // TODO logout is called twice if (error) { return ( ); @@ -53,4 +55,4 @@ const mapDispatchToProps = dispatch => { export default connect( mapStateToProps, mapDispatchToProps -)(Logout); +)(translate("commons")(Logout)); diff --git a/scm-ui/src/i18n.js b/scm-ui/src/i18n.js new file mode 100644 index 0000000000..954d47c605 --- /dev/null +++ b/scm-ui/src/i18n.js @@ -0,0 +1,37 @@ +import i18n from "i18next"; +import Backend from "i18next-fetch-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { reactI18nextModule } from "react-i18next"; + +const loadPath = process.env.PUBLIC_URL + "/locales/{{lng}}/{{ns}}.json"; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(reactI18nextModule) + .init({ + fallbackLng: "en", + + // have a common namespace used around the full app + ns: ["commons"], + defaultNS: "commons", + + debug: true, + + interpolation: { + escapeValue: false // not needed for react!! + }, + + react: { + wait: true + }, + + backend: { + loadPath: loadPath, + init: { + credentials: "same-origin" + } + } + }); + +export default i18n; diff --git a/scm-ui/src/index.js b/scm-ui/src/index.js index 564249494a..ea133dd55b 100644 --- a/scm-ui/src/index.js +++ b/scm-ui/src/index.js @@ -4,6 +4,9 @@ import ReactDOM from "react-dom"; import App from "./containers/App"; import registerServiceWorker from "./registerServiceWorker"; +import { I18nextProvider } from "react-i18next"; +import i18n from "./i18n"; + import { Provider } from "react-redux"; import createHistory from "history/createBrowserHistory"; @@ -30,10 +33,12 @@ if (!root) { ReactDOM.render( - {/* ConnectedRouter will use the store from Provider automatically */} - - - + + {/* ConnectedRouter will use the store from Provider automatically */} + + + + , root ); diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 4063d5135d..ff76d0efc1 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -3574,7 +3574,7 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" -hoist-non-react-statics@^2.5.0: +hoist-non-react-statics@^2.3.1, 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" @@ -3630,6 +3630,12 @@ html-minifier@^3.2.3: relateurl "0.2.x" uglify-js "3.4.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.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.29.0.tgz#e987f421853d3b6938c8c4c8171842e5fd17af23" @@ -3742,6 +3748,24 @@ hyphenate-style-name@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b" +i18next-browser-languagedetector@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.2.tgz#b2599e3e8bc8b66038010e9758c28222688df6aa" + +i18next-fetch-backend@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/i18next-fetch-backend/-/i18next-fetch-backend-0.1.0.tgz#18b67920d0e605e616f93bbdf897e59adf9c9c05" + dependencies: + i18next-xhr-backend "^1.4.3" + +i18next-xhr-backend@^1.4.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-1.5.1.tgz#50282610780c6a696d880dfa7f4ac1d01e8c3ad5" + +i18next@^11.4.0: + version "11.4.0" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.4.0.tgz#9179bc27b74158d773893356f19b039bedbc355a" + iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -6464,6 +6488,14 @@ react-error-overlay@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" +react-i18next@^7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.9.0.tgz#9e4bdfbfbc0d084eddf13d1cd337cbd4beea6232" + dependencies: + hoist-non-react-statics "^2.3.1" + html-parse-stringify2 "2.0.1" + prop-types "^15.6.0" + react-is@^16.4.1: version "16.4.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" @@ -8023,6 +8055,10 @@ 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" + walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"