WIP Handle migration

This commit is contained in:
Manav Rathi 2024-02-23 22:57:33 +05:30
parent 9c58731403
commit c81ecd1ec1
9 changed files with 149 additions and 63 deletions

View file

@ -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 = () => {

View file

@ -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),

View file

@ -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"
}
}

View file

@ -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),

View file

@ -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;

View file

@ -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
packages/utils/logging.ts Normal file
View file

@ -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}`);
};

View file

@ -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": "*",

View file

@ -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"