Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

i18n settings improvements (#2184)

* Handle language selector

* Improve type safety

* Add a little more safety

* Update comment

authored by

Eric Bailey and committed by
GitHub
c6ab6e8b d82b1a10

+79 -39
+12
src/locale/__tests__/helpers.test.ts
··· 1 + import {test, expect} from '@jest/globals' 2 + 3 + import {sanitizeAppLanguageSetting} from '#/locale/helpers' 4 + import {AppLanguage} from '#/locale/languages' 5 + 6 + test('sanitizeAppLanguageSetting', () => { 7 + expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en) 8 + expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi) 9 + expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en) 10 + expect(sanitizeAppLanguageSetting('en,fr')).toBe(AppLanguage.en) 11 + expect(sanitizeAppLanguageSetting('fr,en')).toBe(AppLanguage.en) 12 + })
+28 -5
src/locale/helpers.ts
··· 2 2 import lande from 'lande' 3 3 import {hasProp} from 'lib/type-guards' 4 4 import * as bcp47Match from 'bcp-47-match' 5 - import {LANGUAGES_MAP_CODE2, LANGUAGES_MAP_CODE3} from './languages' 5 + import { 6 + AppLanguage, 7 + LANGUAGES_MAP_CODE2, 8 + LANGUAGES_MAP_CODE3, 9 + } from './languages' 6 10 7 11 export function code2ToCode3(lang: string): string { 8 12 if (lang.length === 2) { ··· 85 89 )}` 86 90 } 87 91 88 - export function sanitizeAppLanguageSetting(appLanguage: string) { 92 + /** 93 + * Returns a valid `appLanguage` value from an arbitrary string. 94 + * 95 + * Contenxt: post-refactor, we populated some user's `appLanguage` setting with 96 + * `postLanguage`, which can be a comma-separated list of values. This breaks 97 + * `appLanguage` handling in the app, so we introduced this util to parse out a 98 + * valid `appLanguage` from the pre-populated `postLanguage` values. 99 + * 100 + * The `appLanguage` will continue to be incorrect until the user returns to 101 + * language settings and selects a new option, at which point we'll re-save 102 + * their choice, which should then be a valid option. Since we don't know when 103 + * this will happen, we should leave this here until we feel it's safe to 104 + * remove, or we re-migrate their storage. 105 + */ 106 + export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { 89 107 const langs = appLanguage.split(',').filter(Boolean) 90 108 91 109 for (const lang of langs) { 92 - if (['en', 'hi'].includes(lang)) { 93 - return lang 110 + switch (lang) { 111 + case 'en': 112 + return AppLanguage.en 113 + case 'hi': 114 + return AppLanguage.hi 115 + default: 116 + continue 94 117 } 95 118 } 96 119 97 - return 'en' 120 + return AppLanguage.en 98 121 }
+11 -12
src/locale/i18n.ts
··· 5 5 import {messages as messagesEn} from '#/locale/locales/en/messages' 6 6 import {messages as messagesHi} from '#/locale/locales/hi/messages' 7 7 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 8 - 9 - export const locales = { 10 - en: 'English', 11 - hi: 'हिंदी', 12 - } 13 - export const defaultLocale = 'en' 8 + import {AppLanguage} from '#/locale/languages' 14 9 15 10 /** 16 11 * We do a dynamic import of just the catalog that we need 17 - * @param locale any locale string 18 12 */ 19 - export async function dynamicActivate(locale: string) { 20 - if (locale === 'hi') { 21 - i18n.loadAndActivate({locale, messages: messagesHi}) 22 - } else { 23 - i18n.loadAndActivate({locale, messages: messagesEn}) 13 + export async function dynamicActivate(locale: AppLanguage) { 14 + switch (locale) { 15 + case AppLanguage.hi: { 16 + i18n.loadAndActivate({locale, messages: messagesHi}) 17 + break 18 + } 19 + default: { 20 + i18n.loadAndActivate({locale, messages: messagesEn}) 21 + break 22 + } 24 23 } 25 24 } 26 25
+11 -12
src/locale/i18n.web.ts
··· 3 3 4 4 import {useLanguagePrefs} from '#/state/preferences' 5 5 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 6 - 7 - export const locales = { 8 - en: 'English', 9 - hi: 'हिंदी', 10 - } 11 - export const defaultLocale = 'en' 6 + import {AppLanguage} from '#/locale/languages' 12 7 13 8 /** 14 9 * We do a dynamic import of just the catalog that we need 15 - * @param locale any locale string 16 10 */ 17 - export async function dynamicActivate(locale: string) { 11 + export async function dynamicActivate(locale: AppLanguage) { 18 12 let mod: any 19 13 20 - if (locale === 'hi') { 21 - mod = await import(`./locales/hi/messages`) 22 - } else { 23 - mod = await import(`./locales/en/messages`) 14 + switch (locale) { 15 + case AppLanguage.hi: { 16 + mod = await import(`./locales/hi/messages`) 17 + break 18 + } 19 + default: { 20 + mod = await import(`./locales/en/messages`) 21 + break 22 + } 24 23 } 25 24 26 25 i18n.load(locale, mod.messages)
+10 -5
src/locale/languages.ts
··· 4 4 name: string 5 5 } 6 6 7 - interface AppLanguage { 8 - code2: string 7 + export enum AppLanguage { 8 + en = 'en', 9 + hi = 'hi', 10 + } 11 + 12 + interface AppLanguageConfig { 13 + code2: AppLanguage 9 14 name: string 10 15 } 11 16 12 - export const APP_LANGUAGES: AppLanguage[] = [ 13 - {code2: 'en', name: 'English'}, 14 - {code2: 'hi', name: 'हिंदी'}, 17 + export const APP_LANGUAGES: AppLanguageConfig[] = [ 18 + {code2: AppLanguage.en, name: 'English'}, 19 + {code2: AppLanguage.hi, name: 'हिंदी'}, 15 20 ] 16 21 17 22 export const LANGUAGES: Language[] = [
+4 -3
src/state/preferences/languages.tsx
··· 1 1 import React from 'react' 2 2 import * as persisted from '#/state/persisted' 3 + import {AppLanguage} from '#/locale/languages' 3 4 4 5 type SetStateCb = ( 5 6 s: persisted.Schema['languagePrefs'], ··· 11 12 toggleContentLanguage: (code2: string) => void 12 13 togglePostLanguage: (code2: string) => void 13 14 savePostLanguageToHistory: () => void 14 - setAppLanguage: (code2: string) => void 15 + setAppLanguage: (code2: AppLanguage) => void 15 16 } 16 17 17 18 const stateContext = React.createContext<StateContext>( ··· 23 24 toggleContentLanguage: (_: string) => {}, 24 25 togglePostLanguage: (_: string) => {}, 25 26 savePostLanguageToHistory: () => {}, 26 - setAppLanguage: (_: string) => {}, 27 + setAppLanguage: (_: AppLanguage) => {}, 27 28 }) 28 29 29 30 export function Provider({children}: React.PropsWithChildren<{}>) { ··· 106 107 .slice(0, 6), 107 108 })) 108 109 }, 109 - setAppLanguage(code2: string) { 110 + setAppLanguage(code2: AppLanguage) { 110 111 setStateWrapped(s => ({...s, appLanguage: code2})) 111 112 }, 112 113 }),
+3 -2
src/view/screens/LanguageSettings.tsx
··· 21 21 import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 22 22 import {Trans, msg} from '@lingui/macro' 23 23 import {useLingui} from '@lingui/react' 24 + import {sanitizeAppLanguageSetting} from '#/locale/helpers' 24 25 25 26 type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> 26 27 ··· 60 61 (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 61 62 if (!value) return 62 63 if (langPrefs.appLanguage !== value) { 63 - setLangPrefs.setAppLanguage(value) 64 + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 64 65 } 65 66 }, 66 67 [langPrefs, setLangPrefs], ··· 103 104 <View style={{position: 'relative'}}> 104 105 <RNPickerSelect 105 106 placeholder={{}} 106 - value={langPrefs.appLanguage} 107 + value={sanitizeAppLanguageSetting(langPrefs.appLanguage)} 107 108 onValueChange={onChangeAppLanguage} 108 109 items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 109 110 label: l.name,