Commit 88cc20a6 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

feat(i18n): initialize locale before React (#2343)

* feat: initialize locale before React

* chore: explain react-router-dom hash/search usage
parent 442ac893
import { DEFAULT_LOCALE, SupportedLocale, SUPPORTED_LOCALES } from 'constants/locales' import { DEFAULT_LOCALE, SupportedLocale, SUPPORTED_LOCALES } from 'constants/locales'
import { useEffect, useMemo } from 'react' import { useMemo } from 'react'
import { useUserLocale, useUserLocaleManager } from 'state/user/hooks' import store from 'state'
import { useUserLocale } from 'state/user/hooks'
import useParsedQueryString from './useParsedQueryString' import useParsedQueryString from './useParsedQueryString'
import { parsedQueryString } from './useParsedQueryString'
/** /**
* Given a locale string (e.g. from user agent), return the best match for corresponding SupportedLocale * Given a locale string (e.g. from user agent), return the best match for corresponding SupportedLocale
* @param maybeSupportedLocale the fuzzy locale identifier * @param maybeSupportedLocale the fuzzy locale identifier
*/ */
function parseLocale(maybeSupportedLocale: string): SupportedLocale | undefined { function parseLocale(maybeSupportedLocale: unknown): SupportedLocale | undefined {
if (typeof maybeSupportedLocale !== 'string') return undefined
const lowerMaybeSupportedLocale = maybeSupportedLocale.toLowerCase() const lowerMaybeSupportedLocale = maybeSupportedLocale.toLowerCase()
return SUPPORTED_LOCALES.find( return SUPPORTED_LOCALES.find(
(locale) => locale.toLowerCase() === lowerMaybeSupportedLocale || locale.split('-')[0] === lowerMaybeSupportedLocale (locale) => locale.toLowerCase() === lowerMaybeSupportedLocale || locale.split('-')[0] === lowerMaybeSupportedLocale
...@@ -29,25 +32,24 @@ export function navigatorLocale(): SupportedLocale | undefined { ...@@ -29,25 +32,24 @@ export function navigatorLocale(): SupportedLocale | undefined {
return parseLocale(language) return parseLocale(language)
} }
export function useSetLocaleFromUrl() { function storeLocale(): SupportedLocale | undefined {
return store.getState().user.userLocale ?? undefined
}
export const initialLocale =
parseLocale(parsedQueryString().lng) ?? storeLocale() ?? navigatorLocale() ?? DEFAULT_LOCALE
function useUrlLocale() {
const parsed = useParsedQueryString() const parsed = useParsedQueryString()
const [userLocale, setUserLocale] = useUserLocaleManager() return parseLocale(parsed.lng)
useEffect(() => {
const urlLocale = typeof parsed.lng === 'string' ? parseLocale(parsed.lng) : undefined
if (urlLocale && urlLocale !== userLocale) {
setUserLocale(urlLocale)
}
}, [parsed.lng, setUserLocale, userLocale])
} }
/** /**
* Returns the currently active locale, from a combination of user agent, query string, and user settings stored in redux * Returns the currently active locale, from a combination of user agent, query string, and user settings stored in redux
* Stores the query string locale in redux (if set) to persist across sessions
*/ */
export function useActiveLocale(): SupportedLocale { export function useActiveLocale(): SupportedLocale {
const urlLocale = useUrlLocale()
const userLocale = useUserLocale() const userLocale = useUserLocale()
return useMemo(() => urlLocale ?? userLocale ?? navigatorLocale() ?? DEFAULT_LOCALE, [urlLocale, userLocale])
return useMemo(() => {
return userLocale ?? navigatorLocale() ?? DEFAULT_LOCALE
}, [userLocale])
} }
...@@ -2,10 +2,16 @@ import { parse, ParsedQs } from 'qs' ...@@ -2,10 +2,16 @@ import { parse, ParsedQs } from 'qs'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
export function parsedQueryString(search?: string): ParsedQs {
if (!search) {
// react-router-dom places search string in the hash
const hash = window.location.hash
search = hash.substr(hash.indexOf('?'))
}
return search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}
}
export default function useParsedQueryString(): ParsedQs { export default function useParsedQueryString(): ParsedQs {
const { search } = useLocation() const { search } = useLocation()
return useMemo( return useMemo(() => parsedQueryString(search), [search])
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
[search]
)
} }
...@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' ...@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { i18n } from '@lingui/core' import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react' import { I18nProvider } from '@lingui/react'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { useActiveLocale, useSetLocaleFromUrl } from 'hooks/useActiveLocale' import { initialLocale, useActiveLocale } from 'hooks/useActiveLocale'
import { SupportedLocale } from 'constants/locales' import { SupportedLocale } from 'constants/locales'
import { import {
af, af,
...@@ -36,6 +36,7 @@ import { ...@@ -36,6 +36,7 @@ import {
zh, zh,
PluralCategory, PluralCategory,
} from 'make-plural/plurals' } from 'make-plural/plurals'
import { useUserLocaleManager } from 'state/user/hooks'
type LocalePlural = { type LocalePlural = {
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory [key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
...@@ -82,21 +83,24 @@ async function dynamicActivate(locale: SupportedLocale) { ...@@ -82,21 +83,24 @@ async function dynamicActivate(locale: SupportedLocale) {
i18n.activate(locale) i18n.activate(locale)
} }
dynamicActivate(initialLocale)
export function LanguageProvider({ children }: { children: ReactNode }) { export function LanguageProvider({ children }: { children: ReactNode }) {
useSetLocaleFromUrl()
const locale = useActiveLocale() const locale = useActiveLocale()
const [, setUserLocale] = useUserLocaleManager()
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
useEffect(() => { useEffect(() => {
dynamicActivate(locale) dynamicActivate(locale)
.then(() => { .then(() => {
document.documentElement.setAttribute('lang', locale) document.documentElement.setAttribute('lang', locale)
setUserLocale(locale) // stores the selected locale to persist across sessions
setLoaded(true) setLoaded(true)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to activate locale', locale, error) console.error('Failed to activate locale', locale, error)
}) })
}, [locale]) }, [locale, setUserLocale])
// prevent the app from rendering with placeholder text before the locale is loaded // prevent the app from rendering with placeholder text before the locale is loaded
if (!loaded) return null if (!loaded) return null
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment