feat: I18n (#1653)

* feat: i18n

* Changing language single source of truth from TEAM to USER

* Changes according to @tommoor comments on PR

* Changed package.json for build:i18n and translation label

* Finished 1st MVP of i18n for outline

* new translation labels & Portuguese from Portugal translation

* Fixes from PR request

* Described language dropdown as an experimental feature

* Set keySeparator to false in order to cowork with html keys

* Added useTranslation to Breadcrumb

* Repositioned <strong> element

* Removed extra space from TemplatesMenu

* Fortified the test suite for i18n

* Fixed trans component problematic

* Check if selected language is available

* Update yarn.lock

* Removed unused Trans

* Removing debug variable from i18n init

* Removed debug variable

* test: update snapshots

* flow: Remove decorator usage to get proper flow typing
It's a shame, but hopefully we'll move to Typescript in the next 6 months and we can forget this whole Flow mistake ever happened

* translate: Drafts

* More translatable strings

* Mo translation strings

* translation: Search

* async translations loading

* cache translations in client

* Revert "cache translations in client"

This reverts commit 08fb61ce36384ff90a704faffe4761eccfb76da1.

* Revert localStorage cache for cache headers

* Update Crowdin configuration file

* Moved translation files to locales folder and fixed english text

* Added CONTRIBUTING File for CrowdIn

* chore: Move translations again to please CrowdIn

* fix: loading paths
chore: Add strings for editor

* fix: Improve validation on documents.import endpoint

* test: mock bull

* fix: Unknown mimetype should fallback to Markdown parsing if markdown extension (#1678)

* closes #1675

* Update CONTRIBUTING

* chore: Add link to translation portal from app UI

* refactor: Centralize language config

* fix: Ensure creation of i18n directory in build

* feat: Add language prompt

* chore: Improve contributing guidelines, add link from README

* chore: Normalize tab header casing

* chore: More string externalization

* fix: Language prompt in dark mode

Co-authored-by: André Glatzl <andreglatzl@gmail.com>
This commit is contained in:
Tom Moor
2020-11-29 20:04:58 -08:00
committed by GitHub
parent 63c73c9a51
commit 1285efc49a
85 changed files with 6432 additions and 2613 deletions

View File

@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { SunIcon, MoonIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
@@ -23,6 +24,7 @@ type Props = {
label: React.Node,
ui: UiStore,
auth: AuthStore,
t: TFunction,
};
@observer
@@ -42,14 +44,14 @@ class AccountMenu extends React.Component<Props> {
};
render() {
const { ui } = this.props;
const { ui, t } = this.props;
return (
<>
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
title="Keyboard shortcuts"
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Modal>
@@ -58,23 +60,23 @@ class AccountMenu extends React.Component<Props> {
label={this.props.label}
>
<DropdownMenuItem as={Link} to={settings()}>
Settings
{t("Settings")}
</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleOpenKeyboardShortcuts}>
Keyboard shortcuts
{t("Keyboard shortcuts")}
</DropdownMenuItem>
<DropdownMenuItem href={developers()} target="_blank">
API documentation
{t("API documentation")}
</DropdownMenuItem>
<hr />
<DropdownMenuItem href={changelog()} target="_blank">
Changelog
{t("Changelog")}
</DropdownMenuItem>
<DropdownMenuItem href={mailToUrl()} target="_blank">
Send us feedback
{t("Send us feedback")}
</DropdownMenuItem>
<DropdownMenuItem href={githubIssuesUrl()} target="_blank">
Report a bug
{t("Report a bug")}
</DropdownMenuItem>
<hr />
<DropdownMenu
@@ -87,7 +89,7 @@ class AccountMenu extends React.Component<Props> {
label={
<DropdownMenuItem>
<ChangeTheme justify="space-between">
Appearance
{t("Appearance")}
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
</ChangeTheme>
</DropdownMenuItem>
@@ -98,24 +100,24 @@ class AccountMenu extends React.Component<Props> {
onClick={() => ui.setTheme("system")}
selected={ui.theme === "system"}
>
System
{t("System")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => ui.setTheme("light")}
selected={ui.theme === "light"}
>
Light
{t("Light")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => ui.setTheme("dark")}
selected={ui.theme === "dark"}
>
Dark
{t("Dark")}
</DropdownMenuItem>
</DropdownMenu>
<hr />
<DropdownMenuItem onClick={this.handleLogout}>
Log out
{t("Log out")}
</DropdownMenuItem>
</DropdownMenu>
</>
@@ -127,4 +129,6 @@ const ChangeTheme = styled(Flex)`
width: 100%;
`;
export default inject("ui", "auth")(AccountMenu);
export default withTranslation()<AccountMenu>(
inject("ui", "auth")(AccountMenu)
);

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
@@ -27,6 +28,7 @@ type Props = {
history: RouterHistory,
onOpen?: () => void,
onClose?: () => void,
t: TFunction,
};
@observer
@@ -112,6 +114,7 @@ class CollectionMenu extends React.Component<Props> {
position,
onOpen,
onClose,
t,
} = this.props;
const can = policies.abilities(collection.id);
@@ -128,7 +131,7 @@ class CollectionMenu extends React.Component<Props> {
</VisuallyHidden>
<Modal
title="Collection permissions"
title={t("Collection permissions")}
onRequestClose={this.handleMembersModalClose}
isOpen={this.showCollectionMembers}
>
@@ -143,12 +146,12 @@ class CollectionMenu extends React.Component<Props> {
<DropdownMenuItems
items={[
{
title: "New document",
title: t("New document"),
visible: !!(collection && can.update),
onClick: this.onNewDocument,
},
{
title: "Import document",
title: t("Import document"),
visible: !!(collection && can.update),
onClick: this.onImportDocument,
},
@@ -156,22 +159,22 @@ class CollectionMenu extends React.Component<Props> {
type: "separator",
},
{
title: "Edit…",
title: t("Edit…"),
visible: !!(collection && can.update),
onClick: this.handleEditCollectionOpen,
},
{
title: "Permissions…",
title: t("Permissions…"),
visible: !!(collection && can.update),
onClick: this.handleMembersModalOpen,
},
{
title: "Export…",
title: t("Export…"),
visible: !!(collection && can.export),
onClick: this.handleExportCollectionOpen,
},
{
title: "Delete…",
title: t("Delete…"),
visible: !!(collection && can.delete),
onClick: this.handleDeleteCollectionOpen,
},
@@ -179,7 +182,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</DropdownMenu>
<Modal
title="Edit collection"
title={t("Edit collection")}
isOpen={this.showCollectionEdit}
onRequestClose={this.handleEditCollectionClose}
>
@@ -189,7 +192,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Delete collection"
title={t("Delete collection")}
isOpen={this.showCollectionDelete}
onRequestClose={this.handleDeleteCollectionClose}
>
@@ -199,7 +202,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Export collection"
title={t("Export collection")}
isOpen={this.showCollectionExport}
onRequestClose={this.handleExportCollectionClose}
>
@@ -213,8 +216,6 @@ class CollectionMenu extends React.Component<Props> {
}
}
export default inject(
"ui",
"documents",
"policies"
)(withRouter(CollectionMenu));
export default withTranslation()<CollectionMenu>(
inject("ui", "documents", "policies")(withRouter(CollectionMenu))
);

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import AuthStore from "stores/AuthStore";
import CollectionStore from "stores/CollectionsStore";
@@ -38,6 +39,7 @@ type Props = {
label?: React.Node,
onOpen?: () => void,
onClose?: () => void,
t: TFunction,
};
@observer
@@ -83,7 +85,8 @@ class DocumentMenu extends React.Component<Props> {
// when duplicating, go straight to the duplicated document content
this.redirectTo = duped.url;
this.props.ui.showToast("Document duplicated");
const { t } = this.props;
this.props.ui.showToast(t("Document duplicated"));
};
handleOpenTemplateModal = () => {
@@ -100,7 +103,8 @@ class DocumentMenu extends React.Component<Props> {
handleArchive = async (ev: SyntheticEvent<>) => {
await this.props.document.archive();
this.props.ui.showToast("Document archived");
const { t } = this.props;
this.props.ui.showToast(t("Document archived"));
};
handleRestore = async (
@@ -108,12 +112,14 @@ class DocumentMenu extends React.Component<Props> {
options?: { collectionId: string }
) => {
await this.props.document.restore(options);
this.props.ui.showToast("Document restored");
const { t } = this.props;
this.props.ui.showToast(t("Document restored"));
};
handleUnpublish = async (ev: SyntheticEvent<>) => {
await this.props.document.unpublish();
this.props.ui.showToast("Document unpublished");
const { t } = this.props;
this.props.ui.showToast(t("Document unpublished"));
};
handlePin = (ev: SyntheticEvent<>) => {
@@ -164,6 +170,7 @@ class DocumentMenu extends React.Component<Props> {
label,
onOpen,
onClose,
t,
} = this.props;
const can = policies.abilities(document.id);
@@ -183,17 +190,17 @@ class DocumentMenu extends React.Component<Props> {
<DropdownMenuItems
items={[
{
title: "Restore",
title: t("Restore"),
visible: !!can.unarchive,
onClick: this.handleRestore,
},
{
title: "Restore",
title: t("Restore"),
visible: !!(collection && can.restore),
onClick: this.handleRestore,
},
{
title: "Restore…",
title: t("Restore…"),
visible: !collection && !!can.restore,
style: {
left: -170,
@@ -204,7 +211,7 @@ class DocumentMenu extends React.Component<Props> {
items: [
{
type: "heading",
title: "Choose a collection",
title: t("Choose a collection"),
},
...collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
@@ -224,37 +231,37 @@ class DocumentMenu extends React.Component<Props> {
],
},
{
title: "Unpin",
title: t("Unpin"),
onClick: this.handleUnpin,
visible: !!(showPin && document.pinned && can.unpin),
},
{
title: "Pin to collection",
title: t("Pin to collection"),
onClick: this.handlePin,
visible: !!(showPin && !document.pinned && can.pin),
},
{
title: "Unstar",
title: t("Unstar"),
onClick: this.handleUnstar,
visible: document.isStarred && !!can.unstar,
},
{
title: "Star",
title: t("Star"),
onClick: this.handleStar,
visible: !document.isStarred && !!can.star,
},
{
title: "Share link…",
title: t("Share link…"),
onClick: this.handleShareLink,
visible: canShareDocuments,
},
{
title: "Enable embeds",
title: t("Enable embeds"),
onClick: document.enableEmbeds,
visible: !!showToggleEmbeds && document.embedsDisabled,
},
{
title: "Disable embeds",
title: t("Disable embeds"),
onClick: document.disableEmbeds,
visible: !!showToggleEmbeds && !document.embedsDisabled,
},
@@ -262,42 +269,42 @@ class DocumentMenu extends React.Component<Props> {
type: "separator",
},
{
title: "New nested document",
title: t("New nested document"),
onClick: this.handleNewChild,
visible: !!can.createChildDocument,
},
{
title: "Create template…",
title: t("Create template…"),
onClick: this.handleOpenTemplateModal,
visible: !!can.update && !document.isTemplate,
},
{
title: "Edit",
title: t("Edit"),
onClick: this.handleEdit,
visible: !!can.update,
},
{
title: "Duplicate",
title: t("Duplicate"),
onClick: this.handleDuplicate,
visible: !!can.update,
},
{
title: "Unpublish",
title: t("Unpublish"),
onClick: this.handleUnpublish,
visible: !!can.unpublish,
},
{
title: "Archive",
title: t("Archive"),
onClick: this.handleArchive,
visible: !!can.archive,
},
{
title: "Delete…",
title: t("Delete…"),
onClick: this.handleDelete,
visible: !!can.delete,
},
{
title: "Move…",
title: t("Move…"),
onClick: this.handleMove,
visible: !!can.move,
},
@@ -305,17 +312,17 @@ class DocumentMenu extends React.Component<Props> {
type: "separator",
},
{
title: "History",
title: t("History"),
onClick: this.handleDocumentHistory,
visible: canViewHistory,
},
{
title: "Download",
title: t("Download"),
onClick: this.handleExport,
visible: !!can.download,
},
{
title: "Print",
title: t("Print"),
onClick: window.print,
visible: !!showPrint,
},
@@ -323,7 +330,9 @@ class DocumentMenu extends React.Component<Props> {
/>
</DropdownMenu>
<Modal
title={`Delete ${this.props.document.noun}`}
title={t("Delete {{ documentName }}", {
documentName: this.props.document.noun,
})}
onRequestClose={this.handleCloseDeleteModal}
isOpen={this.showDeleteModal}
>
@@ -333,7 +342,7 @@ class DocumentMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Create template"
title={t("Create template")}
onRequestClose={this.handleCloseTemplateModal}
isOpen={this.showTemplateModal}
>
@@ -343,7 +352,7 @@ class DocumentMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Share document"
title={t("Share document")}
onRequestClose={this.handleCloseShareModal}
isOpen={this.showShareModal}
>
@@ -357,4 +366,6 @@ class DocumentMenu extends React.Component<Props> {
}
}
export default inject("ui", "auth", "collections", "policies")(DocumentMenu);
export default withTranslation()<DocumentMenu>(
inject("ui", "auth", "collections", "policies")(DocumentMenu)
);

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
@@ -20,6 +21,7 @@ type Props = {
onMembers: () => void,
onOpen?: () => void,
onClose?: () => void,
t: TFunction,
};
@observer
@@ -46,13 +48,13 @@ class GroupMenu extends React.Component<Props> {
};
render() {
const { policies, group, onOpen, onClose } = this.props;
const { policies, group, onOpen, onClose, t } = this.props;
const can = policies.abilities(group.id);
return (
<>
<Modal
title="Edit group"
title={t("Edit group")}
onRequestClose={this.handleEditModalClose}
isOpen={this.editModalOpen}
>
@@ -63,7 +65,7 @@ class GroupMenu extends React.Component<Props> {
</Modal>
<Modal
title="Delete group"
title={t("Delete group")}
onRequestClose={this.handleDeleteModalClose}
isOpen={this.deleteModalOpen}
>
@@ -76,7 +78,7 @@ class GroupMenu extends React.Component<Props> {
<DropdownMenuItems
items={[
{
title: "Members…",
title: t("Members…"),
onClick: this.props.onMembers,
visible: !!(group && can.read),
},
@@ -84,12 +86,12 @@ class GroupMenu extends React.Component<Props> {
type: "separator",
},
{
title: "Edit…",
title: t("Edit…"),
onClick: this.onEdit,
visible: !!(group && can.update),
},
{
title: "Delete…",
title: t("Delete…"),
onClick: this.onDelete,
visible: !!(group && can.delete),
},
@@ -101,4 +103,6 @@ class GroupMenu extends React.Component<Props> {
}
}
export default inject("policies")(withRouter(GroupMenu));
export default withTranslation()<GroupMenu>(
inject("policies")(withRouter(GroupMenu))
);

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -14,6 +15,7 @@ type Props = {
label?: React.Node,
document: Document,
collections: CollectionsStore,
t: TFunction,
};
@observer
@@ -39,7 +41,7 @@ class NewChildDocumentMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { label, document, collections } = this.props;
const { label, document, collections, t } = this.props;
const collection = collections.get(document.collectionId);
return (
@@ -49,14 +51,16 @@ class NewChildDocumentMenu extends React.Component<Props> {
{
title: (
<span>
New document in{" "}
<strong>{collection ? collection.name : "collection"}</strong>
{t("New document in")}{" "}
<strong>
{collection ? collection.name : t("collection")}
</strong>
</span>
),
onClick: this.handleNewDocument,
},
{
title: "New nested document",
title: t("New nested document"),
onClick: this.handleNewChild,
},
]}
@@ -66,4 +70,6 @@ class NewChildDocumentMenu extends React.Component<Props> {
}
}
export default inject("collections")(NewChildDocumentMenu);
export default withTranslation()<NewChildDocumentMenu>(
inject("collections")(NewChildDocumentMenu)
);

View File

@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -19,6 +20,7 @@ type Props = {
documents: DocumentsStore,
collections: CollectionsStore,
policies: PoliciesStore,
t: TFunction,
};
@observer
@@ -29,7 +31,14 @@ class NewDocumentMenu extends React.Component<Props> {
this.redirectTo = undefined;
}
handleNewDocument = (collectionId: string, options) => {
handleNewDocument = (
collectionId: string,
options?: {
parentDocumentId?: string,
template?: boolean,
templateId?: string,
}
) => {
this.redirectTo = newDocumentUrl(collectionId, options);
};
@@ -44,7 +53,7 @@ class NewDocumentMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { collections, documents, policies, label, ...rest } = this.props;
const { collections, documents, policies, label, t, ...rest } = this.props;
const singleCollection = collections.orderedData.length === 1;
return (
@@ -52,14 +61,15 @@ class NewDocumentMenu extends React.Component<Props> {
label={
label || (
<Button icon={<PlusIcon />} small>
New doc{singleCollection ? "" : "…"}
{t("New doc")}
{singleCollection ? "" : "…"}
</Button>
)
}
onOpen={this.onOpen}
{...rest}
>
<Header>Choose a collection</Header>
<Header>{t("Choose a collection")}</Header>
<DropdownMenuItems
items={collections.orderedData.map((collection) => ({
onClick: () => this.handleNewDocument(collection.id),
@@ -77,4 +87,6 @@ class NewDocumentMenu extends React.Component<Props> {
}
}
export default inject("collections", "documents", "policies")(NewDocumentMenu);
export default withTranslation()<NewDocumentMenu>(
inject("collections", "documents", "policies")(NewDocumentMenu)
);

View File

@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -17,6 +18,7 @@ type Props = {
label?: React.Node,
collections: CollectionsStore,
policies: PoliciesStore,
t: TFunction,
};
@observer
@@ -36,20 +38,20 @@ class NewTemplateMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { collections, policies, label, ...rest } = this.props;
const { collections, policies, label, t, ...rest } = this.props;
return (
<DropdownMenu
label={
label || (
<Button icon={<PlusIcon />} small>
New template
{t("New template…")}
</Button>
)
}
{...rest}
>
<Header>Choose a collection</Header>
<Header>{t("Choose a collection")}</Header>
<DropdownMenuItems
items={collections.orderedData.map((collection) => ({
onClick: () => this.handleNewDocument(collection.id),
@@ -67,4 +69,6 @@ class NewTemplateMenu extends React.Component<Props> {
}
}
export default inject("collections", "policies")(NewTemplateMenu);
export default withTranslation()<NewTemplateMenu>(
inject("collections", "policies")(NewTemplateMenu)
);

View File

@@ -1,6 +1,7 @@
// @flow
import { inject } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import UiStore from "stores/UiStore";
@@ -19,22 +20,25 @@ type Props = {
className?: string,
label: React.Node,
ui: UiStore,
t: TFunction,
};
class RevisionMenu extends React.Component<Props> {
handleRestore = async (ev: SyntheticEvent<>) => {
ev.preventDefault();
await this.props.document.restore({ revisionId: this.props.revision.id });
this.props.ui.showToast("Document restored");
const { t } = this.props;
this.props.ui.showToast(t("Document restored"));
this.props.history.push(this.props.document.url);
};
handleCopy = () => {
this.props.ui.showToast("Link copied");
const { t } = this.props;
this.props.ui.showToast(t("Link copied"));
};
render() {
const { className, label, onOpen, onClose } = this.props;
const { className, label, onOpen, onClose, t } = this.props;
const url = `${window.location.origin}${documentHistoryUrl(
this.props.document,
this.props.revision.id
@@ -48,15 +52,17 @@ class RevisionMenu extends React.Component<Props> {
label={label}
>
<DropdownMenuItem onClick={this.handleRestore}>
Restore version
{t("Restore version")}
</DropdownMenuItem>
<hr />
<CopyToClipboard text={url} onCopy={this.handleCopy}>
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
</CopyToClipboard>
</DropdownMenu>
);
}
}
export default withRouter(inject("ui")(RevisionMenu));
export default withTranslation()<RevisionMenu>(
withRouter(inject("ui")(RevisionMenu))
);

View File

@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import SharesStore from "stores/SharesStore";
@@ -16,6 +17,7 @@ type Props = {
shares: SharesStore,
ui: UiStore,
share: Share,
t: TFunction,
};
@observer
@@ -36,36 +38,38 @@ class ShareMenu extends React.Component<Props> {
try {
await this.props.shares.revoke(this.props.share);
this.props.ui.showToast("Share link revoked");
const { t } = this.props;
this.props.ui.showToast(t("Share link revoked"));
} catch (err) {
this.props.ui.showToast(err.message);
}
};
handleCopy = () => {
this.props.ui.showToast("Share link copied");
const { t } = this.props;
this.props.ui.showToast(t("Share link copied"));
};
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { share, onOpen, onClose } = this.props;
const { share, onOpen, onClose, t } = this.props;
return (
<DropdownMenu onOpen={onOpen} onClose={onClose}>
<CopyToClipboard text={share.url} onCopy={this.handleCopy}>
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
</CopyToClipboard>
<DropdownMenuItem onClick={this.handleGoToDocument}>
Go to document
{t("Go to document")}
</DropdownMenuItem>
<hr />
<DropdownMenuItem onClick={this.handleRevoke}>
Revoke link
{t("Revoke link")}
</DropdownMenuItem>
</DropdownMenu>
);
}
}
export default inject("shares", "ui")(ShareMenu);
export default withTranslation()<ShareMenu>(inject("shares", "ui")(ShareMenu));

View File

@@ -2,6 +2,7 @@
import { observer, inject } from "mobx-react";
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Document from "models/Document";
@@ -11,12 +12,13 @@ import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = {
document: Document,
documents: DocumentsStore,
t: TFunction,
};
@observer
class TemplatesMenu extends React.Component<Props> {
render() {
const { documents, document, ...rest } = this.props;
const { documents, document, t, ...rest } = this.props;
const templates = documents.templatesInCollection(document.collectionId);
if (!templates.length) {
@@ -28,7 +30,7 @@ class TemplatesMenu extends React.Component<Props> {
position="left"
label={
<Button disclosure neutral>
Templates
{t("Templates")}
</Button>
}
{...rest}
@@ -42,7 +44,9 @@ class TemplatesMenu extends React.Component<Props> {
<div>
<strong>{template.titleWithDefault}</strong>
<br />
<Author>By {template.createdBy.name}</Author>
<Author>
{t("By {{ author }}", { author: template.createdBy.name })}
</Author>
</div>
</DropdownMenuItem>
))}
@@ -55,4 +59,6 @@ const Author = styled.div`
font-size: 13px;
`;
export default inject("documents")(TemplatesMenu);
export default withTranslation()<TemplatesMenu>(
inject("documents")(TemplatesMenu)
);

View File

@@ -2,6 +2,7 @@
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import UsersStore from "stores/UsersStore";
import User from "models/User";
import { DropdownMenu } from "components/DropdownMenu";
@@ -10,16 +11,20 @@ import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
type Props = {
user: User,
users: UsersStore,
t: TFunction,
};
@observer
class UserMenu extends React.Component<Props> {
handlePromote = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { user, users } = this.props;
const { user, users, t } = this.props;
if (
!window.confirm(
`Are you want to make ${user.name} an admin? Admins can modify team and billing information.`
t(
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
{ userName: user.name }
)
)
) {
return;
@@ -29,8 +34,14 @@ class UserMenu extends React.Component<Props> {
handleDemote = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { user, users } = this.props;
if (!window.confirm(`Are you want to make ${user.name} a member?`)) {
const { user, users, t } = this.props;
if (
!window.confirm(
t("Are you sure you want to make {{ userName }} a member?", {
userName: user.name,
})
)
) {
return;
}
users.demote(user);
@@ -38,10 +49,12 @@ class UserMenu extends React.Component<Props> {
handleSuspend = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { user, users } = this.props;
const { user, users, t } = this.props;
if (
!window.confirm(
"Are you want to suspend this account? Suspended users will be prevented from logging in."
t(
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in."
)
)
) {
return;
@@ -62,19 +75,23 @@ class UserMenu extends React.Component<Props> {
};
render() {
const { user } = this.props;
const { user, t } = this.props;
return (
<DropdownMenu>
<DropdownMenuItems
items={[
{
title: `Make ${user.name} a member…`,
title: t("Make {{ userName }} a member…", {
userName: user.name,
}),
onClick: this.handleDemote,
visible: user.isAdmin,
},
{
title: `Make ${user.name} an admin…`,
title: t("Make {{ userName }} an admin…", {
userName: user.name,
}),
onClick: this.handlePromote,
visible: !user.isAdmin && !user.isSuspended,
},
@@ -82,17 +99,17 @@ class UserMenu extends React.Component<Props> {
type: "separator",
},
{
title: "Revoke invite…",
title: t("Revoke invite…"),
onClick: this.handleRevoke,
visible: user.isInvited,
},
{
title: "Reactivate account",
title: t("Activate account"),
onClick: this.handleActivate,
visible: !user.isInvited && user.isSuspended,
},
{
title: "Suspend account",
title: t("Suspend account…"),
onClick: this.handleSuspend,
visible: !user.isInvited && !user.isSuspended,
},
@@ -103,4 +120,4 @@ class UserMenu extends React.Component<Props> {
}
}
export default inject("users")(UserMenu);
export default withTranslation()<UserMenu>(inject("users")(UserMenu));