From c81ecd1ec1f766f696d956af71abceeb1185b6f5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 23 Feb 2024 22:57:33 +0530 Subject: [PATCH] WIP Handle migration --- apps/accounts/src/pages/_app.tsx | 3 +- apps/auth/src/pages/_app.tsx | 3 +- apps/photos/package.json | 2 - apps/photos/src/pages/_app.tsx | 3 +- packages/ui/i18n.ts | 92 ++++++++++++++++++++++++++------ packages/utils/local-storage.ts | 19 ++++--- packages/utils/logging.ts | 33 ++++++++++++ packages/utils/package.json | 3 +- yarn.lock | 54 ++++++++----------- 9 files changed, 149 insertions(+), 63 deletions(-) create mode 100644 packages/utils/logging.ts diff --git a/apps/accounts/src/pages/_app.tsx b/apps/accounts/src/pages/_app.tsx index 5131bed29..6461ef51e 100644 --- a/apps/accounts/src/pages/_app.tsx +++ b/apps/accounts/src/pages/_app.tsx @@ -13,7 +13,6 @@ import { useLocalState } from '@ente/shared/hooks/useLocalState'; import { setupI18n } from '@/ui/i18n'; import HTTPService from '@ente/shared/network/HTTPService'; import { LS_KEYS, getData } from '@ente/shared/storage/localStorage'; -import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers'; import { getTheme } from '@ente/shared/themes'; import { THEME_COLOR } from '@ente/shared/themes/constants'; import createEmotionCache from '@ente/shared/themes/createEmotionCache'; @@ -64,7 +63,7 @@ export default function App(props: EnteAppProps) { const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK); useEffect(() => { - setupI18n(getUserLocaleString()).finally(() => setIsI18nReady(true)); + setupI18n().finally(() => setIsI18nReady(true)); }, []); const setupPackageName = () => { diff --git a/apps/auth/src/pages/_app.tsx b/apps/auth/src/pages/_app.tsx index e7f611a16..b25fb2511 100644 --- a/apps/auth/src/pages/_app.tsx +++ b/apps/auth/src/pages/_app.tsx @@ -37,7 +37,6 @@ import { useLocalState } from '@ente/shared/hooks/useLocalState'; import { PHOTOS_PAGES as PAGES } from '@ente/shared/constants/pages'; import { getTheme } from '@ente/shared/themes'; import '../../public/css/global.css'; -import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers'; type AppContextType = { showNavBar: (show: boolean) => void; @@ -81,7 +80,7 @@ export default function App(props: EnteAppProps) { useEffect(() => { //setup i18n - setupI18n(getUserLocaleString()).finally(() => setIsI18nReady(true)); + setupI18n().finally(() => setIsI18nReady(true)); // set client package name in headers HTTPService.setHeaders({ 'X-Client-Package': CLIENT_PACKAGE_NAMES.get(APPS.AUTH), diff --git a/apps/photos/package.json b/apps/photos/package.json index 24e3d1e84..02808b1b3 100644 --- a/apps/photos/package.json +++ b/apps/photos/package.json @@ -58,7 +58,6 @@ "uuid": "^9.0.0", "vscode-uri": "^3.0.7", "xml-js": "^1.6.11", - "yup": "^0.29.3", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -76,7 +75,6 @@ "@types/react-window-infinite-loader": "^1.0.3", "@types/uuid": "^9.0.2", "@types/wicg-file-system-access": "^2020.9.5", - "@types/yup": "^0.29.7", "@types/zxcvbn": "^4.4.1" } } diff --git a/apps/photos/src/pages/_app.tsx b/apps/photos/src/pages/_app.tsx index 6341e1017..39cfbb3de 100644 --- a/apps/photos/src/pages/_app.tsx +++ b/apps/photos/src/pages/_app.tsx @@ -66,7 +66,6 @@ import { REDIRECTS } from 'constants/redirects'; import { getLocalMapEnabled, getToken, - getUserLocaleString, setLocalMapEnabled, } from '@ente/shared/storage/localStorage/helpers'; import { isExportInProgress } from 'utils/export'; @@ -163,7 +162,7 @@ export default function App(props: EnteAppProps) { useEffect(() => { //setup i18n - setupI18n(getUserLocaleString()).finally(() => setIsI18nReady(true)); + setupI18n().finally(() => setIsI18nReady(true)); // set client package name in headers HTTPService.setHeaders({ 'X-Client-Package': CLIENT_PACKAGE_NAMES.get(APPS.PHOTOS), diff --git a/packages/ui/i18n.ts b/packages/ui/i18n.ts index 32174c696..5d391ae0e 100644 --- a/packages/ui/i18n.ts +++ b/packages/ui/i18n.ts @@ -4,6 +4,14 @@ import Backend from "i18next-http-backend"; import { isDevBuild } from "@/utils/env"; import { getUserLocales } from "get-user-locale"; import { includes } from "@/utils/type-guards"; +import { + type LSKey, + getLSString, + setLSString, + removeLSString, +} from "@/utils/local-storage"; +import { object, string } from "yup"; +import { logError } from "@/utils/logging"; /** * List of all {@link SupportedLocale}s. @@ -41,8 +49,9 @@ export type SupportedLocale = (typeof supportedLocales)[number]; * * - react-i18next, which adds React specific APIs */ -export const setupI18n = async (savedLocaleString?: string) => { - const locale = closestSupportedLocale(savedLocaleString); +export const setupI18n = async () => { + const localeString = savedLocaleStringMigratingIfNeeded(); + const locale = closestSupportedLocale(localeString); // https://www.i18next.com/overview/api await i18n @@ -100,6 +109,70 @@ export const setupI18n = async (savedLocaleString?: string) => { }); }; +/** + * Read and return the locale (if any) that we'd previously saved in local + * storage. + * + * If it finds a locale stored in the old format, it also updates the saved + * value and returns it in the new format. + */ +const savedLocaleStringMigratingIfNeeded = () => { + const ls = getLSString("locale"); + + // An older version of our code had stored only the language code, not the + // full locale. Migrate these to the new locale format. Luckily, all such + // languages can be unambiguously mapped to locales in our current set. + // + // This migration is dated Feb 2024. And it can be removed after a few + // months, because by then either customers would've opened the app and + // their setting migrated to the new format, or the browser would've cleared + // the older local storage entry anyway. + + if (!ls) { + // Nothing found + return ls; + } + + if (includes(supportedLocales, ls)) { + // Already in the new format + return ls; + } + + let value: string | undefined; + try { + const oldFormatData = object({ value: string() }).json().cast(ls); + value = oldFormatData.value; + } catch (e) { + // Not a valid JSON, or not in the format we expected it. This shouldn't + // have happened, we're the only one setting it. + logError("Failed to parse locale obtained from local storage", e); + // Also remove the old key, it is not parseable by us anymore. + removeLSString("locale"); + return undefined; + } + + const newValue = mapOldValue(value); + if (newValue) setLSString("locale", newValue); + return newValue; +}; + +const mapOldValue = (value: string | undefined) => { + switch (value) { + case "en": + return "en-US"; + case "fr": + return "fr-FR"; + case "zh": + return "zh-CN"; + case "nl": + return "nl-NL"; + case "es": + return "es-ES"; + default: + return undefined; + } +}; + /** * Return the closest / best matching {@link SupportedLocale}. * @@ -117,21 +190,6 @@ export function closestSupportedLocale( const ss = savedLocaleString; if (ss && includes(supportedLocales, ss)) return ss; - // An older version of our code had stored only the language code, not the - // full locale. Map these to the default region we'd started off with. - switch (savedLocaleString) { - case "en": - return "en-US"; - case "fr": - return "fr-FR"; - case "zh": - return "zh-CN"; - case "nl": - return "nl-NL"; - case "es": - return "es-ES"; - } - for (const us of getUserLocales()) { // Exact match if (us && includes(supportedLocales, us)) return us; diff --git a/packages/utils/local-storage.ts b/packages/utils/local-storage.ts index b32377f58..7da0b150c 100644 --- a/packages/utils/local-storage.ts +++ b/packages/utils/local-storage.ts @@ -1,5 +1,5 @@ /** - * Keys corresponding to the values that we save in local storage. + * Keys corresponding to the items that we save in local storage. * * The type of each of the these keys is {@link LSKey}. * @@ -17,17 +17,24 @@ export const lsKeys = ["locale"] as const; export type LSKey = (typeof lsKeys)[number]; /** - * Read a previously saved string value from local storage + * Read a previously saved string from local storage */ export const getLSString = (key: LSKey) => { - const value = localStorage.getItem(key); - if (value === null) return undefined; - return value; + const item = localStorage.getItem(key); + if (item === null) return undefined; + return item; }; /** - * Save a string value in local storage + * Save a string in local storage */ export const setLSString = (key: LSKey, value: string) => { localStorage.setItem(key, value); }; + +/** + * Remove an string from local storage. + */ +export const removeLSString = (key: LSKey) => { + localStorage.removeItem(key); +}; diff --git a/packages/utils/logging.ts b/packages/utils/logging.ts new file mode 100644 index 000000000..cff050e53 --- /dev/null +++ b/packages/utils/logging.ts @@ -0,0 +1,33 @@ +/** + * 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. + * + * 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. + * + * Where and how this error gets logged is dependent on where this code is + * running. The default implementation logs a string to the console, but in + * practice the layers above us will use the hooks provided in this file to + * 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. + */ +export const logError = (message: string, e: unknown) => { + let es: string; + if (e instanceof Error) { + // In practice, we expect ourselves to be called with Error objects, so + // this is the happy path so to say. + es = `${e.name}: ${e.message}\n${e.stack}`; + } else { + // For the rest rare cases, use the default string serialization of e. + es = String(e); + } + console.error(`${message}: ${es}`); +}; diff --git a/packages/utils/package.json b/packages/utils/package.json index 47910f72e..088348e11 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -4,7 +4,8 @@ "private": true, "dependencies": { "is-electron": "^2.2", - "libsodium-wrappers": "0.7.9" + "libsodium-wrappers": "0.7.9", + "yup": "^1.3.3" }, "devDependencies": { "@/build-config": "*", diff --git a/yarn.lock b/yarn.lock index 7e865356a..951720513 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,7 +41,7 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== @@ -579,7 +579,7 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" -"@sentry/cli@^1.74.6": +"@sentry/cli@1.75.0", "@sentry/cli@^1.74.6": version "1.75.0" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.0.tgz#4a5e71b5619cd4e9e6238cc77857c66f6b38d86a" integrity sha512-vT8NurHy00GcN8dNqur4CMIYvFH3PaKdkX3qllVvi4syybKqjwoz+aWRCvprbYv0knweneFkLt1SmBWqazUMfA== @@ -960,11 +960,6 @@ resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz#a8b739854ccb74b8048ef607d3701e9d506830e7" integrity sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A== -"@types/yup@^0.29.7": - version "0.29.14" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.14.tgz#754f1dccedcc66fc2bbe290c27f5323b407ceb00" - integrity sha512-Ynb/CjHhE/Xp/4bhHmQC4U1Ox+I2OpfRYF3dnNgQqn1cHa6LK3H1wJMNPT02tSVZA6FYuXE2ITORfbnb6zBCSA== - "@types/zxcvbn@^4.4.1": version "4.4.4" resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.4.tgz#987f5fcd87e957097433c476c3a1c91a54f53131" @@ -2217,11 +2212,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -fn-name@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c" - integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA== - follow-redirects@^1.15.4: version "1.15.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" @@ -2986,7 +2976,7 @@ libsodium-wrappers@0.7.9: dependencies: libsodium "^0.7.0" -libsodium@^0.7.0: +libsodium@0.7.9, libsodium@^0.7.0: version "0.7.9" resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.9.tgz#4bb7bcbf662ddd920d8795c227ae25bbbfa3821b" integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A== @@ -3024,7 +3014,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.11, lodash-es@^4.17.21: +lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -3039,7 +3029,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3527,7 +3517,7 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.2: +property-expr@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== @@ -4169,11 +4159,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -synchronous-promise@^2.0.13: - version "2.0.17" - resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032" - integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g== - tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -4197,6 +4182,11 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -4286,6 +4276,11 @@ type-fest@^0.7.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + typed-array-buffer@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" @@ -4550,18 +4545,15 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yup@^0.29.3: - version "0.29.3" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea" - integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ== +yup@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.3.tgz#d2f6020ad1679754c5f8178a29243d5447dead04" + integrity sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw== dependencies: - "@babel/runtime" "^7.10.5" - fn-name "~3.0.0" - lodash "^4.17.15" - lodash-es "^4.17.11" - property-expr "^2.0.2" - synchronous-promise "^2.0.13" + property-expr "^2.0.5" + tiny-case "^1.0.3" toposort "^2.0.2" + type-fest "^2.19.0" zxcvbn@^4.4.2: version "4.4.2"