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) => { const student = useSelector(state => state.student as Student); - const dispatch = useDispatch>(); + const dispatch = useDispatch(); const { t } = useTranslation(); const handleUserLogin = () => { @@ -47,17 +51,17 @@ const UserMenu = (props: HTMLProps) => { const LanguageSwitcher = ({ className, ...props }: HTMLProps) => { 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
    { ['pl', 'en'].map(language =>
  • - { language } @@ -66,16 +70,24 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps) } function App() { - const dispatch = useDispatch>(); + const dispatch = useDispatch(); const edition = useSelector(state => state.edition); + const locale = useSelector(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 <>
    @@ -96,7 +108,7 @@ function App() {
    - { isReady && { routes.map(({ name, content, ...route }) => { content() }) } } + { ready && { routes.map(({ name, content, ...route }) => { content() }) } } ; } 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 && - { t('until', { date: until.format("DD MMMM YYYY") }) } + { t('until', { date: until }) } { isLate && - { t('late', { by: moment.duration(now.diff(until)).humanize() }) } } - { !isLate && !completed && - { t('left', { left: left.humanize() }) } } + variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) } } + { !isLate && !completed && - { t('left', { left: left }) } } } 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>() + +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 { + 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; 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 }} -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 }} -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"