From 11bbad49fd91696ed83ce707b964f69f79349860 Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Sat, 25 Jul 2020 16:18:47 +0200
Subject: [PATCH] Fix date formatting on language change

---
 package.json                  |  1 +
 src/app.tsx                   | 42 ++++++++++++++++++++++-------------
 src/i18n.ts                   | 18 +++++++++++----
 src/pages/main.tsx            |  6 ++---
 src/state/actions/index.ts    | 20 +++++++++++++++++
 src/state/actions/settings.ts | 12 ++++++++++
 src/state/reducer/index.ts    |  4 ++++
 src/state/reducer/settings.ts | 24 ++++++++++++++++++++
 translations/en.yaml          |  6 ++---
 translations/pl.yaml          |  6 ++---
 yarn.lock                     |  5 +++++
 11 files changed, 116 insertions(+), 28 deletions(-)
 create mode 100644 src/state/actions/settings.ts
 create mode 100644 src/state/reducer/settings.ts

diff --git a/package.json b/package.json
index f1d1f0f..93cb25f 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "react-dev-utils": "^10.2.1",
     "react-dom": "^16.13.1",
     "react-i18next": "^11.7.0",
+    "react-moment": "^0.9.7",
     "react-redux": "^7.2.0",
     "react-router-dom": "^5.2.0",
     "redux": "^4.0.5",
diff --git a/src/app.tsx b/src/app.tsx
index 0f42d36..3dc9cb6 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,23 +1,27 @@
-import React, { Dispatch, HTMLProps, useEffect } from 'react';
+import React, { HTMLProps, useEffect } from 'react';
 import { Link, Route, Switch } from "react-router-dom"
-import moment from "moment";
 import { route, routes } from "@/routing";
-import { useDispatch, useSelector } from "react-redux";
-import { AppState } from "@/state/reducer";
-import { StudentAction, StudentActions } from "@/state/actions/student";
+import { useSelector } from "react-redux";
+import { AppState, isReady } from "@/state/reducer";
+import { StudentActions } from "@/state/actions/student";
 import { sampleStudent } from "@/provider/dummy/student";
 import { Trans, useTranslation } from "react-i18next";
 import { Student } from "@/data";
 import '@/styles/overrides.scss'
 import '@/styles/header.scss'
 import classNames from "classnames";
-import { EditionAction, EditionActions } from "@/state/actions/edition";
+import { EditionActions } from "@/state/actions/edition";
 import { sampleEdition } from "@/provider/dummy/edition";
 import { Edition } from "@/data/edition";
+import { SettingActions } from "@/state/actions/settings";
+import { useDispatch } from "@/state/actions";
+import { getLocale, Locale } from "@/state/reducer/settings";
+import i18n from "@/i18n";
+import moment from "moment";
 
 const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
     const student = useSelector<AppState, Student>(state => state.student as Student);
-    const dispatch = useDispatch<Dispatch<StudentAction>>();
+    const dispatch = useDispatch();
     const { t } = useTranslation();
 
     const handleUserLogin = () => {
@@ -47,17 +51,17 @@ const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
 const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>) => {
     const { i18n } = useTranslation();
 
-    const handleLanguageChange = (language: string) => () => {
-        i18n.changeLanguage(language);
-        document.documentElement.lang = language;
-        moment.locale(language)
+    const dispatch = useDispatch();
+
+    const handleLanguageChange = (language: Locale) => () => {
+        dispatch({ type: SettingActions.SetLocale, locale: language })
     }
 
     const isActive = (language: string) => language.toLowerCase() === i18n.language.toLowerCase();
 
     return <ul className={ classNames(className, "language-switcher") } { ...props }>
         { ['pl', 'en'].map(language => <li key={ language }>
-            <Link to="#" onClick={ handleLanguageChange(language) }
+            <Link to="#" onClick={ handleLanguageChange(language as Locale) }
                   className={ classNames("language-switcher__language", isActive(language) && "language-switcher__language--active") }>
                 { language }
             </Link>
@@ -66,16 +70,24 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>)
 }
 
 function App() {
-    const dispatch = useDispatch<Dispatch<EditionAction>>();
+    const dispatch = useDispatch();
     const edition = useSelector<AppState, Edition | null>(state => state.edition);
 
+    const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
+
     useEffect(() => {
         if (!edition) {
             dispatch({ type: EditionActions.Set, edition: sampleEdition });
         }
     })
 
-    const isReady = !!edition;
+    useEffect(() => {
+        i18n.changeLanguage(locale);
+        document.documentElement.lang = locale;
+        moment.locale(locale)
+    })
+
+    const ready = useSelector(isReady);
 
     return <>
         <header className="header">
@@ -96,7 +108,7 @@ function App() {
                 </nav>
             </div>
         </header>
-        { isReady && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> }
+        { ready && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> }
     </>;
 }
 
diff --git a/src/i18n.ts b/src/i18n.ts
index 4f8c6a2..eb71721 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -2,10 +2,9 @@ import i18n from "i18next";
 import { initReactI18next } from "react-i18next";
 import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
 
-import moment from "moment";
-
 import "moment/locale/pl"
 import "moment/locale/en-gb"
+import moment, { isDuration, isMoment } from "moment";
 
 const resources = {
     en: {
@@ -21,9 +20,20 @@ i18n
     .use(initReactI18next)
     .init({
         resources,
-        fallbackLng: "en",
+        fallbackLng: "pl",
         interpolation: {
-            escapeValue: false
+            escapeValue: false,
+            format: (value, format, lng) => {
+                if (isMoment(value)) {
+                    return value.locale(lng || "pl").format(format || "DD MMM YYYY");
+                }
+
+                if (isDuration(value)) {
+                    return value.locale(lng || "pl").humanize();
+                }
+
+                return value;
+            }
         }
     })
 
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index 0b773d9..c48e059 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -31,10 +31,10 @@ const Step = ({ until, label, completedOn, children, completed, ...props }: Step
             { label }
             { until && <Box>
                 <Typography variant="subtitle2" color="textSecondary">
-                    { t('until', { date: until.format("DD MMMM YYYY") }) }
+                    { t('until', { date: until }) }
                     { isLate && <Typography color="error" display="inline"
-                                            variant="body2"> - { t('late', { by: moment.duration(now.diff(until)).humanize() }) }</Typography> }
-                    { !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left.humanize() }) }</Typography> }
+                                            variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
+                    { !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
                 </Typography>
             </Box> }
         </StepLabel>
diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts
index e69de29..9959cb5 100644
--- a/src/state/actions/index.ts
+++ b/src/state/actions/index.ts
@@ -0,0 +1,20 @@
+import { StudentAction, StudentActions } from "@/state/actions/student";
+import { EditionAction, EditionActions } from "@/state/actions/edition";
+import { SettingActions, SettingsAction } from "@/state/actions/settings";
+import { Dispatch } from "react";
+
+import { useDispatch as useReduxDispatch } from "react-redux";
+
+export * from "./base"
+export * from "./edition"
+export * from "./settings"
+export * from "./student"
+
+export type Action = StudentAction | EditionAction | SettingsAction;
+
+export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions }
+export type Actions = typeof Actions;
+
+export const useDispatch = () => useReduxDispatch<Dispatch<Action>>()
+
+export default Actions;
diff --git a/src/state/actions/settings.ts b/src/state/actions/settings.ts
new file mode 100644
index 0000000..d506565
--- /dev/null
+++ b/src/state/actions/settings.ts
@@ -0,0 +1,12 @@
+import { Action } from "@/state/actions/base";
+import { Locale } from "@/state/reducer/settings";
+
+export enum SettingActions {
+    SetLocale = "SET_LOCALE",
+}
+
+export interface SetLocaleAction extends Action<SettingActions.SetLocale> {
+    locale: Locale
+}
+
+export type SettingsAction = SetLocaleAction;
diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts
index c839818..5d22892 100644
--- a/src/state/reducer/index.ts
+++ b/src/state/reducer/index.ts
@@ -2,12 +2,16 @@ import { combineReducers } from "redux";
 
 import studentReducer from "./student"
 import editionReducer from "@/state/reducer/edition";
+import settingsReducer from "@/state/reducer/settings";
 
 const rootReducer = combineReducers({
     student: studentReducer,
     edition: editionReducer,
+    settings: settingsReducer,
 })
 
 export type AppState = ReturnType<typeof rootReducer>;
 
 export default rootReducer;
+
+export const isReady = (state: AppState) => !!state.edition;
diff --git a/src/state/reducer/settings.ts b/src/state/reducer/settings.ts
new file mode 100644
index 0000000..05757fa
--- /dev/null
+++ b/src/state/reducer/settings.ts
@@ -0,0 +1,24 @@
+import { SettingActions, SettingsAction } from "@/state/actions/settings";
+
+export type Locale = "pl" | "en"
+
+export type SettingsState = {
+    locale: Locale
+}
+
+const defaultSettingsState: SettingsState = {
+    locale: "pl",
+}
+
+const settingsReducer = (state: SettingsState = defaultSettingsState, action: SettingsAction): SettingsState => {
+    switch (action.type) {
+        case SettingActions.SetLocale:
+            return { ...state, locale: action.locale }
+        default:
+            return state;
+    }
+}
+
+export default settingsReducer;
+
+export const getLocale = (state: SettingsState): Locale => state.locale;
diff --git a/translations/en.yaml b/translations/en.yaml
index ca1e2c6..6593964 100644
--- a/translations/en.yaml
+++ b/translations/en.yaml
@@ -3,9 +3,9 @@ login: login
 logout: logout
 logged-in-as: logged in as <1>{{ name }}</1>
 
-until: until {{ date }}
-late: late by {{ by }}
-left: '{{ left }} left'
+until: until {{ date, DD MMMM YYYY }}
+late: late by {{ by, humanize }}
+left: '{{ left, humanize }} left'
 
 dropzone: "Drag and drop a file here or click to choose"
 
diff --git a/translations/pl.yaml b/translations/pl.yaml
index 8b9e395..3134031 100644
--- a/translations/pl.yaml
+++ b/translations/pl.yaml
@@ -3,9 +3,9 @@ login: zaloguj się
 logout: wyloguj się
 logged-in-as: zalogowany jako <1>{{ name }}</1>
 
-until: do {{ date }}
-late: '{{ by }} spóźnienia'
-left: jeszcze {{ left }}
+until: do {{ date, DD MMMM YYYY }}
+late: '{{ by, humanize }} spóźnienia'
+left: jeszcze {{ left, humanize }}
 
 confirm: zatwierdź
 go-back: wstecz
diff --git a/yarn.lock b/yarn.lock
index bad0fdf..9b75961 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7408,6 +7408,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
+react-moment@^0.9.7:
+  version "0.9.7"
+  resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-0.9.7.tgz#ca570466595b1aa4f7619e62da18b3bb2de8b6f3"
+  integrity sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g==
+
 react-redux@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"