From a9d758bb0c38cf8bcf47134b4c1728587933b9c4 Mon Sep 17 00:00:00 2001 From: Saumya Pandey Date: Thu, 15 Jul 2021 00:30:08 +0530 Subject: [PATCH] fix: Add translation hooks on remaining files (#2311) --- app/components/ErrorBoundary.js | 53 ++-- app/components/InputRich.js | 76 ++--- app/scenes/Invite.js | 340 +++++++++++---------- app/scenes/UserDelete.js | 91 +++--- shared/i18n/locales/en_US/translation.json | 27 +- 5 files changed, 319 insertions(+), 268 deletions(-) diff --git a/app/components/ErrorBoundary.js b/app/components/ErrorBoundary.js index e3a077e33..5ddd7d566 100644 --- a/app/components/ErrorBoundary.js +++ b/app/components/ErrorBoundary.js @@ -3,6 +3,7 @@ import * as Sentry from "@sentry/react"; import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; +import { withTranslation, type TFunction, Trans } from "react-i18next"; import styled from "styled-components"; import Button from "components/Button"; import CenteredContent from "components/CenteredContent"; @@ -11,10 +12,11 @@ import PageTitle from "components/PageTitle"; import { githubIssuesUrl } from "../../shared/utils/routeHelpers"; import env from "env"; -type Props = { +type Props = {| children: React.Node, reloadOnChunkMissing?: boolean, -}; + t: TFunction, +|}; @observer class ErrorBoundary extends React.Component { @@ -55,6 +57,8 @@ class ErrorBoundary extends React.Component { }; render() { + const { t } = this.props; + if (this.error) { const error = this.error; const isReported = !!env.SENTRY_DSN && env.DEPLOYMENT === "hosted"; @@ -63,15 +67,21 @@ class ErrorBoundary extends React.Component { if (isChunkError) { return ( - -

Loading Failed

+ +

+ Loading Failed +

- Sorry, part of the application failed to load. This may be because - it was updated since you opened the tab or because of a failed - network request. Please try reloading. + + Sorry, part of the application failed to load. This may be + because it was updated since you opened the tab or because of a + failed network request. Please try reloading. +

- +

); @@ -79,23 +89,32 @@ class ErrorBoundary extends React.Component { return ( - -

Something Unexpected Happened

+ +

+ Something Unexpected Happened +

- Sorry, an unrecoverable error occurred - {isReported && " – our engineers have been notified"}. Please try - reloading the page, it may have been a temporary glitch. + {this.showDetails &&
{error.toString()}
}

- {" "} + {" "} {this.showDetails ? ( ) : ( )}

@@ -114,4 +133,4 @@ const Pre = styled.pre` white-space: pre-wrap; `; -export default ErrorBoundary; +export default withTranslation()(ErrorBoundary); diff --git a/app/components/InputRich.js b/app/components/InputRich.js index 5238537b7..1bb991ac5 100644 --- a/app/components/InputRich.js +++ b/app/components/InputRich.js @@ -1,58 +1,58 @@ // @flow -import { observable } from "mobx"; -import { observer, inject } from "mobx-react"; +import { observer } from "mobx-react"; import * as React from "react"; +import { Trans } from "react-i18next"; import styled, { withTheme } from "styled-components"; -import UiStore from "stores/UiStore"; import Editor from "components/Editor"; import HelpText from "components/HelpText"; import { LabelText, Outline } from "components/Input"; +import useStores from "hooks/useStores"; type Props = {| label: string, minHeight?: number, maxHeight?: number, readOnly?: boolean, - ui: UiStore, |}; -@observer -class InputRich extends React.Component { - @observable editorComponent: React.ComponentType; - @observable focused: boolean = false; +function InputRich({ label, minHeight, maxHeight, ...rest }: Props) { + const [focused, setFocused] = React.useState(false); + const { ui } = useStores(); - handleBlur = () => { - this.focused = false; - }; + const handleBlur = React.useCallback(() => { + setFocused(false); + }, []); - handleFocus = () => { - this.focused = true; - }; + const handleFocus = React.useCallback(() => { + setFocused(true); + }, []); - render() { - const { label, minHeight, maxHeight, ui, ...rest } = this.props; - - return ( - <> - {label} - + {label} + + + Loading editor… + + } > - Loading editor…}> - - - - - ); - } + + + + + ); } const StyledOutline = styled(Outline)` @@ -67,4 +67,4 @@ const StyledOutline = styled(Outline)` } `; -export default inject("ui")(withTheme(InputRich)); +export default observer(withTheme(InputRich)); diff --git a/app/scenes/Invite.js b/app/scenes/Invite.js index 92191cacb..94291ecdd 100644 --- a/app/scenes/Invite.js +++ b/app/scenes/Invite.js @@ -1,14 +1,10 @@ // @flow -import { observable, action } from "mobx"; -import { inject, observer } from "mobx-react"; +import { observer } from "mobx-react"; import { LinkIcon, CloseIcon } from "outline-icons"; import * as React from "react"; -import { Link, withRouter, type RouterHistory } from "react-router-dom"; +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; import styled from "styled-components"; -import AuthStore from "stores/AuthStore"; -import PoliciesStore from "stores/PoliciesStore"; -import UiStore from "stores/UiStore"; -import UsersStore from "stores/UsersStore"; import Button from "components/Button"; import CopyToClipboard from "components/CopyToClipboard"; import Flex from "components/Flex"; @@ -16,198 +12,210 @@ import HelpText from "components/HelpText"; import Input from "components/Input"; import NudeButton from "components/NudeButton"; import Tooltip from "components/Tooltip"; +import useCurrentTeam from "hooks/useCurrentTeam"; +import useCurrentUser from "hooks/useCurrentUser"; +import useStores from "hooks/useStores"; const MAX_INVITES = 20; -type Props = { - auth: AuthStore, - users: UsersStore, - history: RouterHistory, - policies: PoliciesStore, - ui: UiStore, +type Props = {| onSubmit: () => void, -}; +|}; type InviteRequest = { email: string, name: string, }; -@observer -class Invite extends React.Component { - @observable isSaving: boolean; - @observable linkCopied: boolean = false; - @observable - invites: InviteRequest[] = [ +function Invite({ onSubmit }: Props) { + const [isSaving, setIsSaving] = React.useState(); + const [linkCopied, setLinkCopied] = React.useState(false); + const [invites, setInvites] = React.useState([ { email: "", name: "" }, { email: "", name: "" }, { email: "", name: "" }, - ]; + ]); - handleSubmit = async (ev: SyntheticEvent<>) => { - ev.preventDefault(); - this.isSaving = true; + const { users, policies, ui } = useStores(); + const user = useCurrentUser(); + const team = useCurrentTeam(); + const { t } = useTranslation(); - try { - await this.props.users.invite(this.invites); - this.props.onSubmit(); - this.props.ui.showToast("We sent out your invites!", { type: "success" }); - } catch (err) { - this.props.ui.showToast(err.message, { type: "error" }); - } finally { - this.isSaving = false; - } - }; + const predictedDomain = user.email.split("@")[1]; + const can = policies.abilities(team.id); - @action - handleChange = (ev, index) => { - this.invites[index][ev.target.name] = ev.target.value; - }; + const handleSubmit = React.useCallback( + async (ev: SyntheticEvent<>) => { + ev.preventDefault(); + setIsSaving(true); - @action - handleGuestChange = (ev, index) => { - this.invites[index][ev.target.name] = ev.target.checked; - }; + try { + await users.invite(invites); + onSubmit(); + ui.showToast(t("We sent out your invites!"), { type: "success" }); + } catch (err) { + ui.showToast(err.message, { type: "error" }); + } finally { + setIsSaving(false); + } + }, + [onSubmit, ui, invites, t, users] + ); - @action - handleAdd = () => { - if (this.invites.length >= MAX_INVITES) { - this.props.ui.showToast( - `Sorry, you can only send ${MAX_INVITES} invites at a time`, + const handleChange = React.useCallback((ev, index) => { + setInvites((prevInvites) => { + const newInvites = [...prevInvites]; + newInvites[index][ev.target.name] = ev.target.value; + return newInvites; + }); + }, []); + + const handleAdd = React.useCallback(() => { + if (invites.length >= MAX_INVITES) { + ui.showToast( + t("Sorry, you can only send {{MAX_INVITES}} invites at a time", { + MAX_INVITES, + }), { type: "warning" } ); } - this.invites.push({ email: "", name: "" }); - }; + setInvites((prevInvites) => { + const newInvites = [...prevInvites]; + newInvites.push({ email: "", name: "" }); + return newInvites; + }); + }, [ui, invites, t]); - @action - handleRemove = (ev: SyntheticEvent<>, index: number) => { - ev.preventDefault(); - this.invites.splice(index, 1); - }; + const handleRemove = React.useCallback( + (ev: SyntheticEvent<>, index: number) => { + ev.preventDefault(); - handleCopy = () => { - this.linkCopied = true; - this.props.ui.showToast("Share link copied", { + setInvites((prevInvites) => { + const newInvites = [...prevInvites]; + newInvites.splice(index, 1); + return newInvites; + }); + }, + [] + ); + + const handleCopy = React.useCallback(() => { + setLinkCopied(true); + ui.showToast(t("Share link copied"), { type: "success", }); - }; + }, [ui, t]); - render() { - const { team, user } = this.props.auth; - if (!team || !user) return null; - - const predictedDomain = user.email.split("@")[1]; - const can = this.props.policies.abilities(team.id); - - return ( -
- {team.guestSignin ? ( - - Invite team members or guests to join your knowledge base. Team - members can sign in with {team.signinMethods} or use their email - address. - - ) : ( - - Invite team members to join your knowledge base. They will need to - sign in with {team.signinMethods}.{" "} - {can.update && ( - <> - As an admin you can also{" "} - enable email sign-in. - - )} - - )} - {team.subdomain && ( - - - -    - - - - -

-


-

-
- )} - {this.invites.map((invite, index) => ( - + return ( + + {team.guestSignin ? ( + + + + ) : ( + + {" "} + {can.update && ( + + As an admin you can also{" "} + enable email sign-in. + + )} + + )} + {team.subdomain && ( + + this.handleChange(ev, index)} - placeholder={`example@${predictedDomain}`} - value={invite.email} - required={index === 0} - autoFocus={index === 0} + type="text" + value={team.url} + label={t("Want a link to share directly with your team?")} + readOnly flex />    - this.handleChange(ev, index)} - value={invite.name} - required={!!invite.email} - flex - /> - {index !== 0 && ( - - - this.handleRemove(ev, index)}> - - - - - )} + + + - ))} - - - {this.invites.length <= MAX_INVITES ? ( - - ) : ( - +

+


+

+
+ )} + {invites.map((invite, index) => ( + + handleChange(ev, index)} + placeholder={`example@${predictedDomain}`} + value={invite.email} + required={index === 0} + autoFocus={index === 0} + flex + /> +    + handleChange(ev, index)} + value={invite.name} + required={!!invite.email} + flex + /> + {index !== 0 && ( + + + handleRemove(ev, index)}> + + + + )} - - -
- - ); - } + ))} + + + {invites.length <= MAX_INVITES ? ( + + ) : ( + + )} + + + +
+ + ); } const CopyBlock = styled("div")` @@ -221,4 +229,4 @@ const Remove = styled("div")` right: -32px; `; -export default inject("auth", "users", "policies", "ui")(withRouter(Invite)); +export default observer(Invite); diff --git a/app/scenes/UserDelete.js b/app/scenes/UserDelete.js index 3aa544861..571de217f 100644 --- a/app/scenes/UserDelete.js +++ b/app/scenes/UserDelete.js @@ -1,63 +1,64 @@ // @flow -import { observable } from "mobx"; -import { inject, observer } from "mobx-react"; +import { observer } from "mobx-react"; import * as React from "react"; -import AuthStore from "stores/AuthStore"; -import UiStore from "stores/UiStore"; +import { useTranslation, Trans } from "react-i18next"; import Button from "components/Button"; import Flex from "components/Flex"; import HelpText from "components/HelpText"; import Modal from "components/Modal"; +import useStores from "hooks/useStores"; -type Props = { - auth: AuthStore, - ui: UiStore, +type Props = {| onRequestClose: () => void, -}; +|}; -@observer -class UserDelete extends React.Component { - @observable isDeleting: boolean; +function UserDelete({ onRequestClose }: Props) { + const [isDeleting, setIsDeleting] = React.useState(); + const { auth, ui } = useStores(); + const { t } = useTranslation(); - handleSubmit = async (ev: SyntheticEvent<>) => { - ev.preventDefault(); - this.isDeleting = true; + const handleSubmit = React.useCallback( + async (ev: SyntheticEvent<>) => { + ev.preventDefault(); + setIsDeleting(true); - try { - await this.props.auth.deleteUser(); - this.props.auth.logout(); - } catch (error) { - this.props.ui.showToast(error.message, { type: "error" }); - } finally { - this.isDeleting = false; - } - }; + try { + await auth.deleteUser(); + auth.logout(); + } catch (error) { + ui.showToast(error.message, { type: "error" }); + } finally { + setIsDeleting(false); + } + }, + [auth, ui] + ); - render() { - const { onRequestClose } = this.props; - - return ( - - -
- + return ( + + + + + Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked. - - - Note: Signing back in will cause a new account to - be automatically reprovisioned. - - - - - - ); - } + +
+ + }} + /> + + + +
+
+ ); } -export default inject("auth", "ui")(UserDelete); +export default observer(UserDelete); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 45093506c..70f51d4a7 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -95,10 +95,20 @@ "Tip notice": "Tip notice", "Warning": "Warning", "Warning notice": "Warning notice", + "Module failed to load": "Module failed to load", + "Loading Failed": "Loading Failed", + "Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.", + "Reload": "Reload", + "Something Unexpected Happened": "Something Unexpected Happened", + "Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.": "Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.", + "our engineers have been notified": "our engineers have been notified", + "Report a Bug": "Report a Bug", + "Show Detail": "Show Detail", "Icon": "Icon", "Show menu": "Show menu", "Choose icon": "Choose icon", "Loading": "Loading", + "Loading editor": "Loading editor", "Search": "Search", "Default access": "Default access", "View and edit": "View and edit", @@ -345,6 +355,18 @@ "Group members": "Group members", "Recently viewed": "Recently viewed", "Created by me": "Created by me", + "We sent out your invites!": "We sent out your invites!", + "Sorry, you can only send {{MAX_INVITES}} invites at a time": "Sorry, you can only send {{MAX_INVITES}} invites at a time", + "Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address.": "Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address.", + "Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}.": "Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}.", + "As an admin you can also <2>enable email sign-in.": "As an admin you can also <2>enable email sign-in.", + "Want a link to share directly with your team?": "Want a link to share directly with your team?", + "Email": "Email", + "Full name": "Full name", + "Remove invite": "Remove invite", + "Add another": "Add another", + "Inviting": "Inviting", + "Send Invites": "Send Invites", "Navigation": "Navigation", "Edit current document": "Edit current document", "Move current document": "Move current document", @@ -392,7 +414,6 @@ "No documents found for your search filters. <1>": "No documents found for your search filters. <1>", "Create a new document?": "Create a new document?", "Clear filters": "Clear filters", - "Email": "Email", "Last active": "Last active", "Role": "Role", "Viewer": "Viewer", @@ -428,7 +449,6 @@ "Unable to upload new profile picture": "Unable to upload new profile picture", "Photo": "Photo", "Upload": "Upload", - "Full name": "Full name", "Language": "Language", "Please note that translations are currently in early access.<1>Community contributions are accepted though our <4>translation portal": "Please note that translations are currently in early access.<1>Community contributions are accepted though our <4>translation portal", "Delete Account": "Delete Account", @@ -454,6 +474,9 @@ "There are no templates just yet.": "There are no templates just yet.", "You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.", "Trash is empty at the moment.": "Trash is empty at the moment.", + "Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.": "Are you sure? Deleting your account will destroy identifying data associated with your user and cannot be undone. You will be immediately logged out of Outline and all your API tokens will be revoked.", + "Note: Signing back in will cause a new account to be automatically reprovisioned.": "Note: Signing back in will cause a new account to be automatically reprovisioned.", + "Delete My Account": "Delete My Account", "You joined": "You joined", "Joined": "Joined", "{{ time }} ago.": "{{ time }} ago.",