all repos — caroster @ c4ee8c801635c3fcdb28d1bec4b97d691b098144

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

feat: :globe_with_meridians: Enhance language detection

#381
Simon Mulquin simon@octree.ch
Tue, 17 Jan 2023 15:43:16 +0000
commit

c4ee8c801635c3fcdb28d1bec4b97d691b098144

parent

74feaa47d697b06162103be5cb9fa26bac349ef9

M backend/src/extensions/users-permissions/content-types/user/schema.jsonbackend/src/extensions/users-permissions/content-types/user/schema.json

@@ -102,9 +102,7 @@ "lang": {

"type": "enumeration", "enum": [ "fr", - "en", - "FR", - "EN" + "en" ], "default": "fr" },
M frontend/containers/EventBar/index.tsxfrontend/containers/EventBar/index.tsx

@@ -71,7 +71,6 @@ id="ShareBtn"

onClick={() => share({ title: `Caroster ${event.name}`, - url: `${window.location.href}`, }) } size="large"
M frontend/containers/Languages/Icon.tsxfrontend/containers/Languages/Icon.tsx

@@ -46,14 +46,12 @@ keepMounted

open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)} > - <MenuItem - disabled={language === SupportedLocales['fr']} - onClick={() => onConfirm(SupportedLocales['fr'])} - >{t`languages.fr`}</MenuItem> - <MenuItem - disabled={language === SupportedLocales['en']} - onClick={() => onConfirm(SupportedLocales['en'])} - >{t`languages.en`}</MenuItem> + {Object.keys(SupportedLocales).map(locale => ( + <MenuItem + disabled={language === SupportedLocales[locale]} + onClick={() => onConfirm(SupportedLocales[locale])} + >{t(`languages.${locale}`)}</MenuItem> + ))} </Menu> </>; };
M frontend/containers/Languages/MenuItem.tsxfrontend/containers/Languages/MenuItem.tsx

@@ -38,14 +38,12 @@ overflow: 'hidden',

}} dense > - <MenuItem - disabled={language === SupportedLocales['fr']} - onClick={() => onConfirm(SupportedLocales['fr'])} - >{t`languages.fr`}</MenuItem> - <MenuItem - disabled={language === SupportedLocales['en']} - onClick={() => onConfirm(SupportedLocales['en'])} - >{t`languages.en`}</MenuItem> + {Object.keys(SupportedLocales).map(locale => ( + <MenuItem + disabled={language === SupportedLocales[locale]} + onClick={() => onConfirm(SupportedLocales[locale])} + >{t(`languages.${locale}`)}</MenuItem> + ))} </MenuList> </Box> );
M frontend/containers/ShareEvent/index.tsxfrontend/containers/ShareEvent/index.tsx

@@ -5,10 +5,9 @@ import useShare from '../../hooks/useShare';

interface Props { title: string; - url: string; } -const ShareEvent = ({title, url, sx}: ButtonProps & Props) => { +const ShareEvent = ({title, sx}: ButtonProps & Props) => { const {t} = useTranslation(); const {share, navigatorHasShareCapability} = useShare();

@@ -21,7 +20,7 @@ <Button

variant="outlined" color="primary" startIcon={<Icon>share</Icon>} - onClick={() => share({title, url})} + onClick={() => share({title})} sx={sx} > {text}
M frontend/containers/TravelColumns/NoCar.tsxfrontend/containers/TravelColumns/NoCar.tsx

@@ -44,7 +44,6 @@ <ShareEvent

color="primary" sx={{marginTop: theme.spacing(6), backgroundColor: '#fff'}} title={`Caroster ${eventName}`} - url={`${url}`} /> </Box> );
M frontend/containers/WaitingList/TravelDialog.tsxfrontend/containers/WaitingList/TravelDialog.tsx

@@ -156,7 +156,6 @@ </Typography>

<ShareEvent className={classes.share} title={`Caroster ${eventName}`} - url={`${typeof window !== 'undefined' ? window.location.href : ''}`} /> </Box> )) || (
M frontend/generated/graphql.tsxfrontend/generated/graphql.tsx

@@ -97,8 +97,6 @@ tos = 'tos'

} export enum Enum_Userspermissionsuser_Lang { - EN = 'EN', - FR = 'FR', en = 'en', fr = 'fr' }
M frontend/hooks/useProfile.tsfrontend/hooks/useProfile.ts

@@ -10,7 +10,8 @@ const [profile, setProfile] = useState<UsersPermissionsUser | null>(null);

const [userId, setUserId] = useState<string | null>(null); const fetchProfile = async () => { - const apolloClient = initializeApollo('', session); + const jwt = session?.data?.token?.jwt; + const apolloClient = initializeApollo('', jwt); try { const {data} = await apolloClient.query({
M frontend/hooks/useShare.tsfrontend/hooks/useShare.ts

@@ -1,9 +1,11 @@

import {useTranslation} from 'react-i18next'; +import {Enum_Userspermissionsuser_Lang as SupportedLocales} from '../generated/graphql'; import useToastStore from '../stores/useToastStore'; -const navigatorHasShareCapability = typeof navigator !== 'undefined' && !!navigator.share; -const navigatorHasClipboardCapability = typeof navigator !== 'undefined' && !!navigator.clipboard; - +const navigatorHasShareCapability = + typeof navigator !== 'undefined' && !!navigator.share; +const navigatorHasClipboardCapability = + typeof navigator !== 'undefined' && !!navigator.clipboard; const useShare = () => { const {t} = useTranslation();

@@ -11,17 +13,25 @@ const addToast = useToastStore(s => s.addToast);

return { navigatorHasShareCapability, - share: async ({url, title}) => { + share: async ({title}) => { + const url = typeof window !== 'undefined' ? window.location.href : ''; if (!url || !title) return null; + const splittedUrl = url.split('/'); + const localeParamIndex = splittedUrl.findIndex( + member => SupportedLocales[member] + ); + splittedUrl[localeParamIndex] = 'default'; + const withDefaultLocaleURL = splittedUrl.join('/'); // If navigator share capability - if (navigatorHasShareCapability) + if (navigatorHasShareCapability) { return await navigator.share({ title, - url, + url: withDefaultLocaleURL, }); + } // Else copy URL in clipboard else if (navigatorHasClipboardCapability) { - await navigator.clipboard.writeText(url); + await navigator.clipboard.writeText(withDefaultLocaleURL); addToast(t('event.actions.copied')); return true; }
M frontend/lib/apolloClient.tsfrontend/lib/apolloClient.ts

@@ -6,18 +6,18 @@ import merge from 'deepmerge';

import isEqual from 'lodash/isEqual'; import {signOut, useSession} from 'next-auth/react'; import {Session} from 'next-auth'; +import {JWT} from 'next-auth/jwt'; export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'; let apolloClient: ApolloClient<any>; -const authLink = (session: Session | null) => +const authLink = (jwt: string | null) => setContext(async (_, {headers}) => { - const token = session?.token?.jwt; // return the headers to the context so httpLink can read them return { headers: { ...headers, - authorization: token ? `Bearer ${token}` : '', + authorization: jwt ? `Bearer ${jwt}` : '', }, }; });

@@ -38,20 +38,20 @@ uri, // Server URL (must be absolute)

credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` }); -const createApolloClient = (uri: string, session: Session | null) => { +const createApolloClient = (uri: string, jwt: string | null) => { return new ApolloClient({ ssrMode: typeof window === 'undefined', - link: from([authLink(session), errorLink, httpLink(uri)]), + link: from([authLink(jwt), errorLink, httpLink(uri)]), cache: new InMemoryCache(), }); }; export const initializeApollo = ( uri: string, - session: Session | null, + jwt: string | null, initialState = null ) => { - const _apolloClient = apolloClient ?? createApolloClient(uri, session); + const _apolloClient = apolloClient ?? createApolloClient(uri, jwt); // If your page has Next.js data fetching methods that use Apollo Client, the initial state gets hydrated here if (initialState) {

@@ -87,5 +87,5 @@

export const useApollo = (pageProps: any) => { const state = pageProps[APOLLO_STATE_PROP_NAME]; const {data: session} = useSession(); - return useMemo(() => initializeApollo('', session, state), [state, session]); + return useMemo(() => initializeApollo('', session?.token?.jwt, state), [state, session]); };
M frontend/lib/pageUtils.tsfrontend/lib/pageUtils.ts

@@ -18,7 +18,9 @@ const getServerSideProps =

(extension?: ServerSideExtension) => async (context: any) => { const session = await getSession(context); const {STRAPI_URL = 'http://localhost:1337'} = process.env; - const apolloClient = initializeApollo(`${STRAPI_URL}/graphql`, session); + + const jwt = session?.token?.jwt; + const apolloClient = initializeApollo(`${STRAPI_URL}/graphql`, jwt); const locale = session?.user?.lang || 'fr'; try {
A frontend/middleware.ts

@@ -0,0 +1,76 @@

+import {getToken} from 'next-auth/jwt'; +import {NextRequest, NextResponse} from 'next/server'; +import { + ProfileDocument, + Enum_Userspermissionsuser_Lang as SupportedLocales, +} from './generated/graphql'; +import {initializeApollo} from './lib/apolloClient'; +import {getCookie} from './lib/cookies'; + +const PUBLIC_FILE = /\.(.*)$/; + +export async function middleware(req: NextRequest) { + if ( + req.nextUrl.pathname.startsWith('/_next') || + req.nextUrl.pathname.includes('/api/') || + req.nextUrl.pathname === '/graphql' || + PUBLIC_FILE.test(req.nextUrl.pathname) + ) { + return; + } + + if (req.nextUrl.locale === 'share') { + const registeredUserLanguage = await getRegisteredUserLanguage(req); + const NEXT_LOCALE = getCookie('NEXT_LOCALE', req.headers.get('cookie')); + const browserPreferredSupportedLanguage = + getBrowserPreferredSupportedLanguage(req); + + const locale = + registeredUserLanguage || + NEXT_LOCALE || + browserPreferredSupportedLanguage || + 'fr'; + + return NextResponse.redirect( + new URL(`/${locale}${req.nextUrl.pathname}`, req.url) + ); + } +} + +const getRegisteredUserLanguage = async req => { + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (token?.jwt) { + const {STRAPI_URL = 'http://localhost:1337'} = process.env; + const apolloClient = initializeApollo( + `${STRAPI_URL}/graphql`, + token.jwt as string + ); + + const {data} = await apolloClient.query({ + query: ProfileDocument, + }); + + return data?.me?.profile?.lang; + } +}; + +const getBrowserPreferredSupportedLanguage = req => { + const browserAcceptedLanguages = req.headers + .get('accept-language') + ?.split(','); + let browserPreferredSupportedLanguage = null; + browserAcceptedLanguages?.every(locale => { + const lang = locale.split('-')?.[0].toLowerCase(); + + if (Object.keys(SupportedLocales).includes(lang)) { + browserPreferredSupportedLanguage = lang; + } else { + return false; + } + }); + return browserPreferredSupportedLanguage; +};
M frontend/pages/e/[uuid]/details.tsxfrontend/pages/e/[uuid]/details.tsx

@@ -208,7 +208,6 @@ </Box>

<Box py={4} justifyContent="center" display="flex"> <ShareEvent title={`Caroster ${event.name}`} - url={`${window.location.href}`} />{' '} </Box> <Divider variant="middle" />
M frontend/react-i18next.config.jsfrontend/react-i18next.config.js

@@ -1,8 +1,10 @@

module.exports = { i18n: { - defaultLocale: 'fr', - locales: ['en', 'fr'], + defaultLocale: 'share', + locales: ['share', 'en', 'fr'], + localeDetection: false }, + trailingSlash: true, fallbackLng: { default: ['fr'],