feat: :globe_with_meridians: Enhance language detection #381
Simon Mulquin simon@octree.ch
Tue, 17 Jan 2023 15:43:16 +0000
15 files changed,
125 insertions(+),
47 deletions(-)
jump to
M
backend/src/extensions/users-permissions/content-types/user/schema.json
→
backend/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.tsx
→
frontend/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.tsx
→
frontend/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.tsx
→
frontend/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/TravelColumns/NoCar.tsx
→
frontend/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.tsx
→
frontend/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.tsx
→
frontend/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.ts
→
frontend/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/lib/apolloClient.ts
→
frontend/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.ts
→
frontend/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.tsx
→
frontend/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.js
→
frontend/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'],