all repos — caroster @ f76eb24ad8c71f44a2c0813ca6eaf6a217b0530e

[Octree] Group carpool to your event https://caroster.io

feat: 🌐 Set multiple languages

#173
Tim Izzo tim@octree.ch
Wed, 27 Oct 2021 12:05:35 +0200
commit

f76eb24ad8c71f44a2c0813ca6eaf6a217b0530e

parent

5ecddb30cd1351970186d1d7939cad57554ce781

M backend/extensions/users-permissions/controllers/User.jsbackend/extensions/users-permissions/controllers/User.js

@@ -23,6 +23,7 @@ password,

old_password, firstName, lastName, + lang, events, } = body;

@@ -50,6 +51,7 @@ email,

password, firstName, lastName, + lang, events: updatedEvents, }) );
M frontend/.eslintrcfrontend/.eslintrc

@@ -1,5 +1,5 @@

{ - "extends": "next", + "extends": ["next", "prettier"], "rules": { "react/display-name": "off", "react-hooks/exhaustive-deps": "off"
M frontend/containers/EventBar/index.jsfrontend/containers/EventBar/index.js

@@ -35,7 +35,7 @@ }, [detailsOpen]); // eslint-disable-line react-hooks/exhaustive-deps

const signUp = () => router.push({ - pathname: '/register', + pathname: '/auth/register', state: {event: event?.id}, }); const signIn = () => router.push('/auth/login');
A frontend/containers/Languages/index.tsx

@@ -0,0 +1,81 @@

+import {useState, useEffect} from 'react'; +import Box from '@material-ui/core/Box'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import {useTranslation} from 'react-i18next'; +import useLangStore from '../../stores/useLangStore'; +import useProfile from '../../hooks/useProfile'; +import { + useUpdateMeMutation, + Enum_Userspermissionsuser_Lang, +} from '../../generated/graphql'; + +const Languages = () => { + const {t, i18n} = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const language = useLangStore(s => s.language); + const setLanguage = useLangStore(s => s.setLanguage); + const {profile, connected} = useProfile(); + const [updateProfile] = useUpdateMeMutation(); + + useEffect(() => { + i18n.changeLanguage(language?.toLowerCase()); + }, [language]); + + useEffect(() => { + if (profile) setLanguage(profile.lang); + }, [profile]); + + const handleClick = event => { + setAnchorEl(event.currentTarget); + }; + + const onConfirm = (lang: Enum_Userspermissionsuser_Lang) => { + setLanguage(lang); + setAnchorEl(null); + + if (connected) { + updateProfile({ + variables: { + userUpdate: { + lang, + }, + }, + }); + } + }; + + return ( + <> + <Box p={1} position="fixed" bottom={0} left={0}> + <IconButton + color="primary" + aria-label="Languages" + onClick={handleClick} + > + <Icon>language</Icon> + </IconButton> + </Box> + <Menu + id="LanguagesMenu" + anchorEl={anchorEl} + keepMounted + open={Boolean(anchorEl)} + onClose={() => setAnchorEl(null)} + > + <MenuItem + disabled={language === Enum_Userspermissionsuser_Lang.Fr} + onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.Fr)} + >{t`languages.fr`}</MenuItem> + <MenuItem + disabled={language === Enum_Userspermissionsuser_Lang.En} + onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.En)} + >{t`languages.en`}</MenuItem> + </Menu> + </> + ); +}; + +export default Languages;
M frontend/generated/graphql.tsxfrontend/generated/graphql.tsx

@@ -1854,7 +1854,7 @@ );

export type UserFieldsFragment = ( { __typename?: 'UsersPermissionsUser' } - & Pick<UsersPermissionsUser, 'id' | 'username' | 'email' | 'confirmed' | 'lastName' | 'firstName'> + & Pick<UsersPermissionsUser, 'id' | 'username' | 'email' | 'confirmed' | 'lastName' | 'firstName' | 'lang'> & { events?: Maybe<Array<Maybe<( { __typename?: 'Event' } & Pick<Event, 'id' | 'uuid' | 'name' | 'date' | 'address'>

@@ -1955,6 +1955,7 @@ email

confirmed lastName firstName + lang events { id uuid
M frontend/graphql/user.gqlfrontend/graphql/user.gql

@@ -5,6 +5,7 @@ email

confirmed lastName firstName + lang events { id uuid
M frontend/hooks/useProfile.jsfrontend/hooks/useProfile.js

@@ -5,7 +5,7 @@

const useProfile = () => { const token = useAuthStore(s => s.token); const user = useAuthStore(s => s.user); - const [profile, setProfile] = useState(); + const [profile, setProfile] = useState(null); const [fetchProfile, {data}] = useProfileLazyQuery(); useEffect(() => {
M frontend/i18n.tsfrontend/i18n.ts

@@ -1,10 +1,14 @@

import i18n from 'i18next'; import {initReactI18next} from 'react-i18next'; import translationFr from './locales/fr.json'; +import translationEn from './locales/en.json'; const resources = { fr: { translation: translationFr, + }, + en: { + translation: translationEn, }, };
A frontend/locales/en.json

@@ -0,0 +1,234 @@

+{ + "generic": { + "loading": "Loading ...", + "close": "Close", + "create": "Create", + "cancel": "Cancel", + "remove": "Remove", + "save": "Save", + "confirm": "Confirm", + "errors": { + "date_min": "Select an upcoming date", + "unknown": "An unknown error occurred", + "rejected": "Something went wrong", + "bad_data": "Something is missing", + "unauthorized": "Authentication problem", + "forbidden": "You do not have the right to do this action", + "not_found": "Resource not found", + "server": "Problem on our servers", + "timeout": "Unstable connection, retry later" + } + }, + "languages": { + "fr": "French", + "en": "English" + }, + "menu": { + "logout": "Logout", + "about": "About Caroster", + "dashboard": "Dashboard", + "login": "Login", + "register": "Sign-Up", + "new_event": "Create a caroster", + "profile": "Profile" + }, + "event": { + "title": "{{title}} - Caroster", + "not_found": "Project not found", + "fields": { + "starts_on": "Start on", + "address": "Address", + "name": "Name of the event", + "empty": "Not specified", + "share": "Share link" + }, + "creation": { + "title": "New event", + "event_name": "Event name", + "creator_email": "Your e-mail", + "date": "Date of the event", + "address": "Address of the event", + "next": "Next", + "newsletter": "Keep me informed of developments in Caroster by e-mail", + "actions": { + "dashboard": "$t(menu.dashboard)", + "see_profile": "Profile", + "about": "About Caroster" + }, + "addFromAccount": { + "title": "Do you want to add this caroster to your events?", + "subtitle": "Create it from your account", + "actions": { + "register": "$t(menu.register)", + "login": "$t(menu.login)" + } + } + }, + "actions": { + "show_details": "Event details", + "hide_details": "Hide details", + "find_car": "Find a car", + "copied": "The link has been copied to your clipboard", + "add_to_my_events": "Add to my events", + "see_on_gmap": "See on a map" + }, + "errors": { + "cant_create": "Unable to create event", + "cant_update": "Unable to modify event" + }, + "add_to_my_events": { + "login": "$t(menu.login)", + "register": "$t(menu.register)", + "title": "You must be logged in", + "text_html": "To add <strong> {{eventName}} </strong> to your carosters you must be logged in or create an account." + } + }, + "car": { + "fields": { + "meeting_point": "Meeting place", + "details": "Notes", + "phone": "Contact" + }, + "creation": { + "date": "Date of departure", + "time": "Departure time", + "title": "Add a car", + "name": "Name of the car", + "seats": "Number of seats", + "meeting": "Meeting place", + "phone": "Telephone number", + "notes": "Additional information", + "created": "The car has been created" + }, + "actions": { + "remove_alert": "Are you sure you want to remove this car and add the subscribers to the waiting list?", + "removed": "The car has been removed" + }, + "passengers": { + "empty": "Available seat", + "add": "Add a passenger", + "email": "Your email", + "emailHelper": "Optional - Get notified if cars are added" + }, + "errors": { + "cant_create": "Unable to create the car", + "cant_update": "Unable to modify the car", + "cant_remove": "Unable to remove the car", + "cant_add_passenger": "Unable to add a passenger", + "cant_remove_passenger": "Unable to remove passenger" + } + }, + "dashboard": { + "title": "$t(menu.dashboard)", + "actions": { + "see_event": "See more", + "add_event": "Create a caroster" + }, + "sections": { + "future": "Caroster to come", + "future_plural": "Carosters to come", + "past": "Caroster passed", + "past_plural": "Past carosters", + "noDate": "Caroster without date", + "noDate_plural": "Carosters without date" + }, + "noEvent": { + "title": "Welcome to Caroster", + "text_html": "Here you will see <strong> the carosters you are participating in </strong>, to start creating a Caroster!", + "create_event": "$t(menu.new_event)" + } + }, + "profile": { + "title": "Profile", + "firstName": "First name", + "lastName": "Last name", + "email": "Email", + "current_password": "Current password", + "new_password": "New password", + "password_changed": "Password updated", + "updated": "Profile updated", + "not_defined": "Not specified", + "actions": { + "save": "Save", + "edit": "Edit", + "change_password": "Change your password", + "logout": "Logout", + "cancel": "Cancel", + "save_new_password": "Update" + }, + "errors": { + "password_nomatch": "Wrong password" + } + }, + "passenger": { + "title": "Waiting list", + "availability": { + "seats": "{{count}} seat available", + "seats_plural": "{{count}} seats available" + }, + "creation": { + "seats": "Number of passengers: {{seats}}" + }, + "actions": { + "remove_alert": "Are you sure you want to remove <italic> <bold> {{name}} </bold> </italic> from the waitlist?" + }, + "errors": { + "cant_add_passenger": "Unable to add a passenger", + "cant_save_passengers": "Unable to update passengers", + "cant_remove_passenger": "Unable to remove the passenger", + "cant_select_car": "Unable to select the car" + } + }, + "signup": { + "title": "Sign up", + "email": "Email", + "firstName": "First name", + "lastName": "Last name", + "password": "Password", + "submit": "Create your account", + "login": "$t(menu.login)", + "errors": { + "email_taken": "This email is already associated with an account" + }, + "success": { + "title": "Welcome!", + "text_html": "Lorem Ipsum dolor sit amet, consectetur <strong> adipiscing elit </strong>", + "dashboard": "Go to your dashboard", + "create_event": "Create an event" + } + }, + "confirm": { + "title": "Confirm your account", + "text": "You have received an email with a link. Please click on this link to confirm your account.", + "login": "Return to the login screen" + }, + "signin": { + "title": "Sign in", + "email": "Email", + "password": "Password", + "login": "$t(menu.login)", + "register": "$t(menu.register)", + "errors": "Check your email and password", + "unconfirmed": "Your account has not been confirmed. Please check your emails", + "withGoogle": "Use a Google account" + }, + "lost_password": { + "title": "Password recovery", + "reset_title": "Definition of a new password", + "message": "Lost your password?", + "email": "Your email", + "password": "New password", + "password_confirmation": "Confirmation of the new password", + "sent": "An email has been sent to {{email}}, with a link to recover your password", + "error": "This email does not exist", + "change_success": "Your password has been changed", + "actions": { + "send": "Send a recovery email", + "cancel": "Cancel", + "login": "Return to the login screen", + "resend": "Send again", + "register": "Create an account?", + "save_new_password": "Update" + } + } +}
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -1,13 +1,4 @@

{ - "menu": { - "logout": "Se déconnecter", - "about": "À propos de Caroster", - "dashboard": "Tableau de bord", - "login": "Se connecter", - "register": "Créer un compte", - "new_event": "Créer un caroster", - "profile": "Profil" - }, "generic": { "loading": "Chargement...", "close": "Fermer",

@@ -27,6 +18,19 @@ "not_found": "Ressource introuvable",

"server": "Problème sur nos serveurs", "timeout": "Connexion instable, re-essayé plus tard" } + }, + "languages": { + "fr": "Français", + "en": "Anglais" + }, + "menu": { + "logout": "Se déconnecter", + "about": "À propos de Caroster", + "dashboard": "Tableau de bord", + "login": "Se connecter", + "register": "Créer un compte", + "new_event": "Créer un caroster", + "profile": "Profil" }, "event": { "title": "{{title}} - Caroster",
M frontend/package.jsonfrontend/package.json

@@ -35,6 +35,7 @@ "@graphql-codegen/typescript": "1.20.2",

"@graphql-codegen/typescript-operations": "1.17.14", "@graphql-codegen/typescript-react-apollo": "2.2.1", "eslint": "^7.31.0", - "eslint-config-next": "^11.1.2" + "eslint-config-next": "^11.1.2", + "eslint-config-prettier": "^8.3.0" } }
M frontend/pages/_app.tsxfrontend/pages/_app.tsx

@@ -1,4 +1,4 @@

-import {useEffect, Fragment} from 'react'; +import {useEffect} from 'react'; import {AppProps} from 'next/app'; import {ThemeProvider} from '@material-ui/core/styles'; import {ApolloProvider} from '@apollo/client';

@@ -6,9 +6,10 @@ import CssBaseline from '@material-ui/core/CssBaseline';

import {useApollo} from '../lib/apolloClient'; import theme from '../theme'; import Toasts from '../components/Toasts'; +import Languages from '../containers/Languages'; +import Metas from '../containers/Metas'; import 'moment/locale/fr-ch'; import '../i18n'; -import Metas from '../containers/Metas'; const App = function (props: AppProps) { const {Component, pageProps} = props;

@@ -29,6 +30,7 @@ <ThemeProvider theme={theme}>

<CssBaseline /> <Component {...pageProps} /> <Toasts /> + <Languages /> </ThemeProvider> </ApolloProvider> );
M frontend/pages/dashboard.tsxfrontend/pages/dashboard.tsx

@@ -90,6 +90,7 @@

const useStyles = makeStyles(theme => ({ root: { marginTop: theme.mixins.toolbar.minHeight, + height: '100vh', }, }));
M frontend/stores/useAuthStore.tsxfrontend/stores/useAuthStore.tsx

@@ -11,7 +11,7 @@ };

const hasStorage = typeof localStorage !== 'undefined'; -const useAuth = create<State>((set, get) => ({ +const useAuthStore = create<State>((set, get) => ({ token: hasStorage ? localStorage.getItem('token') : null, setToken: (token: string) => { if (hasStorage) localStorage.setItem('token', token);

@@ -35,4 +35,4 @@ window.location.href = '/auth/login';

}, })); -export default useAuth; +export default useAuthStore;
A frontend/stores/useLangStore.tsx

@@ -0,0 +1,25 @@

+import create from 'zustand'; +import {persist} from 'zustand/middleware'; +import {Enum_Userspermissionsuser_Lang} from '../generated/graphql'; + +const STORAGE_KEY = 'caroster-lang'; + +type State = { + language: Enum_Userspermissionsuser_Lang | null; + setLanguage: (language?: Enum_Userspermissionsuser_Lang) => void; +}; + +const useLangStore = create<State>( + persist( + set => ({ + language: null, + setLanguage: language => set({language}), + }), + { + name: STORAGE_KEY, + getStorage: () => sessionStorage, + } + ) +); + +export default useLangStore;
M frontend/yarn.lockfrontend/yarn.lock

@@ -3478,6 +3478,11 @@ eslint-plugin-jsx-a11y "^6.4.1"

eslint-plugin-react "^7.23.1" eslint-plugin-react-hooks "^4.2.0" +eslint-config-prettier@^8.3.0: + version "8.3.0" + resolved "https://npm-8ee.hidora.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" + integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + eslint-import-resolver-node@^0.3.4, eslint-import-resolver-node@^0.3.6: version "0.3.6" resolved "https://npm-8ee.hidora.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"