Browse Source

WIP Handle migration

Manav Rathi 1 year ago
parent
commit
c81ecd1ec1

+ 1 - 2
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 = () => {

+ 1 - 2
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),

+ 0 - 2
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"
     }
 }

+ 1 - 2
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),

+ 75 - 17
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
@@ -101,25 +110,54 @@ export const setupI18n = async (savedLocaleString?: string) => {
 };
 
 /**
- * Return the closest / best matching {@link SupportedLocale}.
- *
- * It takes as input a {@link savedLocaleString}, which denotes the user's
- * explicitly chosen preference (which we then persist in local storage).
- * Subsequently, we use this to (usually literally) return the supported locale
- * that it represents.
+ * Read and return the locale (if any) that we'd previously saved in local
+ * storage.
  *
- * If {@link savedLocaleString} is `undefined`, it tries to deduce the closest
- * {@link SupportedLocale} that matches the browser's locale.
+ * If it finds a locale stored in the old format, it also updates the saved
+ * value and returns it in the new format.
  */
-export function closestSupportedLocale(
-    savedLocaleString?: string,
-): SupportedLocale {
-    const ss = savedLocaleString;
-    if (ss && includes(supportedLocales, ss)) return ss;
+const savedLocaleStringMigratingIfNeeded = () => {
+    const ls = getLSString("locale");
 
     // 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) {
+    // 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":
@@ -130,7 +168,27 @@ export function closestSupportedLocale(
             return "nl-NL";
         case "es":
             return "es-ES";
+        default:
+            return undefined;
     }
+};
+
+/**
+ * Return the closest / best matching {@link SupportedLocale}.
+ *
+ * It takes as input a {@link savedLocaleString}, which denotes the user's
+ * explicitly chosen preference (which we then persist in local storage).
+ * Subsequently, we use this to (usually literally) return the supported locale
+ * that it represents.
+ *
+ * If {@link savedLocaleString} is `undefined`, it tries to deduce the closest
+ * {@link SupportedLocale} that matches the browser's locale.
+ */
+export function closestSupportedLocale(
+    savedLocaleString?: string,
+): SupportedLocale {
+    const ss = savedLocaleString;
+    if (ss && includes(supportedLocales, ss)) return ss;
 
     for (const us of getUserLocales()) {
         // Exact match

+ 13 - 6
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);
+};

+ 33 - 0
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}`);
+};

+ 2 - 1
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": "*",

+ 24 - 32
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==
-  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"
+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:
+    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"