fix: Add translation hooks on remaining files (#2311)

This commit is contained in:
Saumya Pandey
2021-07-15 00:30:08 +05:30
committed by GitHub
parent 06e16eef12
commit a9d758bb0c
5 changed files with 319 additions and 268 deletions

View File

@ -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<Props> {
@ -55,6 +57,8 @@ class ErrorBoundary extends React.Component<Props> {
};
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<Props> {
if (isChunkError) {
return (
<CenteredContent>
<PageTitle title="Module failed to load" />
<h1>Loading Failed</h1>
<PageTitle title={t("Module failed to load")} />
<h1>
<Trans>Loading Failed</Trans>
</h1>
<HelpText>
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.
<Trans>
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.
</Trans>
</HelpText>
<p>
<Button onClick={this.handleReload}>Reload</Button>
<Button onClick={this.handleReload}>
<Trans>Reload</Trans>
</Button>
</p>
</CenteredContent>
);
@ -79,23 +89,32 @@ class ErrorBoundary extends React.Component<Props> {
return (
<CenteredContent>
<PageTitle title="Something Unexpected Happened" />
<h1>Something Unexpected Happened</h1>
<PageTitle title={t("Something Unexpected Happened")} />
<h1>
<Trans>Something Unexpected Happened</Trans>
</h1>
<HelpText>
Sorry, an unrecoverable error occurred
{isReported && " our engineers have been notified"}. Please try
reloading the page, it may have been a temporary glitch.
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
notified: isReported
? ` ${t("our engineers have been notified")}`
: undefined,
}}
/>
</HelpText>
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>Reload</Button>{" "}
<Button onClick={this.handleReload}>
<Trans>Reload</Trans>
</Button>{" "}
{this.showDetails ? (
<Button onClick={this.handleReportBug} neutral>
Report a Bug
<Trans>Report a Bug</Trans>
</Button>
) : (
<Button onClick={this.handleShowDetails} neutral>
Show Details
<Trans>Show Detail</Trans>
</Button>
)}
</p>
@ -114,4 +133,4 @@ const Pre = styled.pre`
white-space: pre-wrap;
`;
export default ErrorBoundary;
export default withTranslation()<ErrorBoundary>(ErrorBoundary);

View File

@ -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<Props> {
@observable editorComponent: React.ComponentType<any>;
@observable focused: boolean = false;
function InputRich({ label, minHeight, maxHeight, ...rest }: Props) {
const [focused, setFocused] = React.useState<boolean>(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 (
<>
<LabelText>{label}</LabelText>
<StyledOutline
maxHeight={maxHeight}
minHeight={minHeight}
focused={this.focused}
return (
<>
<LabelText>{label}</LabelText>
<StyledOutline
maxHeight={maxHeight}
minHeight={minHeight}
focused={focused}
>
<React.Suspense
fallback={
<HelpText>
<Trans>Loading editor</Trans>
</HelpText>
}
>
<React.Suspense fallback={<HelpText>Loading editor</HelpText>}>
<Editor
onBlur={this.handleBlur}
onFocus={this.handleFocus}
ui={ui}
grow
{...rest}
/>
</React.Suspense>
</StyledOutline>
</>
);
}
<Editor
onBlur={handleBlur}
onFocus={handleFocus}
ui={ui}
grow
{...rest}
/>
</React.Suspense>
</StyledOutline>
</>
);
}
const StyledOutline = styled(Outline)`
@ -67,4 +67,4 @@ const StyledOutline = styled(Outline)`
}
`;
export default inject("ui")(withTheme(InputRich));
export default observer(withTheme(InputRich));

View File

@ -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<Props> {
@observable isSaving: boolean;
@observable linkCopied: boolean = false;
@observable
invites: InviteRequest[] = [
function Invite({ onSubmit }: Props) {
const [isSaving, setIsSaving] = React.useState();
const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
const [invites, setInvites] = React.useState<InviteRequest[]>([
{ 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 (
<form onSubmit={this.handleSubmit}>
{team.guestSignin ? (
<HelpText>
Invite team members or guests to join your knowledge base. Team
members can sign in with {team.signinMethods} or use their email
address.
</HelpText>
) : (
<HelpText>
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{" "}
<Link to="/settings/security">enable email sign-in</Link>.
</>
)}
</HelpText>
)}
{team.subdomain && (
<CopyBlock>
<Flex align="flex-end">
<Input
type="text"
value={team.url}
label="Want a link to share directly with your team?"
readOnly
flex
/>
&nbsp;&nbsp;
<CopyToClipboard text={team.url} onCopy={this.handleCopy}>
<Button
type="button"
icon={<LinkIcon />}
style={{ marginBottom: "16px" }}
neutral
>
{this.linkCopied ? "Link copied" : "Copy link"}
</Button>
</CopyToClipboard>
</Flex>
<p>
<hr />
</p>
</CopyBlock>
)}
{this.invites.map((invite, index) => (
<Flex key={index}>
return (
<form onSubmit={handleSubmit}>
{team.guestSignin ? (
<HelpText>
<Trans
defaults="Invite team members or guests to join your knowledge base. Team members can sign in with {{signinMethods}} or use their email address."
values={{ signinMethods: team.signinMethods }}
/>
</HelpText>
) : (
<HelpText>
<Trans
defaults="Invite team members to join your knowledge base. They will need to sign in with {{signinMethods}}."
values={{ signinMethods: team.signinMethods }}
/>{" "}
{can.update && (
<Trans>
As an admin you can also{" "}
<Link to="/settings/security">enable email sign-in</Link>.
</Trans>
)}
</HelpText>
)}
{team.subdomain && (
<CopyBlock>
<Flex align="flex-end">
<Input
type="email"
name="email"
label="Email"
labelHidden={index !== 0}
onChange={(ev) => 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
/>
&nbsp;&nbsp;
<Input
type="text"
name="name"
label="Full name"
labelHidden={index !== 0}
onChange={(ev) => this.handleChange(ev, index)}
value={invite.name}
required={!!invite.email}
flex
/>
{index !== 0 && (
<Remove>
<Tooltip tooltip="Remove invite" placement="top">
<NudeButton onClick={(ev) => this.handleRemove(ev, index)}>
<CloseIcon />
</NudeButton>
</Tooltip>
</Remove>
)}
<CopyToClipboard text={team.url} onCopy={handleCopy}>
<Button
type="button"
icon={<LinkIcon />}
style={{ marginBottom: "16px" }}
neutral
>
{linkCopied ? t("Link copied") : t("Copy link")}
</Button>
</CopyToClipboard>
</Flex>
))}
<Flex justify="space-between">
{this.invites.length <= MAX_INVITES ? (
<Button type="button" onClick={this.handleAdd} neutral>
Add another
</Button>
) : (
<span />
<p>
<hr />
</p>
</CopyBlock>
)}
{invites.map((invite, index) => (
<Flex key={index}>
<Input
type="email"
name="email"
label={t("Email")}
labelHidden={index !== 0}
onChange={(ev) => handleChange(ev, index)}
placeholder={`example@${predictedDomain}`}
value={invite.email}
required={index === 0}
autoFocus={index === 0}
flex
/>
&nbsp;&nbsp;
<Input
type="text"
name="name"
label={t("Full name")}
labelHidden={index !== 0}
onChange={(ev) => handleChange(ev, index)}
value={invite.name}
required={!!invite.email}
flex
/>
{index !== 0 && (
<Remove>
<Tooltip tooltip={t("Remove invite")} placement="top">
<NudeButton onClick={(ev) => handleRemove(ev, index)}>
<CloseIcon />
</NudeButton>
</Tooltip>
</Remove>
)}
<Button
type="submit"
disabled={this.isSaving}
data-on="click"
data-event-category="invite"
data-event-action="sendInvites"
>
{this.isSaving ? "Inviting…" : "Send Invites"}
</Button>
</Flex>
<br />
</form>
);
}
))}
<Flex justify="space-between">
{invites.length <= MAX_INVITES ? (
<Button type="button" onClick={handleAdd} neutral>
<Trans>Add another</Trans>
</Button>
) : (
<span />
)}
<Button
type="submit"
disabled={isSaving}
data-on="click"
data-event-category="invite"
data-event-action="sendInvites"
>
{isSaving ? `${t("Inviting")}` : t("Send Invites")}
</Button>
</Flex>
<br />
</form>
);
}
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);

View File

@ -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<Props> {
@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 (
<Modal isOpen title="Delete Account" onRequestClose={onRequestClose}>
<Flex column>
<form onSubmit={this.handleSubmit}>
<HelpText>
return (
<Modal isOpen title={t("Delete Account")} onRequestClose={onRequestClose}>
<Flex column>
<form onSubmit={handleSubmit}>
<HelpText>
<Trans>
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.
</HelpText>
<HelpText>
<strong>Note:</strong> Signing back in will cause a new account to
be automatically reprovisioned.
</HelpText>
<Button type="submit" danger>
{this.isDeleting ? "Deleting…" : "Delete My Account"}
</Button>
</form>
</Flex>
</Modal>
);
}
</Trans>
</HelpText>
<HelpText>
<Trans
defaults="<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned."
components={{ em: <strong /> }}
/>
</HelpText>
<Button type="submit" danger>
{isDeleting ? `${t("Deleting")}` : t("Delete My Account")}
</Button>
</form>
</Flex>
</Modal>
);
}
export default inject("auth", "ui")(UserDelete);
export default observer(UserDelete);

View File

@ -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</2>.": "As an admin you can also <2>enable email sign-in</2>.",
"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></1>": "No documents found for your search filters. <1></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></1>Community contributions are accepted though our <4>translation portal</4>": "Please note that translations are currently in early access.<1></1>Community contributions are accepted though our <4>translation portal</4>",
"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.",
"<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned.": "<em>Note:</em> 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.",