From 9f075b13405c7711edbd9309ab521738b74a50c5 Mon Sep 17 00:00:00 2001 From: Stanislas Signoud Date: Tue, 31 Dec 2024 22:27:14 +0100 Subject: [PATCH] Localize lang selectors according to the app language (#6207) * Localize lang selectors according to the app language * Explicitly ignore RangeError when translating locale names --- src/locale/helpers.ts | 42 ++++++++++- src/locale/languages.ts | 2 +- src/screens/Settings/LanguageSettings.tsx | 8 +-- .../select-language/SelectLangBtn.tsx | 6 +- .../select-language/SuggestedLanguage.tsx | 69 +++++++++++-------- .../ContentLanguagesSettings.tsx | 3 +- .../lang-settings/PostLanguagesSettings.tsx | 3 +- src/view/screens/Search/Search.tsx | 9 +-- 8 files changed, 97 insertions(+), 45 deletions(-) diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index c469b3c58d..94e9d4327e 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -5,6 +5,7 @@ import lande from 'lande' import {hasProp} from '#/lib/type-guards' import { AppLanguage, + type Language, LANGUAGES_MAP_CODE2, LANGUAGES_MAP_CODE3, } from './languages' @@ -31,9 +32,44 @@ export function code3ToCode2Strict(lang: string): string | undefined { return undefined } -export function codeToLanguageName(lang: string): string { - const lang2 = code3ToCode2(lang) - return LANGUAGES_MAP_CODE2[lang2]?.name || lang +function getLocalizedLanguage( + langCode: string, + appLang: string, +): string | undefined { + try { + const allNames = new Intl.DisplayNames([appLang], { + type: 'language', + fallback: 'none', + languageDisplay: 'standard', + }) + const translatedName = allNames.of(langCode) + + if (translatedName) { + // force simple title case (as languages do not always start with an uppercase in Unicode data) + return translatedName[0].toLocaleUpperCase() + translatedName.slice(1) + } + } catch (e) { + // ignore RangeError from Intl.DisplayNames APIs + if (!(e instanceof RangeError)) { + throw e + } + } +} + +export function languageName(language: Language, appLang: string): string { + // if Intl.DisplayNames is unavailable on the target, display the English name + if (!(Intl as any).DisplayNames) { + return language.name + } + + return getLocalizedLanguage(language.code2, appLang) || language.name +} + +export function codeToLanguageName(lang2or3: string, appLang: string): string { + const code2 = code3ToCode2(lang2or3) + const knownLanguage = LANGUAGES_MAP_CODE2[code2] + + return knownLanguage ? languageName(knownLanguage, appLang) : code2 } export function getPostLanguage( diff --git a/src/locale/languages.ts b/src/locale/languages.ts index 6ac601dea0..9f599c8c84 100644 --- a/src/locale/languages.ts +++ b/src/locale/languages.ts @@ -1,4 +1,4 @@ -interface Language { +export interface Language { code3: string code2: string name: string diff --git a/src/screens/Settings/LanguageSettings.tsx b/src/screens/Settings/LanguageSettings.tsx index 096f925669..8fece71298 100644 --- a/src/screens/Settings/LanguageSettings.tsx +++ b/src/screens/Settings/LanguageSettings.tsx @@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react' import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {sanitizeAppLanguageSetting} from '#/locale/helpers' +import {languageName, sanitizeAppLanguageSetting} from '#/locale/helpers' import {useModalControls} from '#/state/modals' import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' import {atoms as a, useTheme, web} from '#/alf' @@ -57,10 +57,10 @@ export function LanguageSettingsScreen({}: Props) { .map(lang => LANGUAGES.find(l => l.code2 === lang)) .filter(Boolean) // @ts-ignore - .map(l => l.name) + .map(l => languageName(l, langPrefs.appLanguage)) .join(', ') ) - }, [langPrefs.contentLanguages]) + }, [langPrefs.appLanguage, langPrefs.contentLanguages]) return ( @@ -179,7 +179,7 @@ export function LanguageSettingsScreen({}: Props) { value={langPrefs.primaryLanguage} onValueChange={onChangePrimaryLanguage} items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, + label: languageName(l, langPrefs.appLanguage), value: l.code2, key: l.code2 + l.code3, }))} diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx index cd3cb608d8..f487b1244b 100644 --- a/src/view/com/composer/select-language/SelectLangBtn.tsx +++ b/src/view/com/composer/select-language/SelectLangBtn.tsx @@ -48,7 +48,7 @@ export function SelectLangBtn() { function add(commaSeparatedLangCodes: string) { const langCodes = commaSeparatedLangCodes.split(',') const langName = langCodes - .map(code => codeToLanguageName(code)) + .map(code => codeToLanguageName(code, langPrefs.appLanguage)) .join(' + ') /* @@ -108,7 +108,9 @@ export function SelectLangBtn() { accessibilityHint=""> {postLanguagesPref.length > 0 ? ( - {postLanguagesPref.map(lang => codeToLanguageName(lang)).join(', ')} + {postLanguagesPref + .map(lang => codeToLanguageName(lang, langPrefs.appLanguage)) + .join(', ')} ) : ( cancelIdle(idle) }, [text]) - return suggestedLanguage && - !toPostLanguages(langPrefs.postLanguage).includes(suggestedLanguage) ? ( - - - - - Are you writing in{' '} - - {codeToLanguageName(suggestedLanguage)} - - ? - - + if ( + suggestedLanguage && + !toPostLanguages(langPrefs.postLanguage).includes(suggestedLanguage) + ) { + const suggestedLanguageName = codeToLanguageName( + suggestedLanguage, + langPrefs.appLanguage, + ) - - - ) : null + + + + ) + } else { + return null + } } const styles = StyleSheet.create({ diff --git a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx index 360cc0e404..aae8e29d65 100644 --- a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx +++ b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx @@ -5,6 +5,7 @@ import {Trans} from '@lingui/macro' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {deviceLanguageCodes} from '#/locale/deviceLocales' +import {languageName} from '#/locale/helpers' import {useModalControls} from '#/state/modals' import { useLanguagePrefs, @@ -88,7 +89,7 @@ export function Component({}: {}) { key={lang.code2} code2={lang.code2} langType="contentLanguages" - name={lang.name} + name={languageName(lang, langPrefs.appLanguage)} onPress={() => { onPress(lang.code2) }} diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx index 2b0eb8cf24..8c29696741 100644 --- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx +++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx @@ -5,6 +5,7 @@ import {Trans} from '@lingui/macro' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {deviceLanguageCodes} from '#/locale/deviceLocales' +import {languageName} from '#/locale/helpers' import {useModalControls} from '#/state/modals' import { hasPostLanguage, @@ -91,7 +92,7 @@ export function Component() { return ( (isDisabled ? undefined : onPress(lang.code2))} style={[ diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 2797c9d950..2c91d37a26 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -37,6 +37,7 @@ import { } from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {augmentSearchQuery} from '#/lib/strings/helpers' +import {languageName} from '#/locale/helpers' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' @@ -328,7 +329,7 @@ function SearchLanguageDropdown({ }) { const t = useThemeNew() const {_} = useLingui() - const {contentLanguages} = useLanguagePrefs() + const {appLanguage, contentLanguages} = useLanguagePrefs() const items = React.useMemo(() => { return [ @@ -345,8 +346,8 @@ function SearchLanguageDropdown({ index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen) ) .map(l => ({ - label: l.name, - inputLabel: l.name, + label: languageName(l, appLanguage), + inputLabel: languageName(l, appLanguage), value: l.code2, key: l.code2 + l.code3, })) @@ -365,7 +366,7 @@ function SearchLanguageDropdown({ return a.label.localeCompare(b.label) }), ) - }, [_, contentLanguages]) + }, [_, appLanguage, contentLanguages]) const style = { backgroundColor: t.atoms.bg_contrast_25.backgroundColor,