diff --git a/interface/.env.development b/interface/.env.development index b12cfd04..39d7e88a 100644 --- a/interface/.env.development +++ b/interface/.env.development @@ -1,4 +1,4 @@ # Change the IP address to that of your ESP device to enable local development of the UI. # Remember to also enable CORS in platformio.ini before uploading the code to the device. -REACT_APP_HTTP_ROOT=http://192.168.0.88 -REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.88 +REACT_APP_HTTP_ROOT=http://192.168.0.7 +REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.7 diff --git a/interface/package-lock.json b/interface/package-lock.json index 60dbfd77..8d65aee2 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -1178,6 +1178,49 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, + "@formatjs/ecma402-abstract": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.2.1.tgz", + "integrity": "sha512-LvbwgHxprafjceDfOC7yMl4sP5al71rAWahXk3qug5bV020aCq64WjqAo+zNnkIJk8hqK2pFKnNdDsT58HZJQw==" + }, + "@formatjs/intl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-1.3.1.tgz", + "integrity": "sha512-W1m8vLAQHjrox3ZrJRD6ArMp2AOvUCBeeID7NNhcezZ2fWXtzIKi8k9jeptkO54HZbueLcpshWb2gbUSR5Xy0Q==", + "requires": { + "@formatjs/ecma402-abstract": "^1.2.1", + "@formatjs/intl-displaynames": "^3.3.7", + "@formatjs/intl-listformat": "^4.2.6", + "@formatjs/intl-relativetimeformat": "^7.2.6", + "fast-memoize": "^2.5.2", + "intl-messageformat": "^9.3.7", + "intl-messageformat-parser": "^6.0.6" + } + }, + "@formatjs/intl-displaynames": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-3.3.7.tgz", + "integrity": "sha512-sMdV3QaFy2RMOZ6YaRYInDzThEEIiD8vZjue20/CYvxgNKw3ZIZROvpEaKHvHr197Si3RFaxhAGUWk/lI7NAeA==", + "requires": { + "@formatjs/ecma402-abstract": "^1.2.1" + } + }, + "@formatjs/intl-listformat": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-4.2.6.tgz", + "integrity": "sha512-4JGDYVwZyEMGpwhUKXIcSXjRVuSrz5ox1rQCQVzj0CzgEoojrQoaUkcuW+Vj89JlpCtRS8NS46d3CWLUSDC+2g==", + "requires": { + "@formatjs/ecma402-abstract": "^1.2.1" + } + }, + "@formatjs/intl-relativetimeformat": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-7.2.6.tgz", + "integrity": "sha512-SHwrzk9HuAUwl3/qfupU3ZnW4ZgVOpI2+3gwGmvoPOmAlKFlo7liSCszA5hcRpsnhjS66BqUzfx6BWOwzYvmKQ==", + "requires": { + "@formatjs/ecma402-abstract": "^1.2.1" + } + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -1660,6 +1703,15 @@ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.7.tgz", "integrity": "sha512-2xtoL22/3Mv6a70i4+4RB7VgbDDORoWwjcqeNysojZA0R7NK17RbY5Gof/2QiFfJgX+KkWghbwJ+d/2SB8Ndzg==" }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -5828,6 +5880,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -6987,6 +7044,23 @@ "side-channel": "^1.0.2" } }, + "intl-messageformat": { + "version": "9.3.7", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.3.7.tgz", + "integrity": "sha512-DUc+BJ6QN/IyT05gyKTuSJuauuYheV/5IhP+KEwmLhaJzONu0U/nnL5P6L5Ck9DXAx8iy7HM0CwFUctD7CmqZw==", + "requires": { + "fast-memoize": "^2.5.2", + "intl-messageformat-parser": "^6.0.6" + } + }, + "intl-messageformat-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.0.6.tgz", + "integrity": "sha512-XPAgYvCTSwgr92zzy5sfaglUu4uKjIWXHNTenEQRTo5t3o2TGxuPYPruxZqnvSLgnlAegiT/hBemmPcnFPNjAg==", + "requires": { + "@formatjs/ecma402-abstract": "^1.2.1" + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -11072,6 +11146,24 @@ "react-lifecycles-compat": "^3.0.2" } }, + "react-intl": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.8.1.tgz", + "integrity": "sha512-NGwtadXCoPUYHqQFNYR0Rbwv08QkCLECeBPUUFndhJIudvgmVqNrX3x6A3LMEYY76vLOovmoy+xiTAx7yWGkBg==", + "requires": { + "@formatjs/ecma402-abstract": "^1.2.1", + "@formatjs/intl": "^1.3.1", + "@formatjs/intl-displaynames": "^3.3.7", + "@formatjs/intl-listformat": "^4.2.6", + "@formatjs/intl-relativetimeformat": "^7.2.6", + "@types/hoist-non-react-statics": "^3.3.1", + "fast-memoize": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "intl-messageformat": "^9.3.7", + "intl-messageformat-parser": "^6.0.6", + "shallow-equal": "^1.2.1" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -12007,6 +12099,11 @@ } } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/interface/package.json b/interface/package.json index 5a7b0798..af8d41ed 100644 --- a/interface/package.json +++ b/interface/package.json @@ -23,6 +23,7 @@ "react-dom": "^16.13.1", "react-dropzone": "^11.0.1", "react-form-validator-core": "^0.6.4", + "react-intl": "^5.8.1", "react-material-ui-form-validator": "^2.0.10", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", diff --git a/interface/src/App.tsx b/interface/src/App.tsx index 532050e0..d2caf775 100644 --- a/interface/src/App.tsx +++ b/interface/src/App.tsx @@ -9,6 +9,7 @@ import AppRouting from './AppRouting'; import CustomMuiTheme from './CustomMuiTheme'; import { PROJECT_NAME } from './api'; import FeaturesWrapper from './features/FeaturesWrapper'; +import I18n from './i18n/I18n'; // this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid. const unauthorizedRedirect = () => ; @@ -35,12 +36,14 @@ class App extends Component { )}> - - - - - - + + + + + + + + ); diff --git a/interface/src/SignIn.tsx b/interface/src/SignIn.tsx index e4df446f..6d1ddc9c 100644 --- a/interface/src/SignIn.tsx +++ b/interface/src/SignIn.tsx @@ -7,8 +7,9 @@ import { Paper, Typography, Fab } from '@material-ui/core'; import ForwardIcon from '@material-ui/icons/Forward'; import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext'; -import {PasswordValidator} from './components'; +import { PasswordValidator } from './components'; import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api'; +import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; const styles = (theme: Theme) => createStyles({ signInPage: { @@ -39,7 +40,7 @@ const styles = (theme: Theme) => createStyles({ } }); -type SignInProps = WithSnackbarProps & WithStyles & AuthenticationContextProps; +type SignInProps = WrappedComponentProps & WithSnackbarProps & WithStyles & AuthenticationContextProps; interface SignInState { username: string, @@ -68,7 +69,7 @@ class SignIn extends Component { onSubmit = () => { const { username, password } = this.state; - const { authenticationContext } = this.props; + const { authenticationContext, intl } = this.props; this.setState({ processing: true }); fetch(SIGN_IN_ENDPOINT, { method: 'POST', @@ -81,9 +82,14 @@ class SignIn extends Component { if (response.status === 200) { return response.json(); } else if (response.status === 401) { - throw Error("Invalid credentials."); + throw Error(intl.formatMessage( + { id: 'signIn.invalidCredentials', defaultMessage: 'Invalid credentials' }) + ); } else { - throw Error("Invalid status code: " + response.status); + throw Error(intl.formatMessage( + { id: 'signIn.invalidStatusCode', defaultMessage: 'Invalid status code: {code}' }, + { code: response.status }) + ); } }).then(json => { authenticationContext.signIn(json.access_token); @@ -98,7 +104,7 @@ class SignIn extends Component { render() { const { username, password, processing } = this.state; - const { classes } = this.props; + const { classes, intl } = this.props; return (
@@ -107,9 +113,9 @@ class SignIn extends Component { { { /> - Sign In + @@ -144,4 +150,4 @@ class SignIn extends Component { } -export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn))); +export default injectIntl(withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)))); diff --git a/interface/src/i18n/I18n.tsx b/interface/src/i18n/I18n.tsx new file mode 100644 index 00000000..cd8efc78 --- /dev/null +++ b/interface/src/i18n/I18n.tsx @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl'; +import { messages } from './messages'; + +const defaultLocale = "es"; + +const cache = createIntlCache() + +export const intl = createIntl({ + locale: defaultLocale, + defaultLocale: defaultLocale, + messages: messages[defaultLocale] +}, cache) + +interface LanguageWrapperState { + locale: string; +}; + +class I18n extends Component<{}, LanguageWrapperState> { + + state: LanguageWrapperState = { locale: defaultLocale }; + + // load locale from local storage here + componentDidMount = () => { + + } + + selectLanguage = (locale: string) => { + intl.locale = locale; + intl.messages = messages[locale]; + this.setState({ locale }) + } + + render() { + const { locale } = this.state; + return ( + + {this.props.children} + + ); + } + +} + +export default I18n; diff --git a/interface/src/i18n/messages.ts b/interface/src/i18n/messages.ts new file mode 100644 index 00000000..3ff81c06 --- /dev/null +++ b/interface/src/i18n/messages.ts @@ -0,0 +1,8 @@ +import { merge } from 'lodash'; +import { signInMessages } from '../messages'; +import { projectMessages } from '../project/messages'; + +export const messages: Record> = merge( + signInMessages, + projectMessages +); diff --git a/interface/src/messages.ts b/interface/src/messages.ts new file mode 100644 index 00000000..64e81aa0 --- /dev/null +++ b/interface/src/messages.ts @@ -0,0 +1,11 @@ +export const signInMessages: Record> = { + es: { + "signIn.invalidCredentials": "Credenciales no válidas", + "signIn.invalidStatusCode": "Codigo invalido: {code}", + "signIn.password": "Contraseña", + "signIn.passwordRequired": "Se requiere contraseña", + "signIn.signIn": "Registrarse", + "signIn.username": "Nombre de usuario", + "signIn.usernameRequired": "Se requiere nombre de usuario" + } +}; diff --git a/interface/src/project/DemoProject.tsx b/interface/src/project/DemoProject.tsx index 74f25e53..2568f87b 100644 --- a/interface/src/project/DemoProject.tsx +++ b/interface/src/project/DemoProject.tsx @@ -11,21 +11,37 @@ import DemoInformation from './DemoInformation'; import LightStateRestController from './LightStateRestController'; import LightStateWebSocketController from './LightStateWebSocketController'; import LightMqttSettingsController from './LightMqttSettingsController'; +import { WrappedComponentProps, injectIntl } from 'react-intl'; -class DemoProject extends Component { +type DemoProjectProps = RouteComponentProps & WrappedComponentProps; + +class DemoProject extends Component { handleTabChange = (event: React.ChangeEvent<{}>, path: string) => { this.props.history.push(path); }; render() { + const { intl } = this.props; return ( - - - - + + + + @@ -40,4 +56,4 @@ class DemoProject extends Component { } -export default DemoProject; +export default injectIntl(DemoProject); diff --git a/interface/src/project/messages.ts b/interface/src/project/messages.ts new file mode 100644 index 00000000..943aaf93 --- /dev/null +++ b/interface/src/project/messages.ts @@ -0,0 +1,8 @@ +export const projectMessages: Record> = { + es: { + "project.information": "Información", + "project.restController": "Controlador REST", + "project.webSocketController":"Controlador WebSocket", + "project.mqttController":"Controlador MQTT" + } +};