diff --git a/src/locale/__tests__/helpers.test.ts b/src/locale/__tests__/helpers.test.ts new file mode 100644 index 00000000..d4bc6c33 --- /dev/null +++ b/src/locale/__tests__/helpers.test.ts @@ -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) +}) diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index 4d01459a..d80cb503 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -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 } diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts index 93d21f38..2e54b15e 100644 --- a/src/locale/i18n.ts +++ b/src/locale/i18n.ts @@ -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') { - i18n.loadAndActivate({locale, messages: messagesHi}) - } else { - i18n.loadAndActivate({locale, messages: messagesEn}) +export async function dynamicActivate(locale: AppLanguage) { + switch (locale) { + case AppLanguage.hi: { + i18n.loadAndActivate({locale, messages: messagesHi}) + break + } + default: { + i18n.loadAndActivate({locale, messages: messagesEn}) + break + } } } diff --git a/src/locale/i18n.web.ts b/src/locale/i18n.web.ts index bc484f30..54963836 100644 --- a/src/locale/i18n.web.ts +++ b/src/locale/i18n.web.ts @@ -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') { - mod = await import(`./locales/hi/messages`) - } else { - mod = await import(`./locales/en/messages`) + switch (locale) { + case AppLanguage.hi: { + mod = await import(`./locales/hi/messages`) + break + } + default: { + mod = await import(`./locales/en/messages`) + break + } } i18n.load(locale, mod.messages) diff --git a/src/locale/languages.ts b/src/locale/languages.ts index cfcc60c5..e45fdf42 100644 --- a/src/locale/languages.ts +++ b/src/locale/languages.ts @@ -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[] = [ diff --git a/src/state/preferences/languages.tsx b/src/state/preferences/languages.tsx index 8e779cfe..df774c05 100644 --- a/src/state/preferences/languages.tsx +++ b/src/state/preferences/languages.tsx @@ -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( @@ -23,7 +24,7 @@ const apiContext = React.createContext({ 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})) }, }), diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx index eefbfb2e..819840a4 100644 --- a/src/view/screens/LanguageSettings.tsx +++ b/src/view/screens/LanguageSettings.tsx @@ -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 @@ -60,7 +61,7 @@ export function LanguageSettingsScreen(_props: Props) { (value: Parameters[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) { Boolean(l.code2)).map(l => ({ label: l.name,