Repository: IraSoro/peri (ts)

Issue:
Add menu
Move ```import```, ```export```, ```language``` and future ```edit``` to the menu component.
Detect the language on the device

Pull request description:
Add menu and language detection
Closed #88
Closed #83

In this PR menu was introduced, also I added language autodetection. From now, if user open app for the first time language will be chosen based on current device language. If language isn't setup then it will be English by default.

i18n initialization was moved to `index.tsx` and `i18n.ts` file was removed, because previously we didn't check i18n initialization before react render and it brings us to the situation when react calling i18 with unitialized translations and plugins

I removed `MultiLanguageModal.tsx` file because all logic was moved to menu and now on startup user doesn't need to choose language, it's will be chosen based on device language

I added `getUnsafe` function to the `Storage` to prevent error throwing on undefined language in storage

Changed ecmaScript target to es2015, because es5 doesn't support `for of` on iterable objects

And finally, I removed all translations from `en` except of `locale` to prevent duplication on every sentence. I checked and it's works fine. I leaved this file just because we check current locale to show right calendar dates' format. This may be fixed if we'll add a new locale settings in menu and will store it in config file

Also we can store current language in config file, I didnt impkemented it

To simplify language check you can use `Sensors` from devtools and clear storage from `Application`->`Storage` in devtools

Here's example of the new menu:
![image](https://github.com/IraSoro/peri/assets/28269602/6f08e6b5-2289-4d5b-92fd-72e1c979ab6c)
![image](https://github.com/IraSoro/peri/assets/28269602/f355061a-24c0-48d7-b443-3cd4a6fb0444)
![image](https://github.com/IraSoro/peri/assets/28269602/4b5b6ecf-ef96-4fc7-9744-2928163bfd46)
![image](https://github.com/IraSoro/peri/assets/28269602/c9fb8afe-14ba-47ae-913f-ab2617d8a83e)

And startup menu on different `navigator.lanuage` values:
![image](https://github.com/IraSoro/peri/assets/28269602/8fb34933-02bd-4403-a568-70052e5df331)
![image](https://github.com/IraSoro/peri/assets/28269602/f47fd0ca-d630-4127-9a50-c89815f26a76)

Patch:
diff --git a/src/App.tsx b/src/App.tsx
index 9710ecc..7fe772c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -11,12 +11,15 @@ import {
   IonHeader,
   IonToolbar,
   IonContent,
+  IonMenuButton,
+  IonIcon,
+  IonButtons,
 } from "@ionic/react";
 import { IonReactRouter } from "@ionic/react-router";
+import { menuOutline } from "ionicons/icons";
 import { useTranslation } from "react-i18next";
 import TabHome from "./pages/TabHome";
 import TabDetails from "./pages/TabDetails";
-import MultiLanguage from "./modals/MultiLanguageModal";
 import "./App.css";
 
 /* Core CSS required for Ionic components to work properly */
@@ -42,6 +45,7 @@ import { storage } from "./data/Storage";
 
 import type { Cycle } from "./data/ClassCycle";
 import { CyclesContext } from "./state/Context";
+import { Menu } from "./modals/Menu";
 
 setupIonicReact();
 
@@ -88,17 +92,22 @@ const App: React.FC = () => {
   return (
     <CyclesContext.Provider value={{ cycles, updateCycles }}>
       <IonApp>
+        <Menu contentId="main-content" />
         <IonReactRouter>
           <IonHeader class="ion-no-border">
             <IonToolbar color="basic">
-              <MultiLanguage
-                isOpen={isLanguageModal}
-                setIsOpen={setIsLanguageModal}
-              />
+              <IonButtons slot="end">
+                <IonMenuButton>
+                  <IonIcon
+                    color="light"
+                    icon={menuOutline}
+                  />
+                </IonMenuButton>
+              </IonButtons>
             </IonToolbar>
           </IonHeader>
 
-          <IonContent>
+          <IonContent id="main-content">
             <IonTabs>
               <IonRouterOutlet>
                 <Route
diff --git a/src/data/Storage.ts b/src/data/Storage.ts
index 9a2c0eb..c877da1 100644
--- a/src/data/Storage.ts
+++ b/src/data/Storage.ts
@@ -3,6 +3,7 @@ import type { Cycle } from "./ClassCycle";
 
 export interface Context {
   cycles: Cycle[];
+  language: string;
 }
 
 const storageImpl = new Storage({
@@ -50,6 +51,10 @@ export const storage = {
     cycles: () => getCycles(true),
     language: () => getLanguage(true),
   },
+  getUnsafe: {
+    cycles: () => getCycles(false),
+    language: () => getLanguage(false),
+  },
 };
 
 // NOTE: Predefined templates for test purpose
diff --git a/src/i18n.ts b/src/i18n.ts
deleted file mode 100644
index a0dfd26..0000000
--- a/src/i18n.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import i18n from "i18next";
-import { initReactI18next } from "react-i18next";
-import intervalPlural from "i18next-intervalplural-postprocessor";
-
-import en from "./translations/en";
-import ru from "./translations/ru";
-
-const resources = {
-  en: {
-    translation: en,
-  },
-  ru: {
-    translation: ru,
-  },
-};
-
-i18n
-  .use(initReactI18next)
-  .use(intervalPlural)
-  .init({
-    resources,
-    lng: "en",
-    fallbackLng: "en",
-    interpolation: {
-      escapeValue: false,
-    },
-  })
-  .catch((err) => console.error(err));
-
-export default i18n;
diff --git a/src/index.tsx b/src/index.tsx
index 9cd3c1a..9ca3d5b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,29 +1,69 @@
 import React from "react";
 import { createRoot } from "react-dom/client";
+
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import intervalPlural from "i18next-intervalplural-postprocessor";
+
+import en from "./translations/en";
+import ru from "./translations/ru";
+import { storage } from "./data/Storage";
+
 import App from "./App";
 import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
 import reportWebVitals from "./reportWebVitals";
-import "./i18n";
 
-const container = document.getElementById("root");
+async function initTranslation() {
+  const selectedLanguage =
+    (await storage.getUnsafe.language()) || navigator.language.split("-")[0];
+
+  console.log(selectedLanguage);
+
+  await i18n
+    .use(initReactI18next)
+    .use(intervalPlural)
+    .init({
+      resources: {
+        en: {
+          translation: en,
+        },
+        ru: {
+          translation: ru,
+        },
+      },
+      lng: selectedLanguage,
+      fallbackLng: "en",
+      interpolation: {
+        escapeValue: false,
+      },
+    });
 
-if (!container) {
-  throw new Error("Can't find element in the document with id `root`");
+  await storage.set.language(selectedLanguage);
 }
 
-const root = createRoot(container);
-root.render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-);
-
-// If you want your app to work offline and load faster, you can change
-// unregister() to register() below. Note this comes with some pitfalls.
-// Learn more about service workers: https://cra.link/PWA
-serviceWorkerRegistration.unregister();
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
+initTranslation()
+  .then(() => {
+    const container = document.getElementById("root");
+
+    if (!container) {
+      throw new Error("Can't find element in the document with id `root`");
+    }
+
+    const root = createRoot(container);
+    root.render(
+      <React.StrictMode>
+        <App />
+      </React.StrictMode>,
+    );
+
+    // If you want your app to work offline and load faster, you can change
+    // unregister() to register() below. Note this comes with some pitfalls.
+    // Learn more about service workers: https://cra.link/PWA
+    serviceWorkerRegistration.unregister();
+
+    // If you want to start measuring performance in your app, pass a function
+    // to log results (for example: reportWebVitals(console.log))
+    // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+    reportWebVitals();
+  })
+  .catch((err) => console.error(err));
diff --git a/src/modals/MarkModal.tsx b/src/modals/MarkModal.tsx
index ec4ce90..e41d41f 100644
--- a/src/modals/MarkModal.tsx
+++ b/src/modals/MarkModal.tsx
@@ -97,7 +97,7 @@ const MarkModal = (props: PropsMarkModal) => {
           ref={datetimeRef}
           color="dark-basic"
           presentation="date"
-          locale={t("local")}
+          locale={t("locale")}
           size="cover"
           multiple
           firstDayOfWeek={1}
diff --git a/src/modals/Menu.tsx b/src/modals/Menu.tsx
new file mode 100644
index 0000000..695879f
--- /dev/null
+++ b/src/modals/Menu.tsx
@@ -0,0 +1,181 @@
+import { useContext } from "react";
+import {
+  IonIcon,
+  IonItem,
+  IonLabel,
+  IonList,
+  IonMenu,
+  IonSelect,
+  IonSelectOption,
+  useIonAlert,
+} from "@ionic/react";
+import {
+  cloudDownloadOutline,
+  cloudUploadOutline,
+  createOutline,
+  globeOutline,
+} from "ionicons/icons";
+import { useTranslation } from "react-i18next";
+import { storage } from "../data/Storage";
+import { exportConfig, importConfig } from "../data/Config";
+import { CyclesContext } from "../state/Context";
+
+const LanguageSwitcher = () => {
+  const { t, i18n } = useTranslation();
+
+  type LanguageCode = string;
+  type Language = string;
+  const supportedLanguages = new Map<LanguageCode, Language>([
+    ["en", "english"],
+    ["ru", "русский"],
+  ]);
+
+  const changeLanguage = async (languageCode: string) => {
+    await i18n.changeLanguage(languageCode);
+    await storage.set.language(languageCode);
+  };
+
+  const languages = [];
+  for (const [code, language] of supportedLanguages.entries()) {
+    languages.push(
+      <IonSelectOption
+        key={code}
+        value={code}
+      >
+        {language}
+      </IonSelectOption>,
+    );
+  }
+
+  return (
+    <IonItem>
+      <IonIcon
+        slot="start"
+        icon={globeOutline}
+      />
+      <IonLabel color="dark">{t("Language")}</IonLabel>
+      <IonSelect
+        value={i18n.language}
+        placeholder={supportedLanguages.get(i18n.language)}
+        interface="popover"
+        onIonChange={(event) => {
+          changeLanguage(event.target.value as string).catch((err) => {
+            console.error(err);
+          });
+        }}
+      >
+        {languages}
+      </IonSelect>
+    </IonItem>
+  );
+};
+
+const Importer = () => {
+  const { t } = useTranslation();
+  const [confirmAlert] = useIonAlert();
+  const updateCycles = useContext(CyclesContext).updateCycles;
+
+  const onImportClick = async () => {
+    console.log("Import config");
+    const config = await importConfig();
+    await storage.set.cycles(config.cycles);
+    updateCycles(config.cycles);
+    await confirmAlert({
+      header: t("Configuration has been imported"),
+      cssClass: "header-color",
+      buttons: [
+        {
+          text: "OK",
+          role: "confirm",
+        },
+      ],
+    });
+  };
+
+  return (
+    <IonItem
+      button
+      onClick={() => {
+        onImportClick().catch((err) => console.error(err));
+      }}
+    >
+      <IonIcon
+        slot="start"
+        icon={cloudDownloadOutline}
+      />
+      <IonLabel>{t("Import config")}</IonLabel>
+    </IonItem>
+  );
+};
+
+const Exporter = () => {
+  const { t } = useTranslation();
+
+  const onExportClick = async () => {
+    const cycles = await storage.get.cycles();
+    const language = await storage.get.language();
+    await exportConfig({ cycles, language });
+  };
+
+  return (
+    <IonItem
+      button
+      onClick={() => {
+        onExportClick().catch((err) => console.error(err));
+      }}
+    >
+      <IonIcon
+        slot="start"
+        icon={cloudUploadOutline}
+      />
+      <IonLabel>{t("Export config")}</IonLabel>
+    </IonItem>
+  );
+};
+
+const CyclesEditor = () => {
+  const { t } = useTranslation();
+
+  return (
+    <IonItem
+      button
+      onClick={() => {
+        console.log("Click on CyclesEditor");
+      }}
+    >
+      <IonIcon
+        slot="start"
+        icon={createOutline}
+      />
+      <IonLabel>{t("Edit cycles")}</IonLabel>
+    </IonItem>
+  );
+};
+
+interface MenuProps {
+  contentId: string;
+}
+
+export const Menu = (props: MenuProps) => {
+  const { t } = useTranslation();
+
+  return (
+    <IonMenu
+      contentId={props.contentId}
+      side="end"
+    >
+      <IonList lines="none">
+        <IonItem lines="full">
+          <IonLabel color="primary">{t("Preferences")}</IonLabel>
+        </IonItem>
+        <LanguageSwitcher />
+        <IonItem lines="full">
+          <IonLabel color="primary">{t("Edit")}</IonLabel>
+        </IonItem>
+        <Importer />
+        <Exporter />
+        <CyclesEditor />
+      </IonList>
+    </IonMenu>
+  );
+};
diff --git a/src/modals/MultiLanguageModal.css b/src/modals/MultiLanguageModal.css
deleted file mode 100644
index 27921f5..0000000
--- a/src/modals/MultiLanguageModal.css
+++ /dev/null
@@ -1,14 +0,0 @@
-ion-modal#settings-modal {
-  --width: fit-content;
-  --min-width: 250px;
-  --height: fit-content;
-  --border-radius: 20px;
-  --box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4);
-}
-
-ion-card-content.align-center {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  height: 100%;
-}
diff --git a/src/modals/MultiLanguageModal.tsx b/src/modals/MultiLanguageModal.tsx
deleted file mode 100644
index 6e55f09..0000000
--- a/src/modals/MultiLanguageModal.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import {
-  IonLabel,
-  IonButton,
-  IonIcon,
-  IonModal,
-  IonCard,
-  IonCardContent,
-  IonList,
-  IonRadioGroup,
-  IonRadio,
-  IonItem,
-} from "@ionic/react";
-import { globeOutline } from "ionicons/icons";
-import { useTranslation } from "react-i18next";
-import { enGB, ru } from "date-fns/locale";
-import setDefaultOptions from "date-fns/setDefaultOptions";
-import { storage } from "../data/Storage";
-import "./MultiLanguageModal.css";
-
-interface PropsLanguageModal {
-  isOpen: boolean;
-  setIsOpen: (newIsOpen: boolean) => void;
-}
-
-const MultiLanguage = (props: PropsLanguageModal) => {
-  const { t, i18n } = useTranslation();
-  const changeLanguage = (lng: string) => {
-    i18n.changeLanguage(lng).catch((err) => console.error(err));
-    storage.set.language(lng.toString()).catch((err) => console.error(err));
-  };
-
-  if (t("locale") === "en-GB") {
-    setDefaultOptions({ locale: enGB });
-  } else if (t("locale") === "ru") {
-    setDefaultOptions({ locale: ru });
-  }
-
-  return (
-    <>
-      <IonButton
-        slot="end"
-        fill="clear"
-        onClick={() => props.setIsOpen(true)}
-      >
-        <IonIcon
-          color="light"
-          icon={globeOutline}
-        />
-      </IonButton>
-      <IonModal
-        isOpen={props.isOpen}
-        id="settings-modal"
-        backdropDismiss={false}
-      >
-        <IonCard color="light">
-          <IonCardContent class="align-center">
-            <IonList>
-              <IonRadioGroup value={i18n.language}>
-                <IonItem>
-                  <IonLabel>english</IonLabel>
-                  <IonRadio
-                    color="basic"
-                    slot="end"
-                    value="en"
-                    onClick={() => {
-                      changeLanguage("en");
-                    }}
-                  ></IonRadio>
-                </IonItem>
-
-                <IonItem>
-                  <IonLabel>русский</IonLabel>
-                  <IonRadio
-                    color="basic"
-                    slot="end"
-                    value="ru"
-                    onClick={() => {
-                      changeLanguage("ru");
-                    }}
-                  ></IonRadio>
-                </IonItem>
-              </IonRadioGroup>
-            </IonList>
-            <IonButton
-              color="basic"
-              onClick={() => {
-                props.setIsOpen(false);
-              }}
-            >
-              Ok
-            </IonButton>
-          </IonCardContent>
-        </IonCard>
-      </IonModal>
-    </>
-  );
-};
-
-export default MultiLanguage;
diff --git a/src/modals/WelcomeModal.tsx b/src/modals/WelcomeModal.tsx
index 47d50b5..8fe3c4f 100644
--- a/src/modals/WelcomeModal.tsx
+++ b/src/modals/WelcomeModal.tsx
@@ -7,19 +7,12 @@ import {
   useIonAlert,
   IonCol,
   IonTitle,
-  IonHeader,
-  IonToolbar,
-  IonIcon,
   IonDatetime,
 } from "@ionic/react";
-import { cloudDownloadOutline } from "ionicons/icons";
 import "./WelcomeModal.css";
 
 import type { Cycle } from "../data/ClassCycle";
-import { importConfig } from "../data/Config";
-import { storage } from "../data/Storage";
 import { useTranslation } from "react-i18next";
-import MultiLanguage from "../modals/MultiLanguageModal";
 
 import { CyclesContext } from "../state/Context";
 
@@ -51,14 +44,6 @@ const Welcome = (props: PropsWelcomeModal) => {
       isOpen={props.isOpen}
       backdropDismiss={false}
     >
-      <IonHeader class="ion-no-border">
-        <IonToolbar color="basic">
-          <MultiLanguage
-            isOpen={props.isLanguageModal}
-            setIsOpen={props.setIsLanguageModal}
-          />
-        </IonToolbar>
-      </IonHeader>
       <IonContent
         fullscreen
         color="basic"
@@ -119,50 +104,6 @@ const Welcome = (props: PropsWelcomeModal) => {
             {t("Continue")}
           </IonButton>
         </IonCol>
-        <IonCol>
-          <IonLabel color="dark-basic">{t("or")}</IonLabel>
-        </IonCol>
-        <IonCol>
-          <IonButton
-            class="welcome"
-            color="dark-basic"
-            onClick={() => {
-              importConfig()
-                .then((config) => {
-                  storage.set
-                    .cycles(config.cycles)
-                    .then(() => {
-                      updateCycles(config.cycles);
-                      confirmAlert({
-                        header: "Configuration has been imported",
-                        cssClass: "header-color",
-                        buttons: [
-                          {
-                            text: "OK",
-                            role: "confirm",
-                            handler: () => {
-                              props.setIsOpen(false);
-                            },
-                          },
-                        ],
-                      }).catch((err) => console.error(err));
-                    })
-                    .catch((err) => {
-                      console.error(err);
-                    });
-                })
-                .catch((err) => {
-                  console.error(err);
-                });
-            }}
-          >
-            <IonIcon
-              slot="start"
-              icon={cloudDownloadOutline}
-            />
-            {t("import data")}
-          </IonButton>
-        </IonCol>
       </IonContent>
     </IonModal>
   );
diff --git a/src/pages/TabDetails.css b/src/pages/TabDetails.css
index 10c5b7e..fbcdad7 100644
--- a/src/pages/TabDetails.css
+++ b/src/pages/TabDetails.css
@@ -9,7 +9,7 @@
 
 #rectangle-bottom {
   width: 100%;
-  height: 65%;
+  height: 70%;
   background: #ffffff;
   /* border-radius: 20px; */
 }
diff --git a/src/pages/TabDetails.tsx b/src/pages/TabDetails.tsx
index 0fd579f..4efa116 100644
--- a/src/pages/TabDetails.tsx
+++ b/src/pages/TabDetails.tsx
@@ -6,11 +6,7 @@ import {
   IonProgressBar,
   IonItem,
   IonList,
-  IonButton,
-  IonIcon,
-  useIonAlert,
 } from "@ionic/react";
-import { cloudDownloadOutline, cloudUploadOutline } from "ionicons/icons";
 import "./TabDetails.css";
 
 import {
@@ -22,8 +18,6 @@ import {
 } from "../state/CycleInformationHooks";
 import { useTranslation } from "react-i18next";
 import { CyclesContext } from "../state/Context";
-import { exportConfig, importConfig } from "../data/Config";
-import { storage } from "../data/Storage";
 
 function useTitleLastCycle() {
   const dayOfCycle = useDayOfCycle();
@@ -174,10 +168,6 @@ const ListProgress = () => {
 const TabDetails = () => {
   const { t } = useTranslation();
 
-  const [confirmAlert] = useIonAlert();
-
-  const updateCycles = useContext(CyclesContext).updateCycles;
-
   const averageLengthOfCycle = useAverageLengthOfCycle();
   const averageLengthOfPeriod = useAverageLengthOfPeriod();
 
@@ -219,66 +209,6 @@ const TabDetails = () => {
             </IonLabel>
           </div>
         </div>
-        <div>
-          <IonButton
-            color="dark-basic"
-            onClick={() => {
-              importConfig()
-                .then((config) => {
-                  storage.set
-                    .cycles(config.cycles)
-                    .then(() => {
-                      updateCycles(config.cycles);
-
-                      confirmAlert({
-                        header: "Configuration has been imported",
-                        cssClass: "header-color",
-                        buttons: [
-                          {
-                            text: "OK",
-                            role: "confirm",
-                          },
-                        ],
-                      }).catch((err) => console.error(err));
-                    })
-                    .catch((err) => {
-                      console.error(err);
-                    });
-                })
-                .catch((err) => {
-                  console.error(err);
-                });
-            }}
-          >
-            <IonIcon
-              slot="start"
-              icon={cloudDownloadOutline}
-            />
-            {t("import")}
-          </IonButton>
-          <IonButton
-            color="dark-basic"
-            disabled={averageLengthOfCycle === 0}
-            onClick={() => {
-              storage.get
-                .cycles()
-                .then((cycles) => {
-                  exportConfig({ cycles }).catch((err) => {
-                    console.error(err);
-                  });
-                })
-                .catch((err) => {
-                  console.error(err);
-                });
-            }}
-          >
-            <IonIcon
-              slot="start"
-              icon={cloudUploadOutline}
-            />
-            {t("export")}
-          </IonButton>
-        </div>
         <div id="rectangle-bottom">
           <IonList class="transparent-center">
             <CurrentCycle />
diff --git a/src/translations/en.tsx b/src/translations/en.tsx
index 55f34bc..440a519 100644
--- a/src/translations/en.tsx
+++ b/src/translations/en.tsx
@@ -1,91 +1,6 @@
 const en = {
-  language: "en",
   locale: "en-GB",
-  // Home Tab
-  Home: "Home",
-  "Period in": "Period in",
-  "no info": "no info",
-  Period: "Period",
-  today: "today",
-  Delay: "Delay",
-  Days_interval: "(0,2-inf){Days};(1){Day};",
-  "Current cycle day": "Current cycle day",
-  Ovulation: "Ovulation",
-  possible: "possible",
-  finished: "finished",
-  tomorrow: "tomorrow",
-  in: "in",
-  "Chance of getting pregnant": "Chance of getting pregnant",
-  high: "high",
-  low: "low",
-  // Details Tab
-  Details: "Details",
-  "Period length": "Period length",
-  "Cycle length": "Cycle length",
-  "Current cycle": "Current cycle",
-  started: "Started",
-  "Started date": "Started date",
-  date: "date",
-  import: "import",
-  export: "export",
-  // Mark Modal
-  mark: "mark",
-  "select date range": "select date range",
-  cancel: "cancel",
-  save: "save",
-  // Welcome Modal
-  "Welcome to Peri": "Welcome to Peri",
-  "Please mark the days of your last period":
-    "Please mark the days of your last period",
-  Continue: "Continue",
-  "Forecast will not be generated.": "Forecast will not be generated.",
-  or: "or",
-  "import data": "import data",
-  // Info Modal
-  "learn more about the current state": "learn more about the current state",
-  "Frequent symptoms": "Frequent symptoms",
-  // Phases info
-  "The menstrual cycle can be divided into 4 phases.":
-    "The menstrual cycle can be divided into 4 phases.",
-  "When information about your cycle appears, it will be reported which phase you are in.":
-    "When information about your cycle appears, it will be reported which phase you are in.",
-  "This section will indicate the symptoms characteristic of this cycle.":
-    "This section will indicate the symptoms characteristic of this cycle.",
-
-  "Menstrual phase": "Menstrual phase",
-  "This cycle is accompanied by low hormone levels.":
-    "This cycle is accompanied by low hormone levels.",
-  "lack of energy and strength": "lack of energy and strength",
-  pain: "pain",
-  "weakness and irritability": "weakness and irritability",
-  "increased appetite": "increased appetite",
-
-  "Follicular phase": "Follicular phase",
-  "The level of estrogen in this phase rises and reaches a maximum level.":
-    "The level of estrogen in this phase rises and reaches a maximum level.",
-  "strength and vigor appear": "strength and vigor appear",
-  "endurance increases": "endurance increases",
-  "new ideas and plans appear": "new ideas and plans appear",
-  "libido increases": "libido increases",
-
-  "Ovulation phase": "Ovulation phase",
-  "Once estrogen levels peak, they trigger the release of two important ovulation hormones, follicle-stimulating hormone and luteinizing hormone.":
-    "Once estrogen levels peak, they trigger the release of two important ovulation hormones, follicle-stimulating hormone and luteinizing hormone.",
-  "increased sexual desire": "increased sexual desire",
-  "optimistic mood": "optimistic mood",
-  "mild fever": "mild fever",
-  "lower abdominal pain": "lower abdominal pain",
-  "chest discomfort and bloating": "chest discomfort and bloating",
-  "characteristic secretions": "characteristic secretions",
-
-  "Luteal phase": "Luteal phase",
-  "Levels of the hormones estrogen and progesterone first rise and then drop sharply just before a period. Progesterone reaches its peak in the luteal phase.":
-    "Levels of the hormones estrogen and progesterone first rise and then drop sharply just before a period. Progesterone reaches its peak in the luteal phase.",
-  "breast tenderness": "breast tenderness",
-  puffiness: "puffiness",
-  "acne and skin rashes": "acne and skin rashes",
-  "diarrhea or constipation": "diarrhea or constipation",
-  "irritability and depressed mood": "irritability and depressed mood",
+  Days_interval: "(0)[Days];(1)[Day];(2-inf)[Days];",
 };
 
 export default en;
diff --git a/src/translations/ru.tsx b/src/translations/ru.tsx
index 00f4cfc..ba06fae 100644
--- a/src/translations/ru.tsx
+++ b/src/translations/ru.tsx
@@ -1,5 +1,4 @@
 const ru = {
-  language: "ru",
   locale: "ru",
   // Home Tab
   Home: "Главная",
@@ -27,8 +26,6 @@ const ru = {
   started: "Начался",
   "Started date": "Дата начала",
   date: "дата",
-  import: "импорт",
-  export: "экспорт",
   // Mark Modal
   mark: "отметить",
   "select date range": "отметьте диапазон месячных",
@@ -41,7 +38,6 @@ const ru = {
   Continue: "Продолжить",
   "Forecast will not be generated.": "Прогноз не будет сгенерирован.",
   or: "или",
-  "import data": "импортировать данные",
   // Info Modal
   "learn more about the current state": "узнать больше о текущем состоянии",
   "Frequent symptoms": "Частые симптомы",
@@ -88,6 +84,13 @@ const ru = {
   "diarrhea or constipation": "диарея или запоры",
   "irritability and depressed mood":
     "раздражительность и подавленное настроение",
+  Preferences: "Настройки",
+  Language: "Язык",
+  Edit: "Редактирование",
+  "Import config": "Импортировать данные",
+  "Export config": "Экспортировать данные",
+  "Edit cycles": "Редактировать циклы",
+  "Configuration has been imported": "Данные были успешно импортированы",
 };
 
 export default ru;
diff --git a/tsconfig.json b/tsconfig.json
index 9d379a3..2d519b5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "target": "es5",
+    "target": "es2015",
     "lib": ["dom", "dom.iterable", "esnext"],
     "allowJs": true,
     "skipLibCheck": true,


Test patch:
diff --git a/src/tests/Config.test.ts b/src/tests/Config.test.ts
index 4ff17c3..f051dc5 100644
--- a/src/tests/Config.test.ts
+++ b/src/tests/Config.test.ts
@@ -101,6 +101,7 @@ describe("exportConfig", () => {
           startDate: "2023-05-31",
         },
       ],
+      language: "en",
     } satisfies Context);
 
     expect(mockedCapacitorFilesystem.Filesystem.writeFile).toHaveBeenCalled();
@@ -135,6 +136,7 @@ describe("exportConfig", () => {
           startDate: "2023-05-31",
         },
       ],
+      language: "en",
     } satisfies Context);
 
     expect(URL.createObjectURL).toHaveBeenCalled();
diff --git a/src/tests/Storage.test.ts b/src/tests/Storage.test.ts
index 7bea79a..f3279f6 100644
--- a/src/tests/Storage.test.ts
+++ b/src/tests/Storage.test.ts
@@ -51,6 +51,15 @@ describe("getCycles", () => {
   });
 });
 
+test("getCyclesUnsafe", async () => {
+  const storageGetSpy = jest
+    .spyOn(Storage.prototype, "get")
+    .mockResolvedValueOnce(undefined);
+
+  await expect(storage.getUnsafe.cycles()).resolves.toBe(undefined);
+  expect(storageGetSpy).toHaveBeenCalledWith("cycles");
+});
+
 test("setLanguage", async () => {
   const storageSetSpy = jest
     .spyOn(Storage.prototype, "set")
@@ -86,3 +95,12 @@ describe("getLanguage", () => {
     expect(storageGetSpy).toHaveBeenCalledWith("language");
   });
 });
+
+test("getLanguageUnsafe", async () => {
+  const storageGetSpy = jest
+    .spyOn(Storage.prototype, "get")
+    .mockResolvedValueOnce(undefined);
+
+  await expect(storage.getUnsafe.language()).resolves.toBe(undefined);
+  expect(storageGetSpy).toHaveBeenCalledWith("language");
+});