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 {hasProp} from 'lib/type-guards'
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 {
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)
for (const lang of langs) {
if (['en', 'hi'].includes(lang)) {
return lang
switch (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 messagesHi} from '#/locale/locales/hi/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
export const locales = {
en: 'English',
hi: 'हिंदी',
}
export const defaultLocale = 'en'
import {AppLanguage} from '#/locale/languages'
/**
* We do a dynamic import of just the catalog that we need
* @param locale any locale string
*/
export async function dynamicActivate(locale: string) {
if (locale === 'hi') {
export async function dynamicActivate(locale: AppLanguage) {
switch (locale) {
case AppLanguage.hi: {
i18n.loadAndActivate({locale, messages: messagesHi})
} else {
break
}
default: {
i18n.loadAndActivate({locale, messages: messagesEn})
break
}
}
}

View File

@ -3,24 +3,23 @@ import {i18n} from '@lingui/core'
import {useLanguagePrefs} from '#/state/preferences'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
export const locales = {
en: 'English',
hi: 'हिंदी',
}
export const defaultLocale = 'en'
import {AppLanguage} from '#/locale/languages'
/**
* 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
if (locale === 'hi') {
switch (locale) {
case AppLanguage.hi: {
mod = await import(`./locales/hi/messages`)
} else {
break
}
default: {
mod = await import(`./locales/en/messages`)
break
}
}
i18n.load(locale, mod.messages)

View File

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

View File

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

View File

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