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 { useEffect, useMemo } from 'react'
import { useUserLocale, useUserLocaleManager } from 'state/user/hooks'
import { useMemo } from 'react'
import store from 'state'
import { useUserLocale } from 'state/user/hooks'
import useParsedQueryString from './useParsedQueryString'
import { parsedQueryString } from './useParsedQueryString'
/**
* Given a locale string (e.g. from user agent), return the best match for corresponding SupportedLocale
* @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()
return SUPPORTED_LOCALES.find(
(locale) => locale.toLowerCase() === lowerMaybeSupportedLocale || locale.split('-')[0] === lowerMaybeSupportedLocale
......@@ -29,25 +32,24 @@ export function navigatorLocale(): SupportedLocale | undefined {
return parseLocale(language)
}
export function useSetLocaleFromUrl() {
const parsed = useParsedQueryString()
const [userLocale, setUserLocale] = useUserLocaleManager()
function storeLocale(): SupportedLocale | undefined {
return store.getState().user.userLocale ?? undefined
}
useEffect(() => {
const urlLocale = typeof parsed.lng === 'string' ? parseLocale(parsed.lng) : undefined
if (urlLocale && urlLocale !== userLocale) {
setUserLocale(urlLocale)
}
}, [parsed.lng, setUserLocale, userLocale])
export const initialLocale =
parseLocale(parsedQueryString().lng) ?? storeLocale() ?? navigatorLocale() ?? DEFAULT_LOCALE
function useUrlLocale() {
const parsed = useParsedQueryString()
return parseLocale(parsed.lng)
}
/**
* 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 {
const urlLocale = useUrlLocale()
const userLocale = useUserLocale()
return useMemo(() => {
return userLocale ?? navigatorLocale() ?? DEFAULT_LOCALE
}, [userLocale])
return useMemo(() => urlLocale ?? userLocale ?? navigatorLocale() ?? DEFAULT_LOCALE, [urlLocale, userLocale])
}
......@@ -2,10 +2,16 @@ import { parse, ParsedQs } from 'qs'
import { useMemo } from 'react'
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 {
const { search } = useLocation()
return useMemo(
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
[search]
)
return useMemo(() => parsedQueryString(search), [search])
}
......@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { ReactNode } from 'react'
import { useActiveLocale, useSetLocaleFromUrl } from 'hooks/useActiveLocale'
import { initialLocale, useActiveLocale } from 'hooks/useActiveLocale'
import { SupportedLocale } from 'constants/locales'
import {
af,
......@@ -36,6 +36,7 @@ import {
zh,
PluralCategory,
} from 'make-plural/plurals'
import { useUserLocaleManager } from 'state/user/hooks'
type LocalePlural = {
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
......@@ -82,21 +83,24 @@ async function dynamicActivate(locale: SupportedLocale) {
i18n.activate(locale)
}
dynamicActivate(initialLocale)
export function LanguageProvider({ children }: { children: ReactNode }) {
useSetLocaleFromUrl()
const locale = useActiveLocale()
const [, setUserLocale] = useUserLocaleManager()
const [loaded, setLoaded] = useState(false)
useEffect(() => {
dynamicActivate(locale)
.then(() => {
document.documentElement.setAttribute('lang', locale)
setUserLocale(locale) // stores the selected locale to persist across sessions
setLoaded(true)
})
.catch((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
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