diff --git a/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx b/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx index 5a47825de..15ba2fa9c 100644 --- a/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx +++ b/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx @@ -1,14 +1,12 @@ import DropdownInput, { DropdownOption } from 'components/DropdownInput'; -import { useLocalState } from '@ente/shared/hooks/useLocalState'; import { t } from 'i18next'; import { useRouter } from 'next/router'; import { type SupportedLocale, supportedLocales, - closestSupportedLocale, + getLocaleInUse, + setLocaleInUse, } from '@/ui/i18n'; -import { LS_KEYS } from '@ente/shared/storage/localStorage'; -import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers'; /** * Human readable name for each supported locale @@ -38,15 +36,12 @@ const getLanguageOptions = (): DropdownOption[] => { }; export const LanguageSelector = () => { - const [userLocale, setUserLocale] = useLocalState( - LS_KEYS.LOCALE, - closestSupportedLocale(getUserLocaleString()) - ); - + const locale = getLocaleInUse(); + // Enhancement: Is this full reload needed? const router = useRouter(); const updateCurrentLocale = (newLocale: SupportedLocale) => { - setUserLocale(newLocale); + setLocaleInUse(newLocale); router.reload(); }; @@ -55,7 +50,7 @@ export const LanguageSelector = () => { options={getLanguageOptions()} label={t('LANGUAGE')} labelProps={{ color: 'text.muted' }} - selected={userLocale} + selected={locale} setSelected={updateCurrentLocale} /> ); diff --git a/packages/shared/storage/localStorage/helpers.ts b/packages/shared/storage/localStorage/helpers.ts index d9c48865b..d1e8d50dc 100644 --- a/packages/shared/storage/localStorage/helpers.ts +++ b/packages/shared/storage/localStorage/helpers.ts @@ -29,10 +29,6 @@ export function setLivePhotoInfoShownCount(count: boolean) { setData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT, { count }); } -export function getUserLocaleString(): string { - return getData(LS_KEYS.LOCALE)?.value; -} - export function getLocalMapEnabled(): boolean { return getData(LS_KEYS.MAP_ENABLED)?.value ?? false; } diff --git a/packages/shared/storage/localStorage/index.ts b/packages/shared/storage/localStorage/index.ts index b4091de82..17ecf2c12 100644 --- a/packages/shared/storage/localStorage/index.ts +++ b/packages/shared/storage/localStorage/index.ts @@ -21,7 +21,8 @@ export enum LS_KEYS { THEME = 'theme', WAIT_TIME = 'waitTime', API_ENDPOINT = 'apiEndpoint', - LOCALE = 'locale', + // Moved to the new wrapper @/utils/local-storage + // LOCALE = 'locale', MAP_ENABLED = 'mapEnabled', SRP_SETUP_ATTRIBUTES = 'srpSetupAttributes', SRP_ATTRIBUTES = 'srpAttributes', diff --git a/packages/ui/i18n.ts b/packages/ui/i18n.ts index 5d391ae0e..82530c953 100644 --- a/packages/ui/i18n.ts +++ b/packages/ui/i18n.ts @@ -36,6 +36,8 @@ export const supportedLocales = [ /** The type of {@link supportedLocales}. */ export type SupportedLocale = (typeof supportedLocales)[number]; +const defaultLocale: SupportedLocale = "en-US"; + /** * Load translations. * @@ -82,7 +84,7 @@ export const setupI18n = async () => { returnEmptyString: false, // The language to use if translation for a particular key in the // current `lng` is not available. - fallbackLng: "en-US", + fallbackLng: defaultLocale, interpolation: { escapeValue: false, // not needed for react as it escapes by default }, @@ -153,6 +155,7 @@ const savedLocaleStringMigratingIfNeeded = () => { const newValue = mapOldValue(value); if (newValue) setLSString("locale", newValue); + return newValue; }; @@ -184,9 +187,9 @@ const mapOldValue = (value: string | undefined) => { * If {@link savedLocaleString} is `undefined`, it tries to deduce the closest * {@link SupportedLocale} that matches the browser's locale. */ -export function closestSupportedLocale( +const closestSupportedLocale = ( savedLocaleString?: string, -): SupportedLocale { +): SupportedLocale => { const ss = savedLocaleString; if (ss && includes(supportedLocales, ss)) return ss; @@ -209,5 +212,36 @@ export function closestSupportedLocale( } // Fallback - return "en-US"; -} + return defaultLocale; +}; + +/** + * Return the locale that is currently being used to show the app's UI. + * + * Note that this may be different from the user's locale. For example, the + * browser might be set to en-GB, but since we don't support that specific + * variant of English, this value will be (say) en-US. + */ +export const getLocaleInUse = (): SupportedLocale => { + const locale = i18n.resolvedLanguage; + if (locale && includes(supportedLocales, locale)) { + return locale; + } else { + // This shouldn't have happened. Log an error to attract attention. + logError( + `Expected the i18next locale to be one of the supported values, but instead found ${locale}`, + ); + return defaultLocale; + } +}; + +/** + * Set the locale that should be used to show the app's UI. + * + * This updates both the i18next state, and also the corresponding user + * preference that is stored in local storage. + */ +export const setLocaleInUse = async (locale: SupportedLocale) => { + setLSString("locale", locale); + return i18n.changeLanguage(locale); +}; diff --git a/packages/utils/logging.ts b/packages/utils/logging.ts index cff050e53..b3d7d8567 100644 --- a/packages/utils/logging.ts +++ b/packages/utils/logging.ts @@ -1,11 +1,11 @@ /** * Log an error * - * The {@link message} property describes what went wrong. Generally in such - * situations we also have an "error" object that has specific details about the - * issue - that gets passed as the second parameter. + * The {@link message} property describes what went wrong. Generally (but not + * always) in such situations we also have an "error" object that has specific + * details about the issue - that gets passed as the second parameter. * - * Note that the "error" {@link e} is not typed. This is because in JavaScript, + * Note that the "error" {@link e} is not typed. This is because in JavaScript * any arbitrary value can be thrown. So this function allows us to pass it an * arbitrary value as the error, and will internally figure out how best to deal * with it. @@ -16,10 +16,16 @@ * route and show this error elsewhere. * * TODO (MR): Currently this is a placeholder function to funnel error logs - * through. This needs to do what the existing logError does, but it cannot have - * a direct Sentry dependency here. For now, we just log on the console. + * through. This needs to do what the existing logError in @ente/shared does, + * but it cannot have a direct Electron/Sentry dependency here. For now, we just + * log on the console. */ -export const logError = (message: string, e: unknown) => { +export const logError = (message: string, e?: unknown) => { + if (e === undefined || e === null) { + console.error(message); + return; + } + let es: string; if (e instanceof Error) { // In practice, we expect ourselves to be called with Error objects, so