diff --git a/app/components/LocaleTime.js b/app/components/LocaleTime.js new file mode 100644 index 000000000..f27c8fc97 --- /dev/null +++ b/app/components/LocaleTime.js @@ -0,0 +1,81 @@ +// @flow +import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; +import format from "date-fns/format"; +import * as React from "react"; +import Tooltip from "components/Tooltip"; +import useUserLocale from "hooks/useUserLocale"; + +const locales = { + en: require(`date-fns/locale/en`), + de: require(`date-fns/locale/de`), + es: require(`date-fns/locale/es`), + fr: require(`date-fns/locale/fr`), + ko: require(`date-fns/locale/ko`), + pt: require(`date-fns/locale/pt`), +}; + +let callbacks = []; + +// This is a shared timer that fires every minute, used for +// updating all Time components across the page all at once. +setInterval(() => { + callbacks.forEach((cb) => cb()); +}, 1000 * 60); + +function eachMinute(fn) { + callbacks.push(fn); + + return () => { + callbacks = callbacks.filter((cb) => cb !== fn); + }; +} + +type Props = { + dateTime: string, + children?: React.Node, + tooltipDelay?: number, + addSuffix?: boolean, + shorten?: boolean, +}; + +function Time({ addSuffix, children, dateTime, shorten, tooltipDelay }: Props) { + const userLocale = useUserLocale(); + const [_, setMinutesMounted] = React.useState(0); // eslint-disable-line no-unused-vars + const callback = React.useRef(); + + React.useEffect(() => { + callback.current = eachMinute(() => { + setMinutesMounted((state) => ++state); + }); + + return () => { + if (callback.current) { + callback.current(); + } + }; + }, []); + + let content = distanceInWordsToNow(dateTime, { + addSuffix, + locale: locales[userLocale], + }); + + if (shorten) { + content = content + .replace("about", "") + .replace("less than a minute ago", "just now") + .replace("minute", "min"); + } + + return ( + + + + ); +} + +export default Time; diff --git a/app/components/Time.js b/app/components/Time.js index 927984eed..a7e562760 100644 --- a/app/components/Time.js +++ b/app/components/Time.js @@ -1,24 +1,8 @@ // @flow import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; -import format from "date-fns/format"; import * as React from "react"; -import Tooltip from "components/Tooltip"; -let callbacks = []; - -// This is a shared timer that fires every minute, used for -// updating all Time components across the page all at once. -setInterval(() => { - callbacks.forEach((cb) => cb()); -}, 1000 * 60); - -function eachMinute(fn) { - callbacks.push(fn); - - return () => { - callbacks = callbacks.filter((cb) => cb !== fn); - }; -} +const LocaleTime = React.lazy(() => import("components/LocaleTime")); type Props = { dateTime: string, @@ -28,44 +12,27 @@ type Props = { shorten?: boolean, }; -class Time extends React.Component { - removeEachMinuteCallback: () => void; +function Time(props: Props) { + let content = distanceInWordsToNow(props.dateTime, { + addSuffix: props.addSuffix, + }); - componentDidMount() { - this.removeEachMinuteCallback = eachMinute(() => { - this.forceUpdate(); - }); + if (props.shorten) { + content = content + .replace("about", "") + .replace("less than a minute ago", "just now") + .replace("minute", "min"); } - componentWillUnmount() { - this.removeEachMinuteCallback(); - } - - render() { - const { shorten, addSuffix } = this.props; - let content = distanceInWordsToNow(this.props.dateTime, { - addSuffix, - }); - - if (shorten) { - content = content - .replace("about", "") - .replace("less than a minute ago", "just now") - .replace("minute", "min"); - } - - return ( - - - - ); - } + return ( + {props.children || content} + } + > + + + ); } export default Time; diff --git a/app/hooks/useUserLocale.js b/app/hooks/useUserLocale.js new file mode 100644 index 000000000..721fca6e4 --- /dev/null +++ b/app/hooks/useUserLocale.js @@ -0,0 +1,12 @@ +// @flow +import useStores from "./useStores"; + +export default function useUserLocale() { + const { auth } = useStores(); + + if (!auth.user) { + return undefined; + } + + return auth.user.language.split("_")[0]; +} diff --git a/server/api/__snapshots__/users.test.js.snap b/server/api/__snapshots__/users.test.js.snap index 48e01865f..859cb70f5 100644 --- a/server/api/__snapshots__/users.test.js.snap +++ b/server/api/__snapshots__/users.test.js.snap @@ -9,7 +9,7 @@ Object { "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", "isAdmin": false, "isSuspended": false, - "language": null, + "language": "en_US", "lastActiveAt": null, "name": "User 1", }, @@ -45,7 +45,7 @@ Object { "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", "isAdmin": false, "isSuspended": false, - "language": null, + "language": "en_US", "lastActiveAt": null, "name": "User 1", }, @@ -81,7 +81,7 @@ Object { "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", "isAdmin": true, "isSuspended": false, - "language": null, + "language": "en_US", "lastActiveAt": null, "name": "User 1", }, @@ -126,7 +126,7 @@ Object { "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", "isAdmin": false, "isSuspended": true, - "language": null, + "language": "en_US", "lastActiveAt": null, "name": "User 1", }, diff --git a/server/presenters/__snapshots__/user.test.js.snap b/server/presenters/__snapshots__/user.test.js.snap index 8f84e53dc..4383924a1 100644 --- a/server/presenters/__snapshots__/user.test.js.snap +++ b/server/presenters/__snapshots__/user.test.js.snap @@ -7,7 +7,7 @@ Object { "id": "123", "isAdmin": undefined, "isSuspended": undefined, - "language": undefined, + "language": "en_US", "lastActiveAt": undefined, "name": "Test User", } @@ -20,7 +20,7 @@ Object { "id": "123", "isAdmin": undefined, "isSuspended": undefined, - "language": undefined, + "language": "en_US", "lastActiveAt": undefined, "name": "Test User", } diff --git a/server/presenters/user.js b/server/presenters/user.js index 9f4ed16f3..0b7f75245 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -24,7 +24,7 @@ export default (user: User, options: Options = {}): ?UserPresentation => { userData.isAdmin = user.isAdmin; userData.isSuspended = user.isSuspended; userData.avatarUrl = user.avatarUrl; - userData.language = user.language; + userData.language = user.language || process.env.DEFAULT_LANGUAGE || "en_US"; if (options.includeDetails) { userData.email = user.email; diff --git a/shared/i18n/index.js b/shared/i18n/index.js index db73bf459..2f42984fa 100644 --- a/shared/i18n/index.js +++ b/shared/i18n/index.js @@ -31,6 +31,9 @@ export const initI18n = () => { return i18n; }; +// Note: Updating the available languages? Make sure to also update the +// locales array in app/components/LocaleTime.js to enable translation for timestamps. + export const languageOptions = [ { label: "English (US)", value: "en_US" }, { label: "Deutsch (Deutschland)", value: "de_DE" },