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:
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user