diff --git a/services/client/index.html b/services/client/index.html index e4b78ea..bd9ff54 100644 --- a/services/client/index.html +++ b/services/client/index.html @@ -1,13 +1,13 @@ - + - - - - - Vite + React + TS - - -
- - + + + + + Sintra + + +
+ + diff --git a/services/client/package.json b/services/client/package.json index 93a3ffc..1691a1d 100644 --- a/services/client/package.json +++ b/services/client/package.json @@ -15,10 +15,12 @@ "@fontsource/roboto": "^5.0.8", "@mui/material": "^5.14.18", "firebase": "^10.6.0", + "i18next": "^23.7.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-firebase-hooks": "^5.1.1", "react-hook-form": "^7.48.2", + "react-i18next": "^13.5.0", "react-router-dom": "^6.19.0" }, "devDependencies": { diff --git a/services/client/public/flags/en.png b/services/client/public/flags/en.png new file mode 100644 index 0000000..1f8c87e Binary files /dev/null and b/services/client/public/flags/en.png differ diff --git a/services/client/public/flags/es.png b/services/client/public/flags/es.png new file mode 100644 index 0000000..60ba170 Binary files /dev/null and b/services/client/public/flags/es.png differ diff --git a/services/client/public/letter-s.png b/services/client/public/letter-s.png new file mode 100644 index 0000000..4be6a61 Binary files /dev/null and b/services/client/public/letter-s.png differ diff --git a/services/client/src/_templates/Comp.tsx b/services/client/src/_templates/Comp.tsx index 895a01c..00fdfea 100644 --- a/services/client/src/_templates/Comp.tsx +++ b/services/client/src/_templates/Comp.tsx @@ -1,9 +1,10 @@ +import React from 'react'; import { FC } from 'react'; -export const Comp: FC = () => { +export const Comp: FC = React.memo(() => { return ( <>

Comp Works.

); -}; +}); diff --git a/services/client/src/_templates/CompWithChildren.tsx b/services/client/src/_templates/CompWithChildren.tsx index 76b0588..5053e98 100644 --- a/services/client/src/_templates/CompWithChildren.tsx +++ b/services/client/src/_templates/CompWithChildren.tsx @@ -4,11 +4,11 @@ interface CompProps extends React.PropsWithChildren { children: ReactElement | ReactElement[]; } -export const Comp: FC = props => { +export const Comp: FC = React.memo(props => { return ( <>

Comp Works.

{props.children} ); -}; +}); diff --git a/services/client/src/_templates/CompWithProps.tsx b/services/client/src/_templates/CompWithProps.tsx index 4094e7f..636db23 100644 --- a/services/client/src/_templates/CompWithProps.tsx +++ b/services/client/src/_templates/CompWithProps.tsx @@ -2,10 +2,10 @@ import React, { FC } from 'react'; interface CompProps extends React.PropsWithChildren {} -export const Comp: FC = () => { +export const Comp: FC = React.memo(props => { return ( <>

Comp Works.

); -}; +}); diff --git a/services/client/src/core/translations/LanguagePicker.tsx b/services/client/src/core/translations/LanguagePicker.tsx new file mode 100644 index 0000000..0b198e2 --- /dev/null +++ b/services/client/src/core/translations/LanguagePicker.tsx @@ -0,0 +1,28 @@ +import { ToggleButtonGroup, ToggleButton } from '@mui/material'; +import React, { ReactElement, FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface TProps extends React.PropsWithChildren {} + +const languages = ['es', 'en']; + +export const LanguagePicker: FC = React.memo(props => { + const { i18n } = useTranslation(); + + const [language, setLanguage] = React.useState(i18n.language); + const handleChange = (event: React.MouseEvent, newLanguage: string | null) => { + if (!newLanguage) return; + setLanguage(newLanguage); + i18n.changeLanguage(newLanguage); + }; + + return ( + + {languages.map((lang: string) => ( + + flag + + ))} + + ); +}); diff --git a/services/client/src/core/translations/i18n.ts b/services/client/src/core/translations/i18n.ts new file mode 100644 index 0000000..be1c0f1 --- /dev/null +++ b/services/client/src/core/translations/i18n.ts @@ -0,0 +1,31 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import es from './languages/es'; +import en from './languages/en'; + +// the translations +// (tip move them in a JSON file and import them, +// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) +const resources = { + en: { + translation: en + }, + es: { + translation: es + } +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + lng: 'es', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + + interpolation: { + escapeValue: false // react already safes from xss + } + }); + +export default i18n; diff --git a/services/client/src/core/translations/languages/en.ts b/services/client/src/core/translations/languages/en.ts new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/services/client/src/core/translations/languages/en.ts @@ -0,0 +1 @@ +export default {}; diff --git a/services/client/src/core/translations/languages/es.ts b/services/client/src/core/translations/languages/es.ts new file mode 100644 index 0000000..ca66bf5 --- /dev/null +++ b/services/client/src/core/translations/languages/es.ts @@ -0,0 +1,9 @@ +// Spanish translations +export default { + Login: 'Iniciar sesión', + Email: 'Correo electrónico', + Password: 'Contraseña', + 'Login with Google': 'Iniciar sesión con Google', + 'Register with Email': 'Registrarse con correo electrónico', + '© 2023 Sintra. All rights reserved.': '© 2023 Sintra. Todos los derechos reservados.' +}; diff --git a/services/client/src/core/translations/useTranslate.tsx b/services/client/src/core/translations/useTranslate.tsx new file mode 100644 index 0000000..7412f66 --- /dev/null +++ b/services/client/src/core/translations/useTranslate.tsx @@ -0,0 +1,6 @@ +import { useTranslation } from 'react-i18next'; + +export const useTranslate = () => { + const { t } = useTranslation(); + return t; +}; diff --git a/services/client/src/main.tsx b/services/client/src/main.tsx index fa6420c..322acf3 100644 --- a/services/client/src/main.tsx +++ b/services/client/src/main.tsx @@ -7,6 +7,8 @@ import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; +import './core/translations/i18n'; + ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/services/client/src/view/layout/Shell.tsx b/services/client/src/view/layout/Shell.tsx index dfcb528..a18b8ee 100644 --- a/services/client/src/view/layout/Shell.tsx +++ b/services/client/src/view/layout/Shell.tsx @@ -2,6 +2,7 @@ import { Button, Popover } from '@mui/material'; import React, { ReactElement, FC } from 'react'; import { useLogout } from '../../core/firebase/firebase'; import { ProfileButton } from './ProfileButton'; +import { LanguagePicker } from '../../core/translations/LanguagePicker'; interface CompProps extends React.PropsWithChildren { children: ReactElement | ReactElement[]; @@ -12,13 +13,14 @@ export const Shell: FC = props => {
- + + Sintra diff --git a/services/client/src/view/pages/LoginPage.tsx b/services/client/src/view/pages/LoginPage.tsx index ee08081..43683d4 100644 --- a/services/client/src/view/pages/LoginPage.tsx +++ b/services/client/src/view/pages/LoginPage.tsx @@ -3,6 +3,8 @@ import { FC, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useSignInGoogle, useSignInPassword } from '../../core/firebase/firebase'; import { useForm, SubmitHandler } from 'react-hook-form'; +import { useTranslate } from '../../core/translations/useTranslate'; +import { LanguagePicker } from '../../core/translations/LanguagePicker'; type LoginFormFields = { email: string; @@ -10,7 +12,9 @@ type LoginFormFields = { password2: string; }; -export const LoginPage: FC = () => { +export const LoginPage: FC = props => { + const t = useTranslate(); + const navigate = useNavigate(); const [ signInWithEmailAndPassword, @@ -36,13 +40,16 @@ export const LoginPage: FC = () => { return (
-
+
+
+ +

Sintra

- - + + {loading_signInWithPassword && } @@ -52,7 +59,7 @@ export const LoginPage: FC = () => {
-

© 2023 Sintra. All rights reserved.

+

{t('© 2023 Sintra. All rights reserved.')}

); diff --git a/services/client/src/view/pages/RegisterPage.tsx b/services/client/src/view/pages/RegisterPage.tsx index ef4f2d2..3606dfd 100644 --- a/services/client/src/view/pages/RegisterPage.tsx +++ b/services/client/src/view/pages/RegisterPage.tsx @@ -3,6 +3,8 @@ import { FC, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { SubmitHandler, useForm } from 'react-hook-form'; import { useCreateUser } from '../../core/firebase/firebase'; +import { LanguagePicker } from '../../core/translations/LanguagePicker'; +import { useTranslate } from '../../core/translations/useTranslate'; type RegisterFormFields = { email: string; @@ -11,6 +13,7 @@ type RegisterFormFields = { }; export const RegisterPage: FC = () => { + const t = useTranslate(); const navigate = useNavigate(); const [ createUserWithEmailAndPassword, @@ -34,12 +37,15 @@ export const RegisterPage: FC = () => { return (
-
+
+
+ +

Sintra

- - - + + + diff --git a/yarn.lock b/yarn.lock index eda857c..bd37165 100644 --- a/yarn.lock +++ b/yarn.lock @@ -323,6 +323,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.22.5": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e" + integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -3883,6 +3890,13 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -3924,6 +3938,13 @@ husky@^8.0.3: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== +i18next@^23.7.6: + version "23.7.6" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.7.6.tgz#7328e76c899052d5d33d930164612dd21e575f74" + integrity sha512-O66BhXBw0fH4bEJMA0/klQKPEbcwAp5wjXEL803pdAynNbg2f4qhLIYlNHJyE7icrL6XmSZKPYaaXwy11kJ6YQ== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5520,6 +5541,14 @@ react-hook-form@^7.48.2: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.48.2.tgz#01150354d2be61412ff56a030b62a119283b9935" integrity sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A== +react-i18next@^13.5.0: + version "13.5.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.5.0.tgz#44198f747628267a115c565f0c736a50a76b1ab0" + integrity sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA== + dependencies: + "@babel/runtime" "^7.22.5" + html-parse-stringify "^3.0.1" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -6466,6 +6495,11 @@ vite@^4.4.5: optionalDependencies: fsevents "~2.3.2" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"