i18n settings improvements (#2184)

* Handle language selector

* Improve type safety

* Add a little more safety

* Update comment
zio/stable
Eric Bailey 2023-12-12 12:42:11 -06:00 committed by GitHub
parent d82b1a1047
commit c6ab6e8b8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 79 additions and 39 deletions

View File

@ -0,0 +1,12 @@
import {test, expect} from '@jest/globals'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
test('sanitizeAppLanguageSetting', () => {
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('en,fr')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('fr,en')).toBe(AppLanguage.en)
})

View File

@ -2,7 +2,11 @@ import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import lande from 'lande' import lande from 'lande'
import {hasProp} from 'lib/type-guards' import {hasProp} from 'lib/type-guards'
import * as bcp47Match from 'bcp-47-match' import * as bcp47Match from 'bcp-47-match'
import {LANGUAGES_MAP_CODE2, LANGUAGES_MAP_CODE3} from './languages' import {
AppLanguage,
LANGUAGES_MAP_CODE2,
LANGUAGES_MAP_CODE3,
} from './languages'
export function code2ToCode3(lang: string): string { export function code2ToCode3(lang: string): string {
if (lang.length === 2) { if (lang.length === 2) {
@ -85,14 +89,33 @@ export function getTranslatorLink(text: string, lang: string): string {
)}` )}`
} }
export function sanitizeAppLanguageSetting(appLanguage: string) { /**
* Returns a valid `appLanguage` value from an arbitrary string.
*
* Contenxt: post-refactor, we populated some user's `appLanguage` setting with
* `postLanguage`, which can be a comma-separated list of values. This breaks
* `appLanguage` handling in the app, so we introduced this util to parse out a
* valid `appLanguage` from the pre-populated `postLanguage` values.
*
* The `appLanguage` will continue to be incorrect until the user returns to
* language settings and selects a new option, at which point we'll re-save
* their choice, which should then be a valid option. Since we don't know when
* this will happen, we should leave this here until we feel it's safe to
* remove, or we re-migrate their storage.
*/
export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
const langs = appLanguage.split(',').filter(Boolean) const langs = appLanguage.split(',').filter(Boolean)
for (const lang of langs) { for (const lang of langs) {
if (['en', 'hi'].includes(lang)) { switch (lang) {
return lang case 'en':
return AppLanguage.en
case 'hi':
return AppLanguage.hi
default:
continue
} }
} }
return 'en' return AppLanguage.en
} }

View File

@ -5,22 +5,21 @@ import {useLanguagePrefs} from '#/state/preferences'
import {messages as messagesEn} from '#/locale/locales/en/messages' import {messages as messagesEn} from '#/locale/locales/en/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages' import {messages as messagesHi} from '#/locale/locales/hi/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
export const locales = {
en: 'English',
hi: 'हिंदी',
}
export const defaultLocale = 'en'
/** /**
* We do a dynamic import of just the catalog that we need * We do a dynamic import of just the catalog that we need
* @param locale any locale string
*/ */
export async function dynamicActivate(locale: string) { export async function dynamicActivate(locale: AppLanguage) {
if (locale === 'hi') { switch (locale) {
case AppLanguage.hi: {
i18n.loadAndActivate({locale, messages: messagesHi}) i18n.loadAndActivate({locale, messages: messagesHi})
} else { break
}
default: {
i18n.loadAndActivate({locale, messages: messagesEn}) i18n.loadAndActivate({locale, messages: messagesEn})
break
}
} }
} }

View File

@ -3,24 +3,23 @@ import {i18n} from '@lingui/core'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
export const locales = {
en: 'English',
hi: 'हिंदी',
}
export const defaultLocale = 'en'
/** /**
* We do a dynamic import of just the catalog that we need * We do a dynamic import of just the catalog that we need
* @param locale any locale string
*/ */
export async function dynamicActivate(locale: string) { export async function dynamicActivate(locale: AppLanguage) {
let mod: any let mod: any
if (locale === 'hi') { switch (locale) {
case AppLanguage.hi: {
mod = await import(`./locales/hi/messages`) mod = await import(`./locales/hi/messages`)
} else { break
}
default: {
mod = await import(`./locales/en/messages`) mod = await import(`./locales/en/messages`)
break
}
} }
i18n.load(locale, mod.messages) i18n.load(locale, mod.messages)

View File

@ -4,14 +4,19 @@ interface Language {
name: string name: string
} }
interface AppLanguage { export enum AppLanguage {
code2: string en = 'en',
hi = 'hi',
}
interface AppLanguageConfig {
code2: AppLanguage
name: string name: string
} }
export const APP_LANGUAGES: AppLanguage[] = [ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: 'en', name: 'English'}, {code2: AppLanguage.en, name: 'English'},
{code2: 'hi', name: 'हिंदी'}, {code2: AppLanguage.hi, name: 'हिंदी'},
] ]
export const LANGUAGES: Language[] = [ export const LANGUAGES: Language[] = [

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {AppLanguage} from '#/locale/languages'
type SetStateCb = ( type SetStateCb = (
s: persisted.Schema['languagePrefs'], s: persisted.Schema['languagePrefs'],
@ -11,7 +12,7 @@ type ApiContext = {
toggleContentLanguage: (code2: string) => void toggleContentLanguage: (code2: string) => void
togglePostLanguage: (code2: string) => void togglePostLanguage: (code2: string) => void
savePostLanguageToHistory: () => void savePostLanguageToHistory: () => void
setAppLanguage: (code2: string) => void setAppLanguage: (code2: AppLanguage) => void
} }
const stateContext = React.createContext<StateContext>( const stateContext = React.createContext<StateContext>(
@ -23,7 +24,7 @@ const apiContext = React.createContext<ApiContext>({
toggleContentLanguage: (_: string) => {}, toggleContentLanguage: (_: string) => {},
togglePostLanguage: (_: string) => {}, togglePostLanguage: (_: string) => {},
savePostLanguageToHistory: () => {}, savePostLanguageToHistory: () => {},
setAppLanguage: (_: string) => {}, setAppLanguage: (_: AppLanguage) => {},
}) })
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
@ -106,7 +107,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
.slice(0, 6), .slice(0, 6),
})) }))
}, },
setAppLanguage(code2: string) { setAppLanguage(code2: AppLanguage) {
setStateWrapped(s => ({...s, appLanguage: code2})) setStateWrapped(s => ({...s, appLanguage: code2}))
}, },
}), }),

View File

@ -21,6 +21,7 @@ import {useModalControls} from '#/state/modals'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
@ -60,7 +61,7 @@ export function LanguageSettingsScreen(_props: Props) {
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => { (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
if (!value) return if (!value) return
if (langPrefs.appLanguage !== value) { if (langPrefs.appLanguage !== value) {
setLangPrefs.setAppLanguage(value) setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
} }
}, },
[langPrefs, setLangPrefs], [langPrefs, setLangPrefs],
@ -103,7 +104,7 @@ export function LanguageSettingsScreen(_props: Props) {
<View style={{position: 'relative'}}> <View style={{position: 'relative'}}>
<RNPickerSelect <RNPickerSelect
placeholder={{}} placeholder={{}}
value={langPrefs.appLanguage} value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
onValueChange={onChangeAppLanguage} onValueChange={onChangeAppLanguage}
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
label: l.name, label: l.name,