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 import lande from 'lande' 3 import {hasProp} from 'lib/type-guards' 4 import * as bcp47Match from 'bcp-47-match' 5 - import {LANGUAGES_MAP_CODE2, LANGUAGES_MAP_CODE3} from './languages' 6 7 export function code2ToCode3(lang: string): string { 8 if (lang.length === 2) { ··· 85 )}` 86 } 87 88 - export function sanitizeAppLanguageSetting(appLanguage: string) { 89 const langs = appLanguage.split(',').filter(Boolean) 90 91 for (const lang of langs) { 92 - if (['en', 'hi'].includes(lang)) { 93 - return lang 94 } 95 } 96 97 - return 'en' 98 }
··· 2 import lande from 'lande' 3 import {hasProp} from 'lib/type-guards' 4 import * as bcp47Match from 'bcp-47-match' 5 + import { 6 + AppLanguage, 7 + LANGUAGES_MAP_CODE2, 8 + LANGUAGES_MAP_CODE3, 9 + } from './languages' 10 11 export function code2ToCode3(lang: string): string { 12 if (lang.length === 2) { ··· 89 )}` 90 } 91 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 { 107 const langs = appLanguage.split(',').filter(Boolean) 108 109 for (const lang of langs) { 110 + switch (lang) { 111 + case 'en': 112 + return AppLanguage.en 113 + case 'hi': 114 + return AppLanguage.hi 115 + default: 116 + continue 117 } 118 } 119 120 + return AppLanguage.en 121 }
+11 -12
src/locale/i18n.ts
··· 5 import {messages as messagesEn} from '#/locale/locales/en/messages' 6 import {messages as messagesHi} from '#/locale/locales/hi/messages' 7 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 8 - 9 - export const locales = { 10 - en: 'English', 11 - hi: 'हिंदी', 12 - } 13 - export const defaultLocale = 'en' 14 15 /** 16 * We do a dynamic import of just the catalog that we need 17 - * @param locale any locale string 18 */ 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}) 24 } 25 } 26
··· 5 import {messages as messagesEn} from '#/locale/locales/en/messages' 6 import {messages as messagesHi} from '#/locale/locales/hi/messages' 7 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 8 + import {AppLanguage} from '#/locale/languages' 9 10 /** 11 * We do a dynamic import of just the catalog that we need 12 */ 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 + } 23 } 24 } 25
+11 -12
src/locale/i18n.web.ts
··· 3 4 import {useLanguagePrefs} from '#/state/preferences' 5 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 6 - 7 - export const locales = { 8 - en: 'English', 9 - hi: 'हिंदी', 10 - } 11 - export const defaultLocale = 'en' 12 13 /** 14 * We do a dynamic import of just the catalog that we need 15 - * @param locale any locale string 16 */ 17 - export async function dynamicActivate(locale: string) { 18 let mod: any 19 20 - if (locale === 'hi') { 21 - mod = await import(`./locales/hi/messages`) 22 - } else { 23 - mod = await import(`./locales/en/messages`) 24 } 25 26 i18n.load(locale, mod.messages)
··· 3 4 import {useLanguagePrefs} from '#/state/preferences' 5 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 6 + import {AppLanguage} from '#/locale/languages' 7 8 /** 9 * We do a dynamic import of just the catalog that we need 10 */ 11 + export async function dynamicActivate(locale: AppLanguage) { 12 let mod: any 13 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 + } 23 } 24 25 i18n.load(locale, mod.messages)
+10 -5
src/locale/languages.ts
··· 4 name: string 5 } 6 7 - interface AppLanguage { 8 - code2: string 9 name: string 10 } 11 12 - export const APP_LANGUAGES: AppLanguage[] = [ 13 - {code2: 'en', name: 'English'}, 14 - {code2: 'hi', name: 'हिंदी'}, 15 ] 16 17 export const LANGUAGES: Language[] = [
··· 4 name: string 5 } 6 7 + export enum AppLanguage { 8 + en = 'en', 9 + hi = 'hi', 10 + } 11 + 12 + interface AppLanguageConfig { 13 + code2: AppLanguage 14 name: string 15 } 16 17 + export const APP_LANGUAGES: AppLanguageConfig[] = [ 18 + {code2: AppLanguage.en, name: 'English'}, 19 + {code2: AppLanguage.hi, name: 'हिंदी'}, 20 ] 21 22 export const LANGUAGES: Language[] = [
+4 -3
src/state/preferences/languages.tsx
··· 1 import React from 'react' 2 import * as persisted from '#/state/persisted' 3 4 type SetStateCb = ( 5 s: persisted.Schema['languagePrefs'], ··· 11 toggleContentLanguage: (code2: string) => void 12 togglePostLanguage: (code2: string) => void 13 savePostLanguageToHistory: () => void 14 - setAppLanguage: (code2: string) => void 15 } 16 17 const stateContext = React.createContext<StateContext>( ··· 23 toggleContentLanguage: (_: string) => {}, 24 togglePostLanguage: (_: string) => {}, 25 savePostLanguageToHistory: () => {}, 26 - setAppLanguage: (_: string) => {}, 27 }) 28 29 export function Provider({children}: React.PropsWithChildren<{}>) { ··· 106 .slice(0, 6), 107 })) 108 }, 109 - setAppLanguage(code2: string) { 110 setStateWrapped(s => ({...s, appLanguage: code2})) 111 }, 112 }),
··· 1 import React from 'react' 2 import * as persisted from '#/state/persisted' 3 + import {AppLanguage} from '#/locale/languages' 4 5 type SetStateCb = ( 6 s: persisted.Schema['languagePrefs'], ··· 12 toggleContentLanguage: (code2: string) => void 13 togglePostLanguage: (code2: string) => void 14 savePostLanguageToHistory: () => void 15 + setAppLanguage: (code2: AppLanguage) => void 16 } 17 18 const stateContext = React.createContext<StateContext>( ··· 24 toggleContentLanguage: (_: string) => {}, 25 togglePostLanguage: (_: string) => {}, 26 savePostLanguageToHistory: () => {}, 27 + setAppLanguage: (_: AppLanguage) => {}, 28 }) 29 30 export function Provider({children}: React.PropsWithChildren<{}>) { ··· 107 .slice(0, 6), 108 })) 109 }, 110 + setAppLanguage(code2: AppLanguage) { 111 setStateWrapped(s => ({...s, appLanguage: code2})) 112 }, 113 }),
+3 -2
src/view/screens/LanguageSettings.tsx
··· 21 import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 22 import {Trans, msg} from '@lingui/macro' 23 import {useLingui} from '@lingui/react' 24 25 type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> 26 ··· 60 (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 61 if (!value) return 62 if (langPrefs.appLanguage !== value) { 63 - setLangPrefs.setAppLanguage(value) 64 } 65 }, 66 [langPrefs, setLangPrefs], ··· 103 <View style={{position: 'relative'}}> 104 <RNPickerSelect 105 placeholder={{}} 106 - value={langPrefs.appLanguage} 107 onValueChange={onChangeAppLanguage} 108 items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 109 label: l.name,
··· 21 import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 22 import {Trans, msg} from '@lingui/macro' 23 import {useLingui} from '@lingui/react' 24 + import {sanitizeAppLanguageSetting} from '#/locale/helpers' 25 26 type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> 27 ··· 61 (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 62 if (!value) return 63 if (langPrefs.appLanguage !== value) { 64 + setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 65 } 66 }, 67 [langPrefs, setLangPrefs], ··· 104 <View style={{position: 'relative'}}> 105 <RNPickerSelect 106 placeholder={{}} 107 + value={sanitizeAppLanguageSetting(langPrefs.appLanguage)} 108 onValueChange={onChangeAppLanguage} 109 items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 110 label: l.name,