i18n settings improvements (#2184)
* Handle language selector * Improve type safety * Add a little more safety * Update commentzio/stable
parent
d82b1a1047
commit
c6ab6e8b8e
|
@ -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)
|
||||||
|
})
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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[] = [
|
||||||
|
|
|
@ -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}))
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue